Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions builder/src/evidence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,175 @@ pub fn write_build_evidence(
}
}

/// Emit evidence v2 for a database built in this invocation. Unlike
/// `attest-existing-layout`, this path has no predecessor and refuses a
/// payload whose params hash was not produced from the scanned v2 layout.
pub fn write_build_evidence_v2(
out_dir: &str,
snapshot: &str,
core_version: &str,
builder_git_commit: &str,
builder_binary: &str,
tee_platform: &str,
tee_image_measurement_hex_or_none: &str,
out_evidence: &str,
) -> ExitCode {
let result = (|| {
let out_dir = Path::new(out_dir);
let snapshot = Path::new(snapshot);
let builder_binary = Path::new(builder_binary);
let out_evidence = Path::new(out_evidence);
let payload_path = out_dir.join("root-bundle-payload.bin");
let payload_bytes = fs::read(&payload_path)
.map_err(|e| format!("failed to read {}: {e}", payload_path.display()))?;
let payload = rootbundle::RootBundlePayload::decode(&payload_bytes)
.map_err(|e| format!("failed to decode {}: {e}", payload_path.display()))?;
if payload.build_kind != rootbundle::BuildKind::Snapshot {
return Err("full-build v2 evidence only supports snapshot payloads".into());
}

let layout = dbpipeline::inspect_existing_onion_layout_v2(out_dir)
.map_err(|e| format!("final Onion layout check failed: {e}"))?;
let layout_fields = BuildEvidence {
version: EVIDENCE_VERSION_V2,
builder_git_commit: String::new(),
builder_binary_sha256: [0u8; 32],
tee_platform: String::new(),
tee_image_measurement: Vec::new(),
core_version: String::new(),
snapshot_sha256: [0u8; 32],
snapshot_bytes: 0,
network_magic: payload.network_magic,
build_kind: payload.build_kind,
from_anchor: payload.from_anchor,
anchor: payload.anchor,
utxo_muhash: payload.utxo_muhash,
dust_threshold_sats: payload.dust_threshold_sats,
max_utxos_per_spk: payload.max_utxos_per_spk,
params_hash: [0u8; 32],
index_bins_per_table: 0,
chunk_bins_per_table: 0,
onion_entry_size: 0,
bucket_super_root: [0u8; 32],
onion_super_root: [0u8; 32],
root_bundle_payload_sha256: [0u8; 32],
signed_root_bundle_sha256: None,
database_manifest_sha256: [0u8; 32],
all_artifacts_manifest_sha256: [0u8; 32],
server_db_manifest_sha256: [0u8; 32],
evidence_mode: 0,
predecessor_evidence_sha256: None,
predecessor_report_sha256: None,
onion_layout_v2: Some(OnionLayoutV2 {
total_packed_entries: layout.total_packed_entries,
index_bins_per_table: layout.index_bins_per_table,
chunk_bins_per_table: layout.chunk_bins_per_table,
}),
}
.with_layout_from_summary(out_dir)?;
if layout_fields.onion_entry_size != layout.entry_size {
return Err(format!(
"build summary Onion entry size {} disagrees with scanner {}",
layout_fields.onion_entry_size, layout.entry_size
));
}
let params = rootbundle::BuildParamsV2::current_snapshot(
layout_fields.index_bins_per_table,
layout_fields.chunk_bins_per_table,
layout.entry_size,
layout.total_packed_entries,
layout.index_bins_per_table,
layout.chunk_bins_per_table,
);
if payload.params_hash != params.params_hash() {
return Err(
"root-bundle payload does not contain the canonical full-build v2 params hash"
.into(),
);
}

let (snapshot_sha256, snapshot_bytes) = sha256_file(snapshot)?;
let (builder_binary_sha256, _) = sha256_file(builder_binary)?;
let root_bundle_payload_sha256 = sha256_bytes(&payload_bytes);
let signed_root_bundle_sha256 =
optional_sha256_file(&out_dir.join("signed-root-bundle.bin"))?;
let database_manifest_sha256 = sha256_file_32(&out_dir.join("database.manifest.sha256"))?;
let all_artifacts_manifest_sha256 =
sha256_file_32(&out_dir.join("all-artifacts.manifest.sha256"))?;
let server_db_manifest_sha256 = sha256_file_32(&out_dir.join("server-db/MANIFEST.toml"))?;
let bucket_super_root = *payload
.root("merkle/bucket/super_root")
.ok_or("root-bundle payload missing merkle/bucket/super_root")?;
let onion_super_root = *payload
.root("merkle/onion/super_root")
.ok_or("root-bundle payload missing merkle/onion/super_root")?;
if onion_super_root != layout.onion_super_root {
return Err("root-bundle payload Onion root disagrees with scanner".into());
}
let evidence = BuildEvidence {
version: EVIDENCE_VERSION_V2,
builder_git_commit: builder_git_commit.to_owned(),
builder_binary_sha256,
tee_platform: tee_platform.to_owned(),
tee_image_measurement: parse_optional_hex_bytes(
tee_image_measurement_hex_or_none,
"tee-image-measurement-hex-or-none",
)?,
core_version: core_version.to_owned(),
snapshot_sha256,
snapshot_bytes,
network_magic: payload.network_magic,
build_kind: payload.build_kind,
from_anchor: payload.from_anchor,
anchor: payload.anchor,
utxo_muhash: payload.utxo_muhash,
dust_threshold_sats: payload.dust_threshold_sats,
max_utxos_per_spk: payload.max_utxos_per_spk,
params_hash: params.params_hash(),
index_bins_per_table: layout_fields.index_bins_per_table,
chunk_bins_per_table: layout_fields.chunk_bins_per_table,
onion_entry_size: layout.entry_size,
bucket_super_root,
onion_super_root,
root_bundle_payload_sha256,
signed_root_bundle_sha256,
database_manifest_sha256,
all_artifacts_manifest_sha256,
server_db_manifest_sha256,
evidence_mode: 0,
predecessor_evidence_sha256: None,
predecessor_report_sha256: None,
onion_layout_v2: Some(OnionLayoutV2 {
total_packed_entries: layout.total_packed_entries,
index_bins_per_table: layout.index_bins_per_table,
chunk_bins_per_table: layout.chunk_bins_per_table,
}),
};
let encoded = evidence.encode()?;
create_new_parent(out_evidence)?;
let mut writer = File::create_new(out_evidence)
.map_err(|e| format!("failed to create {}: {e}", out_evidence.display()))?;
writer
.write_all(&encoded)
.map_err(|e| format!("failed to write {}: {e}", out_evidence.display()))?;
writer
.flush()
.map_err(|e| format!("failed to flush {}: {e}", out_evidence.display()))?;
Ok::<_, String>(evidence)
})();

match result {
Ok(evidence) => {
print_evidence_report(&evidence, Some(out_evidence));
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
}
}

/// Re-seal a completed v1 artifact set as proof v2 without rebuilding the
/// database. The checker scans the final Onion tables and Merkle material;
/// only after those checks pass does it emit a new payload and evidence file.
Expand Down
11 changes: 11 additions & 0 deletions builder/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ fn main() -> ExitCode {
&args[10],
)
}
Some("build-root-bundle-payload-v2") if args.len() == 11 => {
root_payload::build_root_bundle_payload_v2(
&args[2], &args[3], &args[4], &args[5], &args[6], &args[7], &args[8], &args[9],
&args[10],
)
}
Some("build-delta-root-bundle-payload") if args.len() == 12 => {
root_payload::build_delta_root_bundle_payload(
&args[2], &args[3], &args[4], &args[5], &args[6], &args[7], &args[8], &args[9],
Expand All @@ -75,6 +81,9 @@ fn main() -> ExitCode {
Some("write-build-evidence") if args.len() == 10 => evidence::write_build_evidence(
&args[2], &args[3], &args[4], &args[5], &args[6], &args[7], &args[8], &args[9],
),
Some("write-build-evidence-v2") if args.len() == 10 => evidence::write_build_evidence_v2(
&args[2], &args[3], &args[4], &args[5], &args[6], &args[7], &args[8], &args[9],
),
Some("attest-existing-layout") if args.len() == 10 => evidence::attest_existing_layout(
&args[2], &args[3], &args[4], &args[5], &args[6], &args[7], &args[8], &args[9],
),
Expand Down Expand Up @@ -248,9 +257,11 @@ fn usage(bin: &str) {
{bin} build-chunk-cuckoo <utxo_chunks_nodust.bin> <out-chunk_pir_cuckoo.bin> [--anchor <chain-or-delta-anchor.bin>]\n\
{bin} build-bucket-merkle <batch_pir_cuckoo.bin> <chunk_pir_cuckoo.bin> <out-dir> [--root-only]\n\
{bin} build-root-bundle-payload <out-dir> <network-magic-hex> <chain_anchor.bin> <muhash-display-hex> <index-bins-per-table> <chunk-bins-per-table> <onion-entry-size> <issued-at-unix> <out-payload.bin>\n\
{bin} build-root-bundle-payload-v2 <out-dir> <network-magic-hex> <chain_anchor.bin> <muhash-display-hex> <index-bins-per-table> <chunk-bins-per-table> <onion-entry-size> <issued-at-unix> <out-payload.bin>\n\
{bin} build-delta-root-bundle-payload <out-dir> <network-magic-hex> <delta_anchor.bin> <from-muhash-display-hex> <to-muhash-display-hex> <index-bins-per-table> <chunk-bins-per-table> <onion-entry-size> <issued-at-unix> <out-payload.bin>\n\
{bin} write-build-receipt <signed-root-bundle.bin> <snapshot.dat> <core-version> <out-receipt.txt>\n\
{bin} write-build-evidence <out-dir> <snapshot.dat> <core-version> <builder-git-commit> <builder-bin> <tee-platform> <tee-image-measurement-hex-or-none> <out-evidence.bin>\n\
{bin} write-build-evidence-v2 <out-dir> <snapshot.dat> <core-version> <builder-git-commit> <builder-bin> <tee-platform> <tee-image-measurement-hex-or-none> <out-evidence.bin>\n\
{bin} attest-existing-layout <v1-proof-dir> <artifact-dir> <builder-git-commit> <builder-bin> <tee-platform> <tee-image-measurement-hex-or-none> <issued-at-unix> <out-v2-proof-dir>\n\
{bin} inspect-build-evidence <build-evidence.bin>\n\
{bin} verify-build-evidence <build-evidence.bin> [--snapshot <snapshot.dat>] [--builder-bin <path>] [--payload <root-bundle-payload.bin>] [--database-manifest <path>] [--all-artifacts-manifest <path>] [--server-db-manifest <path>] [--expected-muhash <hex>] [--expected-anchor-height <height>] [--expected-anchor-hash <hex>] [--expected-report-data <64-byte-hex>] [--sev-snp-report <report.bin>]\n\
Expand Down
90 changes: 90 additions & 0 deletions builder/src/root_payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,96 @@ pub fn build_root_bundle_payload(
}
}

/// Build a snapshot root payload using the final, scanner-verified Onion v2
/// layout. This is deliberately a separate command from the v1 builder so
/// roots-only and existing v1 production paths keep their byte-level format.
pub fn build_root_bundle_payload_v2(
out_dir: &str,
network_magic_hex: &str,
chain_anchor_path: &str,
muhash_display_hex: &str,
index_bins_per_table: &str,
chunk_bins_per_table: &str,
onion_entry_size: &str,
issued_at: &str,
out_payload: &str,
) -> ExitCode {
let result = (|| {
let network_magic = parse_hex_array::<4>(network_magic_hex, "network-magic-hex")?;
let chain_anchor = rootbundle::ChainAnchor::load(chain_anchor_path)
.map_err(|e| format!("failed to read chain anchor {chain_anchor_path}: {e}"))?;
let index_bins_per_table = parse_u32_arg(index_bins_per_table, "index-bins-per-table")?;
let chunk_bins_per_table = parse_u32_arg(chunk_bins_per_table, "chunk-bins-per-table")?;
let onion_entry_size = parse_u32_arg(onion_entry_size, "onion-entry-size")?;
let issued_at = parse_i64_arg(issued_at, "issued-at-unix")?;
let out_dir = Path::new(out_dir);
let layout = dbpipeline::inspect_existing_onion_layout_v2(out_dir)
.map_err(|e| format!("final Onion layout check failed: {e}"))?;
if layout.entry_size != onion_entry_size {
return Err(format!(
"onion entry size mismatch: requested {onion_entry_size}, scanner found {}",
layout.entry_size
));
}
let mut payload = root_bundle_payload_from_dir(
out_dir,
network_magic,
rootbundle::BuildKind::Snapshot,
rootbundle::ChainAnchor {
block_hash: [0u8; 32],
height: 0,
},
chain_anchor,
muhash_display_hex,
index_bins_per_table,
chunk_bins_per_table,
onion_entry_size,
issued_at,
Vec::new(),
)?;
let params = rootbundle::BuildParamsV2::current_snapshot(
index_bins_per_table,
chunk_bins_per_table,
layout.entry_size,
layout.total_packed_entries,
layout.index_bins_per_table,
layout.chunk_bins_per_table,
);
payload.params_hash = params.params_hash();
let (payload_bytes, payload_sha256) = write_payload_file(&payload, Path::new(out_payload))?;
Ok::<_, String>((payload, layout, payload_bytes, payload_sha256))
})();

match result {
Ok((payload, layout, payload_bytes, payload_sha256)) => {
println!("network_magic={}", hex::encode(payload.network_magic));
println!("anchor_height={}", payload.anchor.height);
println!(
"anchor_hash={}",
display_hash_hex(&payload.anchor.block_hash)
);
println!("muhash={muhash_display_hex}");
println!("params_hash={}", hex::encode(payload.params_hash));
println!("params_version=2");
println!("onion_total_packed_entries={}", layout.total_packed_entries);
println!("onion_index_bins_per_table={}", layout.index_bins_per_table);
println!("onion_chunk_bins_per_table={}", layout.chunk_bins_per_table);
println!("root_entries={}", payload.roots.len());
for root in &payload.roots {
println!("root:{}={}", root.label, hex::encode(root.root));
}
println!("payload_bytes={payload_bytes}");
println!("payload_sha256={}", hex::encode(payload_sha256));
println!("payload_path={out_payload}");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
}
}

pub fn build_delta_root_bundle_payload(
out_dir: &str,
network_magic_hex: &str,
Expand Down
30 changes: 30 additions & 0 deletions dbpipeline/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3377,6 +3377,13 @@ fn verify_existing_tree_tops(
seen_nodes += count;
expected_count = count.div_ceil(arity);
}
// A one-bin tree has no level at or above CACHE_FROM_LEVEL. The
// writer encodes an empty cache; its root is already bound by the
// verified ordered roots file rather than repeated here.
let cache_before_root = levels == 0 && total_nodes == 0 && expected_count == 1;
if cache_before_root {
last_root = Some(*root);
}
if seen_nodes != total_nodes || last_root.as_ref() != Some(root) || expected_count != 1 {
return Err(PipelineError::InvalidExistingOnionLayout(format!(
"tree-top record {tree} totals or root mismatch"
Expand Down Expand Up @@ -5317,6 +5324,29 @@ mod tests {
let _ = std::fs::remove_dir_all(dir);
}

#[test]
fn existing_onion_layout_v2_accepts_only_empty_single_bin_tree_tops() {
let dir = fresh_temp_dir("existing-onion-v2-empty-tree-top");
let path = dir.join(ONION_MERKLE_TREE_TOPS_FILENAME);
let roots = vec![[0xa5u8; MERKLE_HASH_SIZE]; INDEX_K + CHUNK_K];
let mut valid = Vec::new();
valid.extend_from_slice(&((INDEX_K + CHUNK_K) as u32).to_le_bytes());
for _ in &roots {
valid.push(ONION_MERKLE_CACHE_FROM_LEVEL as u8);
valid.extend_from_slice(&0u32.to_le_bytes());
valid.extend_from_slice(&104u16.to_le_bytes());
valid.push(0);
}
std::fs::write(&path, &valid).unwrap();
verify_existing_tree_tops(&path, &roots, 1, 1, 104).unwrap();

let mut malformed = valid;
malformed[5] = 1; // total_nodes=1 with no cached levels is invalid.
std::fs::write(&path, malformed).unwrap();
assert!(verify_existing_tree_tops(&path, &roots, 1, 1, 104).is_err());
let _ = std::fs::remove_dir_all(dir);
}

#[test]
fn existing_onion_layout_v2_checks_final_tables_and_roots() {
let dir = fresh_temp_dir("existing-onion-v2");
Expand Down
Loading
Loading