Repository: p2panda/aquadoggo (language: rust)

Issue:
Mistake in logic for calculating skiplink in get_entry_args
After some brain bending in the chat me and @cafca think there may be an error in the logic calculating the skiplink for the return value in get_entry_args here:

https://github.com/p2panda/aquadoggo/blob/9cfa430e65cfb034bc9cbf30be442758d281ba55/aquadoggo/src/rpc/api.rs#L133

We suggest it should be something more like this:

`&entry_backlink.next_seq_num.skiplink_seq_num().unwrap()`

So that the returned value is the skiplink for the entry we are about to create (instead of for the backlink/latest_entry). 


Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index c1a8754c7..a17a2f325 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -37,15 +37,16 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.39"
+version = "1.0.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81cddc5f91628367664cc7c69714ff08deee8a3efc54623011c772544d7b2767"
+checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"
 
 [[package]]
 name = "aquadoggo"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "arrayvec 0.6.1",
  "async-std",
  "bamboo-rs-core",
  "directories",
@@ -90,6 +91,12 @@ version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
 
+[[package]]
+name = "arrayvec"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "269d0f5e68353a7cab87f81e7c736adc008d279a36ebc6a05dfe01193a89f0c9"
+
 [[package]]
 name = "async-attributes"
 version = "1.1.2"
@@ -212,7 +219,7 @@ dependencies = [
  "event-listener",
  "futures-lite",
  "once_cell",
- "signal-hook 0.3.7",
+ "signal-hook 0.3.8",
  "winapi 0.3.9",
 ]
 
@@ -294,7 +301,7 @@ name = "bamboo-rs-core"
 version = "0.1.0-pre-31"
 source = "git+https://github.com/pietgeursen/bamboo-rs?tag=v0.1.0-pre-31#ee8cf5fbfded69ef9c4f9457e85643f23bcc2257"
 dependencies = [
- "arrayvec",
+ "arrayvec 0.5.2",
  "blake2b_simd",
  "ed25519-dalek",
  "hex",
@@ -354,7 +361,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
 dependencies = [
  "arrayref",
- "arrayvec",
+ "arrayvec 0.5.2",
  "constant_time_eq",
 ]
 
@@ -1299,9 +1306,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
 
 [[package]]
 name = "js-sys"
-version = "0.3.49"
+version = "0.3.50"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc15e39392125075f60c95ba416f5381ff6c3a948ff02ab12464715adf56c821"
+checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -1425,7 +1432,7 @@ version = "0.7.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374"
 dependencies = [
- "arrayvec",
+ "arrayvec 0.5.2",
  "bitflags",
  "cfg-if 1.0.0",
  "ryu",
@@ -1434,9 +1441,9 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.91"
+version = "0.2.93"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7"
+checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41"
 
 [[package]]
 name = "libm"
@@ -1463,9 +1470,9 @@ checksum = "d8fbee9dd9064a4e7405d9550402d8d0b9ddebec0f31d84c0f9337a1389c6ef9"
 
 [[package]]
 name = "lock_api"
-version = "0.4.2"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312"
+checksum = "5a3c91c24eae6777794bb1997ad98bbb87daf92890acab859f7eaa4320333176"
 dependencies = [
  "scopeguard",
 ]
@@ -1511,9 +1518,9 @@ checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
 
 [[package]]
 name = "memoffset"
-version = "0.6.1"
+version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "157b4208e3059a8f9e78d559edc658e13df41410cb3ae03979c83130067fdd87"
+checksum = "f83fb6581e8ed1f85fd45c116db8405483899489e38406156c25eb743554361d"
 dependencies = [
  "autocfg 1.0.1",
 ]
@@ -1803,9 +1810,9 @@ dependencies = [
 [[package]]
 name = "p2panda-rs"
 version = "0.1.0"
-source = "git+https://github.com/p2panda/p2panda?branch=experimental-aquadoggo#800197f1d842af7b054ae2c5dc1e4beedcf4b4bf"
+source = "git+https://github.com/p2panda/p2panda?branch=main#e1d92be5d7f93ef9dff97bf4fc85995694768fc7"
 dependencies = [
- "arrayvec",
+ "arrayvec 0.5.2",
  "bamboo-rs-core",
  "cddl",
  "console_error_panic_hook",
@@ -1890,18 +1897,18 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
 
 [[package]]
 name = "pin-project"
-version = "1.0.5"
+version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96fa8ebb90271c4477f144354485b8068bd8f6b78b428b01ba892ca26caf0b63"
+checksum = "bc174859768806e91ae575187ada95c91a29e96a98dc5d2cd9a1fed039501ba6"
 dependencies = [
  "pin-project-internal",
 ]
 
 [[package]]
 name = "pin-project-internal"
-version = "1.0.5"
+version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "758669ae3558c6f74bd2a18b41f7ac0b5a195aea6639d6a9b5e5d1ad5ba24c0b"
+checksum = "a490329918e856ed1b083f244e3bfe2d8c4f336407e4ea9e1a9f479ff09049e5"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1998,9 +2005,9 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.24"
+version = "1.0.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
+checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
 dependencies = [
  "unicode-xid",
 ]
@@ -2236,9 +2243,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
 
 [[package]]
 name = "security-framework"
-version = "2.1.2"
+version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d493c5f39e02dfb062cd8f33301f90f9b13b650e8c1b1d0fd75c19dd64bff69d"
+checksum = "3670b1d2fdf6084d192bc71ead7aabe6c06aa2ea3fbd9cc3ac111fa5c2b1bd84"
 dependencies = [
  "bitflags",
  "core-foundation",
@@ -2249,9 +2256,9 @@ dependencies = [
 
 [[package]]
 name = "security-framework-sys"
-version = "2.1.1"
+version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dee48cdde5ed250b0d3252818f646e174ab414036edb884dde62d80a3ac6082d"
+checksum = "3676258fd3cfe2c9a0ec99ce3038798d847ce3e4bb17746373eb9f0f1ac16339"
 dependencies = [
  "core-foundation-sys",
  "libc",
@@ -2369,9 +2376,9 @@ dependencies = [
 
 [[package]]
 name = "signal-hook"
-version = "0.3.7"
+version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6aa894ef3fade0ee7243422f4fbbd6c2b48e6de767e621d37ef65f2310f53cea"
+checksum = "ef33d6d0cd06e0840fba9985aab098c147e67e05cee14d412d3345ed14ff30ac"
 dependencies = [
  "libc",
  "signal-hook-registry",
@@ -2629,9 +2636,9 @@ checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2"
 
 [[package]]
 name = "syn"
-version = "1.0.64"
+version = "1.0.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fd9d1e9976102a03c542daa2eff1b43f9d72306342f3f8b3ed5fb8908195d6f"
+checksum = "3ce15dd3ed8aa2f8eeac4716d6ef5ab58b6b9256db41d7e1a0224c2788e8fd87"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2720,9 +2727,9 @@ dependencies = [
 
 [[package]]
 name = "tinyvec"
-version = "1.1.1"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "317cca572a0e89c3ce0ca1f1bdc9369547fe318a683418e42ac8f59d14701023"
+checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342"
 dependencies = [
  "tinyvec_macros",
 ]
@@ -2834,9 +2841,9 @@ dependencies = [
 
 [[package]]
 name = "unicode-bidi"
-version = "0.3.4"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
+checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0"
 dependencies = [
  "matches",
 ]
@@ -2971,9 +2978,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.72"
+version = "0.2.73"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fe8f61dba8e5d645a4d8132dc7a0a66861ed5e1045d2c0ed940fab33bac0fbe"
+checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9"
 dependencies = [
  "cfg-if 1.0.0",
  "serde",
@@ -2983,9 +2990,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.72"
+version = "0.2.73"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "046ceba58ff062da072c7cb4ba5b22a37f00a302483f7e2a6cdc18fedbdc1fd3"
+checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae"
 dependencies = [
  "bumpalo",
  "lazy_static",
@@ -2998,9 +3005,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.22"
+version = "0.4.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73157efb9af26fb564bb59a009afd1c7c334a44db171d280690d0c3faaec3468"
+checksum = "81b8b767af23de6ac18bf2168b690bed2902743ddf0fb39252e36f9e2bfc63ea"
 dependencies = [
  "cfg-if 1.0.0",
  "js-sys",
@@ -3010,9 +3017,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.72"
+version = "0.2.73"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ef9aa01d36cda046f797c57959ff5f3c615c9cc63997a8d545831ec7976819b"
+checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -3020,9 +3027,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.72"
+version = "0.2.73"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96eb45c1b2ee33545a813a92dbb53856418bf7eb54ab34f7f7ff1448a5b3735d"
+checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -3033,15 +3040,15 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.72"
+version = "0.2.73"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7148f4696fb4960a346eaa60bbfb42a1ac4ebba21f750f75fc1375b098d5ffa"
+checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489"
 
 [[package]]
 name = "web-sys"
-version = "0.3.49"
+version = "0.3.50"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59fe19d70f5dacc03f6e46777213facae5ac3801575d56ca6cbd4c93dcd12310"
+checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -3058,9 +3065,9 @@ dependencies = [
 
 [[package]]
 name = "whoami"
-version = "1.1.1"
+version = "1.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e296f550993cba2c5c3eba5da0fb335562b2fa3d97b7a8ac9dc91f40a3abc70"
+checksum = "4abacf325c958dfeaf1046931d37f2a901b6dfe0968ee965a29e94c6766b2af6"
 dependencies = [
  "wasm-bindgen",
  "web-sys",
@@ -3131,7 +3138,7 @@ version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "702402e18caf3d1960249ecec2dfac9e75d55cbd073a7fbf4832a6bceb1d6413"
 dependencies = [
- "arrayvec",
+ "arrayvec 0.5.2",
  "blake2b_simd",
  "hex",
  "serde",
diff --git a/aquadoggo/Cargo.toml b/aquadoggo/Cargo.toml
index cb0bf96ac..ad57ea1ff 100644
--- a/aquadoggo/Cargo.toml
+++ b/aquadoggo/Cargo.toml
@@ -12,7 +12,8 @@ readme = "README.md"
 edition = "2018"
 
 [dependencies]
-anyhow = "1.0.39"
+anyhow = "1.0.40"
+arrayvec = "0.6.1"
 async-std = { version = "1.9.0", features = ["unstable", "attributes"] }
 directories = "3.0.1"
 ed25519-dalek = "1.0.1"
@@ -40,5 +41,5 @@ tag = "v0.1.0-pre-31"
 # crate via git. See: https://github.com/p2panda/p2panda/issues/38
 [dependencies.p2panda-rs]
 git = "https://github.com/p2panda/p2panda"
-branch = "experimental-aquadoggo"
+branch = "main"
 features = ["db-sqlx"]
diff --git a/aquadoggo/migrations/20201230002752_create-logs.sql b/aquadoggo/migrations/20201230002752_create-logs.sql
index c37c79ffd..641487c40 100644
--- a/aquadoggo/migrations/20201230002752_create-logs.sql
+++ b/aquadoggo/migrations/20201230002752_create-logs.sql
@@ -1,6 +1,6 @@
 CREATE TABLE IF NOT EXISTS logs (
     author            VARCHAR(64)       NOT NULL,
-    log_id            BIGINT            NOT NULL,
-    schema            VARCHAR(128)      NOT NULL,
+    log_id            UNSIGNED BIGINT   NOT NULL,
+    schema            VARCHAR(132)      NOT NULL,
     PRIMARY KEY (author, schema, log_id)
 );
diff --git a/aquadoggo/src/config.rs b/aquadoggo/src/config.rs
index a29e312e6..be0de7540 100644
--- a/aquadoggo/src/config.rs
+++ b/aquadoggo/src/config.rs
@@ -1,8 +1,9 @@
+use std::fs;
+use std::path::PathBuf;
+
 use anyhow::Result;
 use directories::ProjectDirs;
 use serde::Deserialize;
-use std::fs;
-use std::path::PathBuf;
 
 /// Data directory name.
 const DATA_DIR_NAME: &str = "aquadoggo";
diff --git a/aquadoggo/src/db/models/entry.rs b/aquadoggo/src/db/models/entry.rs
index 565665304..65e95ffb6 100644
--- a/aquadoggo/src/db/models/entry.rs
+++ b/aquadoggo/src/db/models/entry.rs
@@ -1,5 +1,5 @@
-use p2panda_rs::atomic::{Author, Hash, LogId, SeqNum};
-use sqlx::{query_as, FromRow};
+use p2panda_rs::atomic::{Author, EntrySigned, Hash, LogId, MessageEncoded, SeqNum};
+use sqlx::{query, query_as, FromRow};
 
 use crate::db::Pool;
 use crate::errors::Result;
@@ -40,6 +40,46 @@ pub struct Entry {
 }
 
 impl Entry {
+    pub async fn insert(
+        pool: &Pool,
+        author: &Author,
+        entry_bytes: &EntrySigned,
+        entry_hash: &Hash,
+        log_id: &LogId,
+        payload_bytes: &MessageEncoded,
+        payload_hash: &Hash,
+        seq_num: &SeqNum,
+    ) -> Result<bool> {
+        let rows_affected = query(
+            "
+            INSERT INTO
+                entries (
+                    author,
+                    entry_bytes,
+                    entry_hash,
+                    log_id,
+                    payload_bytes,
+                    payload_hash,
+                    seq_num
+                )
+            VALUES
+                (?1, ?2, ?3, ?4, ?5, ?6, ?7)
+            ",
+        )
+        .bind(author)
+        .bind(entry_bytes)
+        .bind(entry_hash)
+        .bind(log_id)
+        .bind(payload_bytes)
+        .bind(payload_hash)
+        .bind(seq_num)
+        .execute(pool)
+        .await?
+        .rows_affected();
+
+        Ok(rows_affected == 1)
+    }
+
     /// Returns the latest Bamboo entry of an author's log.
     pub async fn latest(pool: &Pool, author: &Author, log_id: &LogId) -> Result<Option<Entry>> {
         let latest_entry = query_as::<_, Entry>(
diff --git a/aquadoggo/src/db/models/log.rs b/aquadoggo/src/db/models/log.rs
index d2f7afa33..d177184e7 100644
--- a/aquadoggo/src/db/models/log.rs
+++ b/aquadoggo/src/db/models/log.rs
@@ -25,7 +25,7 @@ impl Log {
     ///
     /// The database will reject duplicate entries.
     #[allow(dead_code)]
-    pub async fn register_log_id(
+    pub async fn insert(
         pool: &Pool,
         author: &Author,
         schema: &Hash,
@@ -95,10 +95,7 @@ impl Log {
     ///
     /// Messages are separated in different logs per schema and author. This method checks if a log
     /// has already been registered for a certain schema and returns its id.
-    ///
-    /// If no log has been found for a USER schema it automatically returns the next unused log_id.
-    /// SYSTEM schema log ids are pre-defined by the protocol specification.
-    pub async fn schema_log_id(pool: &Pool, author: &Author, schema: &Hash) -> Result<LogId> {
+    pub async fn get(pool: &Pool, author: &Author, schema: &Hash) -> Result<Option<LogId>> {
         // @TODO: Look up if system schema was used and return regarding log id
         let result = query_as::<_, LogId>(
             "
@@ -116,10 +113,21 @@ impl Log {
         .fetch_optional(pool)
         .await?;
 
-        // Return result or find next unused user schema log id
-        let log_id = match result {
+        Ok(result)
+    }
+
+    /// Returns registered or possible log_id of an author's schema.
+    ///
+    /// If no log has been found for a USER schema it automatically returns the next unused log_id.
+    /// SYSTEM schema log ids are pre-defined by the protocol specification.
+    pub async fn find_schema_log_id(pool: &Pool, author: &Author, schema: &Hash) -> Result<LogId> {
+        // Determine log_id for author's schema
+        let schema_log_id = Log::get(pool, author, schema).await?;
+
+        // Use result or find next possible log_id automatically
+        let log_id = match schema_log_id {
             Some(value) => value,
-            None => Self::next_user_schema_log_id(pool, author).await?,
+            None => Log::next_user_schema_log_id(pool, author).await?,
         };
 
         Ok(log_id)
@@ -128,7 +136,7 @@ impl Log {
 
 #[cfg(test)]
 mod tests {
-    use p2panda_rs::atomic::{Author, LogId, Hash};
+    use p2panda_rs::atomic::{Author, Hash, LogId};
 
     use super::Log;
 
@@ -143,7 +151,10 @@ mod tests {
         let author = Author::new(TEST_AUTHOR).unwrap();
         let schema = Hash::new(&random_entry_hash()).unwrap();
 
-        let log_id = Log::schema_log_id(&pool, &author, &schema).await.unwrap();
+        let log_id = Log::find_schema_log_id(&pool, &author, &schema)
+            .await
+            .unwrap();
+
         assert_eq!(log_id, LogId::new(1));
     }
 
@@ -154,17 +165,13 @@ mod tests {
         let author = Author::new(TEST_AUTHOR).unwrap();
         let schema = Hash::new(&random_entry_hash()).unwrap();
 
-        assert!(
-            Log::register_log_id(&pool, &author, &schema, &LogId::new(1))
-                .await
-                .is_ok()
-        );
-
-        assert!(
-            Log::register_log_id(&pool, &author, &schema, &LogId::new(1))
-                .await
-                .is_err()
-        );
+        assert!(Log::insert(&pool, &author, &schema, &LogId::new(1))
+            .await
+            .is_ok());
+
+        assert!(Log::insert(&pool, &author, &schema, &LogId::new(1))
+            .await
+            .is_err());
     }
 
     #[async_std::test]
@@ -181,24 +188,24 @@ mod tests {
         let schema_system = Hash::new(&random_entry_hash()).unwrap();
 
         // Register two log ids at the beginning
-        Log::register_log_id(&pool, &author, &schema_system, &LogId::new(9))
+        Log::insert(&pool, &author, &schema_system, &LogId::new(9))
             .await
             .unwrap();
-        Log::register_log_id(&pool, &author, &schema_first, &LogId::new(3))
+        Log::insert(&pool, &author, &schema_first, &LogId::new(3))
             .await
             .unwrap();
 
         // Find next free user log id and register it
         let log_id = Log::next_user_schema_log_id(&pool, &author).await.unwrap();
         assert_eq!(log_id, LogId::new(1));
-        Log::register_log_id(&pool, &author, &schema_second, &log_id)
+        Log::insert(&pool, &author, &schema_second, &log_id)
             .await
             .unwrap();
 
         // Find next free user log id and register it
         let log_id = Log::next_user_schema_log_id(&pool, &author).await.unwrap();
         assert_eq!(log_id, LogId::new(5));
-        Log::register_log_id(&pool, &author, &schema_third, &log_id)
+        Log::insert(&pool, &author, &schema_third, &log_id)
             .await
             .unwrap();
 
diff --git a/aquadoggo/src/errors.rs b/aquadoggo/src/errors.rs
index 25b3d16bd..349143fb7 100644
--- a/aquadoggo/src/errors.rs
+++ b/aquadoggo/src/errors.rs
@@ -1,4 +1,6 @@
-use p2panda_rs::atomic::error::{AuthorError, HashError};
+use p2panda_rs::atomic::error::{
+    AuthorError, EntryError, EntrySignedError, HashError, MessageEncodedError, MessageError,
+};
 
 /// A specialized `Result` type for the node.
 pub type Result<T> = anyhow::Result<T, Error>;
@@ -18,6 +20,30 @@ pub enum Error {
     #[error(transparent)]
     HashValidation(#[from] HashError),
 
+    /// Error returned from validating p2panda-rs `Entry` data types.
+    #[error(transparent)]
+    EntryValidation(#[from] EntryError),
+
+    /// Error returned from validating p2panda-rs `EntrySigned` data types.
+    #[error(transparent)]
+    EntrySignedValidation(#[from] EntrySignedError),
+
+    /// Error returned from validating p2panda-rs `Message` data types.
+    #[error(transparent)]
+    MessageValidation(#[from] MessageError),
+
+    /// Error returned from validating p2panda-rs `MessageEncoded` data types.
+    #[error(transparent)]
+    MessageEncodedValidation(#[from] MessageEncodedError),
+
+    /// Error returned from validating Bamboo entries.
+    #[error(transparent)]
+    BambooValidation(#[from] bamboo_rs_core::verify::Error),
+
+    /// Error returned from `panda_publishEntry` RPC method.
+    #[error(transparent)]
+    PublishEntryValidation(#[from] crate::rpc::PublishEntryError),
+
     /// Error returned from the database.
     #[error(transparent)]
     Database(#[from] sqlx::Error),
@@ -33,6 +59,24 @@ impl From<Error> for jsonrpc_core::Error {
             Error::HashValidation(validation_error) => {
                 handle_validation_error(format!("{}", validation_error))
             }
+            Error::EntryValidation(validation_error) => {
+                handle_validation_error(format!("{}", validation_error))
+            }
+            Error::EntrySignedValidation(validation_error) => {
+                handle_validation_error(format!("{}", validation_error))
+            }
+            Error::MessageValidation(validation_error) => {
+                handle_validation_error(format!("{}", validation_error))
+            }
+            Error::MessageEncodedValidation(validation_error) => {
+                handle_validation_error(format!("{}", validation_error))
+            }
+            Error::BambooValidation(validation_error) => {
+                handle_validation_error(format!("{}", validation_error))
+            }
+            Error::PublishEntryValidation(validation_error) => {
+                handle_validation_error(format!("{}", validation_error))
+            }
             _ => {
                 log::error!("{:#}", error);
 
diff --git a/aquadoggo/src/rpc/api.rs b/aquadoggo/src/rpc/api.rs
index cb545c2dc..4b9163d2e 100644
--- a/aquadoggo/src/rpc/api.rs
+++ b/aquadoggo/src/rpc/api.rs
@@ -2,35 +2,22 @@ use async_std::channel::{unbounded, Sender};
 use async_std::task;
 use jsonrpc_core::{BoxFuture, IoHandler, Params};
 use jsonrpc_derive::rpc;
-use p2panda_rs::atomic::{Author, Hash, LogId, SeqNum, Validation};
-use serde::{Deserialize, Serialize};
+use p2panda_rs::atomic::Validation;
 
-use crate::db::models::{Entry, Log};
 use crate::db::Pool;
 use crate::errors::Result;
-
-/// Request body of `panda_getEntryArguments`.
-#[derive(Deserialize, Debug)]
-pub struct EntryArgsRequest {
-    author: Author,
-    schema: Hash,
-}
-
-/// Response body of `panda_getEntryArguments`.
-#[derive(Serialize, Debug)]
-#[serde(rename_all = "camelCase")]
-pub struct EntryArgsResponse {
-    entry_hash_backlink: Option<Hash>,
-    entry_hash_skiplink: Option<Hash>,
-    last_seq_num: Option<SeqNum>,
-    log_id: LogId,
-}
+use crate::rpc::methods::{get_entry_args, publish_entry};
+use crate::rpc::request::{EntryArgsRequest, PublishEntryRequest};
+use crate::rpc::response::{EntryArgsResponse, PublishEntryResponse};
 
 /// Trait defining all Node RPC API methods.
 #[rpc(server)]
 pub trait Api {
     #[rpc(name = "panda_getEntryArguments", params = "raw")]
     fn get_entry_args(&self, params: Params) -> BoxFuture<Result<EntryArgsResponse>>;
+
+    #[rpc(name = "panda_publishEntry", params = "raw")]
+    fn publish_entry(&self, params: Params) -> BoxFuture<Result<PublishEntryResponse>>;
 }
 
 /// Channel messages to send RPC command requests and their payloads from frontend `Api` to
@@ -41,6 +28,7 @@ pub trait Api {
 #[derive(Debug)]
 enum ApiServiceMessages {
     GetEntryArgs(EntryArgsRequest, Sender<Result<EntryArgsResponse>>),
+    PublishEntry(PublishEntryRequest, Sender<Result<PublishEntryResponse>>),
 }
 
 /// Backend service handling the RPC API methods.
@@ -64,6 +52,12 @@ impl ApiService {
                             .await
                             .unwrap();
                     }
+                    Ok(ApiServiceMessages::PublishEntry(params, back_channel)) => {
+                        back_channel
+                            .send(publish_entry(pool, params).await)
+                            .await
+                            .unwrap();
+                    }
                     Err(_err) => {
                         // Channel closed, there are no more messages.
                     }
@@ -109,104 +103,40 @@ impl Api for ApiService {
             task::block_on(back_channel_notifier.recv()).unwrap()
         })
     }
-}
 
-/// Implementation of `panda_getEntryArguments` RPC method.
-///
-/// Returns required data (backlink and skiplink entry hashes, last sequence number and the schemas
-/// log_id) to encode a new bamboo entry.
-async fn get_entry_args(pool: Pool, params: EntryArgsRequest) -> Result<EntryArgsResponse> {
-    // Determine log_id for author's schema
-    let log_id = Log::schema_log_id(&pool, &params.author, &params.schema).await?;
+    fn publish_entry(&self, params_raw: Params) -> BoxFuture<Result<PublishEntryResponse>> {
+        let service_channel = self.service_channel.clone();
 
-    // Find latest entry in this log
-    let entry_latest = Entry::latest(&pool, &params.author, &log_id).await?;
+        Box::pin(async move {
+            // Parse incoming command parameters
+            let params: PublishEntryRequest = params_raw.parse()?;
 
-    match entry_latest {
-        Some(entry_backlink) => {
-            // Determine skiplink ("lipmaa"-link) entry in this log
-            let entry_skiplink = Entry::at_seq_num(
-                &pool,
-                &params.author,
-                &log_id,
-                // Unwrap as we know that an skiplink exists as soon as previous entry is given
-                &entry_backlink.seq_num.skiplink_seq_num().unwrap(),
+            // Validate entry hex encoding
+            params.entry_encoded.validate()?;
+
+            // Validate message schema and hex encoding
+            params.message_encoded.validate()?;
+
+            // Create back_channel to receive result from backend
+            let (back_channel, back_channel_notifier) = unbounded();
+
+            // Send request to backend and wait for response on back_channel
+            task::block_on(
+                service_channel.send(ApiServiceMessages::PublishEntry(params, back_channel)),
             )
-            .await?
             .unwrap();
-
-            Ok(EntryArgsResponse {
-                entry_hash_backlink: Some(entry_backlink.entry_hash),
-                entry_hash_skiplink: Some(entry_skiplink.entry_hash),
-                last_seq_num: Some(entry_backlink.seq_num),
-                log_id,
-            })
-        }
-        None => Ok(EntryArgsResponse {
-            entry_hash_backlink: None,
-            entry_hash_skiplink: None,
-            last_seq_num: None,
-            log_id,
-        }),
+            task::block_on(back_channel_notifier.recv()).unwrap()
+        })
     }
 }
 
 #[cfg(test)]
 mod tests {
-    use super::ApiService;
-
     use jsonrpc_core::ErrorCode;
 
-    use crate::test_helpers::{initialize_db, random_entry_hash};
+    use crate::test_helpers::{initialize_db, random_entry_hash, rpc_error, rpc_request};
 
-    const TEST_AUTHOR: &str = "8b52ae153142288402382fd6d9619e018978e015e6bc372b1b0c7bd40c6a240a";
-
-    // Helper method to generate valid JSON RPC request string
-    fn rpc_request(method: &str, params: &str) -> String {
-        format!(
-            r#"{{
-                "jsonrpc": "2.0",
-                "method": "{}",
-                "params": {},
-                "id": 1
-            }}"#,
-            method, params
-        )
-        .replace(" ", "")
-        .replace("\n", "")
-    }
-
-    // Helper method to generate valid JSON RPC response string
-    fn rpc_response(result: &str) -> String {
-        format!(
-            r#"{{
-                "jsonrpc": "2.0",
-                "result": {},
-                "id": 1
-            }}"#,
-            result
-        )
-        .replace(" ", "")
-        .replace("\n", "")
-    }
-
-    // Helper method to generate valid JSON RPC error response string
-    fn rpc_error(code: ErrorCode, message: &str) -> String {
-        format!(
-            r#"{{
-                "jsonrpc": "2.0",
-                "error": {{
-                    "code": {},
-                    "message": "<message>"
-                }},
-                "id": 1
-            }}"#,
-            code.code(),
-        )
-        .replace(" ", "")
-        .replace("\n", "")
-        .replace("<message>", message)
-    }
+    use super::ApiService;
 
     #[async_std::test]
     async fn respond_with_missing_param_error() {
@@ -254,33 +184,4 @@ mod tests {
 
         assert_eq!(io.handle_request_sync(&request), Some(response));
     }
-
-    #[async_std::test]
-    async fn get_entry_arguments() {
-        let pool = initialize_db().await;
-        let io = ApiService::io_handler(pool);
-
-        let request = rpc_request(
-            "panda_getEntryArguments",
-            &format!(
-                r#"{{
-                    "author": "{}",
-                    "schema": "{}"
-                }}"#,
-                TEST_AUTHOR,
-                random_entry_hash(),
-            ),
-        );
-
-        let response = rpc_response(
-            r#"{
-                "entryHashBacklink": null,
-                "entryHashSkiplink": null,
-                "lastSeqNum": null,
-                "logId": 1
-            }"#,
-        );
-
-        assert_eq!(io.handle_request_sync(&request), Some(response));
-    }
 }
diff --git a/aquadoggo/src/rpc/methods/entry_args.rs b/aquadoggo/src/rpc/methods/entry_args.rs
new file mode 100644
index 000000000..575746380
--- /dev/null
+++ b/aquadoggo/src/rpc/methods/entry_args.rs
@@ -0,0 +1,99 @@
+use bamboo_rs_core::entry::is_lipmaa_required;
+use p2panda_rs::atomic::Hash;
+
+use crate::db::models::{Entry, Log};
+use crate::db::Pool;
+use crate::errors::Result;
+use crate::rpc::request::EntryArgsRequest;
+use crate::rpc::response::EntryArgsResponse;
+
+/// Implementation of `panda_getEntryArguments` RPC method.
+///
+/// Returns required data (backlink and skiplink entry hashes, last sequence number and the schemas
+/// log_id) to encode a new bamboo entry.
+pub async fn get_entry_args(pool: Pool, params: EntryArgsRequest) -> Result<EntryArgsResponse> {
+    // Determine log_id for author's schema
+    let log_id = Log::find_schema_log_id(&pool, &params.author, &params.schema).await?;
+
+    // Find latest entry in this log
+    let entry_latest = Entry::latest(&pool, &params.author, &log_id).await?;
+
+    match entry_latest {
+        Some(entry_backlink) => {
+            // Determine skiplink ("lipmaa"-link) entry in this log
+            let entry_hash_skiplink = determine_skiplink(pool.clone(), &entry_backlink).await?;
+
+            Ok(EntryArgsResponse {
+                entry_hash_backlink: Some(entry_backlink.entry_hash),
+                entry_hash_skiplink,
+                last_seq_num: Some(entry_backlink.seq_num),
+                log_id,
+            })
+        }
+        None => Ok(EntryArgsResponse {
+            entry_hash_backlink: None,
+            entry_hash_skiplink: None,
+            last_seq_num: None,
+            log_id,
+        }),
+    }
+}
+
+/// Determine skiplink entry hash ("lipmaa"-link) for entry in this log, return `None` when no
+/// skiplink is required for the next entry.
+pub async fn determine_skiplink(pool: Pool, entry: &Entry) -> Result<Option<Hash>> {
+    let next_seq_num = entry.seq_num.clone().next().unwrap();
+
+    // Unwrap as we know that an skiplink exists as soon as previous entry is given
+    let skiplink_seq_num = next_seq_num.skiplink_seq_num().unwrap();
+
+    // Check if skiplink is required and return hash if so
+    let entry_skiplink_hash = if is_lipmaa_required(next_seq_num.as_i64() as u64) {
+        let skiplink_entry =
+            Entry::at_seq_num(&pool, &entry.author, &entry.log_id, &skiplink_seq_num)
+                .await?
+                .unwrap();
+        Some(skiplink_entry.entry_hash)
+    } else {
+        None
+    };
+
+    Ok(entry_skiplink_hash)
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::rpc::ApiService;
+    use crate::test_helpers::{initialize_db, random_entry_hash, rpc_request, rpc_response};
+
+    const TEST_AUTHOR: &str = "8b52ae153142288402382fd6d9619e018978e015e6bc372b1b0c7bd40c6a240a";
+
+    #[async_std::test]
+    async fn get_entry_arguments() {
+        let pool = initialize_db().await;
+        let io = ApiService::io_handler(pool);
+
+        let request = rpc_request(
+            "panda_getEntryArguments",
+            &format!(
+                r#"{{
+                    "author": "{}",
+                    "schema": "{}"
+                }}"#,
+                TEST_AUTHOR,
+                random_entry_hash(),
+            ),
+        );
+
+        let response = rpc_response(
+            r#"{
+                "entryHashBacklink": null,
+                "entryHashSkiplink": null,
+                "lastSeqNum": null,
+                "logId": 1
+            }"#,
+        );
+
+        assert_eq!(io.handle_request_sync(&request), Some(response));
+    }
+}
diff --git a/aquadoggo/src/rpc/methods/mod.rs b/aquadoggo/src/rpc/methods/mod.rs
new file mode 100644
index 000000000..0b75c799c
--- /dev/null
+++ b/aquadoggo/src/rpc/methods/mod.rs
@@ -0,0 +1,9 @@
+mod entry_args;
+mod publish_entry;
+
+pub mod error {
+    pub use super::publish_entry::PublishEntryError;
+}
+
+pub use entry_args::get_entry_args;
+pub use publish_entry::publish_entry;
diff --git a/aquadoggo/src/rpc/methods/publish_entry.rs b/aquadoggo/src/rpc/methods/publish_entry.rs
new file mode 100644
index 000000000..c5da924da
--- /dev/null
+++ b/aquadoggo/src/rpc/methods/publish_entry.rs
@@ -0,0 +1,449 @@
+use std::convert::TryFrom;
+
+use p2panda_rs::atomic::{Entry as EntryUnsigned, Message};
+
+use crate::db::models::{Entry, Log};
+use crate::db::Pool;
+use crate::errors::Result;
+use crate::rpc::request::PublishEntryRequest;
+use crate::rpc::response::PublishEntryResponse;
+
+#[derive(thiserror::Error, Debug)]
+#[allow(missing_copy_implementations)]
+pub enum PublishEntryError {
+    #[error("Could not find backlink entry in database")]
+    BacklinkMissing,
+
+    #[error("Could not find skiplink entry in database")]
+    SkiplinkMissing,
+
+    #[error("Claimed log_id for schema not the same as in database")]
+    InvalidLogId,
+}
+
+/// Implementation of `panda_publishEntry` RPC method.
+///
+/// Stores an author's Bamboo entry with message payload in database after validating it.
+pub async fn publish_entry(
+    pool: Pool,
+    params: PublishEntryRequest,
+) -> Result<PublishEntryResponse> {
+    // Handle error as this conversion validates message hash
+    let entry = EntryUnsigned::try_from((&params.entry_encoded, Some(&params.message_encoded)))?;
+    let message = Message::from(&params.message_encoded);
+
+    // Retreive author and schema
+    let author = params.entry_encoded.author();
+    let schema = message.schema();
+
+    // Determine log_id for author's schema
+    let schema_log_id = Log::get(&pool, &author, &schema).await?;
+
+    // Check if log_id is the same as the previously claimed one (when given)
+    if schema_log_id.is_some() && schema_log_id.as_ref() != Some(entry.log_id()) {
+        Err(PublishEntryError::InvalidLogId)?;
+    }
+
+    // Get related bamboo backlink and skiplink entries
+    let entry_backlink_bytes = if !entry.seq_num().is_first() {
+        Entry::at_seq_num(
+            &pool,
+            &author,
+            &entry.log_id(),
+            &entry.seq_num_backlink().unwrap(),
+        )
+        .await?
+        .map(|link| {
+            Some(
+                hex::decode(link.entry_bytes)
+                    .expect("Backlink entry with invalid hex-encoding detected in database"),
+            )
+        })
+        .ok_or(PublishEntryError::BacklinkMissing)
+    } else {
+        Ok(None)
+    }?;
+
+    let entry_skiplink_bytes = if !entry.seq_num().is_first() {
+        Entry::at_seq_num(
+            &pool,
+            &author,
+            &entry.log_id(),
+            &entry.seq_num_skiplink().unwrap(),
+        )
+        .await?
+        .map(|link| {
+            Some(
+                hex::decode(link.entry_bytes)
+                    .expect("Skiplink entry with invalid hex-encoding detected in database"),
+            )
+        })
+        .ok_or(PublishEntryError::SkiplinkMissing)
+    } else {
+        Ok(None)
+    }?;
+
+    // Verify bamboo entry integrity
+    bamboo_rs_core::verify(
+        &params.entry_encoded.to_bytes(),
+        Some(&params.message_encoded.to_bytes()),
+        entry_skiplink_bytes.as_deref(),
+        entry_backlink_bytes.as_deref(),
+    )?;
+
+    // Register used log id in database when not set yet
+    if schema_log_id.is_none() {
+        Log::insert(&pool, &author, &schema, entry.log_id()).await?;
+    }
+
+    // Finally insert Entry in database
+    Entry::insert(
+        &pool,
+        &author,
+        &params.entry_encoded,
+        &params.entry_encoded.hash(),
+        &entry.log_id(),
+        &params.message_encoded,
+        &params.message_encoded.hash(),
+        &entry.seq_num(),
+    )
+    .await?;
+
+    // Already return arguments for next entry creation
+    let entry_latest = Entry::latest(&pool, &author, &entry.log_id())
+        .await?
+        .expect("Database does not contain any entries");
+    let entry_hash_skiplink = super::entry_args::determine_skiplink(pool, &entry_latest).await?;
+
+    Ok(PublishEntryResponse {
+        entry_hash_backlink: Some(params.entry_encoded.hash()),
+        entry_hash_skiplink,
+        last_seq_num: Some(entry.seq_num().to_owned()),
+        log_id: entry.log_id().to_owned(),
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use std::convert::TryFrom;
+
+    use jsonrpc_core::{ErrorCode, IoHandler};
+    use p2panda_rs::atomic::{
+        Entry, EntrySigned, Hash, LogId, Message, MessageEncoded, MessageFields, MessageValue,
+        SeqNum,
+    };
+    use p2panda_rs::key_pair::KeyPair;
+
+    use crate::rpc::ApiService;
+    use crate::test_helpers::{initialize_db, rpc_error, rpc_request, rpc_response};
+
+    // Helper method to create encoded entries and messages
+    fn create_test_entry(
+        key_pair: &KeyPair,
+        schema: &Hash,
+        log_id: &LogId,
+        skiplink: Option<&EntrySigned>,
+        backlink: Option<&EntrySigned>,
+        previous_seq_num: Option<&SeqNum>,
+    ) -> (EntrySigned, MessageEncoded) {
+        // Create message with dummy data
+        let mut fields = MessageFields::new();
+        fields
+            .add("test", MessageValue::Text("Hello".to_owned()))
+            .unwrap();
+        let message = Message::new_create(schema.clone(), fields).unwrap();
+
+        // Encode message
+        let message_encoded = MessageEncoded::try_from(&message).unwrap();
+
+        // Create, sign and encode entry
+        let entry = Entry::new(
+            log_id,
+            &message,
+            skiplink.map(|e| e.hash()).as_ref(),
+            backlink.map(|e| e.hash()).as_ref(),
+            previous_seq_num,
+        )
+        .unwrap();
+        let entry_encoded = EntrySigned::try_from((&entry, key_pair)).unwrap();
+
+        (entry_encoded, message_encoded)
+    }
+
+    // Helper method to compare expected API responses with what was returned
+    fn assert_request(
+        io: &IoHandler,
+        entry_encoded: &EntrySigned,
+        message_encoded: &MessageEncoded,
+        entry_skiplink: Option<&EntrySigned>,
+        log_id: &LogId,
+        seq_num: &SeqNum,
+    ) {
+        // Prepare request to API
+        let request = rpc_request(
+            "panda_publishEntry",
+            &format!(
+                r#"{{
+                    "entryEncoded": "{}",
+                    "messageEncoded": "{}"
+                }}"#,
+                entry_encoded.as_str(),
+                message_encoded.as_str(),
+            ),
+        );
+
+        // Prepare expected response result
+        let skiplink_str = match entry_skiplink {
+            Some(entry) => {
+                format!("\"{}\"", entry.hash().as_hex())
+            }
+            None => "null".to_owned(),
+        };
+
+        let response = rpc_response(&format!(
+            r#"{{
+                "entryHashBacklink": "{}",
+                "entryHashSkiplink": {},
+                "lastSeqNum": {},
+                "logId": {}
+            }}"#,
+            entry_encoded.hash().as_hex(),
+            skiplink_str,
+            seq_num.as_i64(),
+            log_id.as_i64(),
+        ));
+
+        // Send request to API and compare response with expected result
+        assert_eq!(io.handle_request_sync(&request), Some(response));
+    }
+
+    #[async_std::test]
+    async fn publish_entry() {
+        // Create key pair for author
+        let key_pair = KeyPair::new();
+
+        // Prepare test database
+        let pool = initialize_db().await;
+        let io = ApiService::io_handler(pool);
+
+        // Define schema and log id for entries
+        let schema = Hash::new_from_bytes(vec![1, 2, 3]).unwrap();
+        let log_id = LogId::new(5);
+
+        // Create a couple of entries in the same log and check for consistency
+        //
+        // [1] --
+        let (entry_1, message_1) = create_test_entry(&key_pair, &schema, &log_id, None, None, None);
+        assert_request(
+            &io,
+            &entry_1,
+            &message_1,
+            None,
+            &log_id,
+            &SeqNum::new(1).unwrap(),
+        );
+
+        // [1] <-- [2]
+        let (entry_2, message_2) = create_test_entry(
+            &key_pair,
+            &schema,
+            &log_id,
+            None,
+            Some(&entry_1),
+            Some(&SeqNum::new(1).unwrap()),
+        );
+        assert_request(
+            &io,
+            &entry_2,
+            &message_2,
+            None,
+            &log_id,
+            &SeqNum::new(2).unwrap(),
+        );
+
+        // [1] <-- [2] <-- [3]
+        let (entry_3, message_3) = create_test_entry(
+            &key_pair,
+            &schema,
+            &log_id,
+            None,
+            Some(&entry_2),
+            Some(&SeqNum::new(2).unwrap()),
+        );
+        assert_request(
+            &io,
+            &entry_3,
+            &message_3,
+            Some(&entry_1),
+            &log_id,
+            &SeqNum::new(3).unwrap(),
+        );
+
+        //  /------------------ [4]
+        // [1] <-- [2] <-- [3]
+        let (entry_4, message_4) = create_test_entry(
+            &key_pair,
+            &schema,
+            &log_id,
+            Some(&entry_1),
+            Some(&entry_3),
+            Some(&SeqNum::new(3).unwrap()),
+        );
+        assert_request(
+            &io,
+            &entry_4,
+            &message_4,
+            None,
+            &log_id,
+            &SeqNum::new(4).unwrap(),
+        );
+
+        //  /------------------ [4]
+        // [1] <-- [2] <-- [3]   \-- [5] --
+        let (entry_5, message_5) = create_test_entry(
+            &key_pair,
+            &schema,
+            &log_id,
+            None,
+            Some(&entry_4),
+            Some(&SeqNum::new(4).unwrap()),
+        );
+        assert_request(
+            &io,
+            &entry_5,
+            &message_5,
+            None,
+            &log_id,
+            &SeqNum::new(5).unwrap(),
+        );
+    }
+
+    #[async_std::test]
+    async fn validate() {
+        // Create key pair for author
+        let key_pair = KeyPair::new();
+
+        // Prepare test database
+        let pool = initialize_db().await;
+        let io = ApiService::io_handler(pool);
+
+        // Define schema and log id for entries
+        let schema = Hash::new_from_bytes(vec![1, 2, 3]).unwrap();
+        let log_id = LogId::new(5);
+
+        // Create two valid entries for testing
+        let (entry_1, message_1) = create_test_entry(&key_pair, &schema, &log_id, None, None, None);
+        assert_request(
+            &io,
+            &entry_1,
+            &message_1,
+            None,
+            &log_id,
+            &SeqNum::new(1).unwrap(),
+        );
+
+        let (entry_2, message_2) = create_test_entry(
+            &key_pair,
+            &schema,
+            &log_id,
+            None,
+            Some(&entry_1),
+            Some(&SeqNum::new(1).unwrap()),
+        );
+        assert_request(
+            &io,
+            &entry_2,
+            &message_2,
+            None,
+            &log_id,
+            &SeqNum::new(2).unwrap(),
+        );
+
+        // Send invalid log id for this schema
+        let (entry_wrong_log_id, _) = create_test_entry(
+            &key_pair,
+            &schema,
+            &LogId::new(1),
+            None,
+            Some(&entry_1),
+            Some(&SeqNum::new(1).unwrap()),
+        );
+
+        let request = rpc_request(
+            "panda_publishEntry",
+            &format!(
+                r#"{{
+                    "entryEncoded": "{}",
+                    "messageEncoded": "{}"
+                }}"#,
+                entry_wrong_log_id.as_str(),
+                message_2.as_str(),
+            ),
+        );
+
+        let response = rpc_error(
+            ErrorCode::InvalidParams,
+            "Invalid params: Claimed log_id for schema not the same as in database.",
+        );
+
+        assert_eq!(io.handle_request_sync(&request), Some(response));
+
+        // Send invalid backlink entry / hash
+        let (entry_wrong_hash, _) = create_test_entry(
+            &key_pair,
+            &schema,
+            &log_id,
+            Some(&entry_2),
+            Some(&entry_1),
+            Some(&SeqNum::new(2).unwrap()),
+        );
+
+        let request = rpc_request(
+            "panda_publishEntry",
+            &format!(
+                r#"{{
+                    "entryEncoded": "{}",
+                    "messageEncoded": "{}"
+                }}"#,
+                entry_wrong_hash.as_str(),
+                message_2.as_str(),
+            ),
+        );
+
+        let response = rpc_error(
+            ErrorCode::InvalidParams,
+            "Invalid params: The backlink hash encoded in the entry does not match the lipmaa entry provided.",
+        );
+
+        assert_eq!(io.handle_request_sync(&request), Some(response));
+
+        // Send invalid seq num
+        let (entry_wrong_seq_num, _) = create_test_entry(
+            &key_pair,
+            &schema,
+            &log_id,
+            None,
+            Some(&entry_2),
+            Some(&SeqNum::new(5).unwrap()),
+        );
+
+        let request = rpc_request(
+            "panda_publishEntry",
+            &format!(
+                r#"{{
+                    "entryEncoded": "{}",
+                    "messageEncoded": "{}"
+                }}"#,
+                entry_wrong_seq_num.as_str(),
+                message_2.as_str(),
+            ),
+        );
+
+        let response = rpc_error(
+            ErrorCode::InvalidParams,
+            "Invalid params: Could not find backlink entry in database.",
+        );
+
+        assert_eq!(io.handle_request_sync(&request), Some(response));
+    }
+}
diff --git a/aquadoggo/src/rpc/mod.rs b/aquadoggo/src/rpc/mod.rs
index 5cf1e5ddd..54ab61bcf 100644
--- a/aquadoggo/src/rpc/mod.rs
+++ b/aquadoggo/src/rpc/mod.rs
@@ -1,5 +1,9 @@
 mod api;
+mod methods;
+mod request;
+mod response;
 mod server;
 
 pub use api::{Api, ApiService};
+pub use methods::error::PublishEntryError;
 pub use server::RpcServer;
diff --git a/aquadoggo/src/rpc/request.rs b/aquadoggo/src/rpc/request.rs
new file mode 100644
index 000000000..11b28590d
--- /dev/null
+++ b/aquadoggo/src/rpc/request.rs
@@ -0,0 +1,18 @@
+use serde::Deserialize;
+
+use p2panda_rs::atomic::{Author, EntrySigned, Hash, MessageEncoded};
+
+/// Request body of `panda_getEntryArguments`.
+#[derive(Deserialize, Debug)]
+pub struct EntryArgsRequest {
+    pub author: Author,
+    pub schema: Hash,
+}
+
+/// Request body of `panda_publishEntry`.
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct PublishEntryRequest {
+    pub entry_encoded: EntrySigned,
+    pub message_encoded: MessageEncoded,
+}
diff --git a/aquadoggo/src/rpc/response.rs b/aquadoggo/src/rpc/response.rs
new file mode 100644
index 000000000..415656909
--- /dev/null
+++ b/aquadoggo/src/rpc/response.rs
@@ -0,0 +1,23 @@
+use serde::Serialize;
+
+use p2panda_rs::atomic::{Hash, LogId, SeqNum};
+
+/// Response body of `panda_getEntryArguments`.
+#[derive(Serialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct EntryArgsResponse {
+    pub entry_hash_backlink: Option<Hash>,
+    pub entry_hash_skiplink: Option<Hash>,
+    pub last_seq_num: Option<SeqNum>,
+    pub log_id: LogId,
+}
+
+/// Response body of `panda_publishEntry`.
+#[derive(Serialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct PublishEntryResponse {
+    pub entry_hash_backlink: Option<Hash>,
+    pub entry_hash_skiplink: Option<Hash>,
+    pub last_seq_num: Option<SeqNum>,
+    pub log_id: LogId,
+}
diff --git a/aquadoggo/src/rpc/server.rs b/aquadoggo/src/rpc/server.rs
index 709114661..bb4e083cb 100644
--- a/aquadoggo/src/rpc/server.rs
+++ b/aquadoggo/src/rpc/server.rs
@@ -1,8 +1,9 @@
-use jsonrpc_core::IoHandler;
-use log::error;
 use std::io::{ErrorKind, Result};
 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
 
+use jsonrpc_core::IoHandler;
+use log::error;
+
 use crate::config::Configuration;
 use crate::task::TaskManager;
 
diff --git a/aquadoggo/src/task.rs b/aquadoggo/src/task.rs
index a8ff8e779..2d038350e 100644
--- a/aquadoggo/src/task.rs
+++ b/aquadoggo/src/task.rs
@@ -1,7 +1,8 @@
+use std::error::Error;
+
 use async_std::{future::Future, task};
 use futures::future;
 use log::debug;
-use std::error::Error;
 
 /// Generic Result type for all async tasks used by TaskManager.
 pub type FutureResult<T> = Result<T, Box<dyn Error + Send + Sync>>;
@@ -65,10 +66,11 @@ impl TaskManager {
 mod tests {
     use super::{FutureResult, TaskManager};
 
-    use async_std::task;
     use std::sync::{Arc, Mutex};
     use std::time::Duration;
 
+    use async_std::task;
+
     #[derive(Clone, Debug)]
     struct DropTester(Arc<Mutex<usize>>);
     struct DropTesterRef(DropTester);


Test patch:
diff --git a/aquadoggo/src/test_helpers.rs b/aquadoggo/src/test_helpers.rs
index 0affbca8e..27d095822 100644
--- a/aquadoggo/src/test_helpers.rs
+++ b/aquadoggo/src/test_helpers.rs
@@ -1,3 +1,4 @@
+use jsonrpc_core::ErrorCode;
 use p2panda_rs::atomic::Hash;
 use rand::Rng;
 use sqlx::any::Any;
@@ -36,3 +37,50 @@ pub fn random_entry_hash() -> String {
         .as_hex()
         .to_owned()
 }
+
+// Helper method to generate valid JSON RPC request string
+pub fn rpc_request(method: &str, params: &str) -> String {
+    format!(
+        r#"{{
+            "jsonrpc": "2.0",
+            "method": "{}",
+            "params": {},
+            "id": 1
+        }}"#,
+        method, params
+    )
+    .replace(" ", "")
+    .replace("\n", "")
+}
+
+// Helper method to generate valid JSON RPC response string
+pub fn rpc_response(result: &str) -> String {
+    format!(
+        r#"{{
+            "jsonrpc": "2.0",
+            "result": {},
+            "id": 1
+        }}"#,
+        result
+    )
+    .replace(" ", "")
+    .replace("\n", "")
+}
+
+// Helper method to generate valid JSON RPC error response string
+pub fn rpc_error(code: ErrorCode, message: &str) -> String {
+    format!(
+        r#"{{
+            "jsonrpc": "2.0",
+            "error": {{
+                "code": {},
+                "message": "<message>"
+            }},
+            "id": 1
+        }}"#,
+        code.code(),
+    )
+    .replace(" ", "")
+    .replace("\n", "")
+    .replace("<message>", message)
+}