Skip to content

Commit 16d1822

Browse files
committed
Control block integrity and sovereignty tests
1 parent e722002 commit 16d1822

File tree

3 files changed

+117
-2
lines changed

3 files changed

+117
-2
lines changed

VERIFIED_CAPABILITIES.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,22 @@
262262
* **Test Location:** [`tests/taproot_reconstruction.rs::test_taproot_lexicographical_sorting_integrity`](tests/taproot_reconstruction.rs)
263263
* **What this proves:** The BIP-341 branch sorting rule is enforced internally by the library, not assumed by callers. Two implementations traversing the same tree in different orders will produce identical Merkle roots, ensuring exit transactions are interoperable.
264264

265+
### Control Block Parity Enforcement
266+
267+
* **Description:** Two adversarial tests target the parity bit and leaf version encoded in the control block's first byte. The parity test reconstructs a valid control block, flips only bit 0 (the Y-coordinate parity), and asserts that `verify_control_block` rejects it — the derived tweaked key's parity no longer matches the control byte. The leaf version test replaces `0xc0` (BIP-341 Tapscript) with the unknown version `0xc2` while preserving the original parity bit; the altered version changes the `TapLeaf` tagged hash, which cascades through the Merkle root into a tweaked key mismatch. Together these tests prove that malformed witness stacks — whether from a buggy wallet or a malicious ASP — are rejected before reaching L1.
268+
* **Test Locations:**
269+
* [`tests/control_block_tests.rs::test_control_block_internal_key_parity_mismatch`](tests/control_block_tests.rs)
270+
* [`tests/control_block_tests.rs::test_control_block_invalid_leaf_version_fails`](tests/control_block_tests.rs)
271+
* **What this proves:** The library enforces both the parity bit and the BIP-341 leaf version as first-class security properties. A witness stack with a flipped parity or unknown leaf version cannot pass `verify_control_block`, preventing malformed spends from being accepted off-chain that would be rejected by L1 consensus.
272+
273+
### Internal Key Commitment (Control Block Sovereignty)
274+
275+
* **Description:** Two tests verify that the internal key embedded in the control block is cryptographically bound to the on-chain P2TR output key. The "Shadow Key" test reconstructs a valid Bark control block, XOR-flips byte 16 (the middle of the 32-byte internal key segment) with `0xFF`, and asserts rejection — a single-byte mutation in the key cascades through `TapTweak` into a completely different tweaked key. The "Output Key Mismatch" test provides a valid control block and correct leaf script but supplies the tweaked key from a *different* VTXO (`BARK_COSIGN_TAPROOT` vs `ARKD_2_LEAF_TREE`), asserting that cross-VTXO key confusion is caught. The key comparison in `verify_control_block` uses a constant-time XOR-fold (`ct_eq_32`) to prevent timing side-channels and ensure a `!= → ==` cargo-mutant on the comparison is killed by every positive test.
276+
* **Test Locations:**
277+
* [`tests/control_block_tests.rs::test_control_block_shadow_key_fails`](tests/control_block_tests.rs)
278+
* [`tests/control_block_tests.rs::test_control_block_output_key_mismatch`](tests/control_block_tests.rs)
279+
* **What this proves:** The equation `internal_key + merkle_root = on-chain address` is enforced end-to-end. An ASP cannot inject a "shadow" internal key (one that would add a hidden key-path spend) or confuse the verifier with a different VTXO's output key. The constant-time comparison hardens the check against both timing side-channels and mutation testing.
280+
265281
---
266282

267283
## 4. JSON Conformance & Cross-Implementation Standardization

src/consensus/control_block.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ use crate::payload::tree::VPackTree;
1515
/// P2TR scriptPubKey prefix: OP_1 (0x51) OP_PUSHBYTES_32 (0x20).
1616
const P2TR_PREFIX: [u8; 2] = [0x51, 0x20];
1717

18+
/// Non-short-circuiting 32-byte equality check.
19+
/// Folds XOR across all bytes so a `!=` → `==` cargo-mutant on the final
20+
/// comparison is killed by every positive test, and timing does not leak
21+
/// which byte differs.
22+
#[inline]
23+
fn ct_eq_32(a: &[u8; 32], b: &[u8; 32]) -> bool {
24+
let mut diff = 0u8;
25+
for i in 0..32 {
26+
diff |= a[i] ^ b[i];
27+
}
28+
diff == 0
29+
}
30+
1831
fn p2tr_output_xonly(tree: &VPackTree) -> Result<[u8; 32], VPackError> {
1932
let script = &tree.leaf.script_pubkey;
2033
if script.len() != 34 || script[..2] != P2TR_PREFIX {
@@ -49,7 +62,7 @@ fn try_reconstruct_with_hashes(
4962
let merkle_root = compute_balanced_merkle_root(hashes)?;
5063
let (x_only, parity) =
5164
taproot::compute_taproot_tweaked_key_x_and_parity(tree.internal_key, merkle_root)?;
52-
if x_only != *expected_output {
65+
if !ct_eq_32(&x_only, expected_output) {
5366
return None;
5467
}
5568
let path = balanced_merkle_sibling_path(hashes, leaf_idx)?;
@@ -128,5 +141,5 @@ pub fn verify_control_block(
128141
return false;
129142
};
130143

131-
x_only == *expected_output_key && parity == expected_parity
144+
ct_eq_32(&x_only, expected_output_key) && parity == expected_parity
132145
}

tests/control_block_tests.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,89 @@ fn control_block_deep_tree_length_33_plus_32_times_depth() {
258258
let out_key: [u8; 32] = tree.leaf.script_pubkey[2..34].try_into().unwrap();
259259
assert!(verify_control_block(&cb, leaf.as_slice(), &out_key));
260260
}
261+
262+
// ---------------------------------------------------------------------------
263+
// Chunk 2.2 — Taproot Tweak Sovereignty & Control Block Integrity
264+
// ---------------------------------------------------------------------------
265+
266+
#[test]
267+
fn test_control_block_shadow_key_fails() {
268+
let tree = build_bark_cosign_tree();
269+
let mut cb = reconstruct_control_block(&tree, TxVariant::V3Plain).expect("reconstruct Bark");
270+
let leaf = bark_expiry_leaf_script(&tree);
271+
let out_key: [u8; 32] = tree.leaf.script_pubkey[2..34].try_into().unwrap();
272+
273+
assert!(
274+
verify_control_block(&cb, leaf.as_slice(), &out_key),
275+
"baseline must verify before sabotage"
276+
);
277+
278+
// XOR byte 16 of the internal key (cb index 1+16 = 17) — middle of the key.
279+
cb[17] ^= 0xFF;
280+
281+
assert!(
282+
!verify_control_block(&cb, leaf.as_slice(), &out_key),
283+
"Shadow key (middle-byte mutation) must be rejected"
284+
);
285+
}
286+
287+
#[test]
288+
fn test_control_block_internal_key_parity_mismatch() {
289+
let tree = build_arkd_2_tree();
290+
let mut cb = reconstruct_control_block(&tree, TxVariant::V3Anchored).expect("reconstruct ARKD");
291+
let out_key: [u8; 32] = tree.leaf.script_pubkey[2..34].try_into().unwrap();
292+
293+
assert!(
294+
verify_control_block(&cb, tree.asp_expiry_script.as_slice(), &out_key),
295+
"baseline must verify before parity flip"
296+
);
297+
298+
// Flip only the parity bit (bit 0), leaving the leaf version bits untouched.
299+
cb[0] ^= 0x01;
300+
301+
assert!(
302+
!verify_control_block(&cb, tree.asp_expiry_script.as_slice(), &out_key),
303+
"Parity mismatch must be rejected — witness stack would fail L1 validation"
304+
);
305+
}
306+
307+
#[test]
308+
fn test_control_block_invalid_leaf_version_fails() {
309+
let tree = build_arkd_2_tree();
310+
let mut cb = reconstruct_control_block(&tree, TxVariant::V3Anchored).expect("reconstruct ARKD");
311+
let out_key: [u8; 32] = tree.leaf.script_pubkey[2..34].try_into().unwrap();
312+
313+
assert!(
314+
verify_control_block(&cb, tree.asp_expiry_script.as_slice(), &out_key),
315+
"baseline must verify before leaf version mutation"
316+
);
317+
318+
// Replace leaf version 0xc0 with unknown version 0xc2, preserving the parity bit.
319+
let parity_bit = cb[0] & 0x01;
320+
cb[0] = 0xc2 | parity_bit;
321+
322+
assert!(
323+
!verify_control_block(&cb, tree.asp_expiry_script.as_slice(), &out_key),
324+
"Unknown leaf version 0xc2 must be rejected — \
325+
different version changes the TapLeaf hash, cascading to a Merkle root mismatch"
326+
);
327+
}
328+
329+
#[test]
330+
fn test_control_block_output_key_mismatch() {
331+
let tree = build_arkd_2_tree();
332+
let cb = reconstruct_control_block(&tree, TxVariant::V3Anchored).expect("reconstruct ARKD");
333+
334+
// Valid output key from a *different* VTXO (Bark cosign tree).
335+
let wrong_key = hex_to_32(BARK_COSIGN_TAPROOT.tweaked_pubkey);
336+
let correct_key: [u8; 32] = tree.leaf.script_pubkey[2..34].try_into().unwrap();
337+
assert_ne!(
338+
wrong_key, correct_key,
339+
"test vectors must produce distinct tweaked keys"
340+
);
341+
342+
assert!(
343+
!verify_control_block(&cb, tree.asp_expiry_script.as_slice(), &wrong_key),
344+
"Control block verified against a different VTXO's output key must fail"
345+
);
346+
}

0 commit comments

Comments
 (0)