Skip to main content

kcode_k1_canonical_chain/
lib.rs

1use kcode_k1_order_store::OrderStore;
2pub use kcode_k1_transaction::{GENESIS_PARENT, REGISTER_AT_TIP, SubsystemId};
3use kcode_k1_transaction::{Transaction, build_signed_transaction};
4pub use kcode_k1_transaction_store::TxId;
5use kcode_k1_transaction_store::{PutOutcome, StoreError, TransactionStore};
6use sha2::{Digest, Sha256};
7use std::{cmp::Ordering, fmt, fs, path::Path};
8
9pub struct CanonicalChain {
10    order: OrderStore,
11    store: TransactionStore,
12}
13
14pub struct ReplayCursor {
15    subsystem: SubsystemId,
16    after: Option<TxId>,
17}
18
19pub struct ReplayTransaction {
20    pub id: TxId,
21    pub bytes: Vec<u8>,
22}
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum CommitOutcome {
26    Duplicate,
27    Extension { id: TxId, subsystem: SubsystemId },
28    Reorganization { id: TxId, subsystem: SubsystemId },
29}
30
31#[derive(Debug)]
32pub enum SubmitError {
33    MissingParent,
34    Other(String),
35}
36
37impl fmt::Display for SubmitError {
38    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            Self::MissingParent => formatter.write_str("missing parent"),
41            Self::Other(message) => formatter.write_str(message),
42        }
43    }
44}
45
46impl std::error::Error for SubmitError {}
47
48struct Candidate<'a> {
49    id: TxId,
50    parent: TxId,
51    creator: [u8; 32],
52    timestamp: u64,
53    subsystem: SubsystemId,
54    bytes: &'a [u8],
55}
56
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58enum ForkDecision {
59    Incoming,
60    Incumbent,
61    Duplicate,
62    Collision,
63}
64
65impl CanonicalChain {
66    pub fn open(root: &Path) -> Result<Self, String> {
67        prepare_root(root)?;
68
69        let ordering_path = root.join("ordering.dat");
70        let store_path = root.join("k1-transaction-store");
71        let ordering_type = path_type(&ordering_path)?;
72        let store_type = path_type(&store_path)?;
73
74        if ordering_type.is_some_and(|kind| !kind.is_file()) {
75            return Err("ordering.dat is not a regular file".to_owned());
76        }
77        if store_type.is_some_and(|kind| !kind.is_dir()) {
78            return Err("k1-transaction-store is not a directory".to_owned());
79        }
80
81        let (order, store) = match (ordering_type, store_type) {
82            (None, None) => {
83                let store = TransactionStore::create(&store_path)
84                    .unwrap_or_else(|error| fatal("create-transaction-store", error));
85                let order = OrderStore::create(&ordering_path)
86                    .unwrap_or_else(|error| fatal("create-order-store", error));
87                (order, store)
88            }
89            (Some(_), Some(_)) => (
90                OrderStore::open(&ordering_path)?,
91                TransactionStore::open(&store_path)
92                    .map_err(|error| format!("transaction store error: {error}"))?,
93            ),
94            _ => return Err("canonical chain root is incomplete".to_owned()),
95        };
96
97        Ok(Self { order, store })
98    }
99
100    pub fn submit_validated(&mut self, transaction: &[u8]) -> Result<CommitOutcome, SubmitError> {
101        let parsed = Transaction::parse(transaction)
102            .map_err(|message| SubmitError::Other(format!("invalid transaction: {message}")))?;
103        let candidate = Candidate {
104            id: TxId::for_transaction(transaction),
105            parent: parsed.parent(),
106            creator: *parsed.creator(),
107            timestamp: parsed.timestamp(),
108            subsystem: parsed.subsystem(),
109            bytes: transaction,
110        };
111
112        if is_reserved_id(candidate.id) {
113            return Err(SubmitError::Other(
114                "transaction ID collides with a reserved sentinel".to_owned(),
115            ));
116        }
117
118        self.submit_candidate(candidate)
119    }
120
121    pub fn submit_local<F>(
122        &mut self,
123        timestamp: u64,
124        creator: [u8; 32],
125        subsystem: SubsystemId,
126        payload: &[u8],
127        signer: F,
128    ) -> Result<Vec<u8>, String>
129    where
130        F: FnOnce(&[u8]) -> Result<[u8; 64], String>,
131    {
132        let parent = self.tip().unwrap_or(GENESIS_PARENT);
133        let bytes =
134            build_signed_transaction(parent, timestamp, creator, subsystem, payload, signer)?;
135        let id = TxId::for_transaction(&bytes);
136
137        if is_reserved_id(id) {
138            return Err("transaction ID collides with a reserved sentinel".to_owned());
139        }
140        if self.order.index_of(id).is_some() {
141            return Err("transaction ID collides with a canonical transaction".to_owned());
142        }
143
144        self.persist(&bytes, id)
145            .map_err(|error| error.to_string())?;
146        self.order
147            .commit(self.order.entries().len(), id, subsystem)
148            .unwrap_or_else(|error| fatal("commit-local-order", error));
149        Ok(bytes)
150    }
151
152    pub fn contains(&self, id: TxId) -> bool {
153        !is_reserved_id(id) && self.order.index_of(id).is_some()
154    }
155
156    pub fn tip(&self) -> Option<TxId> {
157        self.order.entries().last().map(|entry| entry.0)
158    }
159
160    pub fn between_txids(&self, older: TxId, newer: TxId) -> Result<Vec<TxId>, String> {
161        let older_index = if older == GENESIS_PARENT {
162            -1_i128
163        } else {
164            self.order
165                .index_of(older)
166                .map(|index| index as i128)
167                .ok_or_else(|| "older boundary is not canonical".to_owned())?
168        };
169        let newer_index = self
170            .order
171            .index_of(newer)
172            .map(|index| index as i128)
173            .ok_or_else(|| "newer boundary is not canonical".to_owned())?;
174
175        if older_index == newer_index {
176            return Ok(Vec::new());
177        }
178        if older_index > newer_index {
179            return Err("transaction boundaries are reversed".to_owned());
180        }
181
182        let interior = newer_index - older_index - 1;
183        if interior <= 128 {
184            return Ok(((older_index + 1)..newer_index)
185                .map(|index| self.order.entries()[index as usize].0)
186                .collect());
187        }
188
189        let distance = newer_index - older_index;
190        Ok((1_i128..=128)
191            .map(|k| self.order.entries()[(older_index + k * distance / 129) as usize].0)
192            .collect())
193    }
194
195    pub fn get_txn(&self, id: TxId) -> Result<Option<Vec<u8>>, String> {
196        if !self.contains(id) {
197            return Ok(None);
198        }
199        Ok(Some(self.canonical_bytes(id)))
200    }
201
202    pub fn replay_cursor(
203        &self,
204        subsystem: SubsystemId,
205        after: Option<TxId>,
206    ) -> Result<ReplayCursor, String> {
207        if let Some(id) = after {
208            let index = self
209                .order
210                .index_of(id)
211                .ok_or_else(|| "replay checkpoint is not canonical".to_owned())?;
212            if self.order.entries()[index].1 != subsystem {
213                return Err("replay checkpoint belongs to another subsystem".to_owned());
214            }
215        }
216
217        Ok(ReplayCursor { subsystem, after })
218    }
219
220    pub fn replay_next(
221        &self,
222        cursor: &mut ReplayCursor,
223    ) -> Result<Option<ReplayTransaction>, String> {
224        let start = match cursor.after {
225            None => 0,
226            Some(id) => {
227                self.order
228                    .index_of(id)
229                    .ok_or_else(|| "replay cursor is no longer canonical".to_owned())?
230                    + 1
231            }
232        };
233
234        for &(id, subsystem) in &self.order.entries()[start..] {
235            if subsystem != cursor.subsystem {
236                continue;
237            }
238
239            let bytes = self.canonical_bytes(id);
240            let parsed = Transaction::parse(&bytes)
241                .unwrap_or_else(|error| fatal("parse-canonical-transaction", error));
242            if TxId::for_transaction(&bytes) != id || parsed.subsystem() != subsystem {
243                fatal(
244                    "verify-canonical-transaction",
245                    "canonical transaction does not match its order record",
246                );
247            }
248
249            cursor.after = Some(id);
250            return Ok(Some(ReplayTransaction { id, bytes }));
251        }
252
253        Ok(None)
254    }
255
256    fn submit_candidate(&mut self, candidate: Candidate<'_>) -> Result<CommitOutcome, SubmitError> {
257        if self.order.index_of(candidate.id).is_some() {
258            return if self.canonical_bytes(candidate.id) == candidate.bytes {
259                Ok(CommitOutcome::Duplicate)
260            } else {
261                Err(SubmitError::Other(
262                    "transaction ID collision with canonical bytes".to_owned(),
263                ))
264            };
265        }
266
267        let shared_len = if candidate.parent == GENESIS_PARENT {
268            0
269        } else {
270            self.order
271                .index_of(candidate.parent)
272                .map(|index| index + 1)
273                .ok_or(SubmitError::MissingParent)?
274        };
275
276        if shared_len == self.order.entries().len() {
277            self.persist(candidate.bytes, candidate.id)?;
278            self.order
279                .commit(shared_len, candidate.id, candidate.subsystem)
280                .unwrap_or_else(|error| fatal("commit-extension-order", error));
281            return Ok(CommitOutcome::Extension {
282                id: candidate.id,
283                subsystem: candidate.subsystem,
284            });
285        }
286
287        let (incumbent_id, incumbent_subsystem) = self.order.entries()[shared_len];
288        let incumbent_bytes = self.canonical_bytes(incumbent_id);
289        let incumbent = Transaction::parse(&incumbent_bytes)
290            .unwrap_or_else(|error| fatal("parse-canonical-incumbent", error));
291        if incumbent.subsystem() != incumbent_subsystem || incumbent.parent() != candidate.parent {
292            fatal(
293                "verify-canonical-incumbent",
294                "canonical incumbent does not match its order record or parent",
295            );
296        }
297
298        match fork_decision(
299            &candidate.creator,
300            candidate.timestamp,
301            candidate.bytes,
302            incumbent.creator(),
303            incumbent.timestamp(),
304            &incumbent_bytes,
305        ) {
306            ForkDecision::Incumbent => {
307                return Err(SubmitError::Other(
308                    "fork loses canonical ordering".to_owned(),
309                ));
310            }
311            ForkDecision::Duplicate => return Ok(CommitOutcome::Duplicate),
312            ForkDecision::Collision => {
313                return Err(SubmitError::Other(
314                    "full transaction digest collision".to_owned(),
315                ));
316            }
317            ForkDecision::Incoming => {}
318        }
319
320        self.persist(candidate.bytes, candidate.id)?;
321        self.order
322            .commit(shared_len, candidate.id, candidate.subsystem)
323            .unwrap_or_else(|error| fatal("commit-reorganization-order", error));
324        Ok(CommitOutcome::Reorganization {
325            id: candidate.id,
326            subsystem: candidate.subsystem,
327        })
328    }
329
330    fn persist(&self, bytes: &[u8], expected: TxId) -> Result<(), SubmitError> {
331        match self.store.put(bytes) {
332            Ok(PutOutcome::Inserted(id)) | Ok(PutOutcome::Duplicate(id)) if id == expected => {
333                Ok(())
334            }
335            Ok(_) => fatal(
336                "persist-transaction",
337                "transaction store returned an unexpected transaction ID",
338            ),
339            Err(StoreError::IdCollision(_)) => Err(SubmitError::Other(
340                "transaction ID collision with stored bytes".to_owned(),
341            )),
342            Err(error) => fatal("persist-transaction", error),
343        }
344    }
345
346    fn canonical_bytes(&self, id: TxId) -> Vec<u8> {
347        match self.store.get(id) {
348            Ok(Some(bytes)) => bytes,
349            Ok(None) => fatal(
350                "load-canonical-transaction",
351                "canonical transaction bytes are missing",
352            ),
353            Err(error) => fatal("load-canonical-transaction", error),
354        }
355    }
356}
357
358fn is_reserved_id(id: TxId) -> bool {
359    id == GENESIS_PARENT || id == REGISTER_AT_TIP
360}
361
362fn prepare_root(root: &Path) -> Result<(), String> {
363    match fs::symlink_metadata(root) {
364        Ok(metadata) if metadata.file_type().is_dir() => Ok(()),
365        Ok(_) => Err("canonical chain root is not a directory".to_owned()),
366        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
367            fs::create_dir_all(root).unwrap_or_else(|error| fatal("create-root", error));
368            Ok(())
369        }
370        Err(error) => Err(format!("cannot inspect canonical chain root: {error}")),
371    }
372}
373
374fn path_type(path: &Path) -> Result<Option<fs::FileType>, String> {
375    match fs::symlink_metadata(path) {
376        Ok(metadata) => Ok(Some(metadata.file_type())),
377        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
378        Err(error) => Err(format!("cannot inspect canonical chain component: {error}")),
379    }
380}
381
382fn fork_decision(
383    incoming_creator: &[u8; 32],
384    incoming_timestamp: u64,
385    incoming_bytes: &[u8],
386    incumbent_creator: &[u8; 32],
387    incumbent_timestamp: u64,
388    incumbent_bytes: &[u8],
389) -> ForkDecision {
390    match incoming_creator.cmp(incumbent_creator) {
391        Ordering::Less => return ForkDecision::Incoming,
392        Ordering::Greater => return ForkDecision::Incumbent,
393        Ordering::Equal => {}
394    }
395    match incoming_timestamp.cmp(&incumbent_timestamp) {
396        Ordering::Less => return ForkDecision::Incoming,
397        Ordering::Greater => return ForkDecision::Incumbent,
398        Ordering::Equal => {}
399    }
400
401    let incoming_digest: [u8; 32] = Sha256::digest(incoming_bytes).into();
402    let incumbent_digest: [u8; 32] = Sha256::digest(incumbent_bytes).into();
403    match incoming_digest.cmp(&incumbent_digest) {
404        Ordering::Less => ForkDecision::Incoming,
405        Ordering::Greater => ForkDecision::Incumbent,
406        Ordering::Equal if incoming_bytes == incumbent_bytes => ForkDecision::Duplicate,
407        Ordering::Equal => ForkDecision::Collision,
408    }
409}
410
411fn fatal(operation: &str, error: impl fmt::Display) -> ! {
412    eprintln!("kcode-k1-canonical-chain fatal {operation}: {error}");
413    std::process::abort()
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419    use std::cell::Cell;
420    use std::path::PathBuf;
421    use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
422    use std::time::{Duration, Instant};
423
424    static NEXT_ROOT: AtomicU64 = AtomicU64::new(0);
425
426    struct TempRoot(PathBuf);
427
428    impl TempRoot {
429        fn new(label: &str) -> Self {
430            let number = NEXT_ROOT.fetch_add(1, AtomicOrdering::Relaxed);
431            let path = std::env::temp_dir().join(format!(
432                "kcode-k1-canonical-chain-{}-{number}-{label}",
433                std::process::id()
434            ));
435            let _ = fs::remove_dir_all(&path);
436            Self(path)
437        }
438    }
439
440    impl Drop for TempRoot {
441        fn drop(&mut self) {
442            let _ = fs::remove_dir_all(&self.0);
443        }
444    }
445
446    fn subsystem(value: u8) -> SubsystemId {
447        SubsystemId::from_bytes([value; 20]).unwrap()
448    }
449
450    fn transaction(
451        parent: TxId,
452        creator: u8,
453        timestamp: u64,
454        subsystem: SubsystemId,
455        payload: &[u8],
456    ) -> Vec<u8> {
457        build_signed_transaction(parent, timestamp, [creator; 32], subsystem, payload, |_| {
458            Ok([creator; 64])
459        })
460        .unwrap()
461    }
462
463    #[test]
464    fn local_submission_parents_and_signer_error_does_not_mutate() {
465        let root = TempRoot::new("local");
466        let mut chain = CanonicalChain::open(&root.0).unwrap();
467        let calls = Cell::new(0);
468        let error = chain.submit_local(0, [0; 32], subsystem(b'a'), b"bad", |_| {
469            calls.set(calls.get() + 1);
470            Err("signer stopped".to_owned())
471        });
472        assert_eq!(error.unwrap_err(), "signer stopped");
473        assert_eq!((calls.get(), chain.tip()), (1, None));
474        assert_eq!(fs::metadata(root.0.join("ordering.dat")).unwrap().len(), 0);
475
476        let first = chain
477            .submit_local(1, [1; 32], subsystem(b'a'), b"first", |_| Ok([2; 64]))
478            .unwrap();
479        let second = chain
480            .submit_local(2, [1; 32], subsystem(b'b'), b"second", |_| Ok([3; 64]))
481            .unwrap();
482        let first_id = TxId::for_transaction(&first);
483        assert_eq!(Transaction::parse(&first).unwrap().parent(), GENESIS_PARENT);
484        assert_eq!(Transaction::parse(&second).unwrap().parent(), first_id);
485        assert_eq!(chain.get_txn(first_id).unwrap(), Some(first));
486
487        drop(chain);
488        assert_eq!(
489            CanonicalChain::open(&root.0).unwrap().tip(),
490            Some(TxId::for_transaction(&second))
491        );
492    }
493
494    #[test]
495    fn remote_reorganization_reopens_and_retains_orphans() {
496        let root = TempRoot::new("remote");
497        let mut chain = CanonicalChain::open(&root.0).unwrap();
498        let owner = subsystem(b'a');
499        let first = transaction(GENESIS_PARENT, 20, 1, owner, b"first");
500        let first_id = TxId::for_transaction(&first);
501        assert!(matches!(
502            chain.submit_validated(&first),
503            Ok(CommitOutcome::Extension { .. })
504        ));
505        assert_eq!(
506            chain.submit_validated(&first).unwrap(),
507            CommitOutcome::Duplicate
508        );
509
510        let incumbent = transaction(first_id, 50, 2, owner, b"incumbent");
511        let incumbent_id = TxId::for_transaction(&incumbent);
512        chain.submit_validated(&incumbent).unwrap();
513        let descendant = transaction(incumbent_id, 50, 3, owner, b"descendant");
514        let descendant_id = TxId::for_transaction(&descendant);
515        chain.submit_validated(&descendant).unwrap();
516
517        let replacement = transaction(first_id, 1, 99, owner, b"replacement");
518        let replacement_id = TxId::for_transaction(&replacement);
519        assert!(matches!(
520            chain.submit_validated(&replacement),
521            Ok(CommitOutcome::Reorganization { .. })
522        ));
523        let loser = transaction(first_id, 250, 0, owner, b"loser");
524        let loser_id = TxId::for_transaction(&loser);
525        assert!(chain.submit_validated(&loser).is_err());
526        let removed_child = transaction(descendant_id, 0, 4, owner, b"removed-parent");
527        let removed_child_id = TxId::for_transaction(&removed_child);
528        assert!(matches!(
529            chain.submit_validated(&removed_child),
530            Err(SubmitError::MissingParent)
531        ));
532
533        drop(chain);
534        let reopened = CanonicalChain::open(&root.0).unwrap();
535        assert_eq!(reopened.tip(), Some(replacement_id));
536        assert!(!reopened.contains(incumbent_id));
537        assert!(!reopened.contains(descendant_id));
538        drop(reopened);
539
540        let store = TransactionStore::open(&root.0.join("k1-transaction-store")).unwrap();
541        assert!(store.contains(incumbent_id));
542        assert!(store.contains(descendant_id));
543        assert!(!store.contains(loser_id));
544        assert!(!store.contains(removed_child_id));
545    }
546
547    #[test]
548    fn fork_ranking_uses_timestamp_then_complete_digest() {
549        let creator = [1; 32];
550        assert_eq!(
551            fork_decision(&creator, 1, b"x", &creator, 2, b"y"),
552            ForkDecision::Incoming
553        );
554        let left: [u8; 32] = Sha256::digest(b"left").into();
555        let right: [u8; 32] = Sha256::digest(b"right").into();
556        assert_eq!(
557            fork_decision(&creator, 1, b"left", &creator, 1, b"right"),
558            if left < right {
559                ForkDecision::Incoming
560            } else {
561                ForkDecision::Incumbent
562            }
563        );
564    }
565
566    #[test]
567    fn queries_replay_and_removed_cursor_behave_canonically() {
568        let root = TempRoot::new("queries");
569        let mut chain = CanonicalChain::open(&root.0).unwrap();
570        let (a, b) = (subsystem(b'a'), subsystem(b'b'));
571        let mut parent = GENESIS_PARENT;
572        let mut ids = Vec::new();
573
574        for index in 0..140_u64 {
575            let owner = if index % 2 == 0 { a } else { b };
576            let bytes = transaction(parent, 1, index, owner, &[index as u8]);
577            parent = TxId::for_transaction(&bytes);
578            ids.push(parent);
579            chain.submit_validated(&bytes).unwrap();
580        }
581
582        assert_eq!(chain.between_txids(ids[3], ids[10]).unwrap(), ids[4..10]);
583        assert_eq!(
584            chain
585                .between_txids(GENESIS_PARENT, *ids.last().unwrap())
586                .unwrap()
587                .len(),
588            128
589        );
590        assert!(chain.between_txids(ids[10], ids[3]).is_err());
591
592        let mut cursor = chain.replay_cursor(a, Some(ids[0])).unwrap();
593        assert_eq!(chain.replay_next(&mut cursor).unwrap().unwrap().id, ids[2]);
594        let replacement = transaction(ids[0], 0, 999, b, b"replacement");
595        chain.submit_validated(&replacement).unwrap();
596        assert!(chain.replay_next(&mut cursor).is_err());
597    }
598
599    #[test]
600    fn registration_sentinels_are_reserved() {
601        let root = TempRoot::new("sentinels");
602        let chain = CanonicalChain::open(&root.0).unwrap();
603
604        assert_eq!(GENESIS_PARENT.into_bytes(), [0xff; 12]);
605        assert_eq!(
606            REGISTER_AT_TIP.into_bytes(),
607            [
608                0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe
609            ]
610        );
611        assert!(is_reserved_id(GENESIS_PARENT));
612        assert!(is_reserved_id(REGISTER_AT_TIP));
613        assert!(!is_reserved_id(TxId::from_bytes([0; 12])));
614        assert!(!chain.contains(GENESIS_PARENT));
615        assert!(!chain.contains(REGISTER_AT_TIP));
616        assert!(
617            chain
618                .replay_cursor(subsystem(b'a'), Some(REGISTER_AT_TIP))
619                .is_err()
620        );
621        assert!(
622            chain
623                .between_txids(REGISTER_AT_TIP, GENESIS_PARENT)
624                .is_err()
625        );
626    }
627
628    #[test]
629    fn root_shapes_remain_compatible() {
630        let empty = TempRoot::new("empty");
631        fs::create_dir_all(&empty.0).unwrap();
632        fs::write(empty.0.join("extra"), []).unwrap();
633        drop(CanonicalChain::open(&empty.0).unwrap());
634
635        let mixed = TempRoot::new("mixed");
636        fs::create_dir_all(&mixed.0).unwrap();
637        fs::write(mixed.0.join("ordering.dat"), []).unwrap();
638        assert!(CanonicalChain::open(&mixed.0).is_err());
639
640        let malformed = TempRoot::new("malformed");
641        drop(CanonicalChain::open(&malformed.0).unwrap());
642        fs::write(malformed.0.join("ordering.dat"), [0; 31]).unwrap();
643        assert!(CanonicalChain::open(&malformed.0).is_err());
644    }
645
646    #[test]
647    fn opens_million_record_fixture_under_five_seconds() {
648        let root = TempRoot::new("million");
649        drop(CanonicalChain::open(&root.0).unwrap());
650        let count = 1_000_000_u64;
651        let owner = subsystem(b'm');
652        let mut bytes = Vec::with_capacity(count as usize * 32);
653
654        for value in 0..count {
655            let mut id = [0_u8; 12];
656            id[..8].copy_from_slice(&value.to_le_bytes());
657            bytes.extend_from_slice(&id);
658            bytes.extend_from_slice(owner.as_bytes());
659        }
660        fs::write(root.0.join("ordering.dat"), bytes).unwrap();
661
662        let started = Instant::now();
663        let chain = CanonicalChain::open(&root.0).unwrap();
664        assert!(started.elapsed() < Duration::from_secs(5));
665        assert_eq!(chain.order.entries().len(), count as usize);
666        let mut last = [0_u8; 12];
667        last[..8].copy_from_slice(&(count - 1).to_le_bytes());
668        assert!(chain.contains(TxId::from_bytes(last)));
669    }
670}