open-standards/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/pack.rs
Transaction Science c4d3a091f3 smart-byte: the cluster state machine, and its conformance vectors
smart-byte-state (new):
- the state and its hash;
- inputs;
- the frame transition;
- checkpoint-and-log persistence. Checkpoints are written atomically, and
  replay refuses a gap or a wrong hash.
- a deterministic demo workload.

smart-byte-lockstep: the cluster runs these state machines in place of the
rolling hash that stood in for the state.

smart-byte-keri-witness: exposes a controller's state and resumes a replay
from it, so the cluster extends a log one event at a time.

The CLI's `cluster simulate` runs real mints and transfers through the
supermajority commit.

smart-byte-conformance: state_vectors.json, 12 vectors. Each is a genesis
state and frames of inputs, with every input's result and every frame's
state and state hash. The founding frame uses kel-001 to kel-003 and
env-001's mint. Planted defects in the state vectors are caught on their
own vectors.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-01 06:19:03 -04:00

965 lines
41 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! The v1 vector pack, built from the reference implementation.
//!
//! Inputs, names and descriptions are written here; every expected value is
//! computed at generation time by the reference: `smart-byte-core` (canonical
//! CBOR, SAIDs, signatures, envelopes, the inbox), `smart-byte-keri-witness`
//! (key-event logs), `smart-byte-state` (the cluster state machine) and
//! `smart-byte-lockstep` (the commit rule). A negative
//! vector also states the error class it is meant to produce, and generation
//! fails if the reference refuses it for any other reason — so a vector
//! cannot pass by accident while testing nothing.
use std::collections::BTreeMap;
use smart_byte_core::Said;
use smart_byte_core::canonical::{self, Value};
use smart_byte_core::inbox::{Delivery, Inbox};
use smart_byte_core::said::{self, SAID_FIELD, SAID_PLACEHOLDER, SaidDerivation};
use smart_byte_core::sign::{self, Signer as _, SigningKey};
use smart_byte_core::wire;
use smart_byte_lockstep::{node_view, supermajority_threshold, tally};
use crate::json::{J, pretty, to_json};
use crate::logs;
use crate::world::{Key, World};
pub use crate::world::{RFC8032_TEST1_SEED, RFC8032_TEST2_SEED};
/// The pack's vector files, in the order the README lists them.
pub const FILES: [&str; 8] = [
"canonicalisation_vectors.json",
"said_vectors.json",
"signature_vectors.json",
"kel_vectors.json",
"envelope_vectors.json",
"state_vectors.json",
"lockstep_vectors.json",
"gossip_vectors.json",
];
/// Every file of the pack: (file name, contents).
pub fn generate() -> Vec<(&'static str, String)> {
let world = World::build();
let saids = SaidVectors::build(&world);
vec![
(FILES[0], pretty(&canonicalisation())),
(FILES[1], pretty(&saids.json)),
(FILES[2], pretty(&signatures(&world))),
(FILES[3], pretty(&logs::kel(&world))),
(FILES[4], pretty(&logs::envelopes(&world))),
(FILES[5], pretty(&crate::states::states(&world))),
(FILES[6], pretty(&lockstep())),
(FILES[7], pretty(&gossip(&saids))),
]
}
fn cbor(v: &Value) -> Vec<u8> {
canonical::encode(v).expect("every value the pack builds has a canonical form")
}
fn text(s: &str) -> Value {
Value::text(s)
}
fn uint(n: u64) -> Value {
Value::Unsigned(n)
}
// ---------------------------------------------------------------------------
// canonicalisation_vectors.json
// ---------------------------------------------------------------------------
fn canon_positive(name: &str, description: &str, input: Value) -> J {
let bytes = cbor(&input);
// The decoder must accept what the encoder wrote, unchanged.
assert_eq!(cbor(&canonical::decode(&bytes).unwrap()), bytes, "{name}");
J::obj([
("name", J::from(name)),
("description", J::from(description)),
("input", to_json(&input)),
("expected_cbor_hex", J::Str(hex::encode(bytes))),
])
}
fn canon_negative(name: &str, description: &str, input: &[u8], class: &str) -> J {
let refused = canonical::decode(input).expect_err(name);
assert_eq!(
refused.class(),
class,
"{name} is refused for the wrong reason"
);
J::obj([
("name", J::from(name)),
("description", J::from(description)),
("input_cbor_hex", J::Str(hex::encode(input))),
("error", J::from(class)),
])
}
fn concat(parts: &[&[u8]]) -> Vec<u8> {
parts.concat()
}
fn canonicalisation() -> J {
let payload = Value::Bytes((0u8..8).collect());
J::Arr(vec![
canon_positive(
"canon-001-sorted-map-keys",
"Map keys are written in the bytewise order of their canonical encodings (identity spec §2.3). The input lists the keys out of order; the canonical encoding sorts them.",
Value::map([("z", uint(1)), ("a", uint(2)), ("m", uint(3))]),
),
canon_positive(
"canon-002-shortest-form-integers",
"Integers use the shortest head that holds them: no leading zeros, no over-wide argument. 0, 23, 24, 255, 256, 65535 and 65536 sit on the boundaries of CBOR's argument widths.",
Value::map([
("i0", uint(0)),
("i23", uint(23)),
("i24", uint(24)),
("i255", uint(255)),
("i256", uint(256)),
("i65535", uint(65535)),
("i65536", uint(65536)),
]),
),
canon_positive(
"canon-003-no-tags",
"A byte string is written untagged, as major type 2. Wrapping bytes in tag 21 (\"expected base64url conversion\") is tempting, but canonical encoding forbids tags (identity spec §2.3). canon-015 is the tagged form, which a decoder refuses rather than silently stripping the tag.",
Value::map([("payload", payload.clone())]),
),
canon_negative(
"canon-004-floats-forbidden",
"Floats are forbidden (identity spec §2.3): a value that would be a float elsewhere is carried as a fixed-point integer pair. The input is a map holding 1.5 as a half-precision float, and a decoder refuses it.",
&concat(&[&[0xa1], &cbor(&text("exact_half")), &[0xf9, 0x3e, 0x00]]),
"float_forbidden",
),
canon_positive(
"canon-005-bytes-not-text-for-byte-arrays",
"Byte values are byte strings (major type 2) — never text, and never an array of small integers, which is what a generic serializer writes for a fixed-size byte array by default. The members hold a 32-byte digest, a 32-byte public key and a 64-byte signature. A SAID's text form (identity spec §2.2) is text; the digest it spells is bytes.",
Value::map([
("said", Value::Bytes(vec![0x00; 32])),
("key", Value::Bytes(vec![0x11; 32])),
("sig", Value::Bytes(vec![0x22; 64])),
]),
),
canon_positive(
"canon-006-key-order-follows-encoded-bytes",
"Keys sort by their encoded bytes, not their text (identity spec §2.3). As text \"aa\" comes before \"b\"; encoded, \"b\" (0x61 0x62) comes before \"aa\" (0x62 0x61 0x61), because a string's length is in its first byte. canon-012 is the text-ordered encoding, which a decoder refuses.",
Value::map([("aa", uint(1)), ("b", uint(2))]),
),
canon_positive(
"canon-007-integer-boundaries-and-negatives",
"Negative integers (major type 1) and the 32-bit/64-bit boundary. -1 is 0x20, and -24 is the last one-byte form; 4294967295 is the largest four-byte argument and 4294967296 the smallest eight-byte one.",
Value::Array(
[
-1i64, -24, -25, -256, -257, -65536, -65537, 4294967295, 4294967296,
]
.into_iter()
.map(Value::int)
.collect(),
),
),
canon_positive(
"canon-008-string-length-boundary",
"A length uses the shortest head too. A 23-character text string carries its length in its first byte; a 24-character one needs the one-byte argument.",
Value::Array(vec![text(&"a".repeat(23)), text(&"b".repeat(24))]),
),
canon_positive(
"canon-009-empty-containers-and-simple-values",
"An empty array, map, text string and byte string, and the simple values null, true and false — each a single byte.",
Value::map([
("a", Value::Array(vec![])),
("b", Value::Map(vec![])),
("c", text("")),
("d", Value::Bytes(vec![])),
("e", Value::Null),
("f", Value::Bool(true)),
("g", Value::Bool(false)),
]),
),
canon_negative(
"canon-010-non-shortest-integer-refused",
"Zero written with a one-byte argument (0x18 0x00). Identity spec §2.3: \"a value of 0 is encoded as 0x00, never as 0x18 0x00\". A decoder refuses it rather than normalising it.",
&[0x18, 0x00],
"non_shortest_integer",
),
canon_negative(
"canon-011-non-shortest-length-refused",
"The one-character text \"a\" with its length in a one-byte argument (0x78 0x01) rather than in the head. A length has one canonical form too.",
&[0x78, 0x01, 0x61],
"non_shortest_length",
),
canon_negative(
"canon-012-text-ordered-keys-refused",
"canon-006's map with its keys in text order (\"aa\" before \"b\"). The keys are distinct and the map is well-formed, but the order is not the canonical one, so the encoding is refused.",
&concat(&[
&[0xa2],
&cbor(&text("aa")),
&[0x01],
&cbor(&text("b")),
&[0x02],
]),
"unsorted_map_keys",
),
canon_negative(
"canon-013-duplicate-key-refused",
"A map holding the key \"a\" twice. It has no canonical form, and which of the two values a lenient decoder keeps differs between implementations.",
&concat(&[
&[0xa2],
&cbor(&text("a")),
&[0x01],
&cbor(&text("a")),
&[0x02],
]),
"duplicate_map_key",
),
canon_negative(
"canon-014-indefinite-length-refused",
"An array written with an indefinite length (0x9f … 0xff). Identity spec §2.3 forbids indefinite-length items: the same array has a definite-length encoding.",
&[0x9f, 0x01, 0x02, 0xff],
"indefinite_length",
),
canon_negative(
"canon-015-tagged-byte-string-refused",
"canon-003's map with its byte string wrapped in tag 21. Tags are forbidden (identity spec §2.3): the decoder refuses the input; it does not strip the tag and accept what is left.",
&concat(&[&[0xa1], &cbor(&text("payload")), &[0xd5], &cbor(&payload)]),
"tag_forbidden",
),
canon_negative(
"canon-016-single-precision-float-refused",
"A single-precision float (the largest finite one). Refused, as every float is.",
&[0xfa, 0x7f, 0x7f, 0xff, 0xff],
"float_forbidden",
),
canon_negative(
"canon-017-double-precision-float-refused",
"A double-precision float (the largest finite one). Refused, as every float is.",
&[0xfb, 0x7f, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
"float_forbidden",
),
canon_negative(
"canon-018-trailing-bytes-refused",
"Two items back to back. A canonical encoding is exactly one item: bytes after it would sit outside the value a SAID commits to, so the input is refused.",
&[0x01, 0x01],
"trailing_bytes",
),
canon_negative(
"canon-019-invalid-utf8-refused",
"A text string whose bytes (0xc3 0x28) are not UTF-8. CBOR text strings are UTF-8 (RFC 8949 §3.1), so the input is refused.",
&[0x62, 0xc3, 0x28],
"invalid_utf8",
),
canon_negative(
"canon-020-simple-value-refused",
"The simple value undefined (0xf7). Identity spec §2.3 admits false, true and null and no other simple value, so the data model has nothing to decode it to and the input is refused.",
&[0xf7],
"simple_value_forbidden",
),
])
}
// ---------------------------------------------------------------------------
// said_vectors.json
// ---------------------------------------------------------------------------
/// The SAID vectors, plus each positive vector's derivation for the files
/// that refer to them.
pub struct SaidVectors {
pub json: J,
pub by_name: BTreeMap<&'static str, SaidDerivation>,
/// said-009's input: said-008's object with `n` written non-canonically.
pub non_canonical_copy: Vec<u8>,
/// said-011's input: said-008's object with `n` changed, `d` kept.
pub changed_copy: Vec<u8>,
}
fn said_positive(
by_name: &mut BTreeMap<&'static str, SaidDerivation>,
name: &'static str,
description: &str,
input: Value,
) -> J {
let d = said::derive(&input, SAID_FIELD).expect(name);
assert_eq!(
said::verify(&d.encoded, SAID_FIELD).as_ref(),
Ok(&d.said),
"{name}"
);
let json = J::obj([
("name", J::from(name)),
("description", J::from(description)),
("said_field", J::from(SAID_FIELD)),
("input", to_json(&input)),
("canonical_cbor_hex", J::Str(hex::encode(&d.hashed))),
("blake3_hex", J::Str(hex::encode(d.said.as_bytes()))),
("expected_said", J::Str(d.said.to_said_text())),
("said_cbor_hex", J::Str(hex::encode(&d.encoded))),
]);
by_name.insert(name, d);
json
}
fn said_negative(
name: &str,
description: &str,
input: &[u8],
class: &str,
extra: Vec<(&str, J)>,
) -> J {
let refused = said::verify(input, SAID_FIELD).expect_err(name);
assert_eq!(
refused.class(),
class,
"{name} is refused for the wrong reason"
);
let mut entries = vec![
("name", J::from(name)),
("description", J::from(description)),
("said_field", J::from(SAID_FIELD)),
("input_cbor_hex", J::Str(hex::encode(input))),
("error", J::from(class)),
];
entries.extend(extra);
J::obj(entries)
}
/// `value` — a map with a `d` member — with `d` holding the placeholder.
fn with_placeholder(value: Value) -> Value {
let Value::Map(entries) = value else {
panic!("a SAID-addressed object is a map")
};
Value::Map(
entries
.into_iter()
.map(|(k, v)| {
if k == Value::text(SAID_FIELD) {
(k, Value::text(SAID_PLACEHOLDER))
} else {
(k, v)
}
})
.collect(),
)
}
/// `v`'s canonical value with its SAID replaced by the placeholder.
fn object<T: serde::Serialize>(v: &T) -> Value {
with_placeholder(wire::to_value(v).expect("the object encodes"))
}
impl SaidVectors {
pub fn build(world: &World) -> Self {
let mut by_name = BTreeMap::new();
let placeholder = || text(SAID_PLACEHOLDER);
let rotation = world.alice.log[1].event();
let interaction = world.alice.log[2].event();
let mut vectors = vec![
said_positive(
&mut by_name,
"said-001-issued-envelope",
"env-001, the USD byte as the issuer issued it to Alice, with its SAID — the snapshot SAID — replaced by the placeholder. The provenance inside carries the byte's identity (said-002) in its own `d`, which is part of what is hashed here. Nested maps, byte strings, text and integers all go through one canonical encoding.",
object(&world.issued),
),
said_positive(
&mut by_name,
"said-002-byte-identity",
"env-001's provenance — the issuer's attestation to the cargo, the recipient and the nonce, with the issuer's signature — with its `d` replaced by the placeholder. Its SAID is the byte's identity (treatise §8.2), fixed at issuance: no transfer changes it.",
object(&world.issued.provenance),
),
said_positive(
&mut by_name,
"said-003-transferred-envelope",
"env-002, the same byte after three transfers. The identity inside is said-002's, unchanged; the snapshot SAID differs from said-001's because a SAID covers the whole object it is derived over, ownership chain included.",
object(&world.transferred),
),
said_positive(
&mut by_name,
"said-004-rotation-event",
"Alice's rotation event (kel-002, event 1) with its `d` replaced by the placeholder. A rotation's `i` is the controller's AID, written out; only an inception's own `i` is placeholdered too (§3.1).",
object(&rotation),
),
said_positive(
&mut by_name,
"said-005-interaction-event",
"Alice's interaction event (kel-002, event 2), whose anchor is said-002's SAID: the identity of the byte she was issued.",
object(&interaction),
),
];
for (name, said) in [
("said-001-issued-envelope", world.issued.said()),
("said-002-byte-identity", world.issued.identity()),
("said-003-transferred-envelope", world.transferred.said()),
("said-004-rotation-event", rotation.said()),
("said-005-interaction-event", interaction.said()),
] {
assert_eq!(
by_name[name].said, said,
"{name} derives the SAID the object carries"
);
}
vectors.push(said_positive(
&mut by_name,
"said-006-only-said-field",
"The empty-map case of identity spec §2.5: an object whose only member is its SAID field. It is the smallest object a SAID can be derived over — a truly empty map has nowhere to put one.",
Value::map([("d", placeholder())]),
));
let prefixed_key = Key::named("issuer-0").public().0;
vectors.push(said_positive(
&mut by_name,
"said-007-one-field-per-key",
"The one-field case: identity spec §3.4's per-key object {d, k}, whose SAID is a next-key commitment. k is a byte string: the algorithm identifier 0x01 (Ed25519) followed by the public key of RFC 8032's first test seed (§3.3) — the issuer's key in kel-001.",
Value::map([("d", placeholder()), ("k", Value::Bytes(prefixed_key.clone()))]),
));
assert_eq!(
said::per_key_said(&prefixed_key),
by_name["said-007-one-field-per-key"].said,
"per_key_said must agree with the vector it implements"
);
vectors.push(said_positive(
&mut by_name,
"said-008-spec-worked-example",
"The worked example of identity spec §2.4: {\"d\": placeholder, \"n\": 1}. Its canonical encoding is 61 bytes, and §2.4 prints the digest and SAID given here.",
Value::map([("d", placeholder()), ("n", uint(1))]),
));
// The adversarial cases of identity spec §2.5, built on said-008.
let example = &by_name["said-008-spec-worked-example"];
let canonical_said = example.said.to_said_text();
// n = 1 is the final byte of the canonical form; spell it 0x18 0x01.
let lax_n = |canonical: &[u8]| {
let mut v = canonical[..canonical.len() - 1].to_vec();
v.extend_from_slice(&[0x18, 0x01]);
v
};
let non_canonical_copy = lax_n(&example.encoded);
let hashed_as_received = Said::hash(&lax_n(&example.hashed)).to_said_text();
vectors.push(said_negative(
"said-009-non-canonical-copy-rejected",
"The adversarial case of identity spec §2.5, aimed at re-encoding. These are said-008's bytes with n written 0x18 0x01 instead of 0x01, still stating said-008's SAID. A verifier that re-encodes before hashing gets said-008's bytes back and accepts, so a second byte string would verify under said-008's SAID. A conforming verifier refuses the input as non-canonical before computing anything.",
&non_canonical_copy,
"non_shortest_integer",
vec![
("said_of_received_bytes", J::Str(hashed_as_received)),
("said_after_reencoding", J::Str(canonical_said.clone())),
],
));
// The same object with its keys reversed, stating the SAID of exactly
// those bytes: self-consistent for a verifier that hashes as received.
let reversed = |d: &Value| {
concat(&[
&[0xa2],
&cbor(&text("n")),
&[0x01],
&cbor(&text("d")),
&cbor(d),
])
};
let reversed_said = Said::hash(&reversed(&placeholder())).to_said_text();
let reversed_bytes = reversed(&text(&reversed_said));
vectors.push(said_negative(
"said-010-self-consistent-non-canonical-rejected",
"The adversarial case aimed the other way. said-008's object with its keys in reverse order, stating the SAID of exactly these bytes (placeholder substituted in place). A verifier that hashes what it received, without checking canonical form, accepts it. A conforming verifier refuses it as non-canonical.",
&reversed_bytes,
"unsorted_map_keys",
vec![
("said_of_received_bytes", J::Str(reversed_said)),
("said_after_reencoding", J::Str(canonical_said)),
],
));
let mut changed_copy = example.encoded.clone();
*changed_copy.last_mut().unwrap() = 0x02;
vectors.push(said_negative(
"said-011-changed-content-rejected",
"said-008's object with n changed from 1 to 2 and its SAID left as it was. The bytes are canonical and well-formed, but they state a SAID they do not hash to.",
&changed_copy,
"said_mismatch",
vec![],
));
SaidVectors {
json: J::Arr(vectors),
by_name,
non_canonical_copy,
changed_copy,
}
}
fn said(&self, name: &str) -> &SaidDerivation {
&self.by_name[name]
}
}
// ---------------------------------------------------------------------------
// signature_vectors.json
// ---------------------------------------------------------------------------
#[allow(clippy::too_many_arguments)]
fn signature_vector(
name: &str,
description: &str,
message: &[u8],
signing_seed: Option<[u8; 32]>,
verifying_key: [u8; 32],
signature: &[u8],
expected_verify: bool,
error: Option<&str>,
) -> J {
if let Some(seed) = signing_seed {
assert_eq!(
SigningKey::from_bytes(&seed).verifying_key().to_bytes(),
verifying_key,
"{name}: a seed's vector carries the key the seed derives"
);
}
let result = sign::verify(&verifying_key, message, signature);
assert_eq!(result.is_ok(), expected_verify, "{name}");
assert_eq!(
result.err().map(|e| e.class()),
error,
"{name} fails for the wrong reason"
);
J::obj([
("name", J::from(name)),
("description", J::from(description)),
("message_hex", J::Str(hex::encode(message))),
(
"signing_key_seed",
signing_seed
.map(|s| J::Str(hex::encode(s)))
.unwrap_or(J::Null),
),
("verifying_key", J::Str(hex::encode(verifying_key))),
("signature", J::Str(hex::encode(signature))),
("expected_verify", J::from(expected_verify)),
("error", error.map(J::from).unwrap_or(J::Null)),
])
}
/// The Ed25519 group order L, little-endian.
const ED25519_L: [u8; 32] = [
0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
];
/// `signature` with its scalar S replaced by S + L: the same scalar, written
/// outside [0, L).
fn with_s_plus_l(signature: &[u8; 64]) -> [u8; 64] {
let mut out = *signature;
let mut carry = 0u16;
for i in 0..32 {
let v = u16::from(signature[32 + i]) + u16::from(ED25519_L[i]) + carry;
out[32 + i] = v as u8;
carry = v >> 8;
}
assert_eq!(carry, 0, "S + L fits in 32 bytes for any S < L");
out
}
/// The point y = 3 (sign bit clear) spelled with y = p + 3: an out-of-range
/// encoding of a valid, large-order point.
const KEY_Y_PLUS_P: [u8; 32] = [
0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f,
];
fn key_of(seed: &[u8; 32]) -> [u8; 32] {
SigningKey::from_bytes(seed).verifying_key().to_bytes()
}
fn signatures(world: &World) -> J {
let (issuer, alice, zero) = (
Key::named("issuer-0").seed,
Key::named("alice-0").seed,
[0u8; 32],
);
let attestation = world.issued.provenance.signed_bytes();
let inception = world.alice.log[0].bytes.clone();
let sig1 = SigningKey::from_bytes(&issuer)
.sign(&attestation)
.to_bytes();
let sig2 = SigningKey::from_bytes(&alice).sign(&inception).to_bytes();
J::Arr(vec![
signature_vector(
"sig-001-issuer-attestation",
"What the issuer signs to issue env-001: the canonical encoding of {cargo, issuer, nonce, recipient} (envelope wire format §3). The key is RFC 8032's first test key, and Ed25519 is deterministic, so this is the one signature that key makes over this message — the 64 bytes after the algorithm identifier in env-001's provenance signature.",
&attestation,
Some(issuer),
key_of(&issuer),
&sig1,
true,
None,
),
signature_vector(
"sig-002-key-event",
"Alice's inception event (kel-002, event 0), signed by the key it reveals: RFC 8032's second test key. The message is the event's canonical encoding (identity spec §3.5).",
&inception,
Some(alice),
key_of(&alice),
&sig2,
true,
None,
),
signature_vector(
"sig-003-message-swapped",
"sig-002's signature presented with sig-001's message: an adversary swaps the message and forwards the original signature. The signature is well-formed but binds another message, so verification MUST fail.",
&attestation,
Some(alice),
key_of(&alice),
&sig2,
false,
Some("signature_invalid"),
),
signature_vector(
"sig-004-wrong-key",
"sig-002's message and signature, presented with another key — the one the all-zero seed derives. The signature is valid for the key that made it, which is not this one, so verification MUST fail.",
&inception,
Some(zero),
key_of(&zero),
&sig2,
false,
Some("signature_invalid"),
),
signature_vector(
"sig-005-malformed-signature",
"sig-002's signature cut to 63 bytes. It is refused as malformed before any cryptography runs, and an implementation MUST NOT panic on it.",
&inception,
Some(alice),
key_of(&alice),
&sig2[..63],
false,
Some("malformed_signature"),
),
signature_vector(
"sig-006-non-canonical-s-rejected",
"sig-002's signature with its scalar S replaced by S + L: the same scalar, written outside [0, L). RFC 8032 §5.1.7 requires refusing it; accepting it would make signatures malleable, giving a second valid-looking signature for one message and key.",
&inception,
Some(alice),
key_of(&alice),
&with_s_plus_l(&sig2),
false,
Some("signature_invalid"),
),
signature_vector(
"sig-007-non-canonical-key-rejected",
"A verifying key spelled with y = p + 3: an out-of-range encoding of the valid, large-order point with y = 3. RFC 8032 §5.1.3 requires decoding to fail when y is not below p, so the key is refused as malformed before any verification runs — one key has one spelling. There is no seed: no key pair has this spelling.",
&inception,
None,
KEY_Y_PLUS_P,
&sig2,
false,
Some("malformed_verifying_key"),
),
])
}
// ---------------------------------------------------------------------------
// lockstep_vectors.json
// ---------------------------------------------------------------------------
/// One node: its name, the hash it reported, and the hash it computed.
type NodeHashes<'a> = (&'a str, u8, u8);
fn lockstep_vector(
name: &str,
description: &str,
frame: u64,
cluster_size: usize,
nodes: &[NodeHashes<'_>],
) -> J {
let reported: Vec<(String, [u8; 32])> = nodes
.iter()
.map(|(node, r, _)| ((*node).to_owned(), [*r; 32]))
.collect();
// What the broadcast hashes decide: the frame's public record.
let public = tally(cluster_size, &reported).expect(name);
let mut outcomes = Vec::new();
let mut logs = Vec::new();
for (node, _, c) in nodes {
let computed = [*c; 32];
let view = node_view(cluster_size, &reported, &(*node).to_owned(), &computed).expect(name);
outcomes.push((
(*node).to_owned(),
J::from(view.outcome(&computed).as_str()),
));
let log: Vec<J> = view
.dissent_entry(frame, &computed)
.into_iter()
.map(|e| {
J::obj([
("frame", J::from(e.frame)),
("dissenting_peers", J::strs(e.dissenting_peers)),
("local_hash", J::Str(hex::encode(e.local_hash))),
(
"supermajority_hash",
J::Str(hex::encode(e.supermajority_hash)),
),
])
})
.collect();
logs.push(((*node).to_owned(), J::Arr(log)));
}
let hashes = |pick: fn(&NodeHashes<'_>) -> u8| {
J::obj(
nodes
.iter()
.map(|n| (n.0.to_owned(), J::Str(hex::encode([pick(n); 32])))),
)
};
J::obj([
("name", J::from(name)),
("description", J::from(description)),
("node_count", J::from(cluster_size)),
("frame_number", J::from(frame)),
("node_reported_hashes", hashes(|n| n.1)),
("node_computed_hashes", hashes(|n| n.2)),
(
"expected_threshold",
J::from(supermajority_threshold(cluster_size)),
),
("expected_commit", J::from(public.committed())),
(
"expected_state_hash",
public
.supermajority_hash
.map(|h| J::Str(hex::encode(h)))
.unwrap_or(J::Null),
),
("expected_outcomes", J::Obj(outcomes)),
("expected_dissent_logs", J::Obj(logs)),
])
}
fn lockstep() -> J {
J::Arr(vec![
lockstep_vector(
"lockstep-001-unanimous-commit",
"All four nodes report the post-frame state hash they computed, and all agree. The threshold is floor(2·4/3)+1 = 3, so the frame commits, every node's outcome is a clean commit (treatise §13.3), and no dissent is logged. State hashes are opaque 32-byte values compared for equality. Each node's outcome comes from its own view: the hashes its peers reported plus the one it computed itself (§13.3, §13.4).",
184237,
4,
&[
("node_1", 0xaa, 0xaa),
("node_2", 0xaa, 0xaa),
("node_3", 0xaa, 0xaa),
("node_4", 0xaa, 0xaa),
],
),
lockstep_vector(
"lockstep-002-supermajority-commit",
"Three of four nodes agree. node_4's state diverged, and it reports the hash it computed. The threshold of 3 is met: nodes 1–3 commit with dissent, and each writes one dissent-log entry naming node_4 (treatise §14.1). node_4 sees a supermajority on a hash that is not its own — minority divergence — so it halts and writes nothing.",
184238,
4,
&[
("node_1", 0xbb, 0xbb),
("node_2", 0xbb, 0xbb),
("node_3", 0xbb, 0xbb),
("node_4", 0xdd, 0xdd),
],
),
lockstep_vector(
"lockstep-003-threshold-not-met",
"No hash is reported by more than two of the four nodes. None reaches the threshold of 3, so the frame does not commit and every node's outcome is a cluster split (§13.3). No dissent is logged: dissent is divergence from a committed hash (§14.2), and there is none.",
184239,
4,
&[
("node_1", 0xcc, 0xcc),
("node_2", 0xcc, 0xcc),
("node_3", 0xdd, 0xdd),
("node_4", 0xee, 0xee),
],
),
lockstep_vector(
"lockstep-004-three-nodes-one-diverged-split",
"The n = 3 exception (treatise §13.2). The threshold is 3, so one node whose state diverged leaves 2 of the 3 votes the frame needs. The cluster detects the divergence but cannot tolerate it: every node halts with a cluster split.",
184240,
3,
&[
("node_1", 0x11, 0x11),
("node_2", 0x11, 0x11),
("node_3", 0x22, 0x22),
],
),
lockstep_vector(
"lockstep-005-seven-nodes-two-dissenters",
"Seven nodes, threshold 5: five agree and two diverged nodes report hashes of their own. The frame commits. The dissent entry names both dissenters in ascending order, so every committing node writes the same entry (§14.2).",
184241,
7,
&[
("node_1", 0x5a, 0x5a),
("node_2", 0x5a, 0x5a),
("node_3", 0x5a, 0x5a),
("node_4", 0x5a, 0x5a),
("node_5", 0x5a, 0x5a),
("node_6", 0x6b, 0x6b),
("node_7", 0x7c, 0x7c),
],
),
lockstep_vector(
"lockstep-006-transport-liar-is-logged",
"Treatise §13.4: node_4 computed the honest hash but broadcast another. Its peers see 3-of-4 agreement and record node_4 in their dissent logs. node_4 tallies its own honest hash, sees unanimity and commits cleanly: its lie surfaces only in its peers' logs, which is why the dissent log is the substrate's misbehaviour signal (§14).",
184242,
4,
&[
("node_1", 0xbb, 0xbb),
("node_2", 0xbb, 0xbb),
("node_3", 0xbb, 0xbb),
("node_4", 0xdd, 0xbb),
],
),
lockstep_vector(
"lockstep-007-three-node-transport-liar",
"§13.2 and §13.4 together. At n = 3 a node that broadcasts a hash other than the one it computed splits the honest pair, who halt. The liar sees its own honest hash agree with both peers and commits. The reported hashes reach no supermajority, so the frame's public record is no commit.",
184243,
3,
&[
("node_1", 0x11, 0x11),
("node_2", 0x11, 0x11),
("node_3", 0x22, 0x11),
],
),
])
}
// ---------------------------------------------------------------------------
// gossip_vectors.json
// ---------------------------------------------------------------------------
fn gossip_vector(
name: &str,
description: &str,
receiver: &str,
deliveries: &[(&str, String, Vec<u8>)],
) -> J {
let mut inbox = Inbox::new();
let mut results = Vec::new();
for (_, claimed, bytes) in deliveries {
results.push(match inbox.deliver(claimed, bytes) {
Delivery::Accepted(s) => J::obj([
("outcome", J::from("accepted")),
("said", J::Str(s.to_said_text())),
]),
Delivery::Duplicate(s) => J::obj([
("outcome", J::from("duplicate")),
("said", J::Str(s.to_said_text())),
]),
Delivery::Rejected(e) => J::obj([
("outcome", J::from("rejected")),
("error", J::from(e.class())),
]),
});
}
J::obj([
("name", J::from(name)),
("description", J::from(description)),
("receiver", J::from(receiver)),
(
"deliveries",
J::Arr(
deliveries
.iter()
.map(|(from, claimed, bytes)| {
J::obj([
("from", J::from(*from)),
("claimed_said", J::Str(claimed.clone())),
("cbor_hex", J::Str(hex::encode(bytes))),
])
})
.collect(),
),
),
("expected_results", J::Arr(results)),
(
"expected_held",
J::Arr(
inbox
.held()
.iter()
.map(|(said, bytes)| {
J::obj([
("said", J::Str(said.to_said_text())),
("cbor_hex", J::Str(hex::encode(bytes))),
])
})
.collect(),
),
),
])
}
fn gossip(saids: &SaidVectors) -> J {
let empty = saids.said("said-006-only-said-field");
let per_key = saids.said("said-007-one-field-per-key");
let example = saids.said("said-008-spec-worked-example");
let text = |d: &SaidDerivation| d.said.to_said_text();
J::Arr(vec![
gossip_vector(
"gossip-001-cross-node-propagation",
"An object published at node_A reaches node_B. Its bytes are canonical, they hash to the SAID they state, and that is the SAID they were announced under, so node_B accepts it (identity spec §2.6) and holds exactly the bytes it received. Gossip carries the object itself, never a re-encoding of it.",
"node_B",
&[("node_A", text(per_key), per_key.encoded.clone())],
),
gossip_vector(
"gossip-002-said-conflict-rejected",
"node_C holds said-008's object when a second object arrives under the same SAID: said-008's with n changed and its SAID left as it was (said-011's bytes). Verification comes before deduplication (identity spec §2.6), so the newcomer is reported as a SAID mismatch, which makes the substitution attempt visible, instead of being absorbed as a duplicate of what node_C holds.",
"node_C",
&[
("node_A", text(example), example.encoded.clone()),
("node_B", text(example), saids.changed_copy.clone()),
],
),
gossip_vector(
"gossip-003-backfill-answer-verified",
"node_B requests the object behind said-007's SAID: a party holding an identifier can request the object from any peer (treatise §21.4). node_A answers with said-008's object, which is genuine and self-consistent but not the one requested, and is refused. node_D's answer hashes to the requested SAID and is accepted (identity spec §2.6).",
"node_B",
&[
("node_A", text(per_key), example.encoded.clone()),
("node_D", text(per_key), per_key.encoded.clone()),
],
),
gossip_vector(
"gossip-004-duplicate-delivery-deduplicated",
"The same object reaches node_B from two peers. The second delivery verifies and is a duplicate of a SAID already held, so it changes nothing: the overlay deduplicates by content address (treatise §11.4).",
"node_B",
&[
("node_A", text(empty), empty.encoded.clone()),
("node_C", text(empty), empty.encoded.clone()),
],
),
gossip_vector(
"gossip-005-non-canonical-copy-rejected",
"said-009's bytes reach node_B announced under said-008's SAID, the SAID a re-encoding receiver would compute for them. The bytes are not canonical, so node_B refuses them (identity spec §2.5, §2.6). Accepting them would let a second byte string circulate under said-008's SAID.",
"node_B",
&[("node_A", text(example), saids.non_canonical_copy.clone())],
),
gossip_vector(
"gossip-006-substitute-arriving-first-rejected",
"gossip-002 in the other order: the altered object reaches node_C first and is refused, then said-008's object arrives and is accepted. Arrival order decides nothing; the content does.",
"node_C",
&[
("node_B", text(example), saids.changed_copy.clone()),
("node_A", text(example), example.encoded.clone()),
],
),
])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::json::bytes_json;
#[test]
fn generation_is_deterministic() {
assert_eq!(generate(), generate());
}
#[test]
fn the_typed_json_for_bytes_is_one_member() {
assert_eq!(bytes_json(&[0xab]), J::obj([("$bytes", J::from("ab"))]));
}
}