diff options
| author | tv <tv@krebsco.de> | 2026-11-07 22:35:21 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-07 22:35:21 +0100 |
| commit | 63b34dfa41e1595145507a88901e332bf4bd3b14 (patch) | |
| tree | e33955ead52d4155a2c5cb5b1abaa3cb7e83f8a6 | |
| parent | c9b05d33f57b7efd69301b56725614a0d9938599 (diff) | |
signatures: kept in the document file
A node's signatures are saved with its document (--document-file) and
loaded with it.
Auth plan M5.
| -rw-r--r-- | docs/api.md | 2 | ||||
| -rw-r--r-- | docs/glossary.md | 3 | ||||
| -rw-r--r-- | src/document/file.rs | 201 | ||||
| -rw-r--r-- | src/document/signatures.rs | 37 | ||||
| -rw-r--r-- | src/main.rs | 21 | ||||
| -rw-r--r-- | src/node.rs | 6 | ||||
| -rw-r--r-- | src/simtest/mod.rs | 1 | ||||
| -rw-r--r-- | src/swarm/bootstrap.rs | 17 | ||||
| -rw-r--r-- | src/swarm/document_file.rs | 42 |
9 files changed, 268 insertions, 62 deletions
diff --git a/docs/api.md b/docs/api.md index 6a328a5..8f5b89c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -261,6 +261,8 @@ by the node's next signed one: with `--sign-every 1s` (the default), the node si latest change at most a second after it if nothing signed it before, so every change is vouched for within a second, at one signature a second at most; `--sign-every all` signs every change as it is made, and `--sign-every none` signs only those above. +The signatures a node has are kept in its document file (`--document-file`) with the +document. ## Protocol data diff --git a/docs/glossary.md b/docs/glossary.md index 42c7aac..644279e 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -31,7 +31,8 @@ address, or a host name with an AAAA record for one; [`transports.md`](transport and some members to rejoin through. Not the document. **document file**: a node's document on disk (`--document-file`), with the actor IDs -it wrote with; the node resumes with it after a restart. +it wrote with and the signatures of its changes; the node resumes with it after a +restart. **standing** of a peer, from one node's side (`Standing`): * **stranger**: has sent us something, nothing more. diff --git a/src/document/file.rs b/src/document/file.rs index 3ea3afd..33f7934 100644 --- a/src/document/file.rs +++ b/src/document/file.rs @@ -1,11 +1,15 @@ //! The document file (`--document-file`): this node's document on disk, so a restart //! resumes with it instead of starting empty. //! -//! Format: the line `continuum-document/1`, a header line (JSON: the actor IDs this +//! Format: the line `continuum-document/2`, a header line (JSON: the actor IDs this //! node wrote with, so a later rejoin replays those writes as its own), then records, -//! each a little-endian `u32` length and that many bytes: first a full save of the -//! document, then increments (the changes after the heads written before). A record cut -//! short by a crash is dropped when reading. +//! each a little-endian `u32` length and that many bytes, the first of them its kind +//! (`Record`): first a full save of the document, then increments (the changes after +//! the heads written before), and the signatures of its changes +//! (`document::signatures`), those the file holds none of yet, each a change hash +//! (32 bytes) and its signature (64). A record cut short by a crash is dropped when +//! reading. A file of `continuum-document/1` has records of the document alone, +//! without their kind: a full save, then increments. //! //! Written by a worker (`swarm::document_file`), only while the node is ready and not //! rejoining (a rejoin keeps the old file until it is done): @@ -30,7 +34,20 @@ use crate::document::replica::Epoch; use crate::files::{FileKind, Files}; use crate::locks::MutexExt; -const MAGIC: &[u8] = b"continuum-document/1\n"; +const MAGIC: &[u8] = b"continuum-document/2\n"; +/// The format before signatures, still read. +const MAGIC_1: &[u8] = b"continuum-document/1\n"; + +/// What a record holds: its first byte. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Record { + Save = 0, + Increment = 1, + Signatures = 2, +} + +/// A signature as a record holds it: the change's hash, then the signature. +const SIGNATURE_BYTES: usize = 32 + 64; #[derive(Debug, Default, Serialize, Deserialize)] struct Header { @@ -45,6 +62,8 @@ pub(crate) struct Loaded { pub(crate) doc: Document, /// The actors this node wrote with before. pub(crate) actors: Vec<ActorId>, + /// The signatures of its changes, in the order they were written. + pub(crate) signatures: Vec<(ChangeHash, [u8; 64])>, } /// Reads the document file; None if there is none. An error if it can't be read or @@ -70,9 +89,15 @@ pub(crate) fn load(path: &Path, files: &Files) -> Result<Option<Loaded>> { /// The file's content, and how many bytes at its end were cut short. fn parse(bytes: &[u8]) -> Result<(Loaded, usize)> { - let rest = bytes - .strip_prefix(MAGIC) - .ok_or_else(|| anyhow!("not a continuum document file"))?; + let (rest, kinds) = match bytes.strip_prefix(MAGIC) { + Some(rest) => (rest, true), + None => ( + bytes + .strip_prefix(MAGIC_1) + .ok_or_else(|| anyhow!("not a continuum document file"))?, + false, + ), + }; let line_end = rest .iter() .position(|byte| *byte == b'\n') @@ -89,6 +114,7 @@ fn parse(bytes: &[u8]) -> Result<(Loaded, usize)> { let mut records = &rest[line_end + 1..]; let mut doc: Option<Document> = None; + let mut signatures = Vec::new(); while !records.is_empty() { let Some((length, after)) = records.split_first_chunk::<4>() else { break; @@ -98,37 +124,88 @@ fn parse(bytes: &[u8]) -> Result<(Loaded, usize)> { break; } let (record, after) = after.split_at(length); - match &mut doc { - None => doc = Some(super::load(record).context("the full save")?), - Some(doc) => { + let (kind, record) = if kinds { + let (kind, record) = record + .split_first() + .ok_or_else(|| anyhow!("an empty record"))?; + let kind = match kind { + 0 => Record::Save, + 1 => Record::Increment, + 2 => Record::Signatures, + other => bail!("a record of an unknown kind ({other})"), + }; + (kind, record) + } else if doc.is_none() { + (Record::Save, record) + } else { + (Record::Increment, record) + }; + match (kind, &mut doc) { + (Record::Save, None) => doc = Some(super::load(record).context("the full save")?), + (Record::Save, Some(_)) => bail!("a second full save"), + (Record::Increment, Some(doc)) => { doc.load_incremental(record).context("an increment")?; } + (Record::Signatures, Some(_)) => { + if record.len() % SIGNATURE_BYTES != 0 { + bail!("signatures of {} bytes", record.len()); + } + for entry in record.chunks_exact(SIGNATURE_BYTES) { + let (hash, signature) = entry.split_at(32); + let hash = ChangeHash(hash.try_into().expect("32 bytes")); + signatures.push((hash, signature.try_into().expect("64 bytes"))); + } + } + (_, None) => bail!("a record before the full save"), } records = after; } let Some(doc) = doc else { bail!("no full save"); }; - let loaded = Loaded { doc, actors }; + let loaded = Loaded { + doc, + actors, + signatures, + }; Ok((loaded, records.len())) } -fn record(bytes: &[u8]) -> Vec<u8> { - let mut out = Vec::with_capacity(bytes.len() + 4); - out.extend_from_slice(&(bytes.len() as u32).to_le_bytes()); +fn record(kind: Record, bytes: &[u8]) -> Vec<u8> { + let mut out = Vec::with_capacity(bytes.len() + 5); + out.extend_from_slice(&(bytes.len() as u32 + 1).to_le_bytes()); + out.push(kind as u8); out.extend_from_slice(bytes); out } -/// A whole file: header and one full save. -fn whole(actors: &[ActorId], save: &[u8]) -> Result<Vec<u8>> { +/// A record of `signatures`; none without any. +fn signatures_record(signatures: &[(ChangeHash, [u8; 64])]) -> Vec<u8> { + if signatures.is_empty() { + return Vec::new(); + } + let mut bytes = Vec::with_capacity(signatures.len() * SIGNATURE_BYTES); + for (hash, signature) in signatures { + bytes.extend_from_slice(&hash.0); + bytes.extend_from_slice(signature); + } + record(Record::Signatures, &bytes) +} + +/// A whole file: header, one full save, and the signatures. +fn whole( + actors: &[ActorId], + save: &[u8], + signatures: &[(ChangeHash, [u8; 64])], +) -> Result<Vec<u8>> { let header = Header { actors: actors.iter().map(ActorId::to_hex_string).collect(), }; let mut out = MAGIC.to_vec(); out.extend(serde_json::to_vec(&header)?); out.push(b'\n'); - out.extend(record(save)); + out.extend(record(Record::Save, save)); + out.extend(signatures_record(signatures)); Ok(out) } @@ -148,6 +225,8 @@ struct Written { /// This node's actors when the file was last rewritten (`RemakeState::own_actors`). actors: usize, heads: Vec<ChangeHash>, + /// How many of the document's signatures (`Signatures::count`). + signatures: usize, /// The full save. base_bytes: u64, /// Appended since. @@ -180,11 +259,13 @@ impl DocumentFile { pub(crate) enum Due { /// Written whole: for this reason. Rewrite(&'static str), - /// What changed since `since` appended, as long as the document is still of `epoch` - /// (an increment of another epoch's document must never be appended). + /// What changed since `since` appended, and the signatures after the first + /// `signatures`, as long as the document is still of `epoch` (an increment of + /// another epoch's document must never be appended). Append { epoch: Epoch, since: Vec<ChangeHash>, + signatures: usize, }, Nothing, } @@ -202,7 +283,15 @@ impl DocumentFile { self.lock().as_ref().map(|written| written.actors) } - pub(crate) fn due(&self, epoch: Epoch, actors: usize, heads: &[ChangeHash]) -> Due { + /// With `signatures`, how many the document has (`Signatures::count`); None for a + /// document whose signatures aren't kept (the one before a rejoin). + pub(crate) fn due( + &self, + epoch: Epoch, + actors: usize, + heads: &[ChangeHash], + signatures: Option<usize>, + ) -> Due { let written = self.lock().clone(); let reason = match &written { None => Some("first write"), @@ -218,23 +307,31 @@ impl DocumentFile { }; match (reason, written) { (Some(reason), _) => Due::Rewrite(reason), - (None, Some(written)) if written.heads != heads => Due::Append { - epoch: written.epoch, - since: written.heads, - }, + (None, Some(written)) + if written.heads != heads + || signatures.is_some_and(|count| count != written.signatures) => + { + Due::Append { + epoch: written.epoch, + since: written.heads, + signatures: written.signatures, + } + } (None, _) => Due::Nothing, } } - /// Writes the whole file, atomically: `copy` (a copy of the document, of `epoch`) - /// and this node's actors `own` that wrote in it. + /// Writes the whole file, atomically: `copy` (a copy of the document, of `epoch`), + /// its `signatures`, and this node's actors `own` that wrote in it. pub(crate) async fn rewrite( self: &Arc<Self>, epoch: Epoch, copy: Document, + signatures: Vec<(ChangeHash, [u8; 64])>, own: Vec<ActorId>, reason: &str, ) { + let signature_count = signatures.len(); let start = Instant::now(); let own_count = own.len(); let writer = Arc::clone(self); @@ -253,7 +350,7 @@ impl DocumentFile { .filter(|actor| *actor == current || present.contains(actor)) .collect(); let save = super::save(©); - let bytes = whole(&actors, &save)?; + let bytes = whole(&actors, &save, &signatures)?; writer .files .write_atomic(FileKind::Document, &writer.path, &bytes)?; @@ -272,6 +369,7 @@ impl DocumentFile { epoch, actors: own_count, heads, + signatures: signature_count, base_bytes, tail_bytes: 0, rewritten_at: Instant::now(), @@ -285,14 +383,22 @@ impl DocumentFile { } /// Appends `increment`: what changed from `since` (the heads written last) to - /// `heads`. + /// `heads`; and `signatures`, those of the document the file holds none of yet + /// (`Due::Append`). pub(crate) async fn append( self: &Arc<Self>, since: Vec<ChangeHash>, heads: Vec<ChangeHash>, increment: Vec<u8>, + signatures: Vec<(ChangeHash, [u8; 64])>, ) { - let bytes = record(&increment); + let mut bytes = if heads == since { + Vec::new() + } else { + record(Record::Increment, &increment) + }; + bytes.extend(signatures_record(&signatures)); + let length = bytes.len() as u64; let writer = Arc::clone(self); let appended = tokio::task::spawn_blocking(move || { writer @@ -305,7 +411,8 @@ impl DocumentFile { let mut guard = self.lock(); if let Some(current) = guard.as_mut().filter(|current| current.heads == since) { current.heads = heads; - current.tail_bytes += increment.len() as u64 + 4; + current.signatures += signatures.len(); + current.tail_bytes += length; debug!( bytes = increment.len(), tail_bytes = current.tail_bytes, @@ -336,20 +443,24 @@ mod tests { change(&mut doc, |doc| doc.put(ROOT, "a", 1_i64).unwrap()); let actor = doc.get_actor().clone(); let heads = doc.get_heads(); - let mut bytes = whole(std::slice::from_ref(&actor), &doc.save()).unwrap(); + let first = (heads[0], [1; 64]); + let mut bytes = whole(std::slice::from_ref(&actor), &doc.save(), &[first]).unwrap(); change(&mut doc, |doc| doc.put(ROOT, "b", 2_i64).unwrap()); - bytes.extend(record(&doc.save_after(&heads))); + bytes.extend(record(Record::Increment, &doc.save_after(&heads))); + let second = (doc.get_heads()[0], [2; 64]); + bytes.extend(signatures_record(&[second])); let (loaded, dropped) = parse(&bytes).unwrap(); assert_eq!(dropped, 0); assert_eq!(loaded.actors, [actor.clone()]); assert_ne!(loaded.doc.get_actor(), &actor); assert!(loaded.doc.get(ROOT, "b").unwrap().is_some()); + assert_eq!(loaded.signatures, [first, second]); // A record cut short by a crash is dropped. let mut cut = bytes.clone(); - cut.extend(record(b"whatever")[..6].iter()); + cut.extend(record(Record::Increment, b"whatever")[..6].iter()); let (loaded, dropped) = parse(&cut).unwrap(); assert_eq!(dropped, 6); assert!(loaded.doc.get(ROOT, "b").unwrap().is_some()); @@ -357,4 +468,26 @@ mod tests { assert!(parse(b"something else").is_err()); assert!(parse(&bytes[..MAGIC.len() + 3]).is_err()); } + + /// A file of the format before signatures: records of the document alone. + #[test] + fn the_format_before_signatures() { + let mut doc = Document::new(); + change(&mut doc, |doc| doc.put(ROOT, "a", 1_i64).unwrap()); + let heads = doc.get_heads(); + let untagged = |bytes: &[u8]| { + let mut out = (bytes.len() as u32).to_le_bytes().to_vec(); + out.extend_from_slice(bytes); + out + }; + let mut bytes = MAGIC_1.to_vec(); + bytes.extend(b"{\"actors\":[]}\n"); + bytes.extend(untagged(&doc.save())); + change(&mut doc, |doc| doc.put(ROOT, "b", 2_i64).unwrap()); + bytes.extend(untagged(&doc.save_after(&heads))); + let (loaded, dropped) = parse(&bytes).unwrap(); + assert_eq!(dropped, 0); + assert!(loaded.doc.get(ROOT, "b").unwrap().is_some()); + assert!(loaded.signatures.is_empty()); + } } diff --git a/src/document/signatures.rs b/src/document/signatures.rs index e92cf9c..c8af89a 100644 --- a/src/document/signatures.rs +++ b/src/document/signatures.rs @@ -154,13 +154,42 @@ impl<'de> serde::Deserialize<'de> for SignEvery { #[derive(Debug, Default)] pub(crate) struct Signatures { by_change: HashMap<ChangeHash, [u8; 64]>, + /// The changes signed, in the order their signatures came: the document file + /// appends those after the ones it wrote (`since`). + order: Vec<ChangeHash>, /// This node's latest change, if no signature vouches for it yet (`SignEvery`). unsigned: Option<ChangeHash>, } impl Signatures { + /// Signatures kept before, as `since` gave them (the document file). + pub(crate) fn from_kept(kept: Vec<(ChangeHash, [u8; 64])>) -> Self { + let mut signatures = Self::default(); + for (hash, signature) in kept { + signatures.insert(hash, signature); + } + signatures + } + pub(crate) fn insert(&mut self, hash: ChangeHash, signature: [u8; 64]) { - self.by_change.insert(hash, signature); + if self.by_change.insert(hash, signature).is_none() { + self.order.push(hash); + } + } + + /// How many signatures there are. + pub(crate) fn count(&self) -> usize { + self.order.len() + } + + /// The signatures after the first `count` (`count`), in the order they came. + pub(crate) fn since(&self, count: usize) -> Vec<(ChangeHash, [u8; 64])> { + self.order + .get(count..) + .unwrap_or_default() + .iter() + .map(|hash| (*hash, self.by_change[hash])) + .collect() } /// Signs `hash`, this node's latest change, with `key` and keeps the signature: it @@ -286,6 +315,12 @@ mod tests { signatures.left_unsigned(a); signatures.sign(b, &key); assert!(!signatures.sign_latest(&key)); + // Kept in the order they came, each once. + assert_eq!(signatures.count(), 2); + assert_eq!(signatures.since(1)[0].0, b); + assert!(signatures.since(5).is_empty()); + let again = Signatures::from_kept(signatures.since(0)); + assert_eq!(again.since(0), signatures.since(0)); } /// Protocol data, listed paths (within or above them) and asked-for writes are diff --git a/src/main.rs b/src/main.rs index 4dda536..24f0556 100644 --- a/src/main.rs +++ b/src/main.rs @@ -164,15 +164,17 @@ async fn main() -> Result<()> { &files, &force, )?; - let (initial_doc, starting_epoch, starting_swarm, earlier_actors) = match starting { - Some(starting) => ( - starting.doc, - Some(starting.epoch), - Some((starting.swarm_id, starting.origin)), - starting.earlier_actors, - ), - None => (Document::new(), None, None, Vec::new()), - }; + let (initial_doc, starting_epoch, starting_swarm, earlier_actors, initial_signatures) = + match starting { + Some(starting) => ( + starting.doc, + Some(starting.epoch), + Some((starting.swarm_id, starting.origin)), + starting.earlier_actors, + starting.signatures, + ), + None => (Document::new(), None, None, Vec::new(), Vec::new()), + }; let mut initial_doc = initial_doc; if let Some(spec) = &args.remake { remake::remake_genesis(spec, &files, &mut initial_doc) @@ -235,6 +237,7 @@ async fn main() -> Result<()> { initial_epoch, initial_doc, earlier_actors, + initial_signatures, timing: state::Timing::new(args.keepalive, sync_reply_timeout, delivery_limit) .with_probe_max(args.dormant_probe_max), neighbours_k: args.neighbours, diff --git a/src/node.rs b/src/node.rs index c1a26de..32a82b2 100644 --- a/src/node.rs +++ b/src/node.rs @@ -40,6 +40,8 @@ pub(crate) struct NodeConfig { pub(crate) initial_doc: Document, /// Actors this node wrote with in earlier runs (`--document-file`). pub(crate) earlier_actors: Vec<ActorId>, + /// The signatures of `initial_doc`'s changes (`--document-file`). + pub(crate) initial_signatures: Vec<(crate::document::ChangeHash, [u8; 64])>, /// `Timing::new(--keepalive, the transports' sync reply timeout and delivery /// limit)`. pub(crate) timing: Timing, @@ -110,7 +112,9 @@ impl Node { None => config.transport, }; - let replica = Replica::new(config.initial_epoch, config.initial_doc); + let mut replica = Replica::new(config.initial_epoch, config.initial_doc); + replica.signatures = + crate::document::signatures::Signatures::from_kept(config.initial_signatures); let stamp = replica.stamps(); let state = AppState { doc: Arc::new(crate::document::lock::DocumentLock::new(replica)), diff --git a/src/simtest/mod.rs b/src/simtest/mod.rs index 3f0f47d..5acc886 100644 --- a/src/simtest/mod.rs +++ b/src/simtest/mod.rs @@ -185,6 +185,7 @@ impl Sim { initial_epoch: Epoch::GENESIS, initial_doc: Document::new(), earlier_actors: Vec::new(), + initial_signatures: Vec::new(), timing: Timing::new(KEEPALIVE, SYNC_REPLY_TIMEOUT, DELIVERY_LIMIT), neighbours_k: 8, alias: Some(id.to_owned()), diff --git a/src/swarm/bootstrap.rs b/src/swarm/bootstrap.rs index 446b846..10ad412 100644 --- a/src/swarm/bootstrap.rs +++ b/src/swarm/bootstrap.rs @@ -241,6 +241,8 @@ pub(crate) struct StartingDocument { pub(crate) origin: Origin, /// The actors this node wrote with before (from the document file). pub(crate) earlier_actors: Vec<ActorId>, + /// The signatures of its changes (from the document file). + pub(crate) signatures: Vec<(crate::document::ChangeHash, [u8; 64])>, } /// What `--recover-from` and `--document-file` say about where to start. @@ -289,7 +291,7 @@ pub(crate) fn starting_document( Some(path) => Some(load_backup(path, files)?), }; - let (doc, origin, earlier_actors) = match (backup, from_file) { + let (doc, origin, earlier_actors, signatures) = match (backup, from_file) { (Some(_), Some(_)) if !force.allows("replaceDocumentFile") => { bail!( "recover-from would replace the document in the document file; add --force replaceDocumentFile to recover the backup anyway" @@ -301,9 +303,14 @@ pub(crate) fn starting_document( "Recovering the backup instead of the document file's document (--force replaceDocumentFile)" ); } - (backup, Origin::Backup, Vec::new()) + (backup, Origin::Backup, Vec::new(), Vec::new()) } - (None, Some(loaded)) => (loaded.doc, Origin::DocumentFile, loaded.actors), + (None, Some(loaded)) => ( + loaded.doc, + Origin::DocumentFile, + loaded.actors, + loaded.signatures, + ), (None, None) => return Ok(None), }; if options.genesis && origin == Origin::DocumentFile { @@ -311,7 +318,8 @@ pub(crate) fn starting_document( "The document file holds a document of a swarm, and genesis would start a new one; move the file away to start a new swarm" ); } - let starting = identify(doc, origin, earlier_actors)?; + let mut starting = identify(doc, origin, earlier_actors)?; + starting.signatures = signatures; if let Some(remembered) = options.remembered.filter(|id| *id != starting.swarm_id) { let what = origin.describe(); @@ -347,6 +355,7 @@ fn identify( swarm_id, origin, earlier_actors, + signatures: Vec::new(), }) } diff --git a/src/swarm/document_file.rs b/src/swarm/document_file.rs index e9deca0..e066e27 100644 --- a/src/swarm/document_file.rs +++ b/src/swarm/document_file.rs @@ -40,29 +40,45 @@ pub(crate) async fn flush(state: &AppState) { return; } let own = state.remake.own_actors(); - let (epoch, heads) = { + let (epoch, heads, signatures) = { let replica = state.doc.read().await; - (replica.epoch, replica.heads().to_vec()) + ( + replica.epoch, + replica.heads().to_vec(), + replica.signatures.count(), + ) }; - match file.due(epoch, own.len(), &heads) { + match file.due(epoch, own.len(), &heads, Some(signatures)) { Due::Rewrite(reason) => { - let (epoch, copy) = { + let (epoch, copy, signatures) = { let replica = state.doc.read().await; - (replica.epoch, replica.doc.clone()) + ( + replica.epoch, + replica.doc.clone(), + replica.signatures.since(0), + ) }; - file.rewrite(epoch, copy, own, reason).await; + file.rewrite(epoch, copy, signatures, own, reason).await; } - Due::Append { epoch, since } => { - let (heads, increment) = { + Due::Append { + epoch, + since, + signatures, + } => { + let (heads, increment, signatures) = { let replica = state.doc.read().await; // Switched since: the next flush rewrites the file instead. if replica.epoch != epoch { return; } let heads = replica.heads().to_vec(); - (heads, crate::document::save_since(&replica.doc, &since)) + ( + heads, + crate::document::save_since(&replica.doc, &since), + replica.signatures.since(signatures), + ) }; - file.append(since, heads, increment).await; + file.append(since, heads, increment, signatures).await; } Due::Nothing => {} } @@ -90,10 +106,12 @@ async fn flush_before_rejoin( let Some(actors) = file.actors() else { return; }; + // Its signatures aren't kept: those of the document after the rejoin are. if let Due::Append { epoch: written, since, - } = file.due(epoch, actors, &heads) + .. + } = file.due(epoch, actors, &heads, None) && written == epoch { let since_then = since.clone(); @@ -105,6 +123,6 @@ async fn flush_before_rejoin( }) .await .expect("document read task panicked"); - file.append(since, heads, increment).await; + file.append(since, heads, increment, Vec::new()).await; } } |
