PqEquivocationEvidence: on-chain equivocation evidence for post-quantum consensus seals (AIP-22 section 6), with its local tests and the drill signer; measured on testnet 28001
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / Ethereum interop (EIP-2537 BLS, prague hardfork) (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / Ethereum interop (EIP-2537 BLS, prague hardfork) (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
This commit is contained in:
parent
d5edf25698
commit
6038f079af
12
README.md
12
README.md
@ -46,6 +46,18 @@ code in this repository.
|
|||||||
default (`test test ... junk`) used for local test signers.
|
default (`test test ... junk`) used for local test signers.
|
||||||
- `package.json` the dependency manifest.
|
- `package.json` the dependency manifest.
|
||||||
|
|
||||||
|
### `contracts/pqc-evidence/`: on-chain equivocation evidence (AIP-22 section 6; added 2026-09-23)
|
||||||
|
|
||||||
|
`PqEquivocationEvidence` records, permanently and without any owner, that ONE validator row of a post-quantum registry epoch sealed
|
||||||
|
two different consensus messages of the same type for the same height and round. It proves the row into the registry hash pinned on
|
||||||
|
chain for the epoch in force (an 11-byte read-only getter, slot 0), rebuilds both signed pre-images exactly as the clients do, and
|
||||||
|
verifies both Falcon-512 seals through the chain's precompile at `0x0AE1`. `contracts/pqc-evidence/test/MockFalcon512.sol` is a
|
||||||
|
stand-in for that precompile used ONLY by the local tests (`test/evidence/`); real seals are checked against the live precompile.
|
||||||
|
`test/evidence/ExercitiuEchivoc.java` is the program that produced the drill seals below, with the clients' own signing code.
|
||||||
|
Measured on the public testnet 28001 on 2026-09-23, not on chain 2800: a drill instance recorded a PREPARE and a COMMIT equivocation
|
||||||
|
of drill keys (322,404 and 303,103 gas), and an instance for the testnet's own registry epochs proves the rows of the live epoch and
|
||||||
|
verifies live anchor seals through the contract. The procedure and the addresses are in the AIP-22 stage records (aere-research).
|
||||||
|
|
||||||
## Verify
|
## Verify
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
269
contracts/pqc-evidence/PqEquivocationEvidence.sol
Normal file
269
contracts/pqc-evidence/PqEquivocationEvidence.sol
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
pragma solidity 0.8.23;
|
||||||
|
|
||||||
|
/// @title PqEquivocationEvidence
|
||||||
|
/// @notice AIP-22 section 6: a permanent, independently verifiable record that ONE validator row sealed, with its post-quantum
|
||||||
|
/// (Falcon-512) key, two different consensus messages of the same type for the same height and round. Immutable and
|
||||||
|
/// ownerless: no setter, no admin, no upgrade path. A registry epoch added after deployment needs a new contract.
|
||||||
|
/// @dev What is verified, all on chain:
|
||||||
|
/// 1. the row: the v3 row pre-image (AERE-PQ-ROW-3, the canonical bytes of PqRegistryV3.rowPreimage) is parsed for chain id,
|
||||||
|
/// epoch, row count, index, id and the Falcon-512 key; the id is the one derived from that key; the row hash is proven into
|
||||||
|
/// the epoch's Merkle root, and the registry hash built from that root equals the value pinned on chain for the epoch in force
|
||||||
|
/// at the height (the 11-byte read-only getter, slot 0), so the key is the one the chain bound to that index;
|
||||||
|
/// 2. the two messages: rebuilt on chain from their pre-image fields (PqAnchor.*Message, one ASCII domain per type), they
|
||||||
|
/// differ, and each seal verifies under the row's key through the live Falcon-512 precompile at 0x0AE1;
|
||||||
|
/// 3. the record: (id, type, height, round) is recorded once and an event names the index and the id.
|
||||||
|
/// The object carries the signed PRE-IMAGE FIELDS rather than the full upstream payloads: a seal binds only its pre-image, so
|
||||||
|
/// nothing else in a payload could be checked here (stated deviation from the AIP-22 text, plan 9.8).
|
||||||
|
/// Equivocation per type: PROPOSAL and PREPARE, same height and round, different digest; COMMIT (the anchor form COMMIT-1,
|
||||||
|
/// which carries no round), same height, different block hash; ROUND-CHANGE, same height and target round, different prepared
|
||||||
|
/// claim. Not in this contract, stated: the second form of section 6 (two conflicting anchor headers).
|
||||||
|
contract PqEquivocationEvidence {
|
||||||
|
address public constant FALCON512 = address(0x0AE1);
|
||||||
|
/// @notice runtime of the pin every epoch must use: PUSH1 0 SLOAD PUSH1 0 MSTORE PUSH1 32 PUSH1 0 RETURN (no SSTORE anywhere)
|
||||||
|
bytes32 public constant PIN_CODEHASH = keccak256(hex"60005460005260206000f3");
|
||||||
|
|
||||||
|
uint8 public constant PROPOSAL = 1;
|
||||||
|
uint8 public constant PREPARE = 2;
|
||||||
|
uint8 public constant COMMIT = 3;
|
||||||
|
uint8 public constant ROUNDCHANGE = 4;
|
||||||
|
|
||||||
|
uint64 public immutable chainId;
|
||||||
|
uint64[] private _bindHeights;
|
||||||
|
address[] private _pins;
|
||||||
|
|
||||||
|
struct Evidence {
|
||||||
|
uint8 kind;
|
||||||
|
uint64 height;
|
||||||
|
uint64 round; // PROPOSAL/PREPARE: the round; ROUNDCHANGE: the target round; COMMIT: must be 0
|
||||||
|
bytes32 digestA; // PROPOSAL/PREPARE: digest; COMMIT: block hash; ROUNDCHANGE: prepared digest (0 when not prepared)
|
||||||
|
bytes32 digestB;
|
||||||
|
bool preparedA; // ROUNDCHANGE only
|
||||||
|
bool preparedB;
|
||||||
|
uint64 preparedRoundA; // ROUNDCHANGE only
|
||||||
|
uint64 preparedRoundB;
|
||||||
|
bytes sealA; // Falcon-512 seal as on the wire: 0x39 || nonce(40) || compressed
|
||||||
|
bytes sealB;
|
||||||
|
bytes rowPreimage; // AERE-PQ-ROW-3 pre-image of the sealing row
|
||||||
|
bytes32[] merkleProof; // sibling path, leaf level first
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Record {
|
||||||
|
bytes20 id;
|
||||||
|
uint32 index;
|
||||||
|
uint64 bindHeight;
|
||||||
|
bytes32 messageA;
|
||||||
|
bytes32 messageB;
|
||||||
|
address submitter;
|
||||||
|
uint64 recordedAtBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice keccak256(abi.encode(id, kind, height, round)) => record
|
||||||
|
mapping(bytes32 => Record) public records;
|
||||||
|
uint256 public recordCount;
|
||||||
|
|
||||||
|
event EquivocationRecorded(
|
||||||
|
bytes20 indexed id,
|
||||||
|
uint32 indexed index,
|
||||||
|
uint64 indexed height,
|
||||||
|
uint8 kind,
|
||||||
|
uint64 round,
|
||||||
|
bytes32 messageA,
|
||||||
|
bytes32 messageB,
|
||||||
|
address submitter
|
||||||
|
);
|
||||||
|
|
||||||
|
error BadEpochs();
|
||||||
|
error BadPin(address pin);
|
||||||
|
error UnknownKind(uint8 kind);
|
||||||
|
error CommitCarriesRound();
|
||||||
|
error NotAConflict();
|
||||||
|
error NoEpochAtHeight(uint64 height);
|
||||||
|
error BadRow(string what);
|
||||||
|
error RowNotInEpoch();
|
||||||
|
error PinMismatch(bytes32 pinned, bytes32 computed);
|
||||||
|
error SealInvalid(uint8 which);
|
||||||
|
error AlreadyRecorded(bytes32 key);
|
||||||
|
|
||||||
|
/// @param chainId_ the chain every pre-image names
|
||||||
|
/// @param bindHeights ascending activation heights of the v3 registry epochs
|
||||||
|
/// @param pins for each epoch, the read-only pin contract whose slot 0 is the epoch's registry hash
|
||||||
|
constructor(uint64 chainId_, uint64[] memory bindHeights, address[] memory pins) {
|
||||||
|
if (bindHeights.length == 0 || bindHeights.length != pins.length) revert BadEpochs();
|
||||||
|
for (uint256 i = 0; i < pins.length; i++) {
|
||||||
|
if (i > 0 && bindHeights[i] <= bindHeights[i - 1]) revert BadEpochs();
|
||||||
|
if (pins[i].codehash != PIN_CODEHASH) revert BadPin(pins[i]);
|
||||||
|
_bindHeights.push(bindHeights[i]);
|
||||||
|
_pins.push(pins[i]);
|
||||||
|
}
|
||||||
|
chainId = chainId_;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice the epochs this contract judges: (bindHeight, pin) in ascending order
|
||||||
|
function epochs() external view returns (uint64[] memory, address[] memory) {
|
||||||
|
return (_bindHeights, _pins);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice verify and record one equivocation; reverts with the reason if anything does not hold
|
||||||
|
function submit(Evidence calldata e) external returns (bytes32 key) {
|
||||||
|
(bytes32 mA, bytes32 mB) = messages(e);
|
||||||
|
if (mA == mB) revert NotAConflict();
|
||||||
|
|
||||||
|
(uint64 bind, address pin) = epochAt(e.height);
|
||||||
|
(bytes20 id, uint32 index, bytes memory pk) = _checkRow(e.rowPreimage, e.merkleProof, bind, pin);
|
||||||
|
|
||||||
|
if (!falconVerify(pk, mA, e.sealA)) revert SealInvalid(1);
|
||||||
|
if (!falconVerify(pk, mB, e.sealB)) revert SealInvalid(2);
|
||||||
|
|
||||||
|
key = keccak256(abi.encode(id, e.kind, e.height, e.round));
|
||||||
|
if (records[key].submitter != address(0)) revert AlreadyRecorded(key);
|
||||||
|
records[key] = Record(id, index, bind, mA, mB, msg.sender, uint64(block.number));
|
||||||
|
recordCount += 1;
|
||||||
|
emit EquivocationRecorded(id, index, e.height, e.kind, e.round, mA, mB, msg.sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice the two 32-byte messages the seals sign, rebuilt from the pre-image fields exactly as PqAnchor builds them
|
||||||
|
function messages(Evidence calldata e) public view returns (bytes32 mA, bytes32 mB) {
|
||||||
|
if (e.kind == PROPOSAL || e.kind == PREPARE) {
|
||||||
|
bytes memory dom = e.kind == PROPOSAL ? bytes("AERE-PQ-PROPOSAL-1") : bytes("AERE-PQ-PREPARE-1");
|
||||||
|
mA = keccak256(_list(bytes.concat(_rlpBytes(dom), _scalar(chainId), _scalar(e.height), _scalar(e.round), _rlpBytes(abi.encodePacked(e.digestA)))));
|
||||||
|
mB = keccak256(_list(bytes.concat(_rlpBytes(dom), _scalar(chainId), _scalar(e.height), _scalar(e.round), _rlpBytes(abi.encodePacked(e.digestB)))));
|
||||||
|
} else if (e.kind == COMMIT) {
|
||||||
|
if (e.round != 0) revert CommitCarriesRound();
|
||||||
|
bytes memory dom = bytes("AERE-PQ-COMMIT-1");
|
||||||
|
mA = keccak256(_list(bytes.concat(_rlpBytes(dom), _scalar(chainId), _scalar(e.height), _rlpBytes(abi.encodePacked(e.digestA)))));
|
||||||
|
mB = keccak256(_list(bytes.concat(_rlpBytes(dom), _scalar(chainId), _scalar(e.height), _rlpBytes(abi.encodePacked(e.digestB)))));
|
||||||
|
} else if (e.kind == ROUNDCHANGE) {
|
||||||
|
mA = _roundChange(e.height, e.round, e.preparedA, e.preparedRoundA, e.digestA);
|
||||||
|
mB = _roundChange(e.height, e.round, e.preparedB, e.preparedRoundB, e.digestB);
|
||||||
|
} else {
|
||||||
|
revert UnknownKind(e.kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _roundChange(uint64 height, uint64 target, bool prepared, uint64 preparedRound, bytes32 digest) internal view returns (bytes32) {
|
||||||
|
// the bare form (PqAnchor.roundChangeMessage without a prepared claim): hasPrepared 0, preparedRound 0, EMPTY digest
|
||||||
|
bytes memory d = prepared ? abi.encodePacked(digest) : bytes("");
|
||||||
|
uint64 pr = prepared ? preparedRound : 0;
|
||||||
|
return keccak256(_list(bytes.concat(
|
||||||
|
_rlpBytes(bytes("AERE-PQ-ROUNDCHANGE-1")), _scalar(chainId), _scalar(height), _scalar(target),
|
||||||
|
_scalar(prepared ? 1 : 0), _scalar(pr), _rlpBytes(d))));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice the epoch in force at a height: the last one whose bindHeight is at or below it
|
||||||
|
function epochAt(uint64 height) public view returns (uint64 bind, address pin) {
|
||||||
|
uint256 n = _bindHeights.length;
|
||||||
|
for (uint256 i = n; i > 0; i--) {
|
||||||
|
if (_bindHeights[i - 1] <= height) return (_bindHeights[i - 1], _pins[i - 1]);
|
||||||
|
}
|
||||||
|
revert NoEpochAtHeight(height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice read-only: does this row pre-image, with this Merkle path, belong to the epoch pinned for `height`? Reverts with the
|
||||||
|
/// reason if not; returns the row's id and index if so. The same check submit() makes, open to anyone as a call.
|
||||||
|
function rowAt(uint64 height, bytes calldata rowPreimage, bytes32[] calldata merkleProof)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (bytes20 id, uint32 index)
|
||||||
|
{
|
||||||
|
(uint64 bind, address pin) = epochAt(height);
|
||||||
|
(id, index, ) = _checkRow(rowPreimage, merkleProof, bind, pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice the registry-v3 row check: parse, derive the id, prove the row into the pinned registry hash; returns id, index, key
|
||||||
|
function _checkRow(bytes calldata p, bytes32[] calldata proof, uint64 bind, address pin)
|
||||||
|
internal
|
||||||
|
view
|
||||||
|
returns (bytes20 id, uint32 index, bytes memory falconPk)
|
||||||
|
{
|
||||||
|
if (p.length < 60 || bytes13(p[0:13]) != bytes13("AERE-PQ-ROW-3") || uint8(p[13]) != 3) revert BadRow("prefix");
|
||||||
|
if (uint64(bytes8(p[14:22])) != chainId) revert BadRow("chain id");
|
||||||
|
if (uint64(bytes8(p[22:30])) != bind) revert RowNotInEpoch();
|
||||||
|
uint32 count = uint32(bytes4(p[30:34]));
|
||||||
|
index = uint32(bytes4(p[34:38]));
|
||||||
|
id = bytes20(p[38:58]);
|
||||||
|
if (count == 0 || index >= count) revert BadRow("index");
|
||||||
|
uint256 nKeys = uint8(p[58]);
|
||||||
|
uint256 o = 59;
|
||||||
|
uint256 lastWire = 0;
|
||||||
|
for (uint256 k = 0; k < nKeys; k++) {
|
||||||
|
if (o + 5 > p.length) revert BadRow("key header");
|
||||||
|
uint256 wire = uint8(p[o]);
|
||||||
|
if (wire <= lastWire) revert BadRow("key order");
|
||||||
|
lastWire = wire;
|
||||||
|
uint256 pkLen = uint32(bytes4(p[o + 1:o + 5]));
|
||||||
|
uint256 pkEnd = o + 5 + pkLen;
|
||||||
|
if (pkEnd + 4 > p.length) revert BadRow("key length");
|
||||||
|
if (wire == 1) {
|
||||||
|
if (pkLen != 897) revert BadRow("falcon key length");
|
||||||
|
falconPk = p[o + 5:pkEnd];
|
||||||
|
}
|
||||||
|
uint256 popLen = uint32(bytes4(p[pkEnd:pkEnd + 4]));
|
||||||
|
o = pkEnd + 4 + popLen;
|
||||||
|
if (o > p.length) revert BadRow("pop length");
|
||||||
|
}
|
||||||
|
if (o >= p.length) revert BadRow("transport flag");
|
||||||
|
uint8 hasTransport = uint8(p[o]);
|
||||||
|
if (!((hasTransport == 0 && p.length == o + 1) || (hasTransport == 1 && p.length == o + 21))) revert BadRow("transport");
|
||||||
|
if (falconPk.length == 0) revert BadRow("no falcon key");
|
||||||
|
// The id is NOT re-derived from the key here: a ROTATED row keeps the id of its first registration (AIP-22 stage 3b), so on a
|
||||||
|
// live epoch the id of a rotated row is not the derivation of its current key (measured 2026-09-23 on 28001: row 2 of epoch
|
||||||
|
// 3,097,600). The id is inside the row hash, which is proven below into the registry hash the chain pinned, and the registry
|
||||||
|
// itself binds each id (derivation for a new id, the previous keys' rotation signatures for a rotated one; PqRegistryV3.load).
|
||||||
|
|
||||||
|
// Merkle: node = keccak256(0x01 || l || r); an odd node is paired with itself; the depth follows from count
|
||||||
|
uint256 depth = 0;
|
||||||
|
for (uint256 n = count; n > 1; n = (n + 1) / 2) depth++;
|
||||||
|
if (proof.length != depth) revert BadRow("proof length");
|
||||||
|
bytes32 acc = keccak256(p);
|
||||||
|
uint256 i = index;
|
||||||
|
for (uint256 d = 0; d < depth; d++) {
|
||||||
|
acc = i % 2 == 0 ? keccak256(abi.encodePacked(bytes1(0x01), acc, proof[d])) : keccak256(abi.encodePacked(bytes1(0x01), proof[d], acc));
|
||||||
|
i /= 2;
|
||||||
|
}
|
||||||
|
bytes32 computed = keccak256(abi.encodePacked("AERE-PQ-REGISTRY-3", chainId, bind, count, acc));
|
||||||
|
(bool ok, bytes memory ret) = pin.staticcall("");
|
||||||
|
if (!ok || ret.length != 32) revert BadPin(pin);
|
||||||
|
bytes32 pinned = abi.decode(ret, (bytes32));
|
||||||
|
if (pinned != computed) revert PinMismatch(pinned, computed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice one Falcon-512 seal through the live precompile: 0x39 || nonce || compressed on the wire becomes
|
||||||
|
/// pk(897) || u16(len esig) || nonce(40) || message || esig, esig = 0x29 || compressed (SPEC 4.2; proven on 28001)
|
||||||
|
function falconVerify(bytes memory pk, bytes32 message, bytes calldata seal) public view returns (bool) {
|
||||||
|
if (pk.length != 897 || seal.length <= 41 || uint8(seal[0]) != 0x39) return false;
|
||||||
|
bytes memory esig = bytes.concat(bytes1(0x29), seal[41:]);
|
||||||
|
if (esig.length > type(uint16).max) return false;
|
||||||
|
bytes memory input = bytes.concat(pk, bytes2(uint16(esig.length)), seal[1:41], message, esig);
|
||||||
|
(bool ok, bytes memory ret) = FALCON512.staticcall(input);
|
||||||
|
return ok && ret.length >= 32 && ret[31] == 0x01;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- RLP, the four shapes PqAnchor writes: bytes, a long scalar (minimal big-endian, 0 is empty), a list ----
|
||||||
|
|
||||||
|
function _rlpLen(uint256 len, uint8 offset) private pure returns (bytes memory) {
|
||||||
|
if (len <= 55) return abi.encodePacked(uint8(offset + len));
|
||||||
|
bytes memory l = _minimal(len);
|
||||||
|
return bytes.concat(abi.encodePacked(uint8(offset + 55 + l.length)), l);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _minimal(uint256 v) private pure returns (bytes memory out) {
|
||||||
|
uint256 n = 0;
|
||||||
|
for (uint256 t = v; t > 0; t >>= 8) n++;
|
||||||
|
out = new bytes(n);
|
||||||
|
for (uint256 i = 0; i < n; i++) out[n - 1 - i] = bytes1(uint8(v >> (8 * i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function _rlpBytes(bytes memory b) private pure returns (bytes memory) {
|
||||||
|
if (b.length == 1 && uint8(b[0]) < 0x80) return b;
|
||||||
|
return bytes.concat(_rlpLen(b.length, 0x80), b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _scalar(uint256 v) private pure returns (bytes memory) {
|
||||||
|
return _rlpBytes(_minimal(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
function _list(bytes memory payload) private pure returns (bytes memory) {
|
||||||
|
return bytes.concat(_rlpLen(payload.length, 0xc0), payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
contracts/pqc-evidence/test/MockFalcon512.sol
Normal file
21
contracts/pqc-evidence/test/MockFalcon512.sol
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
pragma solidity 0.8.23;
|
||||||
|
|
||||||
|
/// @title MockFalcon512 (TESTS ONLY; its runtime is placed at 0x0AE1 on the local test chain)
|
||||||
|
/// @notice A deterministic stand-in for the live Falcon-512 precompile, so the local probes can measure the framing, the pre-images,
|
||||||
|
/// the Merkle path and every refusal of PqEquivocationEvidence without a Falcon implementation in the EVM. It reads the input
|
||||||
|
/// EXACTLY as the live precompile does (pk(897) || u16(len esig) || nonce(40) || message || esig, esig = 0x29 || body) and
|
||||||
|
/// answers 1 iff the 32-byte body equals keccak256(pk || message). Real Falcon seals are measured against the live
|
||||||
|
/// precompile on testnet 28001, never with this contract.
|
||||||
|
contract MockFalcon512 {
|
||||||
|
fallback(bytes calldata input) external returns (bytes memory) {
|
||||||
|
if (input.length < 897 + 2 + 40 + 1) return abi.encode(uint256(0));
|
||||||
|
uint256 esigLen = uint16(bytes2(input[897:899]));
|
||||||
|
if (input.length < 939 + esigLen) return abi.encode(uint256(0));
|
||||||
|
uint256 msgLen = input.length - 939 - esigLen;
|
||||||
|
bytes calldata esig = input[939 + msgLen:];
|
||||||
|
if (esig.length != 33 || uint8(esig[0]) != 0x29) return abi.encode(uint256(0));
|
||||||
|
bytes32 want = keccak256(bytes.concat(input[0:897], input[939:939 + msgLen]));
|
||||||
|
return abi.encode(uint256(bytes32(esig[1:33]) == want ? 1 : 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
97
test/evidence/ExercitiuEchivoc.java
Normal file
97
test/evidence/ExercitiuEchivoc.java
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
// AIP-22 stage 6, the DRILL for the live run on testnet 28001 (2026-09-23). It builds a registry-v3 epoch of THREE rows whose Falcon-512
|
||||||
|
// keys are generated here, in memory, for this run only (drill keys: never written to disk, never printed; the epoch names chain 28099,
|
||||||
|
// the rehearsal chain, so no record of the drill can be read as one about a validator of 28001 or 2800), and has ONE row seal two
|
||||||
|
// conflicting PREPARE messages and two conflicting COMMIT messages, with the same code the clients use (PqAnchor.*Message,
|
||||||
|
// FalconSealScheme through BouncyCastle, PqRegistryV3 row pre-image, Merkle tree and registry hash). Everything it prints is public:
|
||||||
|
// row pre-images, Merkle paths, registry hash, the messages and the seals, plus a positive and a negative control of each seal under the
|
||||||
|
// same verifier the clients use. The contract on 28001 then checks the same seals with the live precompile.
|
||||||
|
// javac -cp "<besu>/lib/*" ExercitiuEchivoc.java && java -cp ".:<besu>/lib/*" ExercitiuEchivoc > exercitiu.json
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import org.apache.tuweni.bytes.Bytes;
|
||||||
|
import org.apache.tuweni.bytes.Bytes32;
|
||||||
|
import org.bouncycastle.pqc.crypto.falcon.FalconKeyGenerationParameters;
|
||||||
|
import org.bouncycastle.pqc.crypto.falcon.FalconKeyPairGenerator;
|
||||||
|
import org.bouncycastle.pqc.crypto.falcon.FalconParameters;
|
||||||
|
import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.FalconSealScheme;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.PqAnchor;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.PqRegistryV3;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.SealSchemes;
|
||||||
|
|
||||||
|
public final class ExercitiuEchivoc {
|
||||||
|
static final long CHAIN = 28099L;
|
||||||
|
static final long BIND = 1000L;
|
||||||
|
static final int COUNT = 3;
|
||||||
|
static final int SIGNER = 1;
|
||||||
|
static final long HEIGHT = 2000L;
|
||||||
|
static final int ROUND = 2;
|
||||||
|
|
||||||
|
static String hx(byte[] b) { return Bytes.wrap(b).toHexString(); }
|
||||||
|
|
||||||
|
public static void main(String[] a) {
|
||||||
|
final SecureRandom rng = new SecureRandom();
|
||||||
|
final FalconSealScheme falcon = (FalconSealScheme) SealSchemes.FALCON_512;
|
||||||
|
final List<FalconPrivateKeyParameters> sks = new ArrayList<>();
|
||||||
|
final List<byte[]> std = new ArrayList<>();
|
||||||
|
final List<byte[]> pre = new ArrayList<>();
|
||||||
|
final List<Bytes32> leaves = new ArrayList<>();
|
||||||
|
for (int i = 0; i < COUNT; i++) {
|
||||||
|
final FalconKeyPairGenerator g = new FalconKeyPairGenerator();
|
||||||
|
g.init(new FalconKeyGenerationParameters(rng, FalconParameters.falcon_512));
|
||||||
|
final FalconPrivateKeyParameters sk = (FalconPrivateKeyParameters) g.generateKeyPair().getPrivate();
|
||||||
|
final byte[] h = sk.getPublicKey();
|
||||||
|
final byte[] s = new byte[PqRegistryV3.FALCON_512_PK_LENGTH];
|
||||||
|
s[0] = PqRegistryV3.FALCON_512_PK_HEADER;
|
||||||
|
System.arraycopy(h, 0, s, 1, h.length);
|
||||||
|
final byte[] id = PqRegistryV3.idOf(s);
|
||||||
|
final byte[] pop = falcon.signWithParams(sk, PqRegistryV3.popMessage(CHAIN, BIND, COUNT, i, id, SealSchemes.FALCON_512.wireId(), s).toArrayUnsafe()).orElseThrow();
|
||||||
|
final TreeMap<Integer, PqRegistryV3.Key> keys = new TreeMap<>();
|
||||||
|
keys.put(SealSchemes.FALCON_512.wireId() & 0xff, new PqRegistryV3.Key(SealSchemes.FALCON_512, s, pop));
|
||||||
|
final PqRegistryV3.Row row = new PqRegistryV3.Row(i, id, keys, null, Map.of(), null, null);
|
||||||
|
final byte[] p = PqRegistryV3.rowPreimage(CHAIN, BIND, COUNT, row);
|
||||||
|
sks.add(sk); std.add(s); pre.add(p); leaves.add(PqRegistryV3.rowHash(CHAIN, BIND, COUNT, row));
|
||||||
|
}
|
||||||
|
final Bytes32 root = PqRegistryV3.merkleRoot(leaves);
|
||||||
|
final Bytes32 rh = PqRegistryV3.registryHash(CHAIN, BIND, COUNT, root);
|
||||||
|
final Bytes32 dA = Bytes32.random(rng), dB = Bytes32.random(rng), hA = Bytes32.random(rng), hB = Bytes32.random(rng);
|
||||||
|
final Bytes32 pA = PqAnchor.prepareMessage(CHAIN, HEIGHT, ROUND, dA), pB = PqAnchor.prepareMessage(CHAIN, HEIGHT, ROUND, dB);
|
||||||
|
final Bytes32 cA = PqAnchor.commitMessage(CHAIN, HEIGHT, hA), cB = PqAnchor.commitMessage(CHAIN, HEIGHT, hB);
|
||||||
|
final FalconPrivateKeyParameters sk = sks.get(SIGNER);
|
||||||
|
final byte[] spA = falcon.signWithParams(sk, pA.toArrayUnsafe()).orElseThrow(), spB = falcon.signWithParams(sk, pB.toArrayUnsafe()).orElseThrow();
|
||||||
|
final byte[] scA = falcon.signWithParams(sk, cA.toArrayUnsafe()).orElseThrow(), scB = falcon.signWithParams(sk, cB.toArrayUnsafe()).orElseThrow();
|
||||||
|
final byte[] reg = sks.get(SIGNER).getPublicKey();
|
||||||
|
final byte[] other = sks.get((SIGNER + 1) % COUNT).getPublicKey();
|
||||||
|
// controls under the clients' own verifier: each seal verifies under its row's key and NOT under another row's, nor over the other message
|
||||||
|
final boolean ok = falcon.verifyRaw(reg, pA.toArrayUnsafe(), spA) && falcon.verifyRaw(reg, pB.toArrayUnsafe(), spB)
|
||||||
|
&& falcon.verifyRaw(reg, cA.toArrayUnsafe(), scA) && falcon.verifyRaw(reg, cB.toArrayUnsafe(), scB);
|
||||||
|
final boolean neg = !falcon.verifyRaw(other, pA.toArrayUnsafe(), spA) && !falcon.verifyRaw(reg, pB.toArrayUnsafe(), spA);
|
||||||
|
final StringBuilder o = new StringBuilder();
|
||||||
|
o.append("{\n \"what\": \"AIP-22 stage 6 drill: chain 28099 epoch of drill keys generated in memory for this run\",\n");
|
||||||
|
o.append(" \"chainId\": ").append(CHAIN).append(", \"bindHeight\": ").append(BIND).append(", \"count\": ").append(COUNT).append(",\n");
|
||||||
|
o.append(" \"root\": \"").append(root.toHexString()).append("\", \"registryHash\": \"").append(rh.toHexString()).append("\",\n");
|
||||||
|
o.append(" \"rows\": [");
|
||||||
|
for (int i = 0; i < COUNT; i++) o.append(i > 0 ? ", " : "").append('"').append(hx(pre.get(i))).append('"');
|
||||||
|
o.append("],\n \"proofs\": [");
|
||||||
|
for (int i = 0; i < COUNT; i++) {
|
||||||
|
o.append(i > 0 ? ", " : "").append('[');
|
||||||
|
final List<Bytes32> pr = PqRegistryV3.merkleProof(leaves, i);
|
||||||
|
for (int j = 0; j < pr.size(); j++) o.append(j > 0 ? ", " : "").append('"').append(pr.get(j).toHexString()).append('"');
|
||||||
|
o.append(']');
|
||||||
|
}
|
||||||
|
o.append("],\n \"signer\": ").append(SIGNER).append(", \"height\": ").append(HEIGHT).append(", \"round\": ").append(ROUND).append(",\n");
|
||||||
|
o.append(" \"prepare\": {\"digestA\": \"").append(dA.toHexString()).append("\", \"digestB\": \"").append(dB.toHexString())
|
||||||
|
.append("\", \"messageA\": \"").append(pA.toHexString()).append("\", \"messageB\": \"").append(pB.toHexString())
|
||||||
|
.append("\", \"sealA\": \"").append(hx(spA)).append("\", \"sealB\": \"").append(hx(spB)).append("\"},\n");
|
||||||
|
o.append(" \"commit\": {\"hashA\": \"").append(hA.toHexString()).append("\", \"hashB\": \"").append(hB.toHexString())
|
||||||
|
.append("\", \"messageA\": \"").append(cA.toHexString()).append("\", \"messageB\": \"").append(cB.toHexString())
|
||||||
|
.append("\", \"sealA\": \"").append(hx(scA)).append("\", \"sealB\": \"").append(hx(scB)).append("\"},\n");
|
||||||
|
o.append(" \"otherRowSeal\": \"").append(hx(falcon.signWithParams(sks.get((SIGNER + 1) % COUNT), pB.toArrayUnsafe()).orElseThrow())).append("\",\n");
|
||||||
|
o.append(" \"controlsUnderClientVerifier\": {\"positive\": ").append(ok).append(", \"negative\": ").append(neg).append("}\n}\n");
|
||||||
|
System.out.print(o);
|
||||||
|
if (!ok || !neg) System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
185
test/evidence/PqEquivocationEvidence.test.js
Normal file
185
test/evidence/PqEquivocationEvidence.test.js
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
// AIP-22 stage 6: PqEquivocationEvidence, local probes (2026-09-23).
|
||||||
|
// A registry v3 epoch of THREE rows (odd, so the Merkle self-pairing is exercised) is built here with the canonical bytes of
|
||||||
|
// PqRegistryV3.rowPreimage (the same code as the published registry-v3.mjs), pinned in the 11-byte getter, and the live Falcon-512
|
||||||
|
// precompile is stood in by MockFalcon512 at 0x0AE1 (a "seal" is keccak256(pk || message) framed like a real one). Every refusal is
|
||||||
|
// asserted by its REASON (custom error), not by a bare revert. Real Falcon seals are measured on testnet 28001, not here.
|
||||||
|
const { expect } = require("chai");
|
||||||
|
const { ethers, network } = require("hardhat");
|
||||||
|
|
||||||
|
const CHAIN = 28001n;
|
||||||
|
const BIND = 3097600n;
|
||||||
|
const PREC = "0x0000000000000000000000000000000000000ae1";
|
||||||
|
const K = (b) => Buffer.from(ethers.getBytes(ethers.keccak256(b)));
|
||||||
|
const u32 = (v) => { const b = Buffer.alloc(4); b.writeUInt32BE(Number(v)); return b; };
|
||||||
|
const u64 = (v) => { const b = Buffer.alloc(8); b.writeBigUInt64BE(BigInt(v)); return b; };
|
||||||
|
|
||||||
|
function falconPk(seed) { const pk = Buffer.alloc(897); pk[0] = 0x09; for (let i = 1; i < 897; i++) pk[i] = (seed * 31 + i * 7) & 0xff; return pk; }
|
||||||
|
const idOf = (pk) => K(Buffer.concat([Buffer.from([0x50, 0x20]), pk])).subarray(12, 32);
|
||||||
|
// idOverride: a ROTATED row keeps the id of its first registration, so its id is not the derivation of its current key (AIP-22 stage 3b)
|
||||||
|
function rowPreimage(chainId, bind, count, index, pk, withSlh, transport, idOverride) {
|
||||||
|
const parts = [Buffer.from("AERE-PQ-ROW-3", "ascii"), Buffer.from([3]), u64(chainId), u64(bind), u32(count), u32(index), idOverride || idOf(pk)];
|
||||||
|
const keys = [[1, pk, Buffer.alloc(666, 0x39)]];
|
||||||
|
if (withSlh) keys.push([2, Buffer.alloc(32, 0x5a + index), Buffer.alloc(7856, 0x11)]);
|
||||||
|
parts.push(Buffer.from([keys.length]));
|
||||||
|
for (const [w, k, pop] of keys) parts.push(Buffer.from([w]), u32(k.length), k, u32(pop.length), pop);
|
||||||
|
parts.push(transport ? Buffer.concat([Buffer.from([1]), transport]) : Buffer.from([0]));
|
||||||
|
return Buffer.concat(parts);
|
||||||
|
}
|
||||||
|
const node = (l, r) => K(Buffer.concat([Buffer.from([1]), l, r]));
|
||||||
|
function merkle(leaves) {
|
||||||
|
const levels = [leaves];
|
||||||
|
while (levels[levels.length - 1].length > 1) {
|
||||||
|
const lv = levels[levels.length - 1], nx = [];
|
||||||
|
for (let i = 0; i < lv.length; i += 2) nx.push(node(lv[i], lv[i + 1] ?? lv[i]));
|
||||||
|
levels.push(nx);
|
||||||
|
}
|
||||||
|
return levels;
|
||||||
|
}
|
||||||
|
function proof(levels, index) {
|
||||||
|
const p = []; let i = index;
|
||||||
|
for (let d = 0; d < levels.length - 1; d++) { const lv = levels[d]; const s = i % 2 === 0 ? i + 1 : i - 1; p.push(lv[s] ?? lv[i]); i = Math.floor(i / 2); }
|
||||||
|
return p.map((x) => "0x" + x.toString("hex"));
|
||||||
|
}
|
||||||
|
const registryHash = (chainId, bind, count, root) =>
|
||||||
|
K(Buffer.concat([Buffer.from("AERE-PQ-REGISTRY-3", "ascii"), u64(chainId), u64(bind), u32(count), root]));
|
||||||
|
|
||||||
|
// RLP and the PqAnchor pre-images, written from the Java source (PqAnchor.prepareMessage / proposalMessage / commitMessage / roundChangePreimage)
|
||||||
|
const rlpLen = (n, off) => { if (n <= 55) return Buffer.from([off + n]); let h = n.toString(16); if (h.length % 2) h = "0" + h; const l = Buffer.from(h, "hex"); return Buffer.concat([Buffer.from([off + 55 + l.length]), l]); };
|
||||||
|
const rb = (b) => (b.length === 1 && b[0] < 0x80 ? b : Buffer.concat([rlpLen(b.length, 0x80), b]));
|
||||||
|
const sc = (v) => { v = BigInt(v); if (v === 0n) return rb(Buffer.alloc(0)); let h = v.toString(16); if (h.length % 2) h = "0" + h; return rb(Buffer.from(h, "hex")); };
|
||||||
|
const list = (...x) => { const p = Buffer.concat(x); return Buffer.concat([rlpLen(p.length, 0xc0), p]); };
|
||||||
|
const A = (s) => rb(Buffer.from(s, "ascii"));
|
||||||
|
const H = (h) => rb(Buffer.from(ethers.getBytes(h)));
|
||||||
|
const msgPR = (dom, h, r, d) => K(list(A(dom), sc(CHAIN), sc(h), sc(r), H(d)));
|
||||||
|
const msgCommit = (h, hash) => K(list(A("AERE-PQ-COMMIT-1"), sc(CHAIN), sc(h), H(hash)));
|
||||||
|
const msgRC = (h, t, prep, pr, d) => K(list(A("AERE-PQ-ROUNDCHANGE-1"), sc(CHAIN), sc(h), sc(t), sc(prep ? 1 : 0), sc(prep ? pr : 0), prep ? H(d) : rb(Buffer.alloc(0))));
|
||||||
|
// a mock "seal": 0x39 || nonce(40) || keccak256(pk || message)
|
||||||
|
const seal = (pk, m) => "0x" + Buffer.concat([Buffer.from([0x39]), Buffer.alloc(40, 0x77), K(Buffer.concat([pk, m]))]).toString("hex");
|
||||||
|
|
||||||
|
describe("PqEquivocationEvidence (AIP-22 stage 6)", function () {
|
||||||
|
let ev, pin, pks, pre, levels, root, count, ROT, ids;
|
||||||
|
const D1 = "0x" + "11".repeat(32), D2 = "0x" + "22".repeat(32);
|
||||||
|
|
||||||
|
async function deployPin(hash) {
|
||||||
|
const [s] = await ethers.getSigners();
|
||||||
|
const init = "0x7f" + hash.toString("hex") + "600055600b80602f6000396000f3" + "60005460005260206000f3";
|
||||||
|
const tx = await s.sendTransaction({ data: init });
|
||||||
|
const r = await tx.wait();
|
||||||
|
return r.contractAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
const Mock = await ethers.getContractFactory("MockFalcon512");
|
||||||
|
const m = await Mock.deploy(); await m.waitForDeployment();
|
||||||
|
await network.provider.send("hardhat_setCode", [PREC, await ethers.provider.getCode(await m.getAddress())]);
|
||||||
|
count = 3n;
|
||||||
|
pks = [falconPk(1), falconPk(2), falconPk(3)];
|
||||||
|
// row 0 is a ROTATED row (its id is another key's derivation), as row 2 of the live epoch 3,097,600 on 28001
|
||||||
|
ROT = idOf(falconPk(99));
|
||||||
|
ids = [ROT, idOf(pks[1]), idOf(pks[2])];
|
||||||
|
pre = [rowPreimage(CHAIN, BIND, count, 0, pks[0], true, null, ROT), rowPreimage(CHAIN, BIND, count, 1, pks[1], true, Buffer.alloc(20, 0xab)), rowPreimage(CHAIN, BIND, count, 2, pks[2], false, null)];
|
||||||
|
levels = merkle(pre.map((p) => K(p)));
|
||||||
|
root = levels[levels.length - 1][0];
|
||||||
|
pin = await deployPin(registryHash(CHAIN, BIND, count, root));
|
||||||
|
const F = await ethers.getContractFactory("PqEquivocationEvidence");
|
||||||
|
ev = await F.deploy(CHAIN, [BIND], [pin]); await ev.waitForDeployment();
|
||||||
|
});
|
||||||
|
|
||||||
|
const base = (kind, h, r, dA, dB, idx, sA, sB, extra = {}) => ({
|
||||||
|
kind, height: h, round: r, digestA: dA, digestB: dB, preparedA: false, preparedB: false, preparedRoundA: 0, preparedRoundB: 0,
|
||||||
|
sealA: sA, sealB: sB, rowPreimage: "0x" + pre[idx].toString("hex"), merkleProof: proof(levels, idx), ...extra,
|
||||||
|
});
|
||||||
|
|
||||||
|
it("the pin is the 11-byte read-only getter and returns the registry hash", async function () {
|
||||||
|
expect(await ethers.provider.getCode(pin)).to.equal("0x60005460005260206000f3");
|
||||||
|
expect(await ethers.provider.call({ to: pin })).to.equal("0x" + registryHash(CHAIN, BIND, count, root).toString("hex"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rebuilds the four pre-images exactly as PqAnchor writes them", async function () {
|
||||||
|
const h = 3131500n;
|
||||||
|
const e1 = base(2, h, 3, D1, D2, 1, "0x", "0x");
|
||||||
|
const [a, b] = await ev.messages(e1);
|
||||||
|
expect(a).to.equal("0x" + msgPR("AERE-PQ-PREPARE-1", h, 3, D1).toString("hex"));
|
||||||
|
expect(b).to.equal("0x" + msgPR("AERE-PQ-PREPARE-1", h, 3, D2).toString("hex"));
|
||||||
|
const [p] = await ev.messages(base(1, h, 0, D1, D2, 1, "0x", "0x"));
|
||||||
|
expect(p).to.equal("0x" + msgPR("AERE-PQ-PROPOSAL-1", h, 0, D1).toString("hex"));
|
||||||
|
const [c] = await ev.messages(base(3, h, 0, D1, D2, 1, "0x", "0x"));
|
||||||
|
expect(c).to.equal("0x" + msgCommit(h, D1).toString("hex"));
|
||||||
|
const [r1, r2] = await ev.messages(base(4, h, 2, D1, D2, 1, "0x", "0x", { preparedA: false, preparedB: true, preparedRoundB: 1 }));
|
||||||
|
expect(r1).to.equal("0x" + msgRC(h, 2, false, 0, D1).toString("hex"));
|
||||||
|
expect(r2).to.equal("0x" + msgRC(h, 2, true, 1, D2).toString("hex"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("records a PREPARE equivocation of one row, once, and names its index and id", async function () {
|
||||||
|
const h = 3131500n, r = 3n;
|
||||||
|
const e = base(2, h, r, D1, D2, 1, seal(pks[1], msgPR("AERE-PQ-PREPARE-1", h, r, D1)), seal(pks[1], msgPR("AERE-PQ-PREPARE-1", h, r, D2)));
|
||||||
|
await expect(ev.submit(e)).to.emit(ev, "EquivocationRecorded");
|
||||||
|
const key = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes20", "uint8", "uint64", "uint64"], ["0x" + idOf(pks[1]).toString("hex"), 2, h, r]));
|
||||||
|
const rec = await ev.records(key);
|
||||||
|
expect(rec.id).to.equal("0x" + idOf(pks[1]).toString("hex"));
|
||||||
|
expect(rec.index).to.equal(1n);
|
||||||
|
expect(rec.bindHeight).to.equal(BIND);
|
||||||
|
expect(await ev.recordCount()).to.equal(1n);
|
||||||
|
await expect(ev.submit(e)).to.be.revertedWithCustomError(ev, "AlreadyRecorded");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("records PROPOSAL, COMMIT and ROUND-CHANGE equivocations (last row of an odd tree: self-paired leaf)", async function () {
|
||||||
|
const h = 3131600n;
|
||||||
|
await expect(ev.submit(base(1, h, 1, D1, D2, 2, seal(pks[2], msgPR("AERE-PQ-PROPOSAL-1", h, 1, D1)), seal(pks[2], msgPR("AERE-PQ-PROPOSAL-1", h, 1, D2))))).to.emit(ev, "EquivocationRecorded");
|
||||||
|
await expect(ev.submit(base(3, h, 0, D1, D2, 0, seal(pks[0], msgCommit(h, D1)), seal(pks[0], msgCommit(h, D2))))).to.emit(ev, "EquivocationRecorded");
|
||||||
|
const x = { preparedA: false, preparedB: true, preparedRoundB: 1 };
|
||||||
|
await expect(ev.submit(base(4, h, 2, D1, D2, 0, seal(pks[0], msgRC(h, 2, false, 0, D1)), seal(pks[0], msgRC(h, 2, true, 1, D2)), x))).to.emit(ev, "EquivocationRecorded");
|
||||||
|
expect(await ev.recordCount()).to.equal(4n);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("refuses each way of not being an equivocation, by its reason", async function () {
|
||||||
|
const h = 3131700n, r = 0n;
|
||||||
|
const mA = msgPR("AERE-PQ-PREPARE-1", h, r, D1), mB = msgPR("AERE-PQ-PREPARE-1", h, r, D2);
|
||||||
|
// the same message twice is not a conflict
|
||||||
|
await expect(ev.submit(base(2, h, r, D1, D1, 1, seal(pks[1], mA), seal(pks[1], mA)))).to.be.revertedWithCustomError(ev, "NotAConflict");
|
||||||
|
// the second seal by ANOTHER row's key: one validator did not sign both
|
||||||
|
await expect(ev.submit(base(2, h, r, D1, D2, 1, seal(pks[1], mA), seal(pks[0], mB)))).to.be.revertedWithCustomError(ev, "SealInvalid").withArgs(2);
|
||||||
|
// a seal over another round
|
||||||
|
await expect(ev.submit(base(2, h, r, D1, D2, 1, seal(pks[1], msgPR("AERE-PQ-PREPARE-1", h, 1, D1)), seal(pks[1], mB)))).to.be.revertedWithCustomError(ev, "SealInvalid").withArgs(1);
|
||||||
|
// a seal of the PROPOSAL domain offered as a PREPARE
|
||||||
|
await expect(ev.submit(base(2, h, r, D1, D2, 1, seal(pks[1], msgPR("AERE-PQ-PROPOSAL-1", h, r, D1)), seal(pks[1], mB)))).to.be.revertedWithCustomError(ev, "SealInvalid").withArgs(1);
|
||||||
|
// a Merkle path that does not reach the pinned root
|
||||||
|
const bad = base(2, h, r, D1, D2, 1, seal(pks[1], mA), seal(pks[1], mB)); bad.merkleProof = [...bad.merkleProof]; bad.merkleProof[0] = "0x" + "00".repeat(32);
|
||||||
|
await expect(ev.submit(bad)).to.be.revertedWithCustomError(ev, "PinMismatch");
|
||||||
|
// a row claiming the index of another row (the path of row 1 used for row 0's preimage)
|
||||||
|
const swap = base(2, h, r, D1, D2, 0, seal(pks[0], mA), seal(pks[0], mB)); swap.merkleProof = proof(levels, 1);
|
||||||
|
await expect(ev.submit(swap)).to.be.revertedWithCustomError(ev, "PinMismatch");
|
||||||
|
// a proof of the wrong depth
|
||||||
|
const short = base(2, h, r, D1, D2, 1, seal(pks[1], mA), seal(pks[1], mB)); short.merkleProof = short.merkleProof.slice(1);
|
||||||
|
await expect(ev.submit(short)).to.be.revertedWithCustomError(ev, "BadRow").withArgs("proof length");
|
||||||
|
// a row of another chain, another epoch, and an id not derived from its key
|
||||||
|
const other = (mut) => { const p = Buffer.from(pre[1]); mut(p); const e = base(2, h, r, D1, D2, 1, seal(pks[1], mA), seal(pks[1], mB)); e.rowPreimage = "0x" + p.toString("hex"); return e; };
|
||||||
|
await expect(ev.submit(other((p) => u64(28002).copy(p, 14)))).to.be.revertedWithCustomError(ev, "BadRow").withArgs("chain id");
|
||||||
|
await expect(ev.submit(other((p) => u64(BIND + 1n).copy(p, 22)))).to.be.revertedWithCustomError(ev, "RowNotInEpoch");
|
||||||
|
// the id is bound by the REGISTRY (inside the row hash), not re-derived on chain: a changed id no longer proves into the pin
|
||||||
|
await expect(ev.submit(other((p) => { p[38] ^= 1; }))).to.be.revertedWithCustomError(ev, "PinMismatch");
|
||||||
|
await expect(ev.submit(other((p) => { p[0] = 0x42; }))).to.be.revertedWithCustomError(ev, "BadRow").withArgs("prefix");
|
||||||
|
// a COMMIT that names a round, an unknown type, a height before the first epoch
|
||||||
|
await expect(ev.submit(base(3, h, 1, D1, D2, 1, "0x", "0x"))).to.be.revertedWithCustomError(ev, "CommitCarriesRound");
|
||||||
|
await expect(ev.submit(base(9, h, r, D1, D2, 1, "0x", "0x"))).to.be.revertedWithCustomError(ev, "UnknownKind").withArgs(9);
|
||||||
|
await expect(ev.submit(base(2, BIND - 1n, r, D1, D2, 1, seal(pks[1], mA), seal(pks[1], mB)))).to.be.revertedWithCustomError(ev, "NoEpochAtHeight");
|
||||||
|
expect(await ev.recordCount()).to.equal(4n);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rowAt: each row proves into the pinned epoch as a plain call, and a tampered row does not", async function () {
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const [id, index] = await ev.rowAt(BIND + 5n, "0x" + pre[i].toString("hex"), proof(levels, i));
|
||||||
|
expect(id).to.equal("0x" + ids[i].toString("hex"));
|
||||||
|
expect(index).to.equal(BigInt(i));
|
||||||
|
}
|
||||||
|
const t = Buffer.from(pre[2]); t[t.length - 1] ^= 1;
|
||||||
|
await expect(ev.rowAt(BIND + 5n, "0x" + t.toString("hex"), proof(levels, 2))).to.be.revertedWithCustomError(ev, "BadRow");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("will not be built on a pin that is not the read-only getter, nor on unordered epochs", async function () {
|
||||||
|
const F = await ethers.getContractFactory("PqEquivocationEvidence");
|
||||||
|
await expect(F.deploy(CHAIN, [BIND], [await ev.getAddress()])).to.be.revertedWithCustomError(F, "BadPin");
|
||||||
|
await expect(F.deploy(CHAIN, [BIND, BIND], [pin, pin])).to.be.revertedWithCustomError(F, "BadEpochs");
|
||||||
|
await expect(F.deploy(CHAIN, [], [])).to.be.revertedWithCustomError(F, "BadEpochs");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user