Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"built_by": "t27c-bootstrap@0.5.2+01d0a635f",
"closure": [],
"config": "gen=c,rust,verilog,zig;tests=zig",
"gen_hash_c": "sha256:000e5eab294c0c753504f25195acc809377d7f7a8e95a2b27cf4980a00db9311",
"gen_hash_rust": "sha256:b3e126bc2a02b0b09abd2ce4b395f3ef014003e70c92e85fab3b97f4a90659e7",
"gen_hash_verilog": "sha256:468becd2cbd4b9d753dd1788c6e233704d0c828736ebad0483ff715e996f967b",
"gen_hash_zig": "sha256:f02dfea66b3e4c7e419cf89902d92d00bb0115b6c77dd54ae65b1fa3ae4fa529",
"module": "port::trios::crates::trios-chat::rings::CR-CHAT-03::src::ratchet_tree_update_integrity_guard",
"ring": 12,
"seal_schema": 2,
"sealed_at": "2026-10-17T14:00:53Z",
"sealed_by": "t27c-bootstrap@0.5.2",
"spec_hash": "sha256:768e5ce725e6b6c2a407b5cd0c9628774cb202409c001b2ebcc4ffd8dadaa67d",
"spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/ratchet_tree_update_integrity_guard.t27",
"tests": {
"failed": 0,
"failing": [],
"forced": false,
"passed": 15,
"total": 15,
"vacuous": 0
},
"toolchain": {
"test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d",
"zig": "0.16.0"
}
}
1 change: 1 addition & 0 deletions docs/reports/t27b_expectations.json
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,7 @@
{"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-01/src/key_package_expiry_guard.t27", "t27b": "pass"},
{"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-02/src/epoch_rollover_wraparound_guard.t27", "t27b": "pass"},
{"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/ratchet_tree_resolution_guard.t27", "t27b": "pass"},
{"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/ratchet_tree_update_integrity_guard.t27", "t27b": "pass"},
{"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/unmerged_leaves_bound_guard.t27", "t27b": "pass"},
{"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-05/src/envelope_size_distribution_uniformity.t27", "t27b": "pass"},
{"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_decoy_ratio_governor.t27", "t27b": "pass"},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,347 @@
module port::trios::crates::trios-chat::rings::CR-CHAT-03::src::ratchet_tree_update_integrity_guard;

// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-03/src/ratchet_tree_update_integrity_guard.rs
// (CR-CHAT-03 - Ratchet tree update integrity guard, Wave-84
// Lane A, R-CHAT-3).
//
// Every TreeKEM update carries the parent hash binding its
// node to the tree structure; without the gate an attacker
// injects an orphan node whose hash sits outside
// consensus, swaps siblings and re-signs so recipients
// derive different keys, or replays a stale update from a
// prior epoch. The rules, in the original's fixed order:
// the batch ceiling (two hundred fifty-six), then per
// update the node index inside the tree (one thousand
// twenty-four), the fresh node (no duplicate in the
// batch), the claimed parent hash equal to the computed
// one, the epoch equal to the current one, and the signer
// an active leaf member.
//
// The original returns Result<(), TreeUpdateError>. This
// port carries the verdict as a named struct (owner
// decision D5): Ok first, then the six variants in the
// original's declaration order - HashMismatch,
// InvalidNodeIndex, EpochMismatch, DuplicateNode,
// BatchTooLarge, NonMemberSigner - with every payload
// field: the node index of the per-update rules, the
// signer leaf of the membership rule, and the expected
// and got epochs of the epoch rule.
//
// The original's BTreeSet of seen nodes becomes a scan
// over the updates already visited, the member set a scan
// over the member leaf slice (conventions section 4). The
// loop fixtures of the original - the node hash as the
// low byte of its index - arrive as a precomputed table
// the loop reads, the computed and tabular forms matching
// exactly (the narrowing cast on the original's counter
// does not exist in t27).

// Maximum nodes in the tree.
pub const RTUI_MAX_NODES: u64 = 1024;

// Maximum update batch size.
pub const RTUI_MAX_BATCH: usize = 256;

// A tree update proposal.
pub struct TreeUpdate {
node_index: u64,
parent_hash: [32]u8,
computed_hash: [32]u8,
epoch: u64,
signer_leaf: u64,
}

// Verdict codes: Ok(()) and TreeUpdateError's variants, in declaration order.
pub enum RtuiCode {
Ok,
HashMismatch,
InvalidNodeIndex,
EpochMismatch,
DuplicateNode,
BatchTooLarge,
NonMemberSigner,
}

// The verdict of the batch validation, with every payload
// field of the original's variants.
pub struct RtuiVerdict {
code: RtuiCode,
idx: u64,
expected: u64,
got: u64,
}

// Whole-form equality over two thirty-two-byte hashes - the
// original's array equality on the parent binding.
pub fn hashes32_equal(a: [32]u8, b: [32]u8) -> bool {
for i in 0..32 {
if (a[i] != b[i]) {
return false;
}
}
return true;
}

// Whether the leaf slice holds this signer - the original's
// member-set contains, answered by a scan.
pub fn member_contains(members: []const u64, leaf: u64) -> bool {
for i in 0..members.len {
if (members[i] == leaf) {
return true;
}
}
return false;
}

// Whether an update before position upto already claimed
// this node - the original's BTreeSet insert probe,
// answered by a scan.
pub fn node_seen_before(updates: []const TreeUpdate, upto: usize, node_index: u64) -> bool {
for j in 0..upto {
if (updates[j].node_index == node_index) {
return true;
}
}
return false;
}

// [VERIFIED] Validate a batch of tree updates. Returns Ok
// only when every rule holds, checked in the original's
// fixed order: the batch ceiling, then per update the node
// bound, the fresh node, the hash agreement, the epoch
// agreement and the member signer.
pub fn validate_tree_updates(updates: []const TreeUpdate, current_epoch: u64, member_leaves: []const u64) -> RtuiVerdict {
if (updates.len > RTUI_MAX_BATCH) {
return RtuiVerdict { code: RtuiCode.BatchTooLarge, idx: 0, expected: 0, got: 0, };
}
for i in 0..updates.len {
if (updates[i].node_index >= RTUI_MAX_NODES) {
return RtuiVerdict { code: RtuiCode.InvalidNodeIndex, idx: updates[i].node_index, expected: 0, got: 0, };
}
if (node_seen_before(updates, i, updates[i].node_index)) {
return RtuiVerdict { code: RtuiCode.DuplicateNode, idx: updates[i].node_index, expected: 0, got: 0, };
}
if (!hashes32_equal(updates[i].parent_hash, updates[i].computed_hash)) {
return RtuiVerdict { code: RtuiCode.HashMismatch, idx: updates[i].node_index, expected: 0, got: 0, };
}
if (updates[i].epoch != current_epoch) {
return RtuiVerdict { code: RtuiCode.EpochMismatch, idx: 0, expected: current_epoch, got: updates[i].epoch, };
}
if (!member_contains(member_leaves, updates[i].signer_leaf)) {
return RtuiVerdict { code: RtuiCode.NonMemberSigner, idx: updates[i].signer_leaf, expected: 0, got: 0, };
}
}
return RtuiVerdict { code: RtuiCode.Ok, idx: 0, expected: 0, got: 0, };
}

// The fixture members: the even leaves zero through six.
pub const MEMBERS: [4]u64 = [4]u64{0, 2, 4, 6,};

// The all-double-F-hex hash of the mismatch probe.
pub const HASH_FF: [32]u8 = [32]u8{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,}

// The low-byte table of the loop fixtures: the node hash
// reads the low byte of its index.
pub const RTUI_LOW: [257]u8 = [257]u8{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 0,}

// The fixture builder the tests share: an update whose
// parent and computed hashes agree, both the low byte of
// its node index.
fn make_update(node_index: u64, epoch: u64, signer_leaf: u64) -> TreeUpdate {
var h: [32]u8 = [32]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,};
const slot: usize = @as(usize, node_index % 256);
for j in 0..32 {
h[j] = RTUI_LOW[slot];
}
return TreeUpdate { node_index: node_index, parent_hash: h, computed_hash: h, epoch: epoch, signer_leaf: signer_leaf, };
}

// **RTUI-01** - the parent hash mismatch rejected: the
// claimed double-F-hex of the computed low byte, the
// verdict naming the node.
test rtui_01_hash_mismatch_rejected {
var u: TreeUpdate = make_update(1, 5, 0);
u.parent_hash = HASH_FF;
const batch: [1]TreeUpdate = [1]TreeUpdate{u,};
const v = validate_tree_updates(&batch, 5, &MEMBERS);
assert(v.code == RtuiCode.HashMismatch);
assert(v.idx == 1);
}

// **RTUI-02** - the invalid node index rejected: the node
// ceiling itself.
test rtui_02_invalid_node_rejected {
const u: TreeUpdate = make_update(1024, 5, 0);
const batch: [1]TreeUpdate = [1]TreeUpdate{u,};
const v = validate_tree_updates(&batch, 5, &MEMBERS);
assert(v.code == RtuiCode.InvalidNodeIndex);
assert(v.idx == 1024);
}

// **RTUI-03** - the epoch mismatch rejected: four of a
// current five, the verdict carrying both.
test rtui_03_epoch_mismatch_rejected {
const u: TreeUpdate = make_update(1, 4, 0);
const batch: [1]TreeUpdate = [1]TreeUpdate{u,};
const v = validate_tree_updates(&batch, 5, &MEMBERS);
assert(v.code == RtuiCode.EpochMismatch);
assert(v.expected == 5);
assert(v.got == 4);
}

// **RTUI-04** - the duplicate node rejected: node one
// updated twice in one batch.
test rtui_04_duplicate_rejected {
const batch: [2]TreeUpdate = [2]TreeUpdate{
make_update(1, 5, 0),
make_update(1, 5, 2),
};
const v = validate_tree_updates(&batch, 5, &MEMBERS);
assert(v.code == RtuiCode.DuplicateNode);
assert(v.idx == 1);
}

// **RTUI-05** - the batch too large rejected: two hundred
// fifty-seven of a ceiling of two hundred fifty-six,
// built in a loop like the original's.
test rtui_05_batch_too_large_rejected {
var batch: [257]TreeUpdate;
for i in 0..257 {
batch[i] = make_update(@as(u64, i), 5, 0);
}
const v = validate_tree_updates(&batch, 5, &MEMBERS);
assert(v.code == RtuiCode.BatchTooLarge);
}

// **RTUI-06** - the non-member signer rejected: ninety-nine
// of the even members.
test rtui_06_non_member_rejected {
const u: TreeUpdate = make_update(1, 5, 99);
const batch: [1]TreeUpdate = [1]TreeUpdate{u,};
const v = validate_tree_updates(&batch, 5, &MEMBERS);
assert(v.code == RtuiCode.NonMemberSigner);
assert(v.idx == 99);
}

// **RTUI-07** - the valid batch accepted.
test rtui_07_valid_accepted {
const batch: [2]TreeUpdate = [2]TreeUpdate{
make_update(1, 5, 0),
make_update(3, 5, 2),
};
const v = validate_tree_updates(&batch, 5, &MEMBERS);
assert(v.code == RtuiCode.Ok);
}

// **RTUI-08** - the empty batch accepted.
test rtui_08_empty_accepted {
const batch: [0]TreeUpdate = [0]TreeUpdate{};
const v = validate_tree_updates(&batch, 5, &MEMBERS);
assert(v.code == RtuiCode.Ok);
}

// **RTUI-09** - a single update accepted.
test rtui_09_single_accepted {
const batch: [1]TreeUpdate = [1]TreeUpdate{
make_update(1, 5, 0),
};
const v = validate_tree_updates(&batch, 5, &MEMBERS);
assert(v.code == RtuiCode.Ok);
}

// **RTUI-10** - the maximum batch accepted: exactly two
// hundred fifty-six distinct nodes.
test rtui_10_max_batch_accepted {
var batch: [256]TreeUpdate;
for i in 0..256 {
batch[i] = make_update(@as(u64, i), 5, 0);
}
const v = validate_tree_updates(&batch, 5, &MEMBERS);
assert(v.code == RtuiCode.Ok);
}

// The helpers have direct tests (coverage: Untested 0).
test hashes32_equal_answers {
const u: TreeUpdate = make_update(1, 5, 0);
assert(hashes32_equal(u.parent_hash, u.computed_hash));
assert(!hashes32_equal(u.parent_hash, HASH_FF));
}

test member_contains_answers {
assert(member_contains(&MEMBERS, 0));
assert(member_contains(&MEMBERS, 6));
assert(!member_contains(&MEMBERS, 99));
const empty: [0]u64 = [0]u64{};
assert(!member_contains(&empty, 0));
}

test node_seen_before_answers {
const batch: [3]TreeUpdate = [3]TreeUpdate{
make_update(1, 5, 0),
make_update(3, 5, 0),
make_update(1, 5, 0),
};
assert(!node_seen_before(&batch, 0, 1));
assert(!node_seen_before(&batch, 1, 3));
assert(node_seen_before(&batch, 2, 1));
assert(!node_seen_before(&batch, 2, 5));
}

// The helper builder has a direct test (coverage: the
// fixture form reads the table exactly).
test make_update_answers {
const u: TreeUpdate = make_update(1, 5, 0);
assert(u.node_index == 1);
assert(u.epoch == 5);
assert(u.signer_leaf == 0);
assert(u.parent_hash[0] == 1);
assert(u.computed_hash[0] == 1);
const big: TreeUpdate = make_update(300, 5, 0);
assert(big.parent_hash[0] == 44);
}

// Rule order is the original's fixed ladder: the batch
// ceiling over a flooded batch, then per update the node
// bound over the duplicate over the hash agreement over
// the epoch over the membership.
test rule_order_matches_original {
var flooded: [257]TreeUpdate;
for i in 0..257 {
flooded[i] = make_update(1024, 99, 99);
}
const v1 = validate_tree_updates(&flooded, 5, &MEMBERS);
assert(v1.code == RtuiCode.BatchTooLarge);

const stacked: [1]TreeUpdate = [1]TreeUpdate{
make_update(1024, 99, 99),
};
const v2 = validate_tree_updates(&stacked, 5, &MEMBERS);
assert(v2.code == RtuiCode.InvalidNodeIndex);

const dup_pair: [2]TreeUpdate = [2]TreeUpdate{
make_update(1, 5, 0),
make_update(1, 99, 99),
};
const v3 = validate_tree_updates(&dup_pair, 5, &MEMBERS);
assert(v3.code == RtuiCode.DuplicateNode);

var lying: TreeUpdate = make_update(1, 99, 99);
lying.parent_hash = HASH_FF;
const lying_batch: [1]TreeUpdate = [1]TreeUpdate{lying,};
const v4 = validate_tree_updates(&lying_batch, 5, &MEMBERS);
assert(v4.code == RtuiCode.HashMismatch);

const stale: [1]TreeUpdate = [1]TreeUpdate{
make_update(1, 99, 99),
};
const v5 = validate_tree_updates(&stale, 5, &MEMBERS);
assert(v5.code == RtuiCode.EpochMismatch);
assert(v5.expected == 5);
assert(v5.got == 99);

const outsider: [1]TreeUpdate = [1]TreeUpdate{
make_update(1, 5, 99),
};
const v6 = validate_tree_updates(&outsider, 5, &MEMBERS);
assert(v6.code == RtuiCode.NonMemberSigner);
}
Loading