Some checks failed
smart-byte conformance gate / Validate conformance JSON files (pull_request) Successful in 4s
smart-byte conformance gate / Second implementation reproduces every vector (pull_request) Successful in 5s
smart-byte conformance gate / Spec changes require vector-pack updates (pull_request) Successful in 4s
smart-byte conformance gate / VERSIONS.md and README.md must exist (pull_request) Successful in 3s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 4s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m51s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m24s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m41s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m10s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 3m38s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 7m45s
standards workspaces build + test / aac-rs (pull_request) Successful in 4m3s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 1m51s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 2m3s
standards workspaces build + test / map-rs (pull_request) Successful in 5m27s
standards workspaces build + test / mesh-rs (pull_request) Successful in 2m19s
standards workspaces build + test / openpay (pull_request) Successful in 10m49s
standards workspaces build + test / arl-rs (pull_request) Successful in 47m4s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 46m44s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 42m33s
standards workspaces build + test / joulehook-rs (pull_request) Failing after 18m22s
standards workspaces build + test / sense-rs (pull_request) Successful in 10m25s
standards workspaces build + test / macOS energy backend (jcp-energy) (pull_request) Successful in 4m56s
smart-byte-state implements cluster_state.md: - governance: Change and Proposal, electorates, the quorum, the time lock; - burns: Burn and the retired record; - exports: ExportStatement and Certificate; - the import checks. smart-byte-core: Transition::sign_export and the destination member. The envelope compares frames within a cluster only. smart-byte-lockstep: the committing nodes sign each committed frame's export certificates. The certificates are built from a committing node's report, never from a node whose state diverged. state_vectors.json grows to 23 vectors (state-013 to state-023 new, state-011 extended) and is regenerated in place; v1 is DRAFT. Every new check has a vector that fails an implementation leaving it out. The README documents the 14 new error classes, and the regeneration log records the change. crosscheck/verify_v1.py implements governance, burns, exports and imports from the spec text. It reproduces every vector, 117 of them, both plain and under -O. Co-Authored-By: Claude Opus 5.5 <[email protected]>
381 lines
12 KiB
Rust
381 lines
12 KiB
Rust
//! The frame transition and persistence, end to end.
|
|
|
|
use smart_byte_core::keys::IndexedSignature;
|
|
use smart_byte_core::ownership::Transition;
|
|
use smart_byte_core::{Aid, Cargo, Envelope, JouleCost, Said, SigningKey};
|
|
use smart_byte_keri_witness::{Controller, KeyEvent, KeyPair, Threshold};
|
|
use smart_byte_state::demo::Workload;
|
|
use smart_byte_state::{Input, InputClass, State, Store};
|
|
|
|
fn key(label: &str) -> KeyPair {
|
|
KeyPair::from_signing(SigningKey::from_bytes(&Said::hash(label.as_bytes()).0))
|
|
}
|
|
|
|
/// A one-key controller, its next key, and the input that incepts it.
|
|
fn party(label: &str) -> (Controller, Vec<u8>) {
|
|
let (c, icp) = Controller::incept(
|
|
vec![key(&format!("{label}-0"))],
|
|
vec![key(&format!("{label}-1"))],
|
|
Threshold(1),
|
|
Threshold(1),
|
|
vec![],
|
|
Threshold(0),
|
|
)
|
|
.unwrap();
|
|
let input = signed(&c, &KeyEvent::Inception(icp));
|
|
(c, input)
|
|
}
|
|
|
|
fn signed(c: &Controller, event: &KeyEvent) -> Vec<u8> {
|
|
let sigs: Vec<IndexedSignature> = c
|
|
.sign_event(event)
|
|
.unwrap()
|
|
.into_iter()
|
|
.map(|(index, signature)| IndexedSignature {
|
|
index: u64::from(index),
|
|
signature,
|
|
})
|
|
.collect();
|
|
Input::key_event(event, &sigs)
|
|
}
|
|
|
|
fn classes(report: &smart_byte_state::FrameReport) -> Vec<Result<(), &'static str>> {
|
|
report
|
|
.results
|
|
.iter()
|
|
.map(|r| r.outcome.as_ref().map(|_| ()).map_err(|e| e.class()))
|
|
.collect()
|
|
}
|
|
|
|
struct World {
|
|
state: State,
|
|
issuer: Controller,
|
|
alice: Controller,
|
|
bob: Controller,
|
|
byte: Envelope,
|
|
mint: Vec<u8>,
|
|
}
|
|
|
|
/// Frame 0 applied: the issuer, Alice and Bob incepted, and a USD byte
|
|
/// minted to Alice.
|
|
fn world() -> World {
|
|
let (issuer, issuer_icp) = party("issuer");
|
|
let (alice, alice_icp) = party("alice");
|
|
let (bob, bob_icp) = party("bob");
|
|
let byte = Envelope::issue(
|
|
&issuer.signer(),
|
|
alice.aid,
|
|
[7; 16],
|
|
Cargo::usd_micros(5_000_000),
|
|
JouleCost::default(),
|
|
);
|
|
let mint = Input::mint(&byte.provenance);
|
|
let mut state = State::genesis(Said::hash(b"cluster"), [(issuer.aid, 1)], [], 0);
|
|
let report = state.apply_frame(&[issuer_icp, alice_icp, bob_icp, mint.clone()]);
|
|
assert_eq!(classes(&report), vec![Ok(()); 4]);
|
|
World {
|
|
state,
|
|
issuer,
|
|
alice,
|
|
bob,
|
|
byte,
|
|
mint,
|
|
}
|
|
}
|
|
|
|
fn transfer(
|
|
owner: &Controller,
|
|
byte: &Said,
|
|
history: [u8; 32],
|
|
to: Aid,
|
|
frame: u64,
|
|
) -> (Vec<u8>, Transition) {
|
|
let t = Transition::sign(&owner.signer(), byte, history, to, frame);
|
|
(Input::transfer(byte, &t), t)
|
|
}
|
|
|
|
#[test]
|
|
fn a_byte_is_minted_and_moves() {
|
|
let mut w = world();
|
|
assert_eq!(w.state.frame, 1);
|
|
let id = w.byte.identity();
|
|
let record = &w.state.bytes[&id];
|
|
assert_eq!(
|
|
(record.owner, record.minted, record.history_hash),
|
|
(w.alice.aid, 0, id.0)
|
|
);
|
|
|
|
let (input, t) = transfer(&w.alice, &id, id.0, w.bob.aid, 1);
|
|
let report = w.state.apply_frame(&[input]);
|
|
assert_eq!(classes(&report), vec![Ok(())]);
|
|
assert_eq!(w.state.bytes[&id].owner, w.bob.aid);
|
|
assert_eq!(w.state.bytes[&id].history_hash, t.next_history_hash());
|
|
// The cluster's view agrees with the envelope a client builds.
|
|
let envelope = w.byte.transfer(&w.alice.signer(), w.bob.aid, 1);
|
|
assert_eq!(envelope.history_hash(), w.state.bytes[&id].history_hash);
|
|
}
|
|
|
|
#[test]
|
|
fn a_replayed_mint_is_refused_in_any_later_frame() {
|
|
let mut w = world();
|
|
let report = w.state.apply_frame(&[w.mint.clone()]);
|
|
assert_eq!(classes(&report), vec![Err("duplicate_byte")]);
|
|
assert_eq!(w.state.bytes.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn only_a_registered_issuer_mints_and_only_its_cargo_types() {
|
|
let mut w = world();
|
|
let by_alice = Envelope::issue(
|
|
&w.alice.signer(),
|
|
w.bob.aid,
|
|
[1; 16],
|
|
Cargo::usd_micros(1),
|
|
JouleCost::default(),
|
|
);
|
|
let joules = Envelope::issue(
|
|
&w.issuer.signer(),
|
|
w.bob.aid,
|
|
[2; 16],
|
|
Cargo::joules(9),
|
|
JouleCost::default(),
|
|
);
|
|
let report = w.state.apply_frame(&[
|
|
Input::mint(&by_alice.provenance),
|
|
Input::mint(&joules.provenance),
|
|
]);
|
|
assert_eq!(classes(&report), vec![Err("issuer_not_registered"); 2]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_rotation_retires_the_old_keys_in_the_frame_it_lands() {
|
|
let mut w = world();
|
|
let id = w.byte.identity();
|
|
// Alice rotates; in the same frame, a transfer signed with her old key.
|
|
let rot = KeyEvent::Rotation(
|
|
w.alice
|
|
.rotate(
|
|
vec![key("alice-2")],
|
|
Threshold(1),
|
|
vec![],
|
|
vec![],
|
|
Threshold(0),
|
|
)
|
|
.unwrap(),
|
|
);
|
|
let rotation = signed(&w.alice, &rot);
|
|
let stale = Transition::sign(
|
|
&smart_byte_core::Signer::single(w.alice.aid, 0, key("alice-0").signing),
|
|
&id,
|
|
id.0,
|
|
w.bob.aid,
|
|
1,
|
|
);
|
|
let report = w
|
|
.state
|
|
.apply_frame(&[Input::transfer(&id, &stale), rotation]);
|
|
// Key events apply first, whatever the inputs' order.
|
|
assert_eq!(report.results[0].class, Some(InputClass::KeyEvent));
|
|
assert_eq!(
|
|
classes(&report),
|
|
vec![Ok(()), Err("establishment_not_in_force")]
|
|
);
|
|
// Under the new establishment, it goes through.
|
|
let (fresh, _) = transfer(&w.alice, &id, id.0, w.bob.aid, 2);
|
|
assert_eq!(classes(&w.state.apply_frame(&[fresh])), vec![Ok(())]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_byte_moves_once_per_frame() {
|
|
let mut w = world();
|
|
let id = w.byte.identity();
|
|
let (to_bob, _) = transfer(&w.alice, &id, id.0, w.bob.aid, 1);
|
|
let (to_issuer, _) = transfer(&w.alice, &id, id.0, w.issuer.aid, 1);
|
|
let report = w.state.apply_frame(&[to_bob.clone(), to_issuer.clone()]);
|
|
let mut outcomes = classes(&report);
|
|
outcomes.sort();
|
|
assert_eq!(outcomes, vec![Ok(()), Err("broken_history_chain")]);
|
|
// The input with the lower digest won.
|
|
let lower = std::cmp::min(
|
|
*blake3::hash(&to_bob).as_bytes(),
|
|
*blake3::hash(&to_issuer).as_bytes(),
|
|
);
|
|
assert_eq!(
|
|
(report.results[0].input, report.results[0].outcome.is_ok()),
|
|
(lower, true)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn transfers_are_checked_against_the_byte_and_the_frame() {
|
|
let mut w = world();
|
|
let id = w.byte.identity();
|
|
let (late, _) = transfer(&w.alice, &id, id.0, w.bob.aid, 9);
|
|
let (not_owner, _) = transfer(&w.bob, &id, id.0, w.alice.aid, 1);
|
|
let (stale_history, _) = transfer(&w.alice, &id, [0; 32], w.bob.aid, 1);
|
|
let (unknown, _) = transfer(
|
|
&w.alice,
|
|
&Said::hash(b"no such byte"),
|
|
[0; 32],
|
|
w.bob.aid,
|
|
1,
|
|
);
|
|
let report = w
|
|
.state
|
|
.apply_frame(&[late, not_owner, stale_history, unknown]);
|
|
let mut outcomes = classes(&report);
|
|
outcomes.sort();
|
|
assert_eq!(
|
|
outcomes,
|
|
vec![
|
|
Err("broken_history_chain"),
|
|
Err("not_current_owner"),
|
|
Err("unknown_byte"),
|
|
Err("wrong_frame")
|
|
]
|
|
);
|
|
assert_eq!(w.state.bytes[&id].owner, w.alice.aid);
|
|
}
|
|
|
|
#[test]
|
|
fn a_mint_and_its_first_transfer_share_a_frame() {
|
|
let (issuer, issuer_icp) = party("issuer");
|
|
let (alice, alice_icp) = party("alice");
|
|
let byte = Envelope::issue(
|
|
&issuer.signer(),
|
|
alice.aid,
|
|
[3; 16],
|
|
Cargo::usd_micros(1),
|
|
JouleCost::default(),
|
|
);
|
|
let id = byte.identity();
|
|
let (move_it, _) = transfer(&alice, &id, id.0, issuer.aid, 0);
|
|
let mut state = State::genesis(Said::hash(b"cluster"), [(issuer.aid, 1)], [], 0);
|
|
let report = state.apply_frame(&[
|
|
move_it,
|
|
Input::mint(&byte.provenance),
|
|
alice_icp,
|
|
issuer_icp,
|
|
]);
|
|
assert_eq!(classes(&report), vec![Ok(()); 4]);
|
|
assert_eq!(state.bytes[&id].owner, issuer.aid);
|
|
}
|
|
|
|
#[test]
|
|
fn identical_inputs_are_one_input() {
|
|
let mut w = world();
|
|
let id = w.byte.identity();
|
|
let (input, _) = transfer(&w.alice, &id, id.0, w.bob.aid, 1);
|
|
let report = w.state.apply_frame(&[input.clone(), input.clone(), input]);
|
|
assert_eq!(classes(&report), vec![Ok(())]);
|
|
assert_eq!(report.inputs.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn a_controller_signs_only_once_its_log_is_in_the_cluster() {
|
|
let (issuer, issuer_icp) = party("issuer");
|
|
let (carol, carol_icp) = party("carol");
|
|
let byte = Envelope::issue(
|
|
&issuer.signer(),
|
|
carol.aid,
|
|
[4; 16],
|
|
Cargo::usd_micros(1),
|
|
JouleCost::default(),
|
|
);
|
|
let id = byte.identity();
|
|
let mut state = State::genesis(Said::hash(b"cluster"), [(issuer.aid, 1)], [], 0);
|
|
state.apply_frame(&[issuer_icp, Input::mint(&byte.provenance)]);
|
|
let (early, _) = transfer(&carol, &id, id.0, issuer.aid, 1);
|
|
assert_eq!(
|
|
classes(&state.apply_frame(&[early])),
|
|
vec![Err("unknown_controller")]
|
|
);
|
|
// Carol's inception and her transfer in one frame: key events first.
|
|
let (now, _) = transfer(&carol, &id, id.0, issuer.aid, 2);
|
|
assert_eq!(
|
|
classes(&state.apply_frame(&[now, carol_icp])),
|
|
vec![Ok(()), Ok(())]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn key_events_extend_the_cluster_s_logs() {
|
|
let mut w = world();
|
|
let (alice_again, _) = party("alice");
|
|
let (dora, _) = party("dora");
|
|
let dora_rot = KeyEvent::Rotation({
|
|
let mut d = dora;
|
|
d.rotate(
|
|
vec![key("dora-2")],
|
|
Threshold(1),
|
|
vec![],
|
|
vec![],
|
|
Threshold(0),
|
|
)
|
|
.unwrap()
|
|
});
|
|
let alice_icp_again = {
|
|
let icp = alice_again.key_event_log[0].clone();
|
|
signed(&alice_again, &icp)
|
|
};
|
|
let report = w
|
|
.state
|
|
.apply_frame(&[alice_icp_again, signed(&w.bob, &dora_rot)]);
|
|
let mut outcomes = classes(&report);
|
|
outcomes.sort();
|
|
assert_eq!(
|
|
outcomes,
|
|
vec![Err("controller_exists"), Err("unknown_controller")]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_demo_workload_runs_and_is_deterministic() {
|
|
let run = || {
|
|
let mut workload = Workload::new(3, 2);
|
|
let mut state = workload.genesis();
|
|
let mut hashes = Vec::new();
|
|
for f in 0..5 {
|
|
let report = state.apply_frame(&workload.frame(f));
|
|
assert_eq!(
|
|
report.applied(),
|
|
report.results.len(),
|
|
"frame {f}: every input applies"
|
|
);
|
|
hashes.push(state.hash());
|
|
}
|
|
(state, hashes)
|
|
};
|
|
let (a, ha) = run();
|
|
let (b, hb) = run();
|
|
assert_eq!((a.frame, a.bytes.len(), a.controllers.len()), (5, 6, 4));
|
|
assert_eq!((ha, a), (hb, b));
|
|
}
|
|
|
|
#[test]
|
|
fn a_store_recovers_the_state_and_refuses_a_tampered_log() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let mut workload = Workload::new(2, 1);
|
|
let genesis = workload.genesis();
|
|
let (mut store, mut state) = Store::open_with_genesis(dir.path(), 3, &genesis).unwrap();
|
|
assert_eq!(state, genesis);
|
|
for f in 0..5 {
|
|
let report = state.apply_frame(&workload.frame(f));
|
|
store.commit(report.frame, &report.inputs, &state).unwrap();
|
|
}
|
|
drop(store);
|
|
// Five frames at a cadence of three: a checkpoint at frame 3, two logged.
|
|
let (_, recovered) = Store::open_with_genesis(dir.path(), 3, &genesis).unwrap();
|
|
assert_eq!(recovered, state);
|
|
|
|
// A flipped byte in the log's last record.
|
|
let log = dir.path().join(smart_byte_state::store::LOG);
|
|
let mut bytes = std::fs::read(&log).unwrap();
|
|
let last = bytes.len() - 1;
|
|
bytes[last] ^= 1;
|
|
std::fs::write(&log, bytes).unwrap();
|
|
assert!(matches!(
|
|
Store::open_with_genesis(dir.path(), 3, &genesis),
|
|
Err(smart_byte_state::StoreError::Corrupt(_))
|
|
));
|
|
}
|