diff --git a/README.md b/README.md index 3c16e12..3efacd5 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,8 @@ Implemented and verified against the Go peer, byte for byte: | `CombinedTreeProof` + `SearchResponse` (`kt-wire::proofs`, `kt-wire::responses`) | §12.3, §13.1 | [`search.json`](interop/vectors/search.json) — 10 responses from a running log | | Greatest-version and fixed-version search (`kt-tree::combined`) | §6.3, §7.1, §7.2, §12.3 | [`search.json`](interop/vectors/search.json) — all 12 replayed, each consuming its proof exactly | | Monitoring response structures (`kt-wire::responses`) | §13.2–§13.4 | [`monitor.json`](interop/vectors/monitor.json) — contact, owner init, owner monitor | -| Contact monitoring (`kt-tree::combined`) | §8.2, §12.3.4 | [`monitor.json`](interop/vectors/monitor.json) — 3 replayed, each consuming its proof exactly | +| Contact monitoring (`kt-tree::combined`) | §8.2, §12.3.4 | [`monitor.json`](interop/vectors/monitor.json) — 4 replayed, each consuming its proof exactly | +| Owner initialization (`kt-tree::combined`) | §8.3, §12.3.5 | [`monitor.json`](interop/vectors/monitor.json) — replayed, consuming its proof exactly | **Refusing what the peer refuses.** [`tampered.json`](interop/vectors/tampered.json) holds 18 proofs and openings that diff --git a/crates/kt-tree/src/combined.rs b/crates/kt-tree/src/combined.rs index 8148df7..608875d 100644 --- a/crates/kt-tree/src/combined.rs +++ b/crates/kt-tree/src/combined.rs @@ -39,6 +39,7 @@ use crate::{distinguished, ibst, ladder, prefix}; use alloc::collections::BTreeMap; +use alloc::vec; use alloc::vec::Vec; use kt_crypto::suite::CipherSuite; use kt_wire::proofs::{CombinedTreeProof, PrefixProof, PrefixSearchResult}; @@ -2654,3 +2655,619 @@ mod monitor_tests { assert!(core::error::Error::source(&errors[0]).is_none()); } } + +/// What owner initialization established (§8.3's first algorithm). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Initialized { + /// The entries inspected, left to right, with the prefix tree root each proof computed. + pub inspected: Vec<(u64, HashValue)>, + /// The greatest version the owner should now consider itself to hold, if the label existed at + /// the starting position. + pub greatest: Option, +} + +/// Runs §8.3's first algorithm: a label owner initializing state at a distinguished entry. +/// +/// An owner is in a different position from a searcher or a contact. A searcher asks about one +/// version; an owner is claiming the label, and has to establish what the log thinks the *whole +/// history* looks like as of the entry it is starting from. So the response carries a greatest +/// version per inspected entry rather than one ladder, and this checks that those versions +/// descend as the entries go back in time — a label's version count can only grow, so seeing it +/// grow backwards means the log is lying about one of them. +/// +/// `start` is the requested starting position, which §13.3 requires to be unexpired and +/// distinguished. `greatest_versions` is the response's list, which may be *shorter* than the +/// list of entries inspected: §13.3 says it ends "at the first log entry where the label doesn't +/// exist", and entries past that are searched for version zero instead. +/// +/// The ladders here are full: §8.3 step 5 says "without omitting redundant lookups", unlike §6.3 +/// and §7.2. An owner is establishing history rather than locating one version, so there is +/// nothing for a later ladder to lean on. +/// +/// # Errors +/// +/// [`InitError`] where the proof does not establish what §8.3 requires. +pub fn owner_init( + suite: CipherSuite, + size: u64, + lifetime: u64, + start: u64, + greatest_versions: &[u32], + keys: &BTreeMap, + reader: &mut Reader<'_>, +) -> core::result::Result { + let last = size.saturating_sub(1); + let rightmost = reader.timestamp(last)?; + + // §12.3.5: "the timestamp of each log entry on the path from the root to the user's requested + // starting position". The timestamps are what let both sides check that the start is + // unexpired and distinguished, which is the precondition the whole operation rests on. + let mut chain = ibst::direct_path(start, size)?; + chain.reverse(); + for entry in &chain { + reader.timestamp(*entry)?; + } + let start_timestamp = reader.timestamp(start)?; + if expired(lifetime, start_timestamp, rightmost) { + return Err(InitError::StartExpired { + start, + timestamp: start_timestamp, + rightmost, + }); + } + + // Step 1: the starting position, then the entries on its direct path and to its left, ending + // just before the first expired one. Left, because an owner is looking *backwards* through + // the history it is adopting — the opposite direction from contact monitoring, which follows + // its versions forward. + let mut list = vec![start]; + let mut path = ibst::direct_path(start, size)?; + path.retain(|entry| *entry < start); + path.sort_unstable_by_key(|entry| core::cmp::Reverse(*entry)); + for entry in path { + let timestamp = reader.timestamp(entry)?; + if expired(lifetime, timestamp, rightmost) { + break; + } + list.push(entry); + } + + // Step 2's check: the greatest version cannot grow as the entries go back in time. + for pair in greatest_versions.windows(2) { + if let [earlier, later] = pair { + if later > earlier { + return Err(InitError::VersionsNotDescending { + earlier: *earlier, + later: *later, + }); + } + } + } + if greatest_versions.len() > list.len() { + return Err(InitError::TooManyVersions { + supplied: greatest_versions.len(), + inspected: list.len(), + }); + } + + // Step 5: one full ladder per inspected entry, targeting that entry's greatest version — or + // zero where the label did not exist there, which is what a list shorter than the entries + // means. + let mut inspected = Vec::new(); + for (index, entry) in list.iter().enumerate() { + let target = greatest_versions.get(index).copied().unwrap_or(0); + let versions = ladder::search_binary_ladder(target, target, &[], &[])?; + let proof = reader.prefix_proof(*entry)?; + let used = versions + .get(..proof.results.len()) + .ok_or(InitError::LadderLength { + position: *entry, + expected: versions.len(), + actual: proof.results.len(), + })?; + + let ordering = ladder::interpret_search_ladder(used, target, &proof.results)?; + let exists = greatest_versions.get(index).is_some(); + // A ladder for an entry where the label exists must place the greatest version *at* the + // claim. Where it does not exist, version zero must be absent — that is what "no value is + // provided" in step 2 has to mean for the proof to say anything. + let expected = if exists { + core::cmp::Ordering::Equal + } else { + core::cmp::Ordering::Less + }; + if ordering != expected { + return Err(InitError::LadderInconsistent { + position: *entry, + claimed: target, + exists, + }); + } + + let mut entries = Vec::new(); + for (version, result) in used.iter().zip(proof.results.iter()) { + let key = keys.get(version).ok_or(InitError::MissingLadderKey { + position: *entry, + version: *version, + })?; + entries.push(if result.is_inclusion() { + prefix::SearchEntry::included( + key.vrf_output, + key.commitment.ok_or(InitError::MissingCommitment { + position: *entry, + version: *version, + })?, + ) + } else { + prefix::SearchEntry::absent(key.vrf_output) + }); + } + let root = prefix::evaluate(suite, &entries, proof)?; + reader.establish_root(*entry, root)?; + inspected.push((*entry, root)); + } + + Ok(Initialized { + inspected, + greatest: greatest_versions.first().copied(), + }) +} + +/// Why owner initialization rejected a proof (§8.3). +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum InitError { + /// The requested starting position is expired, which §13.3 forbids. + StartExpired { + /// The position asked for. + start: u64, + /// Its timestamp. + timestamp: u64, + /// The rightmost entry's timestamp, which expiry is relative to. + rightmost: u64, + }, + /// The greatest versions do not descend (§8.3 step 2, §13.3 step 1). + /// + /// A label's version count only grows, so a later entry in this list — which goes backwards + /// through the log — cannot hold a greater version than an earlier one. + VersionsNotDescending { + /// The version at the earlier position in the list. + earlier: u32, + /// The greater version that followed it. + later: u32, + }, + /// More greatest versions were supplied than there are entries to inspect (§13.3 step 1). + TooManyVersions { + /// How many the response carried. + supplied: usize, + /// How many entries §8.3 step 1 computes. + inspected: usize, + }, + /// An entry's proof carried more results than its ladder specifies. + LadderLength { + /// The log entry. + position: u64, + /// How many lookups the ladder specifies. + expected: usize, + /// How many the proof carried. + actual: usize, + }, + /// An entry's ladder does not place the greatest version where the response claims. + LadderInconsistent { + /// The log entry. + position: u64, + /// The version claimed greatest there, or zero where the label was claimed absent. + claimed: u32, + /// Whether the response claimed the label existed at this entry. + exists: bool, + }, + /// No search key was available for a version the ladder looks up. + MissingLadderKey { + /// The log entry. + position: u64, + /// The version. + version: u32, + }, + /// No commitment was available for a version the ladder proves present. + MissingCommitment { + /// The log entry. + position: u64, + /// The version. + version: u32, + }, + /// The proof could not be read as §12.3 requires. + Proof(Error), + /// A prefix tree proof did not evaluate. + Prefix(prefix::Error), + /// The search tree could not be navigated. + Ibst(ibst::Error), + /// A ladder could not be computed. + Ladder(ladder::Error), +} + +impl From for InitError { + fn from(err: Error) -> Self { + Self::Proof(err) + } +} + +impl From for InitError { + fn from(err: prefix::Error) -> Self { + Self::Prefix(err) + } +} + +impl From for InitError { + fn from(err: ibst::Error) -> Self { + Self::Ibst(err) + } +} + +impl From for InitError { + fn from(err: ladder::Error) -> Self { + Self::Ladder(err) + } +} + +impl core::fmt::Display for InitError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::StartExpired { + start, + timestamp, + rightmost, + } => write!( + f, + "the requested starting entry {start} is expired: its timestamp {timestamp} \ + against a rightmost of {rightmost}" + ), + Self::VersionsNotDescending { earlier, later } => write!( + f, + "the greatest versions do not descend: {later} follows {earlier}, but a label's \ + version count only grows" + ), + Self::TooManyVersions { + supplied, + inspected, + } => write!( + f, + "the response carried {supplied} greatest versions for {inspected} inspected \ + entries" + ), + Self::LadderLength { + position, + expected, + actual, + } => write!( + f, + "log entry {position}'s ladder should have at most {expected} lookups, the proof \ + has {actual}" + ), + Self::LadderInconsistent { + position, + claimed, + exists, + } => { + if *exists { + write!( + f, + "log entry {position}'s ladder does not show version {claimed} as the \ + greatest that exists" + ) + } else { + write!( + f, + "log entry {position} is claimed not to hold the label, but its ladder \ + does not show version 0 absent" + ) + } + } + Self::MissingLadderKey { position, version } => write!( + f, + "no search key for version {version}, looked up at log entry {position}" + ), + Self::MissingCommitment { position, version } => write!( + f, + "no commitment for version {version}, proven present at log entry {position}" + ), + Self::Proof(err) => write!(f, "reading the proof: {err}"), + Self::Prefix(err) => write!(f, "evaluating a prefix tree proof: {err}"), + Self::Ibst(err) => write!(f, "walking the search tree: {err}"), + Self::Ladder(err) => write!(f, "computing a binary ladder: {err}"), + } + } +} + +impl core::error::Error for InitError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Self::Proof(err) => Some(err), + Self::Prefix(err) => Some(err), + Self::Ibst(err) => Some(err), + Self::Ladder(err) => Some(err), + _ => None, + } + } +} + +#[cfg(test)] +#[allow( + clippy::arithmetic_side_effects, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used, + reason = "tests fail loudly by panicking; the lints protect the library paths" +)] +mod init_tests { + use super::search_tests::{commitment_for, key_for, tree_at}; + use super::*; + use kt_wire::proofs::InclusionProof; + + const SUITE: CipherSuite = CipherSuite::Kt128Sha256Ed25519; + + fn keys_upto(greatest: u32) -> BTreeMap { + let mut keys = BTreeMap::new(); + for version in 0..=greatest.saturating_add(16) { + keys.insert( + version, + LadderKey { + vrf_output: key_for(version), + commitment: (version <= greatest).then(|| commitment_for(version)), + }, + ); + } + keys + } + + fn stamps(size: u64) -> Vec { + (0..size).map(|i| 1_700_000_000_000 + i).collect() + } + + /// The list §8.3 step 1 computes, and the proof a log would serve for it. + fn build_init( + size: u64, + start: u64, + lifetime: u64, + stamps: &[u64], + ) -> (CombinedTreeProof, Vec) { + let last = size - 1; + let rightmost = stamps[last as usize]; + let mut timestamps: Vec = Vec::new(); + let mut seen: Vec = Vec::new(); + let push = |entry: u64, timestamps: &mut Vec, seen: &mut Vec| { + if !seen.contains(&entry) { + timestamps.push(stamps[entry as usize]); + seen.push(entry); + } + }; + + // The view update supplies the frontier's timestamps first. + for entry in ibst::frontier(size).unwrap() { + push(entry, &mut timestamps, &mut seen); + } + // §12.3.5: the path from the root to the starting position. + let mut chain = ibst::direct_path(start, size).unwrap(); + chain.reverse(); + for entry in &chain { + push(*entry, &mut timestamps, &mut seen); + } + push(start, &mut timestamps, &mut seen); + + // Step 1's list, and the timestamps the walk reads deciding where it ends. + let mut list = vec![start]; + let mut path = ibst::direct_path(start, size).unwrap(); + path.retain(|entry| *entry < start); + path.sort_unstable_by_key(|entry| core::cmp::Reverse(*entry)); + for entry in path { + push(entry, &mut timestamps, &mut seen); + if expired(lifetime, stamps[entry as usize], rightmost) { + break; + } + list.push(entry); + } + + // In this log version `v` was added at entry `v`, so the greatest version at an entry is + // its own index — which descends as the list goes back in time, as step 2 requires. + let greatest_versions: Vec = list + .iter() + .map(|entry| u32::try_from(*entry).unwrap()) + .collect(); + + let mut proofs = Vec::new(); + for (index, entry) in list.iter().enumerate() { + let target = greatest_versions[index]; + let versions = ladder::search_binary_ladder(target, target, &[], &[]).unwrap(); + let searches: Vec = versions.iter().map(|v| key_for(*v)).collect(); + proofs.push(tree_at(*entry).prove(SUITE, &searches).unwrap()); + } + + let mut owed: Vec = seen + .iter() + .copied() + .filter(|entry| !list.contains(entry)) + .collect(); + owed.sort_unstable(); + let prefix_roots = owed + .iter() + .map(|entry| tree_at(*entry).root(SUITE)) + .collect(); + + ( + CombinedTreeProof { + timestamps, + prefix_proofs: proofs, + prefix_roots, + inclusion: InclusionProof::new(Vec::new()), + }, + greatest_versions, + ) + } + + /// An owner adopting the history at entry 5 of a seven-entry log. Step 1's list is the start + /// plus its ancestors to the *left* — backwards through the history being adopted, which is + /// the opposite direction from contact monitoring. + #[test] + fn an_owner_initializes_and_consumes_the_proof_exactly() { + let size = 7_u64; + let start = 5_u64; + let clock = stamps(size); + let (proof, versions) = build_init(size, start, 0, &clock); + assert!(versions.len() > 1, "the list should reach past the start"); + assert!( + versions.windows(2).all(|pair| pair[0] >= pair[1]), + "descending: {versions:?}" + ); + + let retained = Retained::none(); + let mut reader = Reader::new(&proof, &retained); + for entry in ibst::frontier(size).unwrap() { + reader.timestamp(entry).unwrap(); + } + let initialized = + owner_init(SUITE, size, 0, start, &versions, &keys_upto(6), &mut reader).unwrap(); + assert_eq!(initialized.greatest, Some(5)); + assert_eq!(initialized.inspected.len(), versions.len()); + for position in reader.entries_owed_roots() { + reader.prefix_root(position).unwrap(); + } + reader.finish().unwrap(); + } + + /// Step 2's ordering check. A version count only grows, so a list going backwards through the + /// log cannot rise — a log claiming otherwise is lying about one of the two entries. + #[test] + fn greatest_versions_must_descend() { + let size = 7_u64; + let start = 5_u64; + let clock = stamps(size); + let (proof, _) = build_init(size, start, 0, &clock); + + let retained = Retained::none(); + let mut reader = Reader::new(&proof, &retained); + for entry in ibst::frontier(size).unwrap() { + reader.timestamp(entry).unwrap(); + } + assert_eq!( + owner_init(SUITE, size, 0, start, &[3, 5], &keys_upto(6), &mut reader), + Err(InitError::VersionsNotDescending { + earlier: 3, + later: 5 + }) + ); + } + + /// §13.3 step 1: no more versions than there are entries to inspect. + #[test] + fn more_versions_than_entries_is_refused() { + let size = 7_u64; + let start = 5_u64; + let clock = stamps(size); + let (proof, _) = build_init(size, start, 0, &clock); + + let retained = Retained::none(); + let mut reader = Reader::new(&proof, &retained); + for entry in ibst::frontier(size).unwrap() { + reader.timestamp(entry).unwrap(); + } + let refused = owner_init( + SUITE, + size, + 0, + start, + &[5, 4, 3, 2, 1, 0], + &keys_upto(6), + &mut reader, + ); + assert!( + matches!(refused, Err(InitError::TooManyVersions { .. })), + "expected too many versions, got {refused:?}" + ); + } + + /// §13.3 requires the starting position to be unexpired, and it is the one precondition a + /// client can check for itself rather than taking on trust. + #[test] + fn an_expired_starting_position_is_refused() { + let size = 7_u64; + let start = 5_u64; + let day = 86_400_000_u64; + let clock: Vec = (0..size).map(|i| 1_700_000_000_000 + i * day).collect(); + let lifetime = day; // entry 5 is a day behind entry 6, so exactly at the boundary + let (proof, versions) = build_init(size, start, lifetime, &clock); + + let retained = Retained::none(); + let mut reader = Reader::new(&proof, &retained); + for entry in ibst::frontier(size).unwrap() { + reader.timestamp(entry).unwrap(); + } + assert!(matches!( + owner_init( + SUITE, + size, + lifetime, + start, + &versions, + &keys_upto(6), + &mut reader + ), + Err(InitError::StartExpired { .. }) + )); + } + + #[test] + fn init_errors_describe_themselves() { + use alloc::string::ToString; + let errors = [ + InitError::StartExpired { + start: 5, + timestamp: 1, + rightmost: 9, + }, + InitError::VersionsNotDescending { + earlier: 1, + later: 2, + }, + InitError::TooManyVersions { + supplied: 3, + inspected: 2, + }, + InitError::LadderLength { + position: 5, + expected: 2, + actual: 3, + }, + InitError::LadderInconsistent { + position: 5, + claimed: 3, + exists: true, + }, + InitError::LadderInconsistent { + position: 5, + claimed: 0, + exists: false, + }, + InitError::MissingLadderKey { + position: 5, + version: 1, + }, + InitError::MissingCommitment { + position: 5, + version: 1, + }, + InitError::from(Error::Exhausted { + array: "timestamps", + position: 5, + }), + InitError::from(prefix::Error::DepthOverflow { depth: 256 }), + InitError::from(ibst::Error::EmptyLog), + InitError::from(ladder::Error::UnrepresentableRung { + rung: 1 << 32, + greatest: u32::MAX, + }), + ]; + for error in &errors { + assert!(!error.to_string().is_empty(), "{error:?}"); + } + assert!(core::error::Error::source(&errors[8]).is_some()); + assert!(core::error::Error::source(&errors[0]).is_none()); + } +} diff --git a/docs/interop.md b/docs/interop.md index d9e46ad..6ceed4b 100644 --- a/docs/interop.md +++ b/docs/interop.md @@ -112,7 +112,7 @@ disagrees: Steps 1 through 6 are done, step 7 apart from the combined tree, step 9, and §6.1 out of step 8. -Eighteen vector files pass from the Rust side — 6696 checks across 787 cases — and +Eighteen vector files pass from the Rust side — 6697 checks across 787 cases — and `from-kt.json` runs the other way: 209 artifacts built by the Rust side, 109 of which katie must accept and 100 of which it must reject. "Agrees" here means a committed vector asserts it, not that the two implementations were eyeballed. @@ -181,6 +181,25 @@ visits. It does not have to. §6.1 brackets a node by the timestamps of the entr of it in the search tree, and a descent maintains exactly those bounds as it goes — so distinguishedness along a search path is decidable from the path itself. +### An owner looks backwards, everyone else looks forward + +§8.3's first algorithm is the one operation that walks *left*. A searcher descends toward a +version, contact monitoring follows its versions up and to the right as new nodes are built over +them, and an auditor moves forward one entry at a time. An owner adopting a label is doing +something else: it is accepting a history it has not seen, so step 1's list is the starting +position followed by "the log entries that are on the starting position's direct path and to its +left, ending just before the first expired log entry". + +Two consequences fall out of that direction. The ladders are full — step 5 says "without omitting +redundant lookups", unlike §6.3 and §7.2 — because there is no earlier entry for a later ladder to +lean on when the walk is going backwards. And the response carries a greatest version *per entry* +rather than one target, with §8.3 step 2 requiring each to be no greater than the one before: a +label's version count only grows, so a list running backwards through the log that rises is a log +lying about one of the two entries. That check has no analogue in any other operation. + +`monitor.json`'s `owner-init` case replays and consumes its proof exactly, and the version +ordering, the expired-start refusal and the too-many-versions case are covered by Rust tests. + ### Monitoring mostly does nothing, which is the part to get right §8.2 moves a user's map up the tree as new intermediate nodes are built over the versions they diff --git a/interop/report/src/check.rs b/interop/report/src/check.rs index 30dcda4..5c01bf0 100644 --- a/interop/report/src/check.rs +++ b/interop/report/src/check.rs @@ -1467,6 +1467,13 @@ fn monitor_suite(dir: &Path) -> Result { replay_contact_monitor(case, &bytes, mode).unwrap_or_else(|detail| detail), )); } + if case.input.operation == "owner-init" { + checks.push(Check::new( + "replaying §8.3's first algorithm consumes the proof exactly (§12.3.5)", + "every element read, none left over", + replay_owner_init(case, &bytes, mode, suite).unwrap_or_else(|detail| detail), + )); + } cases.push(Case { name: name.to_owned(), @@ -1603,6 +1610,106 @@ fn replay_contact_monitor( .map_err(|err| format!("§12.3: {err}")) } +/// Replays §8.3's first algorithm over a recorded owner initialization response. +/// +/// Unlike a monitoring response, this one carries its own ladder — an owner is adopting a history +/// it has not seen, so the log has to supply the VRF proofs and commitments for every version +/// involved. §13.3 step 2 requires a commitment for each version in `greatest_versions`, and +/// notes that "the existence of a version does not require the existence of all lesser versions", +/// so the commitments are not a prefix of the ladder. +fn replay_owner_init( + case: &crate::vectors::Case, + bytes: &[u8], + mode: DeploymentMode, + suite: CipherSuite, +) -> Result { + let mut dec = Decoder::new(bytes); + let response = OwnerInitResponse::decode_with(&mut dec, mode, suite.np()) + .map_err(|err| format!("decode failed: {err}"))?; + + let label = hex::decode(&case.input.label).map_err(|err| format!("label: {err}"))?; + let vrf_key = + hex::decode(&case.input.vrf_public_key).map_err(|err| format!("vrf key: {err}"))?; + let vrf_key = <[u8; 32]>::try_from(vrf_key.as_slice()) + .map_err(|_| "the VRF public key is not 32 bytes".to_owned()) + .and_then(|value| vrf::PublicKey::from_bytes(value).map_err(|err| err.to_string()))?; + + // §8.3 step 3: the ladder covers version zero and every version a search ladder for any of + // the greatest versions would look up. Recovering which version each step is for means + // reproducing that set in the same order the log built it — ascending, per §13.3. + let mut wanted: Vec = vec![0]; + for greatest in &response.greatest_versions { + for version in ladder::search_binary_ladder(*greatest, *greatest, &[], &[]) + .map_err(|err| format!("ladder for version {greatest}: {err}"))? + { + if !wanted.contains(&version) { + wanted.push(version); + } + } + } + wanted.sort_unstable(); + if wanted.len() != response.binary_ladder.len() { + return Err(format!( + "the response's ladder has {} steps; §8.3 step 3 calls for {} ({wanted:?})", + response.binary_ladder.len(), + wanted.len() + )); + } + + let mut keys = BTreeMap::new(); + for (version, step) in wanted.iter().zip(response.binary_ladder.iter()) { + let input = kt_wire::structs::VrfInput { + label: label.clone(), + version: *version, + }; + let proof = vrf::Proof::from_slice(&step.proof) + .map_err(|err| format!("version {version}: {err}"))?; + let output = vrf_key + .verify(suite, &input, &proof) + .map_err(|err| format!("version {version}: the VRF proof does not verify: {err}"))?; + keys.insert( + *version, + combined::LadderKey { + vrf_output: output.search_key(), + commitment: step.commitment, + }, + ); + } + + let size = case.expect.tree_size; + let retained = combined::Retained::none(); + let mut reader = combined::Reader::new(&response.init, &retained); + + // §12.3.1's view update first, as everywhere else. + for position in ibst::frontier(size).map_err(|err| format!("frontier: {err}"))? { + reader + .timestamp(position) + .map_err(|err| format!("view update at entry {position}: {err}"))?; + } + + let initialized = combined::owner_init( + suite, + size, + case.input.maximum_lifetime, + case.input.start, + &response.greatest_versions, + &keys, + &mut reader, + ) + .map_err(|err| format!("§8.3: {err}"))?; + let _ = initialized; + + for position in reader.entries_owed_roots() { + reader + .prefix_root(position) + .map_err(|err| format!("prefix root for entry {position}: {err}"))?; + } + reader + .finish() + .map(|()| "every element read, none left over".to_owned()) + .map_err(|err| format!("§12.3: {err}")) +} + /// §6.1: which log entries are distinguished. fn distinguished_suite(dir: &Path) -> Result { const FILE: &str = "distinguished.json"; diff --git a/interop/report/src/report.rs b/interop/report/src/report.rs index 1d9c195..61d5fd2 100644 --- a/interop/report/src/report.rs +++ b/interop/report/src/report.rs @@ -513,9 +513,15 @@ pub fn coverage_table() -> Vec { "kt-tree::combined", &["monitor.json"], ), + verified( + "§8.3 first algorithm, §12.3.5", + "Owner initialization: adopting a label's history at a distinguished entry", + "kt-tree::combined", + &["monitor.json"], + ), todo( - "§8.3, §12.3.5–§12.3.6", - "Owner initialization and owner monitoring algorithms", + "§8.3 second algorithm, §12.3.6", + "Owner monitoring: the recursive walk over recent distinguished entries", ), todo("§9, §13.5", "Updating a label"), todo(