diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2a80190 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + rootbundle: + name: rootbundle compatibility + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo test --locked -p rootbundle --all-targets + + workspace: + name: workspace tests + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo test --locked --workspace --all-targets diff --git a/rootbundle/COMPATIBILITY.md b/rootbundle/COMPATIBILITY.md new file mode 100644 index 0000000..6babdfd --- /dev/null +++ b/rootbundle/COMPATIBILITY.md @@ -0,0 +1,18 @@ +# Root bundle compatibility policy + +`rootbundle` is a wire-format and trust-boundary crate. Changes to its +canonical payload, signing preimage, decoder, or quorum semantics require: + +1. updating the checked-in vectors under `testdata/` with the + `golden_vector` example; +2. explaining why existing production bundle bytes remain accepted, or + introducing a new explicitly versioned format and signing domain; +3. passing both the rootbundle compatibility job and the complete workspace + test job; and +4. cutting a protected `rootbundle-vMAJOR.MINOR.PATCH` tag before downstream + consumers move their exact git revision. + +The seeds used by the golden vector are public test constants. They are not +builder credentials. Downstream compatibility tests should verify the exact +payload and signed-bundle hex from this directory before removing any local +copy of the implementation. diff --git a/rootbundle/examples/golden_vector.rs b/rootbundle/examples/golden_vector.rs new file mode 100644 index 0000000..9959fda --- /dev/null +++ b/rootbundle/examples/golden_vector.rs @@ -0,0 +1,55 @@ +use ed25519_dalek::SigningKey; +use rootbundle::{ + sign_root_bundle, BuildKind, BuildParamsV2, ChainAnchor, NamedRoot, + RootBundlePayload, SignedRootBundle, +}; + +fn main() { + let params = BuildParamsV2::current_snapshot( + 565_684, 1_064_454, 3_328, 815_432, 612_345, 1_345_678, + ); + let payload = RootBundlePayload { + network_magic: [0xf9, 0xbe, 0xb4, 0xd9], + build_kind: BuildKind::Snapshot, + from_anchor: ChainAnchor { + block_hash: [0; 32], + height: 0, + }, + anchor: ChainAnchor { + block_hash: [0xab; 32], + height: 950_000, + }, + utxo_muhash: [0xcd; 32], + dust_threshold_sats: 576, + max_utxos_per_spk: 100, + params_hash: params.params_hash(), + issued_at: 1_780_000_000, + roots: vec![ + NamedRoot { + label: "dpf/chunk/super_root".into(), + root: [2; 32], + }, + NamedRoot { + label: "dpf/index/super_root".into(), + root: [1; 32], + }, + NamedRoot { + label: "onion/super_root".into(), + root: [3; 32], + }, + ], + }; + let key0 = SigningKey::from_bytes(&[7; 32]); + let key1 = SigningKey::from_bytes(&[9; 32]); + let bundle = SignedRootBundle { + signatures: vec![ + sign_root_bundle(&payload, &key0).unwrap(), + sign_root_bundle(&payload, &key1).unwrap(), + ], + payload, + }; + println!("payload={}", hex::encode(bundle.payload.encode().unwrap())); + println!("bundle={}", hex::encode(bundle.encode().unwrap())); + println!("trusted0={}", hex::encode(key0.verifying_key().to_bytes())); + println!("trusted1={}", hex::encode(key1.verifying_key().to_bytes())); +} diff --git a/rootbundle/testdata/v1_bundle.hex b/rootbundle/testdata/v1_bundle.hex new file mode 100644 index 0000000..fae7811 --- /dev/null +++ b/rootbundle/testdata/v1_bundle.hex @@ -0,0 +1 @@ +0100f9beb4d900000000000000000000000000000000000000000000000000000000000000000000000000ababababababababababababababababababababababababababababababababf07e0e00cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd400200000000000064000000375e7a9fed689aea244526915a84a503d074158ec1f28fee50c537e4408e4f6c00a5186a000000000300146470662f6368756e6b2f73757065725f726f6f740202020202020202020202020202020202020202020202020202020202020202146470662f696e6465782f73757065725f726f6f740101010101010101010101010101010101010101010101010101010101010101106f6e696f6e2f73757065725f726f6f7403030303030303030303030303030303030303030303030303030303030303030200ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c2716113468bd18085b411f9f69123bd2803b087ccafacdf204bdb83d7a90733ef0e260b51a8671648cda71728fbb4dd0c128d48486e841777068f103f14ef908fd1724385aa0c75b64fb78cd602fa1d991fdebf76b13c58ed702eac835e9f6182eff55f656a6433232a8e7d57214095eea67d955e58a06abe34a3b768c89dd242fde0fff91188b0aeb81b145c47fc9f19b0a036c4d927bc5f24e87c4d7c7ff0e diff --git a/rootbundle/testdata/v1_payload.hex b/rootbundle/testdata/v1_payload.hex new file mode 100644 index 0000000..8024c20 --- /dev/null +++ b/rootbundle/testdata/v1_payload.hex @@ -0,0 +1 @@ +0100f9beb4d900000000000000000000000000000000000000000000000000000000000000000000000000ababababababababababababababababababababababababababababababababf07e0e00cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd400200000000000064000000375e7a9fed689aea244526915a84a503d074158ec1f28fee50c537e4408e4f6c00a5186a000000000300146470662f6368756e6b2f73757065725f726f6f740202020202020202020202020202020202020202020202020202020202020202146470662f696e6465782f73757065725f726f6f740101010101010101010101010101010101010101010101010101010101010101106f6e696f6e2f73757065725f726f6f740303030303030303030303030303030303030303030303030303030303030303 diff --git a/rootbundle/testdata/v1_trusted_keys.hex b/rootbundle/testdata/v1_trusted_keys.hex new file mode 100644 index 0000000..91616f5 --- /dev/null +++ b/rootbundle/testdata/v1_trusted_keys.hex @@ -0,0 +1,2 @@ +ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c +fd1724385aa0c75b64fb78cd602fa1d991fdebf76b13c58ed702eac835e9f618 diff --git a/rootbundle/tests/golden_vectors.rs b/rootbundle/tests/golden_vectors.rs new file mode 100644 index 0000000..c287ac6 --- /dev/null +++ b/rootbundle/tests/golden_vectors.rs @@ -0,0 +1,69 @@ +use ed25519_dalek::SigningKey; +use rootbundle::{ + sign_root_bundle, BuildKind, BuildParamsV2, ChainAnchor, NamedRoot, + RootBundlePayload, SignedRootBundle, +}; + +fn payload() -> RootBundlePayload { + RootBundlePayload { + network_magic: [0xf9, 0xbe, 0xb4, 0xd9], + build_kind: BuildKind::Snapshot, + from_anchor: ChainAnchor { + block_hash: [0; 32], + height: 0, + }, + anchor: ChainAnchor { + block_hash: [0xab; 32], + height: 950_000, + }, + utxo_muhash: [0xcd; 32], + dust_threshold_sats: 576, + max_utxos_per_spk: 100, + params_hash: BuildParamsV2::current_snapshot( + 565_684, 1_064_454, 3_328, 815_432, 612_345, 1_345_678, + ) + .params_hash(), + issued_at: 1_780_000_000, + roots: vec![ + NamedRoot { + label: "dpf/chunk/super_root".into(), + root: [2; 32], + }, + NamedRoot { + label: "dpf/index/super_root".into(), + root: [1; 32], + }, + NamedRoot { + label: "onion/super_root".into(), + root: [3; 32], + }, + ], + } +} + +#[test] +fn v1_payload_and_signed_bundle_are_byte_stable() { + let payload = payload(); + let key0 = SigningKey::from_bytes(&[7; 32]); + let key1 = SigningKey::from_bytes(&[9; 32]); + let bundle = SignedRootBundle { + signatures: vec![ + sign_root_bundle(&payload, &key0).unwrap(), + sign_root_bundle(&payload, &key1).unwrap(), + ], + payload, + }; + + let expected_payload = include_str!("../testdata/v1_payload.hex").trim(); + let expected_bundle = include_str!("../testdata/v1_bundle.hex").trim(); + assert_eq!(hex::encode(bundle.payload.encode().unwrap()), expected_payload); + assert_eq!(hex::encode(bundle.encode().unwrap()), expected_bundle); + + let decoded = SignedRootBundle::decode(&hex::decode(expected_bundle).unwrap()).unwrap(); + assert_eq!(decoded.encode().unwrap(), bundle.encode().unwrap()); + let trusted: Vec<[u8; 32]> = include_str!("../testdata/v1_trusted_keys.hex") + .lines() + .map(|line| hex::decode(line).unwrap().try_into().unwrap()) + .collect(); + assert_eq!(decoded.verify_quorum(&trusted, 2), Ok(2)); +}