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
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions rootbundle/COMPATIBILITY.md
Original file line number Diff line number Diff line change
@@ -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.
55 changes: 55 additions & 0 deletions rootbundle/examples/golden_vector.rs
Original file line number Diff line number Diff line change
@@ -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()));
}
1 change: 1 addition & 0 deletions rootbundle/testdata/v1_bundle.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0100f9beb4d900000000000000000000000000000000000000000000000000000000000000000000000000ababababababababababababababababababababababababababababababababf07e0e00cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd400200000000000064000000375e7a9fed689aea244526915a84a503d074158ec1f28fee50c537e4408e4f6c00a5186a000000000300146470662f6368756e6b2f73757065725f726f6f740202020202020202020202020202020202020202020202020202020202020202146470662f696e6465782f73757065725f726f6f740101010101010101010101010101010101010101010101010101010101010101106f6e696f6e2f73757065725f726f6f7403030303030303030303030303030303030303030303030303030303030303030200ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c2716113468bd18085b411f9f69123bd2803b087ccafacdf204bdb83d7a90733ef0e260b51a8671648cda71728fbb4dd0c128d48486e841777068f103f14ef908fd1724385aa0c75b64fb78cd602fa1d991fdebf76b13c58ed702eac835e9f6182eff55f656a6433232a8e7d57214095eea67d955e58a06abe34a3b768c89dd242fde0fff91188b0aeb81b145c47fc9f19b0a036c4d927bc5f24e87c4d7c7ff0e
1 change: 1 addition & 0 deletions rootbundle/testdata/v1_payload.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0100f9beb4d900000000000000000000000000000000000000000000000000000000000000000000000000ababababababababababababababababababababababababababababababababf07e0e00cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd400200000000000064000000375e7a9fed689aea244526915a84a503d074158ec1f28fee50c537e4408e4f6c00a5186a000000000300146470662f6368756e6b2f73757065725f726f6f740202020202020202020202020202020202020202020202020202020202020202146470662f696e6465782f73757065725f726f6f740101010101010101010101010101010101010101010101010101010101010101106f6e696f6e2f73757065725f726f6f740303030303030303030303030303030303030303030303030303030303030303
2 changes: 2 additions & 0 deletions rootbundle/testdata/v1_trusted_keys.hex
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c
fd1724385aa0c75b64fb78cd602fa1d991fdebf76b13c58ed702eac835e9f618
69 changes: 69 additions & 0 deletions rootbundle/tests/golden_vectors.rs
Original file line number Diff line number Diff line change
@@ -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));
}
Loading