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]>
491 lines
16 KiB
Rust
491 lines
16 KiB
Rust
//! Governance, burns, and bytes moving between clusters.
|
|
|
|
use smart_byte_core::keys::IndexedSignature;
|
|
use smart_byte_core::{Aid, Cargo, Envelope, JouleCost, Said, SigningKey};
|
|
use smart_byte_keri_witness::{Controller, KeyEvent, KeyPair, Threshold};
|
|
use smart_byte_state::certificate::node_key;
|
|
use smart_byte_state::{Approval, Burn, Certificate, Change, Input, Proposal, State};
|
|
|
|
fn key(label: &str) -> KeyPair {
|
|
KeyPair::from_signing(SigningKey::from_bytes(&Said::hash(label.as_bytes()).0))
|
|
}
|
|
|
|
/// A one-key controller and the signed events of its log so far.
|
|
struct Party {
|
|
c: Controller,
|
|
log: Vec<(KeyEvent, Vec<IndexedSignature>)>,
|
|
}
|
|
|
|
impl Party {
|
|
fn new(label: &str) -> Self {
|
|
let (c, icp) = Controller::incept(
|
|
vec![key(&format!("{label}-0"))],
|
|
vec![key(&format!("{label}-1"))],
|
|
Threshold(1),
|
|
Threshold(1),
|
|
vec![],
|
|
Threshold(0),
|
|
)
|
|
.unwrap();
|
|
let event = KeyEvent::Inception(icp);
|
|
let sigs = sigs(&c, &event);
|
|
Party {
|
|
c,
|
|
log: vec![(event, sigs)],
|
|
}
|
|
}
|
|
fn aid(&self) -> Aid {
|
|
self.c.aid
|
|
}
|
|
fn incept(&self) -> Vec<u8> {
|
|
Input::key_event(&self.log[0].0, &self.log[0].1)
|
|
}
|
|
fn approve(&self, proposal: &Proposal) -> Approval {
|
|
Approval {
|
|
approver: self.aid(),
|
|
signature: self
|
|
.c
|
|
.signer()
|
|
.sign(&smart_byte_core::wire::to_vec(proposal).unwrap()),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn sigs(c: &Controller, event: &KeyEvent) -> Vec<IndexedSignature> {
|
|
c.sign_event(event)
|
|
.unwrap()
|
|
.into_iter()
|
|
.map(|(index, signature)| IndexedSignature {
|
|
index: u64::from(index),
|
|
signature,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
fn sorted(mut v: Vec<Result<(), &'static str>>) -> Vec<Result<(), &'static str>> {
|
|
v.sort();
|
|
v
|
|
}
|
|
|
|
const USD: u64 = 1;
|
|
|
|
fn cluster(label: &str) -> Said {
|
|
Said::hash(label.as_bytes())
|
|
}
|
|
|
|
#[test]
|
|
fn an_issuer_is_added_by_the_others_and_only_after_the_delay() {
|
|
let (a, b, c, new) = (
|
|
Party::new("a"),
|
|
Party::new("b"),
|
|
Party::new("c"),
|
|
Party::new("new"),
|
|
);
|
|
let id = cluster("one");
|
|
let mut state = State::genesis(id, [(a.aid(), USD), (b.aid(), USD), (c.aid(), USD)], [], 2);
|
|
state.apply_frame(&[a.incept(), b.incept(), c.incept(), new.incept()]);
|
|
|
|
let proposal = Proposal {
|
|
cluster: id,
|
|
sequence: 0,
|
|
change: Change::AddIssuer {
|
|
cargo_type: USD,
|
|
issuer: new.aid(),
|
|
},
|
|
};
|
|
// Three issuers: the quorum is floor(2·3/3)+1 = 3.
|
|
let two = Input::governance(&proposal, &[a.approve(&proposal), b.approve(&proposal)]);
|
|
let three = Input::governance(
|
|
&proposal,
|
|
&[
|
|
a.approve(&proposal),
|
|
b.approve(&proposal),
|
|
c.approve(&proposal),
|
|
],
|
|
);
|
|
assert_eq!(
|
|
classes(&state.apply_frame(&[two])),
|
|
vec![Err("below_quorum")]
|
|
);
|
|
assert_eq!(classes(&state.apply_frame(&[three.clone()])), vec![Ok(())]);
|
|
assert_eq!(state.governance.pending.len(), 1);
|
|
assert_eq!(state.governance.pending[0].effective, 2 + 1 + 2);
|
|
|
|
// The same proposal again: its sequence number is spent.
|
|
assert_eq!(
|
|
classes(&state.apply_frame(&[three])),
|
|
vec![Err("stale_sequence")]
|
|
);
|
|
|
|
let mint = |label: &str, frame_note: &str| {
|
|
let e = Envelope::issue(
|
|
&new.c.signer(),
|
|
a.aid(),
|
|
[frame_note.len() as u8; 16],
|
|
Cargo::usd_micros(1),
|
|
JouleCost::default(),
|
|
);
|
|
let _ = label;
|
|
Input::mint(&e.provenance)
|
|
};
|
|
// Frame 4: not yet in force.
|
|
assert_eq!(state.frame, 4);
|
|
assert_eq!(
|
|
classes(&state.apply_frame(&[mint("early", "x")])),
|
|
vec![Err("issuer_not_registered")]
|
|
);
|
|
// Frame 5: in force from its start.
|
|
assert_eq!(
|
|
classes(&state.apply_frame(&[mint("in force", "xy")])),
|
|
vec![Ok(())]
|
|
);
|
|
assert!(state.authorizes(&new.aid(), USD) && state.governance.pending.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn governance_is_checked_before_it_is_counted() {
|
|
let (a, b, outsider) = (Party::new("a"), Party::new("b"), Party::new("outsider"));
|
|
let id = cluster("one");
|
|
let mut state = State::genesis(id, [(a.aid(), USD), (b.aid(), USD)], [], 0);
|
|
state.apply_frame(&[a.incept(), b.incept(), outsider.incept()]);
|
|
let p = |change: Change, sequence: u64, cluster: Said| Proposal {
|
|
cluster,
|
|
sequence,
|
|
change,
|
|
};
|
|
let add = |issuer: Aid| Change::AddIssuer {
|
|
cargo_type: USD,
|
|
issuer,
|
|
};
|
|
let remove = |issuer: Aid| Change::RemoveIssuer {
|
|
cargo_type: USD,
|
|
issuer,
|
|
};
|
|
|
|
let elsewhere = p(add(outsider.aid()), 0, cluster("two"));
|
|
let stale = p(add(outsider.aid()), 7, id);
|
|
let registered = p(add(a.aid()), 0, id);
|
|
let unregistered = p(remove(outsider.aid()), 0, id);
|
|
let by_outsider = p(add(outsider.aid()), 0, id);
|
|
let twice = p(add(outsider.aid()), 0, id);
|
|
let report = state.apply_frame(&[
|
|
Input::governance(&elsewhere, &[a.approve(&elsewhere), b.approve(&elsewhere)]),
|
|
Input::governance(&stale, &[a.approve(&stale), b.approve(&stale)]),
|
|
Input::governance(
|
|
®istered,
|
|
&[a.approve(®istered), b.approve(®istered)],
|
|
),
|
|
Input::governance(
|
|
&unregistered,
|
|
&[a.approve(&unregistered), b.approve(&unregistered)],
|
|
),
|
|
Input::governance(
|
|
&by_outsider,
|
|
&[a.approve(&by_outsider), outsider.approve(&by_outsider)],
|
|
),
|
|
Input::governance(&twice, &[a.approve(&twice), a.approve(&twice)]),
|
|
]);
|
|
assert_eq!(
|
|
sorted(classes(&report)),
|
|
sorted(vec![
|
|
Err("wrong_cluster"),
|
|
Err("stale_sequence"),
|
|
Err("already_registered"),
|
|
Err("not_registered"),
|
|
Err("not_an_elector"),
|
|
Err("duplicate_approver"),
|
|
])
|
|
);
|
|
assert_eq!(state.governance.sequence, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn an_issuer_cannot_block_its_own_removal() {
|
|
let (a, b, c) = (Party::new("a"), Party::new("b"), Party::new("c"));
|
|
let id = cluster("one");
|
|
let mut state = State::genesis(id, [(a.aid(), USD), (b.aid(), USD), (c.aid(), USD)], [], 0);
|
|
state.apply_frame(&[a.incept(), b.incept(), c.incept()]);
|
|
let proposal = Proposal {
|
|
cluster: id,
|
|
sequence: 0,
|
|
change: Change::RemoveIssuer {
|
|
cargo_type: USD,
|
|
issuer: c.aid(),
|
|
},
|
|
};
|
|
// The electorate is the other two; both approve. The target's vote is
|
|
// not counted, and not needed.
|
|
let with_target = Input::governance(&proposal, &[a.approve(&proposal), c.approve(&proposal)]);
|
|
assert_eq!(
|
|
classes(&state.apply_frame(&[with_target])),
|
|
vec![Err("not_an_elector")]
|
|
);
|
|
let others = Input::governance(&proposal, &[a.approve(&proposal), b.approve(&proposal)]);
|
|
assert_eq!(classes(&state.apply_frame(&[others])), vec![Ok(())]);
|
|
state.apply_frame(&[]);
|
|
assert!(!state.authorizes(&c.aid(), USD));
|
|
}
|
|
|
|
#[test]
|
|
fn redemption_is_a_return_to_the_issuer_and_a_burn() {
|
|
let (issuer, alice) = (Party::new("issuer"), Party::new("alice"));
|
|
let mut state = State::genesis(cluster("one"), [(issuer.aid(), USD)], [], 0);
|
|
let byte = Envelope::issue(
|
|
&issuer.c.signer(),
|
|
alice.aid(),
|
|
[1; 16],
|
|
Cargo::usd_micros(5),
|
|
JouleCost::default(),
|
|
);
|
|
let mint = Input::mint(&byte.provenance);
|
|
state.apply_frame(&[issuer.incept(), alice.incept(), mint.clone()]);
|
|
let id = byte.identity();
|
|
|
|
// Alice returns it; the issuer burns it in the same frame: burns apply
|
|
// after transfers.
|
|
let back = byte.transfer(&alice.c.signer(), issuer.aid(), 1);
|
|
let t = back.ownership_chain.last().unwrap();
|
|
let burn = Burn::sign(&issuer.c.signer(), id, back.history_hash(), 1);
|
|
let by_alice = Burn::sign(&alice.c.signer(), id, back.history_hash(), 1);
|
|
let report = state.apply_frame(&[
|
|
Input::burn(&burn),
|
|
Input::burn(&by_alice),
|
|
Input::transfer(&id, t),
|
|
]);
|
|
assert_eq!(
|
|
sorted(classes(&report)),
|
|
sorted(vec![Ok(()), Ok(()), Err("not_current_owner")])
|
|
);
|
|
assert!(state.bytes.is_empty());
|
|
assert_eq!(state.retired[&id].owner, issuer.aid());
|
|
|
|
// Burned for good: no transfer, no re-mint.
|
|
let after = Burn::sign(&issuer.c.signer(), id, back.history_hash(), 2);
|
|
assert_eq!(
|
|
sorted(classes(&state.apply_frame(&[Input::burn(&after), mint]))),
|
|
vec![Err("byte_retired"), Err("duplicate_byte")]
|
|
);
|
|
}
|
|
|
|
/// Two clusters, each with four nodes, peering with each other.
|
|
struct Federation {
|
|
a: State,
|
|
b: State,
|
|
a_nodes: Vec<SigningKey>,
|
|
b_nodes: Vec<SigningKey>,
|
|
}
|
|
|
|
fn federation(registry: &[(Aid, u64)]) -> Federation {
|
|
let nodes = |label: &str| {
|
|
(0..4)
|
|
.map(|i| SigningKey::from_bytes(&Said::hash(format!("{label}-node-{i}").as_bytes()).0))
|
|
.collect::<Vec<_>>()
|
|
};
|
|
let (a_nodes, b_nodes) = (nodes("a"), nodes("b"));
|
|
let keys = |n: &[SigningKey]| n.iter().map(node_key).collect::<Vec<_>>();
|
|
Federation {
|
|
a: State::genesis(
|
|
cluster("A"),
|
|
registry.iter().copied(),
|
|
[(cluster("B"), keys(&b_nodes))],
|
|
0,
|
|
),
|
|
b: State::genesis(
|
|
cluster("B"),
|
|
registry.iter().copied(),
|
|
[(cluster("A"), keys(&a_nodes))],
|
|
0,
|
|
),
|
|
a_nodes,
|
|
b_nodes,
|
|
}
|
|
}
|
|
|
|
fn certify(
|
|
statement: smart_byte_state::ExportStatement,
|
|
nodes: &[SigningKey],
|
|
signers: &[u64],
|
|
) -> Certificate {
|
|
let picked: Vec<(u64, &SigningKey)> =
|
|
signers.iter().map(|i| (*i, &nodes[*i as usize])).collect();
|
|
Certificate::sign(statement, &picked)
|
|
}
|
|
|
|
#[test]
|
|
fn a_byte_moves_between_clusters_and_back() {
|
|
let (issuer, alice, bob) = (Party::new("issuer"), Party::new("alice"), Party::new("bob"));
|
|
let mut f = federation(&[(issuer.aid(), USD)]);
|
|
let byte = Envelope::issue(
|
|
&issuer.c.signer(),
|
|
alice.aid(),
|
|
[1; 16],
|
|
Cargo::usd_micros(5),
|
|
JouleCost::default(),
|
|
);
|
|
let id = byte.identity();
|
|
f.a.apply_frame(&[
|
|
issuer.incept(),
|
|
alice.incept(),
|
|
Input::mint(&byte.provenance),
|
|
]);
|
|
f.b.apply_frame(&[bob.incept()]);
|
|
|
|
// Alice sends it to Bob, in cluster B.
|
|
let out = byte.export(&alice.c.signer(), bob.aid(), 1, cluster("B"));
|
|
let report =
|
|
f.a.apply_frame(&[Input::transfer(&id, out.ownership_chain.last().unwrap())]);
|
|
assert_eq!(classes(&report), vec![Ok(())]);
|
|
assert!(f.a.bytes.is_empty() && f.a.exported.contains_key(&id));
|
|
let statement = report.exports[0].clone();
|
|
// A transfer of a byte that has left is refused.
|
|
let stale = byte.transfer(&alice.c.signer(), issuer.aid(), 2);
|
|
assert_eq!(
|
|
classes(&f.a.apply_frame(&[Input::transfer(&id, stale.ownership_chain.last().unwrap())])),
|
|
vec![Err("byte_exported")]
|
|
);
|
|
|
|
let logs = vec![issuer.log.clone(), alice.log.clone()];
|
|
let import = |cert: &Certificate, logs: &[Vec<(KeyEvent, Vec<IndexedSignature>)>]| {
|
|
Input::import(cert, &out, logs)
|
|
};
|
|
let three = certify(statement.clone(), &f.a_nodes, &[0, 1, 2]);
|
|
let two = certify(statement.clone(), &f.a_nodes, &[0, 2]);
|
|
let missing_log = import(&three, &logs[..1]);
|
|
let report = f.b.apply_frame(&[import(&two, &logs), missing_log]);
|
|
assert_eq!(
|
|
sorted(classes(&report)),
|
|
vec![Err("below_threshold"), Err("unknown_key_state")]
|
|
);
|
|
// With three of A's four nodes and every log the history needs: applied.
|
|
assert_eq!(
|
|
classes(&f.b.apply_frame(&[import(&three, &logs)])),
|
|
vec![Ok(())]
|
|
);
|
|
assert_eq!(f.b.bytes[&id].owner, bob.aid());
|
|
assert_eq!(f.b.bytes[&id].history_hash, out.history_hash());
|
|
// Once.
|
|
assert_eq!(
|
|
classes(&f.b.apply_frame(&[import(&three, &logs)])),
|
|
vec![Err("duplicate_import")]
|
|
);
|
|
|
|
// Bob sends it back to Alice in A; A takes it back over its own export.
|
|
let home = out.export(&bob.c.signer(), alice.aid(), f.b.frame, cluster("A"));
|
|
let report =
|
|
f.b.apply_frame(&[Input::transfer(&id, home.ownership_chain.last().unwrap())]);
|
|
assert_eq!(classes(&report), vec![Ok(())]);
|
|
let cert = certify(report.exports[0].clone(), &f.b_nodes, &[1, 2, 3]);
|
|
let logs = vec![issuer.log.clone(), alice.log.clone(), bob.log.clone()];
|
|
assert_eq!(
|
|
classes(&f.a.apply_frame(&[Input::import(&cert, &home, &logs)])),
|
|
vec![Ok(())]
|
|
);
|
|
assert!(f.a.bytes.contains_key(&id) && !f.a.exported.contains_key(&id));
|
|
// A's first certificate, replayed in A, is not for A.
|
|
assert_eq!(
|
|
classes(&f.a.apply_frame(&[import(&three, &[issuer.log.clone(), alice.log.clone()])])),
|
|
vec![Err("wrong_cluster")]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn an_import_must_be_the_byte_certified() {
|
|
let (issuer, alice, bob) = (Party::new("issuer"), Party::new("alice"), Party::new("bob"));
|
|
let mut f = federation(&[(issuer.aid(), USD)]);
|
|
let byte = Envelope::issue(
|
|
&issuer.c.signer(),
|
|
alice.aid(),
|
|
[1; 16],
|
|
Cargo::usd_micros(5),
|
|
JouleCost::default(),
|
|
);
|
|
let id = byte.identity();
|
|
f.a.apply_frame(&[
|
|
issuer.incept(),
|
|
alice.incept(),
|
|
Input::mint(&byte.provenance),
|
|
]);
|
|
let out = byte.export(&alice.c.signer(), bob.aid(), 1, cluster("B"));
|
|
let report =
|
|
f.a.apply_frame(&[Input::transfer(&id, out.ownership_chain.last().unwrap())]);
|
|
let cert = certify(report.exports[0].clone(), &f.a_nodes, &[0, 1, 2]);
|
|
let logs = vec![issuer.log.clone(), alice.log.clone()];
|
|
// The envelope before the export: not the history A certified.
|
|
let report = f.b.apply_frame(&[Input::import(&cert, &byte, &logs)]);
|
|
assert_eq!(classes(&report), vec![Err("certificate_mismatch")]);
|
|
// A certificate from a cluster B does not peer with.
|
|
let mut stranger = f.b.clone();
|
|
stranger.peers.clear();
|
|
assert_eq!(
|
|
classes(&stranger.apply_frame(&[Input::import(&cert, &out, &logs)])),
|
|
vec![Err("unknown_cluster")]
|
|
);
|
|
// An export to a cluster A does not peer with is refused in A.
|
|
let lost = byte.export(&alice.c.signer(), bob.aid(), 2, cluster("nowhere"));
|
|
let mut a = f.a.clone();
|
|
a.exported.clear();
|
|
a.bytes.insert(
|
|
id,
|
|
smart_byte_state::ByteRecord {
|
|
issuer: issuer.aid(),
|
|
cargo: Cargo::usd_micros(5),
|
|
minted: 0,
|
|
owner: alice.aid(),
|
|
history_hash: id.0,
|
|
},
|
|
);
|
|
assert_eq!(
|
|
classes(&a.apply_frame(&[Input::transfer(&id, lost.ownership_chain.last().unwrap())])),
|
|
vec![Err("unknown_cluster")]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn peers_change_by_governance() {
|
|
let (a, b) = (Party::new("a"), Party::new("b"));
|
|
let id = cluster("one");
|
|
let mut state = State::genesis(id, [(a.aid(), USD), (b.aid(), 2)], [], 0);
|
|
state.apply_frame(&[a.incept(), b.incept()]);
|
|
let node = SigningKey::from_bytes(&[9; 32]);
|
|
let proposal = Proposal {
|
|
cluster: id,
|
|
sequence: 0,
|
|
change: Change::SetPeer {
|
|
peer: cluster("two"),
|
|
keys: vec![node_key(&node)],
|
|
},
|
|
};
|
|
// Peer changes are every issuer's business: both must approve.
|
|
let one = Input::governance(&proposal, &[a.approve(&proposal)]);
|
|
let both = Input::governance(&proposal, &[a.approve(&proposal), b.approve(&proposal)]);
|
|
assert_eq!(
|
|
classes(&state.apply_frame(&[one])),
|
|
vec![Err("below_quorum")]
|
|
);
|
|
assert_eq!(classes(&state.apply_frame(&[both])), vec![Ok(())]);
|
|
state.apply_frame(&[]);
|
|
assert!(state.peers.contains_key(&cluster("two")));
|
|
// A peer's keys must be keys.
|
|
let bad = Proposal {
|
|
cluster: id,
|
|
sequence: 1,
|
|
change: Change::SetPeer {
|
|
peer: cluster("three"),
|
|
keys: vec![smart_byte_core::PrefixedKey(vec![1, 2, 3])],
|
|
},
|
|
};
|
|
assert_eq!(
|
|
classes(
|
|
&state.apply_frame(&[Input::governance(&bad, &[a.approve(&bad), b.approve(&bad)])])
|
|
),
|
|
vec![Err("malformed_verifying_key")]
|
|
);
|
|
}
|