Repository: p2panda/aquadoggo (language: rust)

Issue:
Replace schema logs with instance logs
## Language

This issue uses new language:

- an *operation* (former _message_) is a data change encoded in a bamboo entry. It is identified by the hash of its encoding.
- a *document* is the entirety of operations that are directed at a shared root `CREATE` operation. A document is identified by the hash of the root `CREATE` operation.
- an *instance* is the result of applying any series of operations from a *document*. An instance is identified by the hash of the last operation applied.

## Background

Bamboo let's us assign many logs to any public key. Up until now we have used this to organise p2panda entries by the *schema* they are using. The reasoning for this was that nodes in the p2panda network would be selecting data to replicate based on a selection of schemas they are configured for. We wanted to ease the replication process by storing data in the way it's needed later during replication.

During further specification of p2panda we have discovered the impact this decision has on our ability to delete entry payloads between skip links. Even when a document is tombstoned we can not delete entry payloads between skip links in a log that is organised by schema as long as at least one of those entries carries information required for a document that has not been tombstoned.

Organising operations by the document they are part of allows us to delete all entry payloads in those logs once the document author has tombstoned it.

## Requirements

This issue asks to replace organising operations into logs by their association to a _schema_ to organising operations into logs based on their association to a  _document_.

- replace schema logs with document logs
- introduce a new way to retrieve all entries for a given schema



Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5d976582..f943b433f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Changed
 
+- **Breaking change**: Replace schema logs with document logs, changing the behavior the `nextEntryArgs` and `publishEntry` RPC methods, invalidating and deleting all previously published entries  [#44](https://github.com/p2panda/aquadoggo/pull/44)
 - Nicer looking `README.md` for crate [#42](https://github.com/p2panda/aquadoggo/42)
 
 ## [0.1.0]
diff --git a/Cargo.lock b/Cargo.lock
index 6b3599f21..4bcddbec6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1838,9 +1838,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
 
 [[package]]
 name = "p2panda-rs"
-version = "0.2.0"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0810c9735a16edd465e6053e60f9da100aeb93b45babc28b125afb49291f1a24"
+checksum = "6fc5fa439d498fc31abb6d1a51bf8b512e52cd33e49a2fd5345a1b5285b37ab7"
 dependencies = [
  "arrayvec",
  "bamboo-rs-core",
diff --git a/aquadoggo/migrations/20201229215646_create-entries.sql b/aquadoggo/migrations/20201229215646_create-entries.sql
index ee6109632..958f2403e 100644
--- a/aquadoggo/migrations/20201229215646_create-entries.sql
+++ b/aquadoggo/migrations/20201229215646_create-entries.sql
@@ -1,3 +1,5 @@
+-- SPDX-License-Identifier: AGPL-3.0-or-later
+
 CREATE TABLE IF NOT EXISTS entries (
     author            VARCHAR(64)       NOT NULL,
     entry_bytes       TEXT              NOT NULL,
diff --git a/aquadoggo/migrations/20201230002752_create-logs.sql b/aquadoggo/migrations/20201230002752_create-logs.sql
index 93bacdeac..faf39b330 100644
--- a/aquadoggo/migrations/20201230002752_create-logs.sql
+++ b/aquadoggo/migrations/20201230002752_create-logs.sql
@@ -1,3 +1,5 @@
+-- SPDX-License-Identifier: AGPL-3.0-or-later
+
 CREATE TABLE IF NOT EXISTS logs (
     author            VARCHAR(64)       NOT NULL,
     log_id            BIGINT            NOT NULL,
diff --git a/aquadoggo/migrations/20211127170238_instance-logs.sql b/aquadoggo/migrations/20211127170238_instance-logs.sql
new file mode 100644
index 000000000..914e2f926
--- /dev/null
+++ b/aquadoggo/migrations/20211127170238_instance-logs.sql
@@ -0,0 +1,19 @@
+-- SPDX-License-Identifier: AGPL-3.0-or-later
+
+-- The change from schema logs to document logs invalidates all entries so
+-- all logs and entries are deleted. The logs table is also dropped because its
+-- primary key changes.
+
+DROP TABLE logs;
+DELETE FROM entries;
+
+CREATE TABLE logs (
+    author            VARCHAR(64)       NOT NULL,
+    log_id            BIGINT            NOT NULL,
+    document          VARCHAR(132)      NOT NULL,
+    schema            VARCHAR(132)      NOT NULL,
+    PRIMARY KEY (author, document, log_id)
+);
+
+-- Create an index for querying by schema
+CREATE INDEX idx_logs_schema ON logs (author, log_id, schema);
diff --git a/aquadoggo/src/db/models/entry.rs b/aquadoggo/src/db/models/entry.rs
index eff8af12a..1a6481959 100644
--- a/aquadoggo/src/db/models/entry.rs
+++ b/aquadoggo/src/db/models/entry.rs
@@ -1,9 +1,9 @@
 // SPDX-License-Identifier: AGPL-3.0-or-later
 
 use p2panda_rs::entry::{EntrySigned, LogId, SeqNum};
-use p2panda_rs::message::MessageEncoded;
 use p2panda_rs::hash::Hash;
 use p2panda_rs::identity::Author;
+use p2panda_rs::message::MessageEncoded;
 
 use serde::Serialize;
 use sqlx::{query, query_as, FromRow};
@@ -185,8 +185,8 @@ impl Entry {
 #[cfg(test)]
 mod tests {
     use p2panda_rs::entry::LogId;
-    use p2panda_rs::identity::Author;
     use p2panda_rs::hash::Hash;
+    use p2panda_rs::identity::Author;
 
     use super::Entry;
 
diff --git a/aquadoggo/src/db/models/log.rs b/aquadoggo/src/db/models/log.rs
index 50826d787..17df5a506 100644
--- a/aquadoggo/src/db/models/log.rs
+++ b/aquadoggo/src/db/models/log.rs
@@ -1,15 +1,15 @@
 // SPDX-License-Identifier: AGPL-3.0-or-later
 
 use p2panda_rs::entry::LogId;
-use p2panda_rs::identity::Author;
 use p2panda_rs::hash::Hash;
+use p2panda_rs::identity::Author;
 
 use sqlx::{query, query_as, FromRow};
 
 use crate::db::Pool;
 use crate::errors::Result;
 
-/// Keeps track of which log_id has been used for which schema per author.
+/// Tracks the assigment of an author's logs to documents and records their schema.
 ///
 /// This serves as an indexing layer on top of the lower-level bamboo entries. The node updates
 /// this data according to what it sees in the newly incoming entries.
@@ -18,21 +18,24 @@ pub struct Log {
     /// Public key of the author.
     author: Author,
 
-    /// Log id used for the author's schema.
+    /// Log id used for this document.
     log_id: LogId,
 
+    /// Hash that identifies the document this log is for.
+    document: Hash,
+
     /// Schema hash used by author.
     schema: Hash,
 }
 
 impl Log {
-    /// Register any new log_id for an author's schema.
+    /// Register any new log_id for a document.
     ///
     /// The database will reject duplicate entries.
-    #[allow(dead_code)]
     pub async fn insert(
         pool: &Pool,
         author: &Author,
+        document: &Hash,
         schema: &Hash,
         log_id: &LogId,
     ) -> Result<bool> {
@@ -40,13 +43,14 @@ impl Log {
         let rows_affected = query(
             "
             INSERT INTO
-                logs (author, log_id, schema)
+                logs (author, log_id, document, schema)
             VALUES
-                ($1, $2, $3)
+                ($1, $2, $3, $4)
             ",
         )
         .bind(author)
         .bind(log_id)
+        .bind(document)
         .bind(schema)
         .execute(pool)
         .await?
@@ -96,11 +100,11 @@ impl Log {
         Ok(next_log_id)
     }
 
-    /// Returns the registered log_id of an author's schema.
+    /// Returns the registered log_id for a document.
     ///
-    /// 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.
-    pub async fn get(pool: &Pool, author: &Author, schema: &Hash) -> Result<Option<LogId>> {
+    /// Messages are separated in different logs per document and author. This method checks if a log
+    /// has already been registered for a document and returns its id.
+    pub async fn get(pool: &Pool, author: &Author, document: &Hash) -> Result<Option<LogId>> {
         // @TODO: Look up if system schema was used and return regarding log id
         let result = query_as::<_, LogId>(
             "
@@ -110,44 +114,80 @@ impl Log {
                 logs
             WHERE
                 author = $1
-                AND schema = $2
+                AND document = $2
             ",
         )
         .bind(author)
-        .bind(schema)
+        .bind(document)
         .fetch_optional(pool)
         .await?;
 
         Ok(result)
     }
 
-    /// Returns registered or possible log_id of an author's schema.
+    /// Returns registered or possible log id for a document.
     ///
-    /// If no log has been found for a USER schema it automatically returns the next unused log_id.
+    /// If no log has been previously registered for this document 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?;
+    pub async fn find_document_log_id(
+        pool: &Pool,
+        author: &Author,
+        document: &Hash,
+    ) -> Result<LogId> {
+        // Determine log_id for this document
+        let document_log_id = Log::get(pool, author, document).await?;
 
         // Use result or find next possible log_id automatically
-        let log_id = match schema_log_id {
+        let log_id = match document_log_id {
             Some(value) => value,
             None => Log::next_user_schema_log_id(pool, author).await?,
         };
 
         Ok(log_id)
     }
+
+    /// Returns the registered log id for an instance.
+    ///
+    /// Every instance is part of a document and, through that, associated with a specific log id
+    /// of its author. This method returns that log id by looking up the log that the instance's
+    /// last operation was stored in.
+    pub async fn get_log_id_by_instance(
+        pool: &Pool,
+        instance: &Hash,
+    ) -> Result<Option<LogId>> {
+        let result = query_as::<_, LogId>(
+            "
+            SELECT
+                entries.log_id
+            FROM
+                entries
+            WHERE
+                entries.entry_hash = $1
+            ",
+        )
+        .bind(instance)
+        .fetch_optional(pool)
+        .await?;
+
+        Ok(result)
+    }
 }
 
 #[cfg(test)]
 mod tests {
-    use p2panda_rs::entry::LogId;
-    use p2panda_rs::identity::Author;
+    use p2panda_rs::entry::{sign_and_encode, Entry, LogId, SeqNum};
     use p2panda_rs::hash::Hash;
-    
+    use p2panda_rs::identity::{Author, KeyPair};
+    use p2panda_rs::message::{Message, MessageEncoded, MessageFields, MessageValue};
+    use std::convert::TryFrom;
+
     use super::Log;
 
-    use crate::test_helpers::{initialize_db, random_entry_hash};
+    use crate::{
+        db::models::Entry as dbEntry,
+        test_helpers::{initialize_db, random_entry_hash},
+    };
 
     const TEST_AUTHOR: &str = "58223678ab378f1b07d1d8c789e6da01d16a06b1a4d17cc10119a0109181156c";
 
@@ -156,9 +196,9 @@ mod tests {
         let pool = initialize_db().await;
 
         let author = Author::new(TEST_AUTHOR).unwrap();
-        let schema = Hash::new(&random_entry_hash()).unwrap();
+        let document = Hash::new(&random_entry_hash()).unwrap();
 
-        let log_id = Log::find_schema_log_id(&pool, &author, &schema)
+        let log_id = Log::find_document_log_id(&pool, &author, &document)
             .await
             .unwrap();
 
@@ -170,15 +210,71 @@ mod tests {
         let pool = initialize_db().await;
 
         let author = Author::new(TEST_AUTHOR).unwrap();
+        let document = Hash::new(&random_entry_hash()).unwrap();
         let schema = Hash::new(&random_entry_hash()).unwrap();
 
-        assert!(Log::insert(&pool, &author, &schema, &LogId::new(1))
-            .await
-            .is_ok());
+        assert!(
+            Log::insert(&pool, &author, &document, &schema, &LogId::new(1))
+                .await
+                .is_ok()
+        );
+
+        assert!(
+            Log::insert(&pool, &author, &document, &schema, &LogId::new(1))
+                .await
+                .is_err()
+        );
+    }
 
-        assert!(Log::insert(&pool, &author, &schema, &LogId::new(1))
-            .await
-            .is_err());
+    #[async_std::test]
+    async fn instance_log_id() {
+        let pool = initialize_db().await;
+
+        // Create an instance
+        // TODO: use p2panda-rs test utils once available
+        let key_pair = KeyPair::new();
+        let author = Author::try_from(key_pair.public_key().clone()).unwrap();
+        let log_id = LogId::new(1);
+        let schema = Hash::new_from_bytes(vec![1, 2, 3]).unwrap();
+        let seq_num = SeqNum::new(1).unwrap();
+        let mut fields = MessageFields::new();
+        fields
+            .add("test", MessageValue::Text("Hello".to_owned()))
+            .unwrap();
+        let message = Message::new_create(schema.clone(), fields).unwrap();
+        let message_encoded = MessageEncoded::try_from(&message).unwrap();
+        let entry = Entry::new(&log_id, Some(&message), None, None, &seq_num).unwrap();
+        let entry_encoded = sign_and_encode(&entry, &key_pair).unwrap();
+
+        // Expect no log id when instance not in database
+        assert_eq!(
+            Log::get_log_id_by_instance(&pool, &entry_encoded.hash())
+                .await
+                .unwrap(),
+            None
+        );
+
+        // Store instance in db
+        assert!(dbEntry::insert(
+            &pool,
+            &author,
+            &entry_encoded,
+            &entry_encoded.hash(),
+            &log_id,
+            &message_encoded,
+            &message_encoded.hash(),
+            &seq_num
+        )
+        .await
+        .is_ok());
+
+        // Expect to find a log id for the instance
+        assert_eq!(
+            Log::get_log_id_by_instance(&pool, &entry_encoded.hash())
+                .await
+                .unwrap(),
+            Some(log_id)
+        );
     }
 
     #[async_std::test]
@@ -188,31 +284,34 @@ mod tests {
         // Mock author
         let author = Author::new(TEST_AUTHOR).unwrap();
 
-        // Mock four different scheme hashes
-        let schema_first = Hash::new(&random_entry_hash()).unwrap();
-        let schema_second = Hash::new(&random_entry_hash()).unwrap();
-        let schema_third = Hash::new(&random_entry_hash()).unwrap();
-        let schema_system = Hash::new(&random_entry_hash()).unwrap();
+        // Mock schema
+        let schema = Hash::new(&random_entry_hash()).unwrap();
+
+        // Mock four different document hashes
+        let document_first = Hash::new(&random_entry_hash()).unwrap();
+        let document_second = Hash::new(&random_entry_hash()).unwrap();
+        let document_third = Hash::new(&random_entry_hash()).unwrap();
+        let document_system = Hash::new(&random_entry_hash()).unwrap();
 
         // Register two log ids at the beginning
-        Log::insert(&pool, &author, &schema_system, &LogId::new(9))
+        Log::insert(&pool, &author, &document_system, &schema, &LogId::new(9))
             .await
             .unwrap();
-        Log::insert(&pool, &author, &schema_first, &LogId::new(3))
+        Log::insert(&pool, &author, &document_first, &schema, &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::insert(&pool, &author, &schema_second, &log_id)
+        Log::insert(&pool, &author, &document_second, &schema, &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::insert(&pool, &author, &schema_third, &log_id)
+        Log::insert(&pool, &author, &document_third, &schema, &log_id)
             .await
             .unwrap();
 
diff --git a/aquadoggo/src/rpc/methods/entry_args.rs b/aquadoggo/src/rpc/methods/entry_args.rs
index 3052b229e..a628e4911 100644
--- a/aquadoggo/src/rpc/methods/entry_args.rs
+++ b/aquadoggo/src/rpc/methods/entry_args.rs
@@ -15,25 +15,25 @@ use crate::rpc::RpcApiState;
 
 /// 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.
+/// Returns required data (backlink and skiplink entry hashes, last sequence number and the
+/// document's log_id) to encode a new bamboo entry.
 pub async fn get_entry_args(
     data: Data<RpcApiState>,
     Params(params): Params<EntryArgsRequest>,
 ) -> Result<EntryArgsResponse> {
     // Validate request parameters
     params.author.validate()?;
-    params.schema.validate()?;
+    params.document.validate()?;
 
     // Get database connection pool
     let pool = data.pool.clone();
 
-    // Determine log_id for author's schema
-    let log_id = Log::find_schema_log_id(&pool, &params.author, &params.schema).await?;
+    // Determine log_id for this document
+    let log_id = Log::find_document_log_id(&pool, &params.author, &params.document).await?;
 
     // Find latest entry in this log
     let entry_latest = Entry::latest(&pool, &params.author, &log_id).await?;
-    
+
     match entry_latest {
         Some(mut entry_backlink) => {
             // Determine skiplink ("lipmaa"-link) entry in this log
@@ -98,7 +98,7 @@ mod tests {
             &format!(
                 r#"{{
                     "author": "1234",
-                    "schema": "{}"
+                    "document": "{}"
                 }}"#,
                 random_entry_hash()
             ),
@@ -123,7 +123,7 @@ mod tests {
             &format!(
                 r#"{{
                     "author": "{}",
-                    "schema": "{}"
+                    "document": "{}"
                 }}"#,
                 TEST_AUTHOR,
                 random_entry_hash(),
diff --git a/aquadoggo/src/rpc/methods/publish_entry.rs b/aquadoggo/src/rpc/methods/publish_entry.rs
index 2b4ad6d13..001e09b8f 100644
--- a/aquadoggo/src/rpc/methods/publish_entry.rs
+++ b/aquadoggo/src/rpc/methods/publish_entry.rs
@@ -20,8 +20,11 @@ pub enum PublishEntryError {
     #[error("Could not find skiplink entry in database")]
     SkiplinkMissing,
 
-    #[error("Claimed log_id for schema not the same as in database")]
-    InvalidLogId,
+    #[error("Requested log id {0} does not match expected log id {1}")]
+    InvalidLogId(i64, i64),
+
+    #[error("The instance this message is referring to is unknown")]
+    InstanceMissing,
 }
 
 /// Implementation of `panda_publishEntry` RPC method.
@@ -42,16 +45,32 @@ pub async fn publish_entry(
     let entry = decode_entry(&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)?;
+    // Determine expected log id for new entry: a `CREATE` entry is always stored in the next free
+    // user log. An `UPDATE` or `DELETE` message is always stored in the same log that its original
+    // `CREATE` message was stored in.
+    let document_log_id = match message.is_create() {
+        true => {
+            // A document is identified by the hash of its `CREATE` message
+            let document_hash = &params.entry_encoded.hash();
+            Log::find_document_log_id(&pool, &author, document_hash).await?
+        }
+        false => {
+            // An instance is identified by the hash of its previous operation
+            let instance_hash = message.id().unwrap();
+            Log::get_log_id_by_instance(&pool, instance_hash)
+                .await?
+                .ok_or(PublishEntryError::InstanceMissing)?
+        }
+    };
+
+    // Check if provided log id matches expected log id
+    if &document_log_id != entry.log_id() {
+        Err(PublishEntryError::InvalidLogId(
+            entry.log_id().as_i64(),
+            document_log_id.as_i64(),
+        ))?;
     }
 
     // Get related bamboo backlink and skiplink entries
@@ -101,9 +120,16 @@ pub async fn publish_entry(
         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?;
+    // Register log in database when a new document is created
+    if message.is_create() {
+        Log::insert(
+            &pool,
+            &author,
+            &params.entry_encoded.hash(),
+            &message.schema(),
+            entry.log_id(),
+        )
+        .await?;
     }
 
     // Finally insert Entry in database
@@ -126,7 +152,7 @@ pub async fn publish_entry(
     let entry_hash_skiplink = super::entry_args::determine_skiplink(pool, &entry_latest).await?;
 
     let next_seq_num = entry_latest.seq_num.next().unwrap();
-    
+
     Ok(PublishEntryResponse {
         entry_hash_backlink: Some(params.entry_encoded.hash()),
         entry_hash_skiplink,
@@ -138,7 +164,7 @@ pub async fn publish_entry(
 mod tests {
     use std::convert::TryFrom;
 
-    use p2panda_rs::entry::{Entry, EntrySigned, LogId, SeqNum, sign_and_encode};
+    use p2panda_rs::entry::{sign_and_encode, Entry, EntrySigned, LogId, SeqNum};
     use p2panda_rs::hash::Hash;
     use p2panda_rs::identity::KeyPair;
     use p2panda_rs::message::{Message, MessageEncoded, MessageFields, MessageValue};
@@ -147,11 +173,12 @@ mod tests {
     use crate::rpc::server::{build_rpc_server, RpcServer};
     use crate::test_helpers::{handle_http, initialize_db, rpc_error, rpc_request, rpc_response};
 
-    // Helper method to create encoded entries and messages
+    /// Create encoded entries and messages for testing.
     fn create_test_entry(
         key_pair: &KeyPair,
         schema: &Hash,
         log_id: &LogId,
+        instance: Option<&Hash>,
         skiplink: Option<&EntrySigned>,
         backlink: Option<&EntrySigned>,
         seq_num: &SeqNum,
@@ -161,7 +188,12 @@ mod tests {
         fields
             .add("test", MessageValue::Text("Hello".to_owned()))
             .unwrap();
-        let message = Message::new_create(schema.clone(), fields).unwrap();
+        let message = match instance {
+            Some(instance_id) => {
+                Message::new_update(schema.clone(), instance_id.clone(), fields).unwrap()
+            }
+            None => Message::new_create(schema.clone(), fields).unwrap(),
+        };
 
         // Encode message
         let message_encoded = MessageEncoded::try_from(&message).unwrap();
@@ -180,14 +212,15 @@ mod tests {
         (entry_encoded, message_encoded)
     }
 
-    // Helper method to compare expected API responses with what was returned
+    /// Compare API response from publishing an encoded entry and message to expected skiplink,
+    /// log id and sequence number.
     async fn assert_request(
         app: &RpcServer,
         entry_encoded: &EntrySigned,
         message_encoded: &MessageEncoded,
-        entry_skiplink: Option<&EntrySigned>,
-        log_id: &LogId,
-        seq_num: &SeqNum,
+        expect_skiplink: Option<&EntrySigned>,
+        expect_log_id: &LogId,
+        expect_seq_num: &SeqNum,
     ) {
         // Prepare request to API
         let request = rpc_request(
@@ -203,7 +236,7 @@ mod tests {
         );
 
         // Prepare expected response result
-        let skiplink_str = match entry_skiplink {
+        let skiplink_str = match expect_skiplink {
             Some(entry) => {
                 format!("\"{}\"", entry.hash().as_str())
             }
@@ -219,8 +252,8 @@ mod tests {
             }}"#,
             entry_encoded.hash().as_str(),
             skiplink_str,
-            log_id.as_i64(),
-            seq_num.as_i64(),
+            expect_log_id.as_i64(),
+            expect_seq_num.as_i64(),
         ));
 
         assert_eq!(handle_http(&app, request).await, response);
@@ -240,19 +273,22 @@ mod tests {
 
         // Define schema and log id for entries
         let schema = Hash::new_from_bytes(vec![1, 2, 3]).unwrap();
-        let log_id = LogId::new(5);
-        let seq_num = SeqNum::new(1).unwrap();
+        let log_id_1 = LogId::new(1);
+        let seq_num_1 = SeqNum::new(1).unwrap();
 
-        // Create a couple of entries in the same log and check for consistency
+        // Create a couple of entries in the same log and check for consistency. The little diagrams
+        // show back links and skip links analogous to this diagram from the bamboo spec:
+        // https://github.com/AljoschaMeyer/bamboo/blob/61415747af34bfcb8f40a47ae3b02136083d3276/README.md#links-and-entry-verification
         //
         // [1] --
-        let (entry_1, message_1) = create_test_entry(&key_pair, &schema, &log_id, None, None, &seq_num);
+        let (entry_1, message_1) =
+            create_test_entry(&key_pair, &schema, &log_id_1, None, None, None, &seq_num_1);
         assert_request(
             &app,
             &entry_1,
             &message_1,
             None,
-            &log_id,
+            &log_id_1,
             &SeqNum::new(2).unwrap(),
         )
         .await;
@@ -261,7 +297,8 @@ mod tests {
         let (entry_2, message_2) = create_test_entry(
             &key_pair,
             &schema,
-            &log_id,
+            &log_id_1,
+            Some(&entry_1.hash()),
             None,
             Some(&entry_1),
             &SeqNum::new(2).unwrap(),
@@ -271,7 +308,7 @@ mod tests {
             &entry_2,
             &message_2,
             None,
-            &log_id,
+            &log_id_1,
             &SeqNum::new(3).unwrap(),
         )
         .await;
@@ -280,7 +317,8 @@ mod tests {
         let (entry_3, message_3) = create_test_entry(
             &key_pair,
             &schema,
-            &log_id,
+            &log_id_1,
+            Some(&entry_2.hash()),
             None,
             Some(&entry_2),
             &SeqNum::new(3).unwrap(),
@@ -290,7 +328,7 @@ mod tests {
             &entry_3,
             &message_3,
             Some(&entry_1),
-            &log_id,
+            &log_id_1,
             &SeqNum::new(4).unwrap(),
         )
         .await;
@@ -300,7 +338,8 @@ mod tests {
         let (entry_4, message_4) = create_test_entry(
             &key_pair,
             &schema,
-            &log_id,
+            &log_id_1,
+            Some(&entry_3.hash()),
             Some(&entry_1),
             Some(&entry_3),
             &SeqNum::new(4).unwrap(),
@@ -310,7 +349,7 @@ mod tests {
             &entry_4,
             &message_4,
             None,
-            &log_id,
+            &log_id_1,
             &SeqNum::new(5).unwrap(),
         )
         .await;
@@ -320,7 +359,8 @@ mod tests {
         let (entry_5, message_5) = create_test_entry(
             &key_pair,
             &schema,
-            &log_id,
+            &log_id_1,
+            Some(&entry_4.hash()),
             None,
             Some(&entry_4),
             &SeqNum::new(5).unwrap(),
@@ -330,7 +370,7 @@ mod tests {
             &entry_5,
             &message_5,
             None,
-            &log_id,
+            &log_id_1,
             &SeqNum::new(6).unwrap(),
         )
         .await;
@@ -350,11 +390,12 @@ mod tests {
 
         // Define schema and log id for entries
         let schema = Hash::new_from_bytes(vec![1, 2, 3]).unwrap();
-        let log_id = LogId::new(5);
+        let log_id = LogId::new(1);
         let seq_num = SeqNum::new(1).unwrap();
 
         // Create two valid entries for testing
-        let (entry_1, message_1) = create_test_entry(&key_pair, &schema, &log_id, None, None, &seq_num);
+        let (entry_1, message_1) =
+            create_test_entry(&key_pair, &schema, &log_id, None, None, None, &seq_num);
         assert_request(
             &app,
             &entry_1,
@@ -369,6 +410,7 @@ mod tests {
             &key_pair,
             &schema,
             &log_id,
+            Some(&entry_1.hash()),
             None,
             Some(&entry_1),
             &SeqNum::new(2).unwrap(),
@@ -383,14 +425,44 @@ mod tests {
         )
         .await;
 
-        // Send invalid log id for this schema
-        let (entry_wrong_log_id, _) = create_test_entry(
+        // Send invalid log id for a new document: The entries entry_1 and entry_2 are assigned to
+        // log 1, which makes log 3 the required log for the next new document.
+        let (entry_wrong_log_id, message_wrong_log_id) = create_test_entry(
             &key_pair,
             &schema,
-            &LogId::new(1),
+            &LogId::new(5),
             None,
-            Some(&entry_1),
-            &SeqNum::new(2).unwrap(),
+            None,
+            None,
+            &SeqNum::new(1).unwrap(),
+        );
+
+        let request = rpc_request(
+            "panda_publishEntry",
+            &format!(
+                r#"{{
+                    "entryEncoded": "{}",
+                    "messageEncoded": "{}"
+                }}"#,
+                entry_wrong_log_id.as_str(),
+                message_wrong_log_id.as_str(),
+            ),
+        );
+
+        let response = rpc_error("Requested log id 5 does not match expected log id 3");
+
+        assert_eq!(handle_http(&app, request).await, response);
+
+        // Send invalid log id for an existing document: This entry is an update for the existing
+        // document in log 1, however, we are trying to publish it in log 3.
+        let (entry_wrong_log_id, message_wrong_log_id) = create_test_entry(
+            &key_pair,
+            &schema,
+            &LogId::new(3),
+            Some(&entry_2.hash()),
+            None,
+            None,
+            &SeqNum::new(1).unwrap(),
         );
 
         let request = rpc_request(
@@ -401,19 +473,20 @@ mod tests {
                     "messageEncoded": "{}"
                 }}"#,
                 entry_wrong_log_id.as_str(),
-                message_2.as_str(),
+                message_wrong_log_id.as_str(),
             ),
         );
 
-        let response = rpc_error("Claimed log_id for schema not the same as in database");
+        let response = rpc_error("Requested log id 3 does not match expected log id 1");
 
         assert_eq!(handle_http(&app, request).await, response);
 
         // Send invalid backlink entry / hash
-        let (entry_wrong_hash, _) = create_test_entry(
+        let (entry_wrong_hash, message_wrong_hash) = create_test_entry(
             &key_pair,
             &schema,
             &log_id,
+            Some(&entry_1.hash()),
             Some(&entry_2),
             Some(&entry_1),
             &SeqNum::new(3).unwrap(),
@@ -427,7 +500,7 @@ mod tests {
                     "messageEncoded": "{}"
                 }}"#,
                 entry_wrong_hash.as_str(),
-                message_2.as_str(),
+                message_wrong_hash.as_str(),
             ),
         );
 
@@ -438,10 +511,11 @@ mod tests {
         assert_eq!(handle_http(&app, request).await, response);
 
         // Send invalid seq num
-        let (entry_wrong_seq_num, _) = create_test_entry(
+        let (entry_wrong_seq_num, message_wrong_seq_num) = create_test_entry(
             &key_pair,
             &schema,
             &log_id,
+            Some(&entry_2.hash()),
             None,
             Some(&entry_2),
             &SeqNum::new(5).unwrap(),
@@ -455,7 +529,7 @@ mod tests {
                     "messageEncoded": "{}"
                 }}"#,
                 entry_wrong_seq_num.as_str(),
-                message_2.as_str(),
+                message_wrong_seq_num.as_str(),
             ),
         );
 
diff --git a/aquadoggo/src/rpc/request.rs b/aquadoggo/src/rpc/request.rs
index ee360a3bb..2f5426798 100644
--- a/aquadoggo/src/rpc/request.rs
+++ b/aquadoggo/src/rpc/request.rs
@@ -11,7 +11,7 @@ use p2panda_rs::message::MessageEncoded;
 #[derive(Deserialize, Debug)]
 pub struct EntryArgsRequest {
     pub author: Author,
-    pub schema: Hash,
+    pub document: Hash,
 }
 
 /// Request body of `panda_publishEntry`.
@@ -21,7 +21,6 @@ pub struct PublishEntryRequest {
     pub entry_encoded: EntrySigned,
     pub message_encoded: MessageEncoded,
 }
-
 #[derive(Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
 pub struct QueryEntriesRequest {


Test patch: