aere-contracts/test/interop/trie.js
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (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
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

154 lines
5.0 KiB
JavaScript

// -----------------------------------------------------------------------------
// Minimal, correct Ethereum hexary Merkle-Patricia trie BUILDER + proof generator,
// plus EIP-2718 receipt encoding, for the message-inbox tests.
//
// This is an INDEPENDENT implementation from the Solidity MerklePatriciaProof
// verifier: the builder here constructs the trie and the on-chain verifier walks it.
// A passing test means the two agree, which is a real cross-check on both.
//
// Receipt values are always > 32 bytes (a receipt carries a 256-byte logsBloom), so
// every trie node is > 32 bytes and referenced by hash — no inline-node case arises.
// -----------------------------------------------------------------------------
const { RLP } = require("@ethereumjs/rlp");
const { keccak256 } = require("ethers");
const toBuf = (x) => {
if (Buffer.isBuffer(x)) return x;
if (typeof x === "string" && x.startsWith("0x")) return Buffer.from(x.slice(2), "hex");
return Buffer.from(x);
};
function keccakBuf(b) {
return Buffer.from(keccak256("0x" + toBuf(b).toString("hex")).slice(2), "hex");
}
function rlpEncode(x) {
return Buffer.from(RLP.encode(x));
}
function rlpDecode(b) {
return RLP.decode(Uint8Array.from(b));
}
function bytesToNibbles(b) {
const out = [];
for (const x of b) {
out.push(x >> 4);
out.push(x & 0x0f);
}
return out;
}
function hpEncode(nibbles, isLeaf) {
const odd = nibbles.length % 2 === 1;
const flags = (isLeaf ? 2 : 0) | (odd ? 1 : 0);
const bytes = [];
let start = 0;
if (odd) {
bytes.push((flags << 4) | nibbles[0]);
start = 1;
} else {
bytes.push(flags << 4);
}
for (let i = start; i < nibbles.length; i += 2) {
bytes.push((nibbles[i] << 4) | nibbles[i + 1]);
}
return Buffer.from(bytes);
}
// Build a trie over [{key: Buffer, value: Buffer}]. Returns { root, nodes } where
// nodes maps hex(hash)->rlp Buffer.
function buildTrie(entries) {
const nodes = {};
function store(items) {
const rlp = rlpEncode(items);
const h = keccakBuf(rlp);
nodes[h.toString("hex")] = rlp;
return h; // referenced by 32-byte hash (all our nodes are > 32 bytes)
}
function commonPrefix(pairs) {
let cp = pairs[0].nibbles.slice();
for (const p of pairs) {
let i = 0;
while (i < cp.length && i < p.nibbles.length && cp[i] === p.nibbles[i]) i++;
cp = cp.slice(0, i);
}
return cp;
}
function build(pairs) {
if (pairs.length === 1) {
return store([hpEncode(pairs[0].nibbles, true), pairs[0].value]);
}
const cp = commonPrefix(pairs);
if (cp.length > 0) {
const stripped = pairs.map((p) => ({ nibbles: p.nibbles.slice(cp.length), value: p.value }));
const child = build(stripped);
return store([hpEncode(cp, false), child]);
}
// branch
const slots = new Array(16).fill(Buffer.alloc(0));
let branchValue = Buffer.alloc(0);
const groups = {};
for (const p of pairs) {
if (p.nibbles.length === 0) {
branchValue = p.value;
} else {
const nib = p.nibbles[0];
(groups[nib] = groups[nib] || []).push({ nibbles: p.nibbles.slice(1), value: p.value });
}
}
for (let nib = 0; nib < 16; nib++) {
if (groups[nib]) slots[nib] = build(groups[nib]);
}
return store([...slots, branchValue]);
}
const pairs = entries.map((e) => ({ nibbles: bytesToNibbles(toBuf(e.key)), value: toBuf(e.value) }));
const root = build(pairs);
return { root, nodes };
}
// Collect the proof node set (rlp Buffers) along `key` from `root`.
function getProof(root, nodes, key) {
const path = bytesToNibbles(toBuf(key));
const out = [];
let cur = root;
let idx = 0;
for (let hop = 0; hop < 512; hop++) {
const rlp = nodes[toBuf(cur).toString("hex")];
if (!rlp) break;
out.push(rlp);
const items = rlpDecode(rlp).map((x) => Buffer.from(x));
if (items.length === 2) {
const enc = items[0];
const isLeaf = (enc[0] >> 4) & 0x2;
const odd = (enc[0] >> 4) & 0x1;
let fragLen = (enc.length - 1) * 2 + (odd ? 1 : 0);
if (isLeaf) break;
idx += fragLen;
cur = items[1];
} else {
if (idx >= path.length) break;
const nib = path[idx];
idx += 1;
const child = items[nib];
if (!child || child.length === 0) break;
cur = child;
}
}
return out;
}
// rlp(transactionIndex) — the receipts-trie key.
function txIndexKey(i) {
if (i === 0) return Buffer.from([0x80]);
return rlpEncode(i);
}
// Build an EIP-2718 type-2 receipt value: 0x02 || rlp([status, cumGas, bloom, logs]).
// log = [address(20), [topics...], data].
function encodeReceipt(type, { status, cumulativeGas, logs }) {
const bloom = Buffer.alloc(256, 0);
const rlpLogs = logs.map((l) => [toBuf(l.address), l.topics.map(toBuf), toBuf(l.data)]);
const body = rlpEncode([status, cumulativeGas, bloom, rlpLogs]);
if (type === 0) return body; // legacy: value is the rlp list directly
return Buffer.concat([Buffer.from([type]), body]); // typed
}
module.exports = { buildTrie, getProof, txIndexKey, encodeReceipt, keccakBuf, rlpEncode, bytesToNibbles };