diff --git a/builder/src/evidence.rs b/builder/src/evidence.rs index 1052208..71987aa 100644 --- a/builder/src/evidence.rs +++ b/builder/src/evidence.rs @@ -378,6 +378,53 @@ pub fn write_build_evidence_v2( tee_platform: &str, tee_image_measurement_hex_or_none: &str, out_evidence: &str, +) -> ExitCode { + write_full_build_evidence_v2( + out_dir, + snapshot, + core_version, + builder_git_commit, + builder_binary, + tee_platform, + tee_image_measurement_hex_or_none, + out_evidence, + rootbundle::BuildKind::Snapshot, + ) +} + +pub fn write_delta_build_evidence_v2( + out_dir: &str, + to_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 { + write_full_build_evidence_v2( + out_dir, + to_snapshot, + core_version, + builder_git_commit, + builder_binary, + tee_platform, + tee_image_measurement_hex_or_none, + out_evidence, + rootbundle::BuildKind::Delta, + ) +} + +fn write_full_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, + expected_build_kind: rootbundle::BuildKind, ) -> ExitCode { let result = (|| { let out_dir = Path::new(out_dir); @@ -389,8 +436,12 @@ pub fn write_build_evidence_v2( .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()); + if payload.build_kind != expected_build_kind { + return Err(format!( + "full-build v2 evidence expected {}, got {}", + build_kind_label(expected_build_kind), + build_kind_label(payload.build_kind) + )); } let layout = dbpipeline::inspect_existing_onion_layout_v2(out_dir) @@ -438,6 +489,8 @@ pub fn write_build_evidence_v2( layout_fields.onion_entry_size, layout.entry_size )); } + verify_layout_anchor_and_seeds(&layout_fields, &layout) + .map_err(|e| format!("final Onion anchor/seed check failed: {e}"))?; let params = rootbundle::BuildParamsV2::current_snapshot( layout_fields.index_bins_per_table, layout_fields.chunk_bins_per_table, diff --git a/builder/src/main.rs b/builder/src/main.rs index b1b2faa..7405d27 100644 --- a/builder/src/main.rs +++ b/builder/src/main.rs @@ -75,6 +75,12 @@ fn main() -> ExitCode { &args[10], &args[11], ) } + Some("build-delta-root-bundle-payload-v2") if args.len() == 12 => { + root_payload::build_delta_root_bundle_payload_v2( + &args[2], &args[3], &args[4], &args[5], &args[6], &args[7], &args[8], &args[9], + &args[10], &args[11], + ) + } Some("write-build-receipt") if args.len() == 6 => { receipt::write_build_receipt(&args[2], &args[3], &args[4], &args[5]) } @@ -84,6 +90,11 @@ fn main() -> ExitCode { 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("write-delta-build-evidence-v2") if args.len() == 10 => { + evidence::write_delta_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], ), @@ -259,9 +270,11 @@ fn usage(bin: &str) { {bin} build-root-bundle-payload \n\ {bin} build-root-bundle-payload-v2 \n\ {bin} build-delta-root-bundle-payload \n\ + {bin} build-delta-root-bundle-payload-v2 \n\ {bin} write-build-receipt \n\ {bin} write-build-evidence \n\ {bin} write-build-evidence-v2 \n\ + {bin} write-delta-build-evidence-v2 \n\ {bin} attest-existing-layout \n\ {bin} inspect-build-evidence \n\ {bin} verify-build-evidence [--snapshot ] [--builder-bin ] [--payload ] [--database-manifest ] [--all-artifacts-manifest ] [--server-db-manifest ] [--expected-muhash ] [--expected-anchor-height ] [--expected-anchor-hash ] [--expected-report-data <64-byte-hex>] [--sev-snp-report ]\n\ diff --git a/builder/src/root_payload.rs b/builder/src/root_payload.rs index 3c415be..834cfa4 100644 --- a/builder/src/root_payload.rs +++ b/builder/src/root_payload.rs @@ -463,6 +463,104 @@ pub fn build_delta_root_bundle_payload( } } +/// Build a delta root payload using the final, scanner-verified Onion v2 +/// layout. Kept separate from the v1 command so existing delta proof output +/// remains byte-compatible unless explicitly upgraded. +pub fn build_delta_root_bundle_payload_v2( + out_dir: &str, + network_magic_hex: &str, + delta_anchor_path: &str, + from_muhash_display_hex: &str, + to_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 delta_anchor = rootbundle::DeltaAnchor::load(delta_anchor_path) + .map_err(|e| format!("failed to read delta anchor {delta_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 from_muhash = parse_muhash_display_hex(from_muhash_display_hex)?; + let mut payload = root_bundle_payload_from_dir( + out_dir, + network_magic, + rootbundle::BuildKind::Delta, + delta_anchor.from, + delta_anchor.to, + to_muhash_display_hex, + index_bins_per_table, + chunk_bins_per_table, + onion_entry_size, + issued_at, + vec![rootbundle::NamedRoot { + label: "input/from-utxo-muhash".to_owned(), + root: from_muhash, + }], + )?; + 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!("build_kind=delta"); + println!("from_anchor_height={}", payload.from_anchor.height); + println!( + "from_anchor_hash={}", + display_hash_hex(&payload.from_anchor.block_hash) + ); + println!("anchor_height={}", payload.anchor.height); + println!( + "anchor_hash={}", + display_hash_hex(&payload.anchor.block_hash) + ); + println!("muhash={to_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) + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/scripts/build-delta-database.sh b/scripts/build-delta-database.sh index 73b1ccd..cd56989 100755 --- a/scripts/build-delta-database.sh +++ b/scripts/build-delta-database.sh @@ -21,6 +21,8 @@ # WRITE_RECEIPT, BIN, SKIP_CARGO_BUILD, RELEASE, ROOTS_ONLY, # WRITE_BUILD_EVIDENCE, BUILDER_GIT_COMMIT, TEE_PLATFORM, # TEE_IMAGE_MEASUREMENT, EMIT_SEV_SNP_QUOTE. +# BUILD_EVIDENCE_VERSION 1 for the legacy/v1-compatible path; 2 for a +# native full-build v2 delta (default 1). set -euo pipefail @@ -307,10 +309,14 @@ stage_server_db() { merkle_bucket_tree_tops.bin onion_chunk_cuckoo.bin onion_data_bin_hashes.bin + onion_index_all.bin onion_index_bin_hashes.bin onion_index_meta.bin + onion_shared_ntt.bin merkle_onion_root.bin merkle_onion_roots.bin + merkle_onion_sib_data.bin + merkle_onion_sib_index.bin merkle_onion_tree_tops.bin ) @@ -324,6 +330,68 @@ stage_server_db() { write_server_db_manifest "$server_dir" } +stage_direct_oram_inputs() { + local out_dir=$1 + local server_dir=$2 + local direct_dir="$out_dir/oram-direct-inputs" + local index_src="$out_dir/utxo_chunks_index_nodust.bin" + local chunk_src="$out_dir/utxo_chunks_nodust.bin" + local index_bytes chunk_bytes index_records chunk_records + + [[ -f "$index_src" ]] || fail "Direct ORAM source missing: $index_src" + [[ -f "$chunk_src" ]] || fail "Direct ORAM source missing: $chunk_src" + [[ ! -e "$direct_dir" ]] || fail "Direct ORAM staging dir already exists: $direct_dir" + mkdir -p "$direct_dir" + link_or_copy "$index_src" "$direct_dir/utxo_chunks_index_nodust.bin" + link_or_copy "$chunk_src" "$direct_dir/utxo_chunks_nodust.bin" + { + printf '%s %s\n' "$(hash_one "$direct_dir/utxo_chunks_index_nodust.bin")" utxo_chunks_index_nodust.bin + printf '%s %s\n' "$(hash_one "$direct_dir/utxo_chunks_nodust.bin")" utxo_chunks_nodust.bin + } > "$direct_dir/direct-inputs.sha256" + chmod 0644 "$direct_dir/direct-inputs.sha256" + + index_bytes=$(wc -c < "$direct_dir/utxo_chunks_index_nodust.bin" | tr -d ' ') + chunk_bytes=$(wc -c < "$direct_dir/utxo_chunks_nodust.bin" | tr -d ' ') + ((index_bytes > 0 && index_bytes % 25 == 0)) || fail "Direct ORAM index must contain 25-byte records" + ((chunk_bytes > 0 && chunk_bytes % 40 == 0)) || fail "Direct ORAM chunk must contain 40-byte records" + index_records=$((index_bytes / 25)) + chunk_records=$((chunk_bytes / 40)) + + local manifest="$server_dir/MANIFEST.toml" + local tmp="$server_dir/.MANIFEST.toml.$$" + grep -qx '\[files\]' "$manifest" || fail "server DB manifest lacks [files] section" + awk \ + -v index_sha256="$(hash_one "$direct_dir/utxo_chunks_index_nodust.bin")" \ + -v index_bytes="$index_bytes" \ + -v index_records="$index_records" \ + -v chunk_sha256="$(hash_one "$direct_dir/utxo_chunks_nodust.bin")" \ + -v chunk_bytes="$chunk_bytes" \ + -v chunk_records="$chunk_records" \ + -v index_slots_per_bin="$DIRECT_ORAM_INDEX_SLOTS_PER_BIN" \ + -v index_hash_fns="$DIRECT_ORAM_INDEX_HASH_FNS" \ + -v index_load_factor_ppb="$DIRECT_ORAM_INDEX_LOAD_FACTOR_PPB" \ + -v index_seed="$DIRECT_ORAM_INDEX_SEED" ' + /^\[files\]$/ { + print "[direct_oram]" + print "version = 1" + print "index_sha256 = \"" index_sha256 "\"" + print "index_bytes = " index_bytes + print "index_records = " index_records + print "chunk_sha256 = \"" chunk_sha256 "\"" + print "chunk_bytes = " chunk_bytes + print "chunk_records = " chunk_records + print "index_slots_per_bin = " index_slots_per_bin + print "index_hash_fns = " index_hash_fns + print "index_load_factor_ppb = " index_load_factor_ppb + print "index_seed = " index_seed + print "" + } + { print } + ' "$manifest" > "$tmp" + mv -f "$tmp" "$manifest" + chmod 0644 "$manifest" +} + diff_manifest_if_requested() { local label=$1 local expected=${2:-} @@ -364,11 +432,14 @@ RELEASE=${RELEASE:-1} SKIP_CARGO_BUILD=${SKIP_CARGO_BUILD:-0} WRITE_RECEIPT=${WRITE_RECEIPT:-auto} ROOTS_ONLY=${ROOTS_ONLY:-1} +RUN_ONION_FFI=${RUN_ONION_FFI:-0} STAGE_SERVER_DB=${STAGE_SERVER_DB:-1} if is_truthy "$ROOTS_ONLY"; then STAGE_SERVER_DB=0 + RUN_ONION_FFI=0 fi WRITE_BUILD_EVIDENCE=${WRITE_BUILD_EVIDENCE:-1} +BUILD_EVIDENCE_VERSION=${BUILD_EVIDENCE_VERSION:-1} BUILDER_GIT_COMMIT=${BUILDER_GIT_COMMIT:-$(current_git_commit)} TEE_PLATFORM=${TEE_PLATFORM:-none} TEE_IMAGE_MEASUREMENT=${TEE_IMAGE_MEASUREMENT:-none} @@ -378,6 +449,22 @@ SERVER_DB_DIR=${SERVER_DB_DIR:-"$OUT_DIR/server-db"} [[ "$ONION_ENTRY_SIZE" =~ ^[1-9][0-9]*$ ]] || fail "ONION_ENTRY_SIZE must be positive" [[ "$ISSUED_AT" =~ ^-?[0-9]+$ ]] || fail "ISSUED_AT must be an integer" +[[ "$BUILD_EVIDENCE_VERSION" == "1" || "$BUILD_EVIDENCE_VERSION" == "2" ]] || + fail "BUILD_EVIDENCE_VERSION must be 1 or 2" +if [[ "$BUILD_EVIDENCE_VERSION" == "2" ]] && is_truthy "$ROOTS_ONLY"; then + fail "BUILD_EVIDENCE_VERSION=2 requires a full delta build, not ROOTS_ONLY=1" +fi +if [[ "$BUILD_EVIDENCE_VERSION" == "2" ]] && ! is_truthy "$RUN_ONION_FFI"; then + fail "BUILD_EVIDENCE_VERSION=2 requires RUN_ONION_FFI=1 for the final scanner-verified server layout" +fi +DIRECT_ORAM_INDEX_SLOTS_PER_BIN=${DIRECT_ORAM_INDEX_SLOTS_PER_BIN:-4} +DIRECT_ORAM_INDEX_HASH_FNS=${DIRECT_ORAM_INDEX_HASH_FNS:-2} +DIRECT_ORAM_INDEX_LOAD_FACTOR_PPB=${DIRECT_ORAM_INDEX_LOAD_FACTOR_PPB:-950000000} +DIRECT_ORAM_INDEX_SEED=${DIRECT_ORAM_INDEX_SEED:-8030603977422561841} +[[ "$DIRECT_ORAM_INDEX_SLOTS_PER_BIN" =~ ^[1-9][0-9]*$ ]] || fail "DIRECT_ORAM_INDEX_SLOTS_PER_BIN must be positive" +[[ "$DIRECT_ORAM_INDEX_HASH_FNS" =~ ^[1-9][0-9]*$ ]] || fail "DIRECT_ORAM_INDEX_HASH_FNS must be positive" +[[ "$DIRECT_ORAM_INDEX_LOAD_FACTOR_PPB" =~ ^[1-9][0-9]*$ ]] || fail "DIRECT_ORAM_INDEX_LOAD_FACTOR_PPB must be positive" +[[ "$DIRECT_ORAM_INDEX_SEED" =~ ^[0-9]+$ ]] || fail "DIRECT_ORAM_INDEX_SEED must be an integer" ensure_empty_or_absent_dir "$OUT_DIR" LOG_DIR="$OUT_DIR/logs" @@ -398,6 +485,8 @@ ENV_FILE="$OUT_DIR/build.env" printf 'onion_entry_size=%s\n' "$ONION_ENTRY_SIZE" printf 'issued_at=%s\n' "$ISSUED_AT" printf 'roots_only=%s\n' "$ROOTS_ONLY" + printf 'run_onion_ffi=%s\n' "$RUN_ONION_FFI" + printf 'build_evidence_version=%s\n' "$BUILD_EVIDENCE_VERSION" printf 'builder_git_commit=%s\n' "$BUILDER_GIT_COMMIT" printf 'tee_platform=%s\n' "$TEE_PLATFORM" printf 'tee_image_measurement=%s\n' "$TEE_IMAGE_MEASUREMENT" @@ -420,6 +509,22 @@ fi [[ -x "$BIN" ]] || fail "pir-attested-builder binary is not executable: $BIN" printf 'builder_bin=%s\n' "$BIN" | tee -a "$SUMMARY" +if is_truthy "$RUN_ONION_FFI"; then + if ! is_truthy "$SKIP_CARGO_BUILD"; then + if is_truthy "$RELEASE"; then + cargo build -q --release -p onionffi --features ffi + ONIONFFI_BIN=${ONIONFFI_BIN:-"$REPO_ROOT/target/release/onionffi"} + else + cargo build -q -p onionffi --features ffi + ONIONFFI_BIN=${ONIONFFI_BIN:-"$REPO_ROOT/target/debug/onionffi"} + fi + else + require_env ONIONFFI_BIN + fi + [[ -x "$ONIONFFI_BIN" ]] || fail "onionffi binary is not executable: $ONIONFFI_BIN" + printf 'onionffi_bin=%s\n' "$ONIONFFI_BIN" | tee -a "$SUMMARY" +fi + run_step 01-materialize-from-utxo-set \ "$BIN" materialize-utxo-set \ "$FROM_SNAPSHOT" \ @@ -543,6 +648,11 @@ if is_truthy "$ROOTS_ONLY"; then fi run_step 12-build-onion-merkle "${onion_merkle_args[@]}" +if is_truthy "$RUN_ONION_FFI"; then + run_step 12b-onionffi-preprocess-all \ + "$ONIONFFI_BIN" preprocess-all "$OUT_DIR" 256 +fi + remove_roots_only_files \ "$OUT_DIR/delta_grouped.bin" \ "$OUT_DIR/onion_index_bin_hashes.bin" \ @@ -557,8 +667,25 @@ chunk_bins=$(kv "$LOG_DIR/07-build-chunk-cuckoo.out" "bins_per_table") [[ -n "$index_bins" ]] || fail "could not parse index bins from 06-build-index-cuckoo.out" [[ -n "$chunk_bins" ]] || fail "could not parse chunk bins from 07-build-chunk-cuckoo.out" +if [[ "$BUILD_EVIDENCE_VERSION" == "2" ]]; then + is_truthy "$STAGE_SERVER_DB" || fail "BUILD_EVIDENCE_VERSION=2 requires STAGE_SERVER_DB=1" + stage_server_db "$OUT_DIR" "$SERVER_DB_DIR" + stage_direct_oram_inputs "$OUT_DIR" "$SERVER_DB_DIR" + printf 'server_db_dir=%s\n' "$SERVER_DB_DIR" | tee -a "$SUMMARY" + printf 'server_db_manifest=%s\n' "$SERVER_DB_DIR/MANIFEST.toml" | tee -a "$SUMMARY" + printf 'server_db_manifest_sha256=%s\n' "$(hash_one "$SERVER_DB_DIR/MANIFEST.toml")" | tee -a "$SUMMARY" + printf 'direct_oram_inputs=%s\n' "$OUT_DIR/oram-direct-inputs" | tee -a "$SUMMARY" +fi + +ROOT_PAYLOAD_COMMAND=build-delta-root-bundle-payload +EVIDENCE_COMMAND=write-build-evidence +if [[ "$BUILD_EVIDENCE_VERSION" == "2" ]]; then + ROOT_PAYLOAD_COMMAND=build-delta-root-bundle-payload-v2 + EVIDENCE_COMMAND=write-delta-build-evidence-v2 +fi + run_step 13-build-delta-root-bundle-payload \ - "$BIN" build-delta-root-bundle-payload \ + "$BIN" "$ROOT_PAYLOAD_COMMAND" \ "$OUT_DIR" \ "$NETWORK_MAGIC" \ "$OUT_DIR/delta_anchor.bin" \ @@ -612,7 +739,7 @@ if is_truthy "$WRITE_RECEIPT"; then "$OUT_DIR/build-receipt.txt" fi -if is_truthy "$STAGE_SERVER_DB"; then +if [[ "$BUILD_EVIDENCE_VERSION" == "1" ]] && is_truthy "$STAGE_SERVER_DB"; then stage_server_db "$OUT_DIR" "$SERVER_DB_DIR" printf 'server_db_dir=%s\n' "$SERVER_DB_DIR" | tee -a "$SUMMARY" printf 'server_db_manifest=%s\n' "$SERVER_DB_DIR/MANIFEST.toml" | tee -a "$SUMMARY" @@ -647,7 +774,7 @@ diff_manifest_if_requested \ if is_truthy "$WRITE_BUILD_EVIDENCE"; then run_step 17-write-build-evidence \ - "$BIN" write-build-evidence \ + "$BIN" "$EVIDENCE_COMMAND" \ "$OUT_DIR" \ "$TO_SNAPSHOT" \ "$CORE_VERSION" \ diff --git a/scripts/local-regtest-e2e.sh b/scripts/local-regtest-e2e.sh index a4cc850..aa81412 100755 --- a/scripts/local-regtest-e2e.sh +++ b/scripts/local-regtest-e2e.sh @@ -302,6 +302,134 @@ if SNAPSHOT="$FIXTURE" \ fi grep -q 'BUILD_EVIDENCE_VERSION=2 requires a full snapshot build' "$LOG_DIR/rejected-roots-only-v2.out" +# Derive a second, syntactically valid regtest snapshot deterministically from +# the committed fixture. The builder recomputes its MuHash below; this keeps a +# non-empty delta fixture local to the E2E rather than committing generated +# chain data. +DELTA_TO_SNAPSHOT="$WORK/txoutset_regtest_112-derived.dat" +node - "$FIXTURE" "$DELTA_TO_SNAPSHOT" <<'NODE' +const fs = require('fs'); +const [from, to] = process.argv.slice(2); +const data = fs.readFileSync(from); +if (data.length < 84) throw new Error('snapshot is too short'); +data[11] ^= 0x5a; // Base block hash: derive a distinct test-only anchor. +data[51] ^= 0xa5; // First outpoint txid: force a deterministic spent/created delta. +fs.writeFileSync(to, data); +NODE + +DELTA_TO_MUHASH=$(run_builder compute-muhash "$DELTA_TO_SNAPSHOT" | awk -F= '$1 == "muhash" {print $2}') +DELTA_TO_ANCHOR_HASH=$(run_builder compute-muhash "$DELTA_TO_SNAPSHOT" | awk -F= '$1 == "base_hash" {print $2}') +[[ "$DELTA_TO_MUHASH" =~ ^[0-9a-f]{64}$ ]] || { + printf 'error: derived snapshot did not produce a MuHash\n' >&2 + exit 1 +} +[[ "$DELTA_TO_MUHASH" != "$EXPECTED_MUHASH" ]] || { + printf 'error: derived snapshot MuHash must differ from from snapshot\n' >&2 + exit 1 +} +[[ "$DELTA_TO_ANCHOR_HASH" != "4e4003e955a41b187ad32e26fc837a98ca284df84ca3bbea19e6d164b3ebb3e7" ]] || { + printf 'error: derived snapshot anchor must differ from from snapshot\n' >&2 + exit 1 +} + +DELTA_DIR="$WORK/full-build-v2-delta" +FROM_SNAPSHOT="$FIXTURE" \ +FROM_EXPECTED_MUHASH="$EXPECTED_MUHASH" \ +FROM_ANCHOR_HEIGHT="$ANCHOR_HEIGHT" \ +TO_SNAPSHOT="$DELTA_TO_SNAPSHOT" \ +TO_EXPECTED_MUHASH="$DELTA_TO_MUHASH" \ +TO_ANCHOR_HEIGHT="$((ANCHOR_HEIGHT + 1))" \ +NETWORK_MAGIC="$NETWORK_MAGIC" \ +CORE_VERSION="$CORE_VERSION" \ +ONION_ENTRY_SIZE="$ONION_ENTRY_SIZE" \ +ISSUED_AT="$ISSUED_AT" \ +OUT_DIR="$DELTA_DIR" \ +BIN="$BIN" \ +SKIP_CARGO_BUILD=1 \ +RUN_ONION_FFI=1 \ +ONIONFFI_BIN="$ONIONFFI_BIN" \ +WRITE_RECEIPT=0 \ +EMIT_SEV_SNP_QUOTE=0 \ +ROOTS_ONLY=0 \ +BUILD_EVIDENCE_VERSION=2 \ +"$REPO_ROOT/scripts/build-delta-database.sh" > "$LOG_DIR/full-build-v2-delta.out" + +for rel in \ + root-bundle-payload.bin \ + build-evidence.bin \ + build-evidence.report-data \ + database.manifest.sha256 \ + all-artifacts.manifest.sha256 \ + server-db/MANIFEST.toml \ + oram-direct-inputs/utxo_chunks_index_nodust.bin \ + oram-direct-inputs/utxo_chunks_nodust.bin \ + oram-direct-inputs/direct-inputs.sha256; do + [[ -s "$DELTA_DIR/$rel" ]] || { + printf 'error: native full-build v2 delta missing staging artifact: %s\n' "$rel" >&2 + exit 1 + } +done + +DELTA_PAYLOAD_LOG="$DELTA_DIR/logs/13-build-delta-root-bundle-payload.out" +DELTA_EVIDENCE_LOG="$DELTA_DIR/logs/17-write-build-evidence.out" +grep -qx 'build_kind=delta' "$DELTA_PAYLOAD_LOG" +grep -qx "from_anchor_height=$ANCHOR_HEIGHT" "$DELTA_PAYLOAD_LOG" +grep -qx "anchor_height=$((ANCHOR_HEIGHT + 1))" "$DELTA_PAYLOAD_LOG" +grep -qx "muhash=$DELTA_TO_MUHASH" "$DELTA_PAYLOAD_LOG" +FROM_MUHASH_INTERNAL=$(node -e 'process.stdout.write(Buffer.from(process.argv[1], "hex").reverse().toString("hex"))' "$EXPECTED_MUHASH") +grep -qx "root:input/from-utxo-muhash=$FROM_MUHASH_INTERNAL" "$DELTA_PAYLOAD_LOG" +grep -qx 'evidence_version=2' "$DELTA_EVIDENCE_LOG" +grep -qx 'build_kind=delta' "$DELTA_EVIDENCE_LOG" +grep -qx 'evidence_mode=full_build' "$DELTA_EVIDENCE_LOG" +grep -qx 'predecessor_evidence_sha256=none' "$DELTA_EVIDENCE_LOG" +grep -qx 'predecessor_report_sha256=none' "$DELTA_EVIDENCE_LOG" + +delta_index_hash=$(hash_one "$DELTA_DIR/oram-direct-inputs/utxo_chunks_index_nodust.bin") +delta_chunk_hash=$(hash_one "$DELTA_DIR/oram-direct-inputs/utxo_chunks_nodust.bin") +delta_index_records=$(($(wc -c < "$DELTA_DIR/oram-direct-inputs/utxo_chunks_index_nodust.bin") / 25)) +delta_chunk_records=$(($(wc -c < "$DELTA_DIR/oram-direct-inputs/utxo_chunks_nodust.bin") / 40)) +((delta_index_records > 0 && delta_chunk_records > 0)) || { + printf 'error: derived delta must produce non-empty Direct ORAM inputs\n' >&2 + exit 1 +} +grep -qx "$delta_index_hash utxo_chunks_index_nodust.bin" "$DELTA_DIR/oram-direct-inputs/direct-inputs.sha256" +grep -qx "$delta_chunk_hash utxo_chunks_nodust.bin" "$DELTA_DIR/oram-direct-inputs/direct-inputs.sha256" +grep -qx "index_sha256 = \"$delta_index_hash\"" "$DELTA_DIR/server-db/MANIFEST.toml" +grep -qx "chunk_sha256 = \"$delta_chunk_hash\"" "$DELTA_DIR/server-db/MANIFEST.toml" +grep -qx "index_records = $delta_index_records" "$DELTA_DIR/server-db/MANIFEST.toml" +grep -qx "chunk_records = $delta_chunk_records" "$DELTA_DIR/server-db/MANIFEST.toml" + +run_builder verify-build-evidence \ + "$DELTA_DIR/build-evidence.bin" \ + --snapshot "$DELTA_TO_SNAPSHOT" \ + --builder-bin "$BIN" \ + --payload "$DELTA_DIR/root-bundle-payload.bin" \ + --database-manifest "$DELTA_DIR/database.manifest.sha256" \ + --all-artifacts-manifest "$DELTA_DIR/all-artifacts.manifest.sha256" \ + --server-db-manifest "$DELTA_DIR/server-db/MANIFEST.toml" \ + --expected-muhash "$DELTA_TO_MUHASH" \ + --expected-anchor-height "$((ANCHOR_HEIGHT + 1))" \ + --expected-anchor-hash "$DELTA_TO_ANCHOR_HASH" \ + > "$LOG_DIR/full-build-v2-delta.verify-evidence.out" + +if FROM_SNAPSHOT="$FIXTURE" \ + FROM_EXPECTED_MUHASH="$EXPECTED_MUHASH" \ + FROM_ANCHOR_HEIGHT="$ANCHOR_HEIGHT" \ + TO_SNAPSHOT="$DELTA_TO_SNAPSHOT" \ + TO_EXPECTED_MUHASH="$DELTA_TO_MUHASH" \ + TO_ANCHOR_HEIGHT="$((ANCHOR_HEIGHT + 1))" \ + NETWORK_MAGIC="$NETWORK_MAGIC" \ + OUT_DIR="$WORK/rejected-delta-roots-only-v2" \ + BIN="$BIN" \ + SKIP_CARGO_BUILD=1 \ + ROOTS_ONLY=1 \ + BUILD_EVIDENCE_VERSION=2 \ + "$REPO_ROOT/scripts/build-delta-database.sh" > "$LOG_DIR/rejected-delta-roots-only-v2.out" 2>&1; then + printf 'error: roots-only delta mode must not claim native full-build v2 evidence\n' >&2 + exit 1 +fi +grep -q 'BUILD_EVIDENCE_VERSION=2 requires a full delta build' "$LOG_DIR/rejected-delta-roots-only-v2.out" + printf 'status=ok\n' printf 'fixture=%s\n' "$FIXTURE" printf 'muhash=%s\n' "$EXPECTED_MUHASH" @@ -314,3 +442,4 @@ printf 'bundle_sha256=%s\n' "$bundle_sha256" printf 'receipt_sha256=%s\n' "$receipt_sha256" printf 'manifest_sha256=%s\n' "$manifest_sha256" printf 'native_full_build_v2=%s\n' "$V2_DIR" +printf 'native_full_build_v2_delta=%s\n' "$DELTA_DIR"