Final Testneteksplorator K_J · Final Testnet · 48359
pl

Kontrakt

0xd219f8fc34502e6a64482241f72c15da5e8883cb

Adres
0xd219f8fc34502e6a64482241f72c15da5e8883cb
Rodzaj
zweryfikowany kontrakt FinalBundleLog
Saldo
0 vETH
Nonce
1
Kod
9,427 bajtów codehash 0xb06ea386d59083e39debc38a53a2b18dd84588b52d51f1729a223131b77f76de

drzewo kont

Drzewo
1 · konta
Obecność
brak liścia
Klucz
0xda103f08f2e7596c4b7cbc4eaa8989170d9d47230b42fecaae22ab919a8a1d60
Korzeń bieżący
0x18f30182962d8af79e7ab628ce200be69d28f54119890d737c7e736de62e703c
Ten adres nie ma liścia w drzewie kont. Każdy Final Wallet — łącznie z tożsamościami usługowymi — go ma, więc brak liścia oznacza zwykłe konto, a nie portfel.
transakcjezdarzeniatransfery tokenówkontrakt

źródło zweryfikowane

Kontrakt
FinalBundleLog dokładne dopasowanie · immutables zamaskowane
Kompilator
v0.8.33+commit.64118f21
Optymalizator
włączony · 200 przebiegów
Wersja EVM
prague
Zweryfikowano
2026-09-06T09:30:32.513Z
Pochodzenie
preverify-final-chain (forge artifact, bytecode compared against live code)

contracts/finalchain/FinalBundleLog.sol

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2024-2026 Final DeFi
// Licensed under the Business Source License 1.1 (the "License")
//
// Change Date: 2029-01-01
// Change License: GPL-2.0-or-later
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;

import {FinalMmr} from "../utils/FinalMmr.sol";
import {FinalChainPrecompiles} from "./FinalChainPrecompiles.sol";
import {FinalIntentLog} from "./FinalIntentLog.sol";
import {FinalBundleTree} from "../utils/FinalBundleTree.sol";
import {FinalIdentityRegistry} from "./FinalIdentityRegistry.sol";
import {FinalPqQuorum} from "./FinalPqQuorum.sol";

/**
 * @title FinalBundleLog
 * @notice The PQ bundle append-only log, on Final Chain.
 *
 * Deployed on **Final Chain**, alongside `FinalPriceOracle` — another tree in
 * the set the backend chain publishes, and the one the PQ anchor depends on.
 *
 * ## Why this exists at all
 *
 * `PqAnchorModule` has always said where this log lives: the head
 * (`masterRoot` @ `mmrSize`) is "advanced by the Final-chain validator set as
 * it folds newly-finalized bundles into the MMR". The log itself was never on
 * a chain. It lived in a Redis list maintained by a single `mmr-index` worker,
 * because Final Chain did not exist and the backend was standing in for it.
 *
 * That stand-in is what this replaces. The MMR anchor is the **sole
 * authorization for PQ execution** — a bundle executes because its root is
 * proven under `masterRoot`, and nothing else vouches for it — so the question
 * "what is leaf 41?" is a question about custody, and it was being answered by
 * a list in a cache that one worker held a lock on.
 *
 * ## The log answers; nothing folds an MMR to ask it
 *
 * The obvious cheaper design is to emit leaves and let each reader fold them
 * into an MMR itself. It is wrong for this log. Every co-signer would derive
 * the root it is about to attest to, so a quorum of correct signers would be
 * attesting to their own agreement about a computation rather than to a fact
 * the chain states — and a subtly divergent folder in one implementation is
 * indistinguishable from a dishonest one.
 *
 * So the chain states `(root, size)` — and it also states the PROOF. `proofAt`
 * and `rootAt` answer for any historical size, so a consumer that needs "leaf
 * 41 under the root the gateway anchored" makes one `eth_call` and holds no
 * MMR of its own. That closes the last place two implementations of this
 * construction had to agree: the backend used to replay `BundleAppended`,
 * re-fold every leaf in JavaScript and build the audit path there, which is a
 * second folder whose divergence from this one presents as a proof that
 * verifies nowhere with both sides internally consistent.
 *
 * A historical proof is derivable because an MMR node is written ONCE, when
 * its perfect subtree completes, and is never revised. Truncating the log to
 * `atSize` therefore does not change any node the proof reads — it only
 * changes which nodes the border bags — so `proofAt(i, atSize)` is exact for
 * every `atSize <= size`, which is what `advanceMmrRoot` lagging behind the
 * log requires.
 *
 * ## Storage: every node, not just the peaks
 *
 * `_node[level][index]` holds each completed perfect subtree, `_payload[i]`
 * the pre-image at each position. That is ~2 slots per leaf and it is what
 * makes the two views above possible; the peak bag is derived from `size`
 * rather than stored, because a bag kept alongside the nodes would be a second
 * representation of one fact.
 *
 * An append is `O(log n)` worst case and amortised `O(1)`: a leaf carries
 * while the two highest peaks are equal-sized, which is binary increment on
 * `size`, so the carry count is the number of trailing ones. It is the
 * cheapest thing in the PQ path by a wide margin, and on Final Chain — one
 * block every 100 ms at a 1 wei base fee — the extra `SSTORE`s cost nothing
 * that matters.
 *
 * ## The layout is exactly `FinalMmr`'s
 *
 * The log is an RFC-6962 append-only log: perfect subtrees ("peaks") in
 * strictly decreasing size, folded right-to-left into the root.
 *
 * Leaf hashing and the proof's shape come from `FinalMmr` rather than being
 * restated — including `bitLength`/`popcount`, which are `internal` there
 * precisely so the proof this contract BUILDS and the proof
 * `PqAnchorModule.verifyInclusion` CHECKS decompose with one implementation.
 * A proof built to one shape and verified against another reverts on the
 * gateway naming neither side.
 *
 * ## Append-only is enforced, not documented
 *
 * There is no setter for a leaf, no way to shorten the log, and no owner path
 * that rewrites one. The only mutation is `append`. A log whose operator can
 * revise leaf 41 does not constrain a PQ dispatch at all — it only records
 * what the operator was willing to admit, which is what an authorization root
 * must not be.
 *
 * ## Who may append: a PQ quorum, not a role
 *
 * `FinalPqQuorum` over `ROLE_MMR_COSIGNER`, which is the same roster that
 * anchors the head on the execution chains — deliberately, since splitting them
 * would create an authority that can admit bundles nobody anchors, or anchor a
 * head over leaves nobody admitted.
 *
 * It was a single `FINAL_MANAGER` role, and that was the largest hole left in
 * the PQ lane. Admission IS the authorization: a bundle executes because its
 * intent root proves under `masterRoot`, and nothing downstream re-checks it.
 * So one compromised manager key admitted an arbitrary bundle and the chain
 * then *stated* that root as fact, with every co-signer and every gateway
 * correctly deferring to it. A K-of-N quorum of post-quantum signatures, each
 * verified by this chain's own precompiles against keys read from the registry
 * rather than from calldata, is what makes the log's contents cost more than
 * one key.
 *
 * The whole authority plane moved with it. The log used to answer to a
 * `FinalAccessManager`, which does not exist on Final Chain — the registry is
 * the only role plane here, so `configure` is gated by it exactly as
 * `FinalStateTrees.configureTree` is, and the access-manager pointer and its
 * rotation setter are gone rather than left dangling.
 */
contract FinalBundleLog {
    /// @notice Commitment space. Must equal `PqAnchorModule.DOMAIN_PQ_MMR`, so
    /// a proof against this log's root is accepted by the gateway and a proof
    /// from any other log is not.
    ///
    /// Restated rather than imported because the gateway declares it
    /// `internal` and lives on a different chain — there is no import that
    /// would make this one value. `test_DomainMatchesTheGateway` pins the two
    /// together; without it, a `_v3` bump on one side would produce a log whose
    /// every proof is rejected, and the first symptom would be a PQ bundle that
    /// will not dispatch.
    bytes32 public constant DOMAIN = keccak256("FINAL_PQ_MMR_NODE_v01");

    /// @dev What a co-signer's approval authorizes. Per-action, so an approval
    /// to append cannot be replayed as one for any other quorum on this chain.
    /// @dev `.v2`: an append is approved over each bundle's INTENT LEAVES now,
    /// not over a claimed root. The shapes differ under `abi.encode`, so a v1
    /// approval could not be replayed in any case — but the action is what a
    /// signer reads to know what it is approving, and it is now approving a
    /// different thing.
    /// @dev `.v02`: each bundle is approved over its bundle-TERMS leaf as well
    /// as its intent leaves (review C3). The terms leaf is folded at position 0
    /// ahead of the intents, so the anchored root commits to the payout terms
    /// the co-signers admitted — dispatch method, recipient, fee cap, flags,
    /// includer tip/cap, committed submitter, searcher bids — and a submitter
    /// who alters any of them on the execution chain fails `BundleNotAnchored`.
    bytes32 private constant ACTION_APPEND = keccak256("FinalBundleLog.append.v02");
    /// @dev Standalone leaves (reviews C1/C2/C5): staged-envelope and
    /// settlement-credit leaves are MMR entries of their own, appended under
    /// the same quorum but consuming nothing from `FinalIntentLog` — they are
    /// not intents. A distinct action so an approval to append bundles can
    /// never be replayed as one to append payloads, and vice versa.
    bytes32 private constant ACTION_APPEND_PAYLOADS = keccak256("FinalBundleLog.appendPayloads.v01");
    /// @dev Registrar-quorum action, verified by the registry with this log as
    /// the verifying contract.
    bytes32 public constant ACTION_CONFIGURE = keccak256("FINAL_BUNDLE_LOG_CONFIGURE_v01");
    bytes32 public constant ACTION_SEED = keccak256("FINAL_BUNDLE_LOG_SEED_v01");
    /// @dev Registrar-quorum action: a fresh log taking over the previous log's
    /// LEAVES, payload by payload (the small-log form of {seed}).
    bytes32 public constant ACTION_SEED_PAYLOADS = keccak256("FINAL_BUNDLE_LOG_SEED_PAYLOADS_v01");

    /// @notice Where every signer, key and role is resolved. Immutable, so the
    /// quorum can never be pointed at a registry that arrived in calldata.
    FinalIdentityRegistry public immutable registry;

    /// @notice The posting log every anchored intent must already appear in.
    ///
    /// @dev Immutable for the same reason as `registry`, and for a sharper one:
    /// this is the whole of Final-Chain-first admission. A log that could be
    /// repointed could be repointed at one that says yes to everything, and the
    /// gate would be gone with nothing on chain looking different.
    FinalIntentLog public immutable intentLog;

    /// @notice The role a member must hold to approve an append.
    uint256 public writerRole;
    /// @notice How many approvals one append needs. Zero means unconfigured,
    /// and an unconfigured log refuses every write.
    uint256 public threshold;
    /// @notice Bound into every quorum digest. One per successful `append`
    /// call, not per leaf — a batch is one authorization.
    uint64 public nonce;

    /// @dev Every completed perfect subtree, by level and by index at that
    /// level. Written once and never revised — that immutability is the whole
    /// reason `proofAt` can answer for a historical `size`.
    mapping(uint256 level => mapping(uint256 index => bytes32)) private _node;

    /// @dev The pre-image at each position: the bundle's intent-Merkle root,
    /// untagged. Stored rather than left to the event log so a consumer needs
    /// one `eth_call` and no `getLogs` range — the chain caps that range at
    /// 100,000 blocks and mints one every 100 ms.
    mapping(uint256 index => bytes32) private _payload;

    /// @dev First position a payload took, plus one. Zero means never
    /// appended. First-occurrence-wins: a duplicate bundle root is a duplicate
    /// bundle, and either position proves the same root under the same head,
    /// so the earlier one is the one a lagging anchor can already reach.
    mapping(bytes32 payload => uint256) private _firstIndexPlusOne;

    /// @notice Leaf count. The `size` half of the head the execution chains'
    /// `advanceMmrRoot` is called with.
    uint256 public size;

    /// @notice Current log root over all `size` leaves — the `root` half of
    /// that head. Zero exactly when `size == 0`: an empty log commits to no
    /// leaves, matching `FinalMmr.MmrEmptyLog`.
    bytes32 public root;

    /// @notice A bundle root was folded in at `index`.
    /// @dev Carries the payload AND the resulting head, so a reader that wants
    /// the log replays this event and needs no other source. `payload` is the
    /// pre-image (the bundle's intent-Merkle root), not the tagged leaf — a
    /// consumer proving inclusion re-tags it via `FinalMmr.hashLeaf`, and
    /// emitting the tagged form would invite proving against the wrong one.
    event BundleAppended(uint256 indexed index, bytes32 indexed payload, bytes32 root, uint256 size);

    /// @notice The writer role or the threshold moved.
    event LogConfigured(uint256 writerRole, uint256 threshold);
    /// @notice A fresh log took over the previous log's frontier.
    event Seeded(uint256 size, uint256 peaks);
    /// @notice A fresh log took over the previous log's leaves, whole.
    event SeededPayloads(uint256 size);

    /// @notice `payload` is zero. A zero leaf is almost always an uninitialised
    /// read upstream, and it is unrecoverable here: the log cannot be shortened.
    error ZeroBundleRoot();
    /// @notice The frontier can be seeded only into an empty log.
    error NotFresh();
    error PeaksMismatch(uint256 expected, uint256 given);
    /// @notice A view was asked about more leaves than the log holds.
    error SizeAhead(uint256 asked, uint256 held);
    /// @notice The audit-path walk and the `(index, size)` decomposition
    /// disagreed about how many siblings a proof has.
    error ProofShapeMismatch(uint256 expected, uint256 walked);
    /// @notice `append` with no leaves. A quorum round that admits nothing is
    /// always a caller bug, and burning a nonce for it would invalidate every
    /// approval already collected for the real batch.
    error EmptyBatch();
    /// @notice `threshold` is zero: no quorum has been configured yet.
    error LogNotConfigured();
    /// @notice A threshold no live roster can meet. Register the members first.
    error ThresholdUnreachable(uint256 live, uint256 required);
    /// @notice Not the bootstrap admin and not a registrar.
    error NotAuthorized(address caller);
    /// @dev A bundle with no intents has no root to fold and nothing to consume.
    error EmptyBundleLeaves(uint256 bundleIndex);
    error ZeroIntentLog();
    /// @notice `append` was handed a different number of terms leaves than
    /// bundles — every bundle carries exactly one.
    error TermsLeafCountMismatch(uint256 termsLeaves, uint256 bundles);
    /// @notice A bundle's terms leaf was zero. The terms are what the
    /// co-signers admitted; a bundle with none has no admitted payout terms.
    error ZeroTermsLeaf(uint256 bundleIndex);

    /**
     * @param registry_ The identity registry. Every signer, key and role comes
     *        from it, and it is fixed at deployment for the same reason the
     *        keys are read from storage: a registry supplied per call is a
     *        registry the caller chooses.
     *
     * @dev A constructor, unlike the salt-only contracts in `contracts/`. This
     * one is deployed by ordinary CREATE alongside `FinalIdentityRegistry` and
     * `FinalStateTrees` — it lives on exactly one chain, so an address that is
     * identical across chains buys nothing, and the vanity path costs a
     * `FinalDeployer` bootstrap Final Chain has no other use for.
     *
     * The precompile probe is the point of having one at all: a log deployed
     * where ML-DSA cannot be verified would accept no approval it was ever
     * given, and the first symptom would be a PQ lane that silently never
     * admits a bundle.
     */
    constructor(FinalIdentityRegistry registry_, FinalIntentLog intentLog_) {
        FinalChainPrecompiles.assertAvailable();
        if (address(intentLog_) == address(0)) revert ZeroIntentLog();
        registry = registry_;
        intentLog = intentLog_;
    }

    /**
     * @notice Set which role may approve an append, and how many approvals.
     * @dev The registry's bootstrap admin alone while its window is open, the
     * sealed `ROLE_REGISTRAR` quorum afterwards — the same window and the same
     * quorum the registry and the trees use, because a threshold is membership
     * by another name. `approvals` is empty during bootstrap.
     *
     * Deliberately re-callable. A co-signer set that grows or shrinks has to be
     * able to move its threshold, and the alternative is a log that must be
     * redeployed — which for an append-only log means abandoning its contents.
     */
    function configure(
        uint256 role,
        uint256 k,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        if (registry.bootstrapSealed() || msg.sender != registry.bootstrapAdmin()) {
            registry.requireRegistrarQuorum(
                ACTION_CONFIGURE, keccak256(abi.encode(role, k)), anchorBlock, approvals
            );
        }
        // Refuse a threshold nobody can meet. A 4-of-5 configured against three
        // registered co-signers is a log that reverts on every append, and the
        // revert would name the threshold rather than the roster.
        if (k != 0) {
            uint256 live = registry.liveMemberCount(role);
            if (live < k) revert ThresholdUnreachable(live, k);
        }
        writerRole = role;
        threshold = k;
        emit LogConfigured(role, k);
    }

    /**
     * @notice Take over the previous log's frontier — its `peaks()` and `size`
     *         — so appends continue at the old positions and the accumulator
     *         (roots, `masterRoot @ mmrSize` on every gateway) never runs
     *         backwards across a redeploy. NO-WIPE redeploy, ruled 2026-09-03.
     * @dev The peaks are stored as the nodes they are (level = bit, index =
     *      leaves-before >> level), which is all a future append or `peaksAt`
     *      ever reads. Leaves BELOW the seed are the old log's: `payloadAt`
     *      and proofs for them answer from there, not here. Same authority as
     *      {configure}; only while this log holds nothing.
     */
    function seed(
        bytes32[] calldata peaks_,
        uint256 size_,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        if (registry.bootstrapSealed() || msg.sender != registry.bootstrapAdmin()) {
            registry.requireRegistrarQuorum(
                ACTION_SEED, keccak256(abi.encode(peaks_, size_)), anchorBlock, approvals
            );
        }
        if (size != 0) revert NotFresh();
        uint256 expected;
        for (uint256 x = size_; x != 0; x >>= 1) {
            if (x & 1 == 1) expected++;
        }
        if (expected != peaks_.length) revert PeaksMismatch(expected, peaks_.length);
        uint256 prefix;
        uint256 p;
        for (uint256 level = 255; ; level--) {
            if ((size_ >> level) & 1 == 1) {
                if (peaks_[p] == bytes32(0)) revert ZeroBundleRoot();
                _node[level][prefix >> level] = peaks_[p++];
                prefix += (uint256(1) << level);
            }
            if (level == 0) break;
        }
        size = size_;
        // `head()` answers (root, size) as one pair; a seeded size beside a
        // zero root is a torn head until the first append recomputes it.
        root = size_ == 0 ? bytes32(0) : rootAt(size_);
        emit Seeded(size_, peaks_.length);
    }

    /**
     * @notice Take over the previous log's LEAVES — every payload, in order —
     *         so this log answers `payloadAt`, `indexOf`, `proofAt` and
     *         `peaksAt` for every position the old one did, with the same
     *         roots at every size. The small-log form of {seed}: a peak-seeded
     *         log holds no payload below its seed, and the first live one
     *         (2026-09-05) was seeded one leaf ABOVE what the gateways had
     *         anchored — the proposer could not read that leaf's payload here,
     *         could not build a verifiable proposal, and every anchor stalled
     *         behind it. Copying the leaves has no such edge; it costs one
     *         fold per leaf, which on this chain is cheap for a log of
     *         thousands. Same authority as {configure}; only while this log
     *         holds nothing.
     */
    function seedPayloads(
        bytes32[] calldata payloads,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        if (registry.bootstrapSealed() || msg.sender != registry.bootstrapAdmin()) {
            registry.requireRegistrarQuorum(
                ACTION_SEED_PAYLOADS, keccak256(abi.encode(payloads)), anchorBlock, approvals
            );
        }
        if (size != 0) revert NotFresh();
        if (payloads.length == 0) revert EmptyBatch();
        for (uint256 i = 0; i < payloads.length; i++) {
            _append(payloads[i]);
        }
        emit SeededPayloads(payloads.length);
    }

    /**
     * @notice Fold one or more bundles in as the next leaves, under a
     *         post-quantum quorum.
     * @param termsLeaves One bundle-terms leaf per bundle (review C3) —
     *        `keccak256(abi.encode(DOMAIN_BUNDLE_TERMS, chainId, method,
     *        recipient, maxFeeWei, bundleFlags, tipPerGas, capPerGas,
     *        submitter, bidsHash))`, exactly as the execution chain's gateway
     *        rebuilds it. Folded at position 0, ahead of the intents; never
     *        consumed from the intent log, because it is not an intent.
     * @param bundles One inner array per bundle: that bundle's intent leaves,
     *        in bundle order. The root is RECOMPUTED from `[terms, leaves…]`.
     * @param approvals At least `threshold` of them, ascending by signer.
     * @return firstIndex The 0-based position the FIRST leaf occupies, forever.
     *
     * @dev **Leaves, not a root.** A claimed root is a claim about intents
     * nobody on this chain checked. Taking the leaves lets the contract do two
     * things it could not do before: recompute the payload with the same fold
     * the execution chain's gateway uses, and require every leaf to be an open
     * posting in `FinalIntentLog`.
     *
     * That is what makes Final-Chain-first admission a mechanism instead of a
     * quorum policy. A bundle cannot be anchored unless every intent in it was
     * posted here first, and since the execution chains' whole authorization is
     * this log's head, an intent that was never posted can never execute. The
     * co-signers already recomputed the root off-chain in
     * `validateBundleForAnchor`; this moves the check to where a compromised
     * quorum cannot skip it.
     *
     * Consumption is also the anti-replay gate for intents. The execution chain
     * keeps a consumed-seqId set that stops a BUNDLE re-executing; it cannot see
     * one intent being re-anchored inside a second bundle. This can.
     *
     * A batch rather than a leaf, because the quorum round — not the
     * `O(log n)` carry — is what an append costs. One round per bundle would
     * put a K-of-N collection on the critical path of every PQ dispatch; one
     * round per run of bundles is the same authorization over more work. A
     * batch of one is the degenerate case and is exactly as safe.
     *
     * The digest binds the nonce AND the pre-append `size`, so a co-signer
     * approves the POSITIONS as well as the contents. `size` is redundant for
     * replay — the nonce already covers that — and it is not redundant for what
     * this log is: "what is leaf 41?" is the question the whole contract exists
     * to answer, and an approval that named the leaves but not where they
     * land would leave that answer to whoever assembled the transaction.
     *
     * ML-DSA-87 is required rather than accepted, AND every approval carries a
     * seal. Admission is the authorization for execution — a root that lands
     * here executes — so it takes both key classes: the transaction key's
     * ML-DSA-87 signature and the seal key's SLH-DSA-SHAKE-256s over the same
     * digest. A break in either family leaves the log unmovable rather than
     * taken. `anchorBlock` is the tree-1 view the members decided the roster
     * against; `FinalPqQuorum.require_` bounds how stale it may be.
     */
    function append(
        bytes32[] calldata termsLeaves,
        bytes32[][] calldata bundles,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external returns (uint256 firstIndex) {
        if (bundles.length == 0) revert EmptyBatch();
        if (termsLeaves.length != bundles.length) revert TermsLeafCountMismatch(termsLeaves.length, bundles.length);
        uint256 k = threshold;
        if (k == 0) revert LogNotConfigured();

        uint64 n = nonce;
        firstIndex = size;
        FinalPqQuorum.require_(
            registry,
            approvals,
            FinalPqQuorum.digest(
                address(this),
                ACTION_APPEND,
                anchorBlock,
                keccak256(abi.encode(n, firstIndex, termsLeaves, bundles))
            ),
            writerRole,
            k,
            FinalPqQuorum.ALG_ML_DSA_87,
            anchorBlock,
            true
        );
        nonce = n + 1;

        for (uint256 i = 0; i < bundles.length; i++) {
            bytes32[] calldata leaves = bundles[i];
            if (leaves.length == 0) revert EmptyBundleLeaves(i);
            if (termsLeaves[i] == bytes32(0)) revert ZeroTermsLeaf(i);

            // Consume BEFORE folding. Either order works today, but consuming
            // first means a bundle that names an unposted or already-spent leaf
            // reverts on the leaf itself rather than after doing the fold — and
            // the revert names which leaf, which is the thing an operator needs.
            // The terms leaf sits at position 0 and is NOT consumed: it is the
            // admitted payout terms, not a posting.
            bytes32[] memory layer = new bytes32[](leaves.length + 1);
            layer[0] = termsLeaves[i];
            for (uint256 j = 0; j < leaves.length; j++) {
                intentLog.consume(leaves[j]);
                layer[j + 1] = leaves[j];
            }

            _append(FinalBundleTree.foldLeaves(layer));
        }
    }

    /**
     * @notice Append standalone leaves — staged-envelope and settlement-credit
     *         commitments (reviews C1/C2/C5) — under the same post-quantum
     *         quorum, consuming nothing.
     * @param payloads The leaf preimages, already domain-separated by their
     *        own kind (`DOMAIN_STAGED_ENVELOPE`, `DOMAIN_SETTLEMENT_CREDIT` on
     *        the gateway side). Each becomes one position in the log and is
     *        provable to any gateway exactly as a bundle root is.
     * @return firstIndex The 0-based position the FIRST payload occupies.
     *
     * @dev These are not intents and were never posted, so nothing is consumed
     * from `FinalIntentLog`; what authorizes them is what authorizes a bundle
     * root — K of N co-signers, each of whom recomputed the leaf from the facts
     * it commits to (a settlement credit from the collecting chain's recorded
     * charge and the terminal execution fact; a staged envelope from the
     * admitted intent's anchored bound) before signing. A separate action id
     * keeps a bundle approval from being replayed here and vice versa; the
     * digest binds the nonce and the pre-append size, so positions are approved
     * along with contents, as for `append`.
     */
    function appendPayloads(
        bytes32[] calldata payloads,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external returns (uint256 firstIndex) {
        if (payloads.length == 0) revert EmptyBatch();
        uint256 k = threshold;
        if (k == 0) revert LogNotConfigured();

        uint64 n = nonce;
        firstIndex = size;
        FinalPqQuorum.require_(
            registry,
            approvals,
            FinalPqQuorum.digest(
                address(this),
                ACTION_APPEND_PAYLOADS,
                anchorBlock,
                keccak256(abi.encode(n, firstIndex, payloads))
            ),
            writerRole,
            k,
            FinalPqQuorum.ALG_ML_DSA_87,
            anchorBlock,
            true
        );
        nonce = n + 1;

        for (uint256 i = 0; i < payloads.length; i++) {
            _append(payloads[i]);
        }
    }

    /// @dev The fold itself. Separated from authorization so the batch loop is
    /// one concern and the quorum is the other.
    function _append(bytes32 payload) private {
        if (payload == bytes32(0)) revert ZeroBundleRoot();

        uint256 index = size;
        _payload[index] = payload;
        // First occurrence wins, so `indexOf` is stable for the life of the
        // log. A later duplicate is still appended and still provable at its
        // own position; it just is not the one this lookup names.
        if (_firstIndexPlusOne[payload] == 0) _firstIndexPlusOne[payload] = index + 1;

        // Push the new leaf as a size-1 peak, then carry while the top two
        // peaks are equal-sized. Equal size is exactly "the low bits of the
        // pre-increment count are set", so the carry count is the number of
        // trailing ones — binary increment, and the reason this is amortised
        // O(1) rather than O(log n) per append.
        bytes32 carry = FinalMmr.hashLeaf(DOMAIN, payload);
        _node[0][index] = carry;

        uint256 completed = index; // leaves already in the log
        uint256 level = 0;
        while (completed & 1 == 1) {
            // The left sibling is the perfect subtree that ends where this one
            // begins, and it is already stored — `_append` is the only writer
            // and it wrote that node on the append that completed it.
            //
            // Positional: the earlier subtree is always the LEFT child. Sorting
            // the pair here would make two different logs hash alike and is the
            // second-preimage hole `FinalMmr`'s tags exist to close.
            bytes32 left = _node[level][completed - 1];
            carry = _hashNode(left, carry);
            unchecked {
                completed >>= 1;
                ++level;
            }
            _node[level][completed] = carry;
        }

        unchecked {
            size = index + 1;
        }
        root = rootAt(size);
        emit BundleAppended(index, payload, root, size);
    }

    // ------------------------------------------------------------------ reads

    /// @notice The pre-image at one position — the bundle's intent-Merkle root.
    /// @dev Untagged, as the event carries it. A consumer proving inclusion
    /// re-tags through `FinalMmr.hashLeaf`; returning the tagged form here
    /// would invite proving against the wrong one.
    function payloadAt(uint256 index) public view returns (bytes32) {
        if (index >= size) revert FinalMmr.MmrIndexOutOfRange(index, size);
        return _payload[index];
    }

    /// @notice The pre-images at `[from, to)`, in order.
    /// @dev Batched because the proposer needs every unanchored leaf between
    /// the gateway's head and this log's, and one `eth_call` per leaf turns a
    /// pass into `size` round trips on a chain that mints a block every 100 ms.
    function payloadsBetween(uint256 from, uint256 to) external view returns (bytes32[] memory out) {
        if (to > size) revert SizeAhead(to, size);
        if (from > to) revert FinalMmr.MmrIndexOutOfRange(from, to);
        out = new bytes32[](to - from);
        for (uint256 i = from; i < to; ) {
            out[i - from] = _payload[i];
            unchecked { ++i; }
        }
    }

    /// @notice Where a bundle root first landed.
    /// @return index Its position. @return found False when it never landed,
    /// which is a state — a proposed bundle that has not been admitted yet —
    /// rather than an error.
    function indexOf(bytes32 payload) external view returns (uint256 index, bool found) {
        uint256 plusOne = _firstIndexPlusOne[payload];
        if (plusOne == 0) return (0, false);
        return (plusOne - 1, true);
    }

    /// @notice The peak bag backing the current root.
    /// @dev Derived from `size` rather than stored: a bag kept alongside the
    /// nodes would be a second representation of one fact, and the interesting
    /// property — `length == popcount(size)` — is then a consequence rather
    /// than something to maintain.
    function peaks() external view returns (bytes32[] memory) {
        return peaksAt(size);
    }

    /// @notice The peak bag as of `atSize` leaves.
    function peaksAt(uint256 atSize) public view returns (bytes32[] memory bag) {
        if (atSize > size) revert SizeAhead(atSize, size);
        bag = new bytes32[](FinalMmr.popcount(atSize));
        uint256 n = 0;
        uint256 offset = 0;
        uint256 level = FinalMmr.bitLength(atSize);
        // MSB first, so the peaks come out largest-first — the order
        // `_rootFromPeaks` and every reader expect.
        while (level > 0) {
            unchecked { --level; }
            if ((atSize >> level) & 1 == 1) {
                bag[n] = _node[level][offset >> level];
                unchecked {
                    ++n;
                    offset += (1 << level);
                }
            }
        }
    }

    /// @notice The head the execution chains' `advanceMmrRoot` should be called
    /// with, read in one call so the pair can never be torn across two.
    function head() external view returns (bytes32 root_, uint256 size_) {
        return (root, size);
    }

    /**
     * @notice The root over the FIRST `atSize` leaves.
     * @dev Exact for every `atSize <= size`, not only the current one, because
     * an MMR node is written once and never revised. That is what lets a
     * consumer prove against the head the gateway actually anchored, which
     * lags this log by design — a proof built against a larger size is
     * well-formed and verifies nowhere.
     */
    function rootAt(uint256 atSize) public view returns (bytes32) {
        if (atSize > size) revert SizeAhead(atSize, size);
        if (atSize == 0) return bytes32(0);
        return _rangeRoot(0, atSize);
    }

    /**
     * @notice The audit path proving leaf `leafIndex` under `rootAt(atSize)`.
     * @param leafIndex 0-based position, `< atSize`.
     * @param atSize The head to prove against — `gateway.mmrSize()`, not this
     *        log's size, whenever the two differ.
     *
     * @dev The walk is the Trillian formulation: rise level by level and record
     * the sibling, which is a stored perfect subtree unless it is the ragged
     * last node of its level, in which case it is the bag of the peaks that
     * remain. Both cases resolve to nodes this contract wrote, so there is
     * nothing to recompute from leaves and nothing for a caller to fold.
     *
     * The result feeds `FinalMmr.computeRoot` verbatim on the gateway. Its
     * length is derived here from the same `(index, size)` decomposition that
     * verifier uses, and the walk's own count is checked against it — a
     * disagreement is a proof rejected on another chain naming neither side,
     * so it is caught where both derivations are visible.
     */
    function proofAt(uint256 leafIndex, uint256 atSize) public view returns (bytes32[] memory proof) {
        if (atSize > size) revert SizeAhead(atSize, size);
        if (atSize == 0) revert FinalMmr.MmrEmptyLog();
        if (leafIndex >= atSize) revert FinalMmr.MmrIndexOutOfRange(leafIndex, atSize);

        uint256 inner = FinalMmr.bitLength(leafIndex ^ (atSize - 1));
        uint256 border = FinalMmr.popcount(leafIndex >> inner);
        proof = new bytes32[](inner + border);

        uint256 idx = leafIndex;
        uint256 last = atSize - 1; // index of the last node at the current level
        uint256 level = 0;
        uint256 n = 0;
        while (last != 0) {
            uint256 sibling = idx ^ 1;
            if (sibling < last) {
                // A complete perfect subtree, stored when it completed.
                proof[n] = _node[level][sibling];
                unchecked { ++n; }
            } else if (sibling == last) {
                // The ragged right edge: this level's last node may cover fewer
                // than `2 ** level` leaves, so it is the bag of the peaks in
                // `[sibling << level, atSize)` rather than a node of its own.
                proof[n] = _rangeRoot(sibling << level, atSize);
                unchecked { ++n; }
            }
            // `sibling > last` — no sibling at this level. Rise without
            // recording, which is what makes an imperfect tree's path shorter
            // than its depth.
            unchecked {
                idx >>= 1;
                last >>= 1;
                ++level;
            }
        }
        if (n != proof.length) revert ProofShapeMismatch(proof.length, n);
    }

    /**
     * @notice Everything needed to prove one bundle, in one call.
     * @dev One call rather than three, because payload, proof and root are only
     * meaningful together: fetched separately, an append landing between two of
     * them yields a proof against a root the caller did not read, and the
     * dispatch reverts on another chain with three individually-correct values.
     */
    function bundleAt(uint256 leafIndex, uint256 atSize)
        external
        view
        returns (bytes32 payload, bytes32[] memory proof, bytes32 root_)
    {
        proof = proofAt(leafIndex, atSize);
        payload = payloadAt(leafIndex);
        root_ = rootAt(atSize);
    }

    /**
     * @dev The Merkle-Tree-Hash of leaves `[lo, hi)`, where `lo` is aligned to
     * the largest perfect subtree the range can hold.
     *
     * Every part is a COMPLETE perfect subtree and therefore a stored node,
     * which is the property that makes a historical proof derivable at all.
     * The parts come out largest-first and fold right-to-left, which is the
     * same peak bag `FinalMmr.computeRoot` walks as a proof's border — and is
     * what makes a proof against this root verify on the gateway.
     */
    function _rangeRoot(uint256 lo, uint256 hi) private view returns (bytes32) {
        uint256 remaining = hi - lo;
        bytes32[] memory bag = new bytes32[](FinalMmr.popcount(remaining));
        uint256 n = 0;
        uint256 offset = lo;
        uint256 level = FinalMmr.bitLength(remaining);
        while (level > 0) {
            unchecked { --level; }
            if ((remaining >> level) & 1 == 1) {
                bag[n] = _node[level][offset >> level];
                unchecked {
                    ++n;
                    offset += (1 << level);
                }
            }
        }
        bytes32 acc = bag[n - 1];
        for (uint256 i = n - 1; i > 0; ) {
            unchecked { --i; }
            acc = _hashNode(bag[i], acc);
        }
        return acc;
    }

    /// @dev `FinalMmr` keeps its node hash private (proof verification is its
    /// only caller there), so the one construction that must not fork is
    /// restated once, here, against its published spec:
    /// `keccak256(0x01 ‖ domain ‖ left ‖ right)`. `test_LayoutMatchesFinalMmr`
    /// pins it against a proof the library itself verifies, so a change to
    /// either side fails rather than producing a log the gateway rejects.
    function _hashNode(bytes32 left, bytes32 right) private pure returns (bytes32) {
        return keccak256(abi.encodePacked(uint8(0x01), DOMAIN, left, right));
    }
}

contracts/finalchain/FinalCertificate.sol

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2024-2026 Final DeFi
// Licensed under the Business Source License 1.1 (the "License")
//
// Change Date: 2029-01-01
// Change License: GPL-2.0-or-later
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;

import {FinalChainPrecompiles} from "./FinalChainPrecompiles.sol";
import {FinalChainTime} from "./FinalChainTime.sol";

/**
 * @title FinalCertificate
 * @notice Reads a Final Certificate (`.fcert`, schema v3) on chain.
 *
 * @dev Final Chain only — it needs the SHA3-256 precompile, because the schema
 * hashes with FIPS-202 SHA3 and the EVM has `keccak256`, which is a different
 * function.
 *
 * ## Why the chain parses this at all
 *
 * `FinalIdentityRegistry.registerWithCertificate` used to take the TBS bytes
 * AND the public keys as separate arguments. It derived `certHash` from the
 * bytes, which sounds like verification and is not: nothing compared the keys
 * to the certificate, so a registrar could bind any certificate to any keypair.
 * The registry would then hold a key the certificate does not contain, and every
 * signature that key produced would verify against a certificate that never
 * authorised it.
 *
 * So the keys are read OUT of the certificate. There is one input and no way for
 * two arguments to disagree.
 *
 * ## The SubjectKeyId check
 *
 * The schema defines `SubjectKeyId` as SHA3-256 of the `PublicKeyBlock`. Having
 * parsed the block, this recomputes that digest and compares. The field is
 * inside the TBS, so it is covered by the CA's signatures — which makes the
 * check a statement about what the CA attested, not merely about internal
 * consistency of bytes the caller supplied.
 *
 * ## What this does NOT do
 *
 * It does not verify the CA's signatures over the TBS, and it does not walk the
 * chain to the root. Both are possible here — the precompiles verify ML-DSA-87
 * and SLH-DSA-SHAKE-256s — and both are deliberately out of scope for the
 * registry's bootstrap path, where the registrar is the party that issued the
 * certificate in the first place. `verifyIssuerSignatures` below is provided for
 * callers that need it, and the identity registry uses it once a CA is itself
 * registered.
 */
library FinalCertificate {
    /// `"PQCF"`.
    uint32 internal constant MAGIC = 0x50514346;
    /// The current wire generation — v5's `Version = 2` (chain-attested
    /// issuance; ruled 2026-09-01). The v4 wire (`Version = 1`) stays
    /// PARSEABLE so pre-cutover artifacts still read; encoders write 2.
    /// fails to parse rather than being reinterpreted: `pqKeysHash` and every
    /// wallet address derive from this exact layout.
    uint32 internal constant VERSION = 2;
    /// The v4 generation, accepted on parse for pre-cutover artifacts.
    uint32 internal constant VERSION_V4 = 1;

    /// @notice The 0x0102 Institution identity extension (issuer profile).
    uint16 internal constant EXT_INSTITUTION = 0x0102;

    /// Algorithm ids ARE the FIPS numbers, in one space for signatures and KEMs
    /// — the same ids the quorum wire format and the backend registry use, and
    /// the numbers the precompile addresses end in.
    /// ML-KEM-1024 (FIPS 203), the lattice half of the encapsulation pair.
    uint16 internal constant ALG_ML_KEM_1024 = 0x0003;
    /// ML-DSA-87 (FIPS 204). Transaction class.
    uint16 internal constant ALG_ML_DSA_87 = 0x0004;
    /// SLH-DSA-SHAKE-256s (FIPS 205). Access class, and the seal.
    uint16 internal constant ALG_SLH_DSA_SHAKE_256S = 0x0005;
    /// FN-DSA (FIPS 206). Reserved: no implementation, never accepted.
    uint16 internal constant ALG_FN_DSA = 0x0006;
    /// HQC-5 (FIPS 207), the code-based half of the encapsulation pair.
    uint16 internal constant ALG_HQC_5 = 0x0007;

    /// Certificate signing. Says which key to verify WITH; it grants nothing —
    /// that comes from `Depth` and `MaxDelegationDepth`.
    uint16 internal constant PURPOSE_CERT_SIGNING = 0x0004;

    /// The wallet's four slots, in two stages of two.
    ///
    /// A certificate carries ONE stage, never all four. The stage is what gets
    /// issued, rotated and revoked as a unit, and a holder presenting a live
    /// certificate presents both of that stage's keys or neither — splitting
    /// them per slot would let half a stage be presented as if it were whole.
    ///
    /// This applies to services exactly as it applies to a user's wallet.
    /// A co-signer is a Final Wallet: same four slots, same split, same
    /// algorithms. There is no second kind of identity in this system.
    uint16 internal constant PURPOSE_ACTIVE_TX = 0x0010;
    uint16 internal constant PURPOSE_ACTIVE_ACCESS = 0x0011;
    uint16 internal constant PURPOSE_RECOVERY_TX = 0x0012;
    uint16 internal constant PURPOSE_RECOVERY_ACCESS = 0x0013;
    /// @dev v4's encapsulation purposes. Parsed, and each stage's pair is
    ///      resolved alongside its signing pair — `FinalIdentityRegistry` then
    ///      stores them so a sender can encapsulate to a registered party
    ///      without a second lookup somewhere less authoritative.
    ///
    ///      They were declared and skipped for one release, which is how the
    ///      registry's four encapsulation-key mappings ended up read in three
    ///      places and written in none: `kemCommitments` hashed the empty
    ///      string for every account and `kemKeysOf` returned nothing.
    uint16 internal constant PURPOSE_ACTIVE_KEM = 0x0014;
    uint16 internal constant PURPOSE_RECOVERY_KEM = 0x0015;
    /// @dev The seal: a second SLH-DSA-SHAKE-256s key, distinct from the access
    ///      key, that co-signs execution-class quorum decisions. Carried by
    ///      SERVICE certificates only — a user's wallet never seals — and
    ///      optional in the schema, so a certificate without it parses
    ///      unchanged. Outside `keysHash`: a seal is operational, rotated by
    ///      issuing a new live certificate, and it must not move a wallet
    ///      address it plays no part in.
    uint16 internal constant PURPOSE_ACTIVE_SEAL = 0x0016;

    /// @dev A purpose no certificate can carry, so `parse` can be told "this
    ///      stage has no encapsulation slot" without a second boolean. `0xffff`
    ///      is outside the registry and reserved by being used here.
    uint16 internal constant NO_KEM_PURPOSE = 0xffff;

    /// Nanoseconds per second. The schema's validity fields are nanoseconds and
    /// `block.timestamp` is seconds; a comparison across the two units is a bug
    /// waiting for the first certificate anybody actually checks.
    /// @dev The schema stamps validity in NANOseconds and this chain's clock is
    ///      MILLIseconds, so a certificate converts down by 1e6 rather than by
    ///      1e9. It was 1e9 — seconds — which made every `notBefore` look 1000x
    ///      too small against `block.timestamp` and every certificate
    ///      permanently "already valid", including one issued for the future.
    uint64 internal constant NS_PER_MILLISECOND = FinalChainTime.NS_PER_MILLISECOND;

    /// @notice What the chain keeps out of one certificate.
    struct Parsed {
        bytes32 certHash;
        bytes32 serial;
        /// keccak256 of the IssuerDN bytes, for the chain-issuer pin: a
        /// chain-attested certificate carries the ruled constant DN and the
        /// registry compares hashes rather than strings.
        bytes32 issuerDnHash;
        /// The SubjectDN bytes verbatim — the jurisdiction rule reads its
        /// `C=` component at issuer registration.
        bytes subjectDn;
        /// The 0x0102 Institution extension VALUE, when present; empty
        /// otherwise. Issuer registration parses jurisdiction out of it.
        bytes institutionExt;
        /// SHA3-256 of the ISSUER's public key block. Zero-length — and so
        /// `bytes32(0)` here — for exactly one certificate in the hierarchy,
        /// which is what terminates chain validation.
        bytes32 authorityKeyId;
        /// SHA3-256 of this certificate's own public key block. The child's
        /// `authorityKeyId` must equal it, which is what links the two.
        bytes32 subjectKeyId;
        uint8 depth;
        uint8 maxDelegationDepth;
        /// MILLISECONDS, converted from the schema's nanoseconds — this chain's clock.
        uint64 notBefore;
        /// Milliseconds. Zero means never expires, which the schema allows.
        uint64 notAfter;
        /// The stage's transaction-class key. ML-DSA-87 — spending, and every
        /// high-cadence protocol action.
        bytes transactionKey;
        /// The stage's access-class key. SLH-DSA-SHAKE-256s — identity,
        /// rotation, recovery-pair promotion. A different hardness assumption,
        /// so a lattice break leaves the key that governs identity standing.
        bytes accessKey;
        /// The stage's ML-KEM-1024 encapsulation key. Empty on a CA, which has
        /// no encapsulation stage, and on any v4 certificate issued without
        /// one — see `parse` for why that is tolerated rather than refused.
        bytes kemMlKem;
        /// The stage's HQC-5 encapsulation key. Carried under the SAME purpose
        /// as the lattice half and distinguished only by algorithm, which is
        /// why the parser matches on the `(purpose, algorithm)` pair.
        bytes kemHqc;
        /// The service's seal key (`PURPOSE_ACTIVE_SEAL`, SLH-DSA-SHAKE-256s).
        /// Empty on every certificate that does not carry one — a user wallet,
        /// a recovery stage, a CA.
        bytes sealKey;
        /// Where the TBS ends, so a caller holding the whole certificate can
        /// find the `SignatureBlock` without parsing forward again.
        uint256 tbsLength;
    }

    error BadMagic(uint32 got);
    error BadVersion(uint32 got);
    error Truncated(uint256 needed, uint256 got);
    error SubjectKeyIdMismatch(bytes32 derived, bytes32 declared);
    error MissingSlot(uint16 purpose);
    error WrongAlgorithmForSlot(uint16 purpose, uint16 algorithm);
    error DuplicateKey(uint16 purpose, uint16 algorithm);
    error KeysNotSorted();
    error BadKeyLength(uint16 algorithm, uint256 length);
    error InvalidDepth(uint8 depth, uint8 maxDelegationDepth);
    error ValidityInverted(uint64 notBefore, uint64 notAfter);

    /**
     * @notice Parse and self-check a `TBSCertificate`.
     * @param tbs the TBS bytes, verbatim. Not the whole certificate.
     * @param txPurpose the transaction-class purpose this stage should carry.
     * @param accessPurpose the access-class purpose for the same stage.
     *
     * @dev Checking for a CAPABILITY rather than a type is the schema's own
     * rule, and the reason there is no type field to check instead. Passing the
     * LIVE purposes to a recovery certificate finds neither key and reverts —
     * which is what stops a recovery certificate being registered as a live one
     * and handing the recovery pair everyday authority.
     */
    function parse(bytes calldata tbs, uint16 txPurpose, uint16 accessPurpose, uint16 kemPurpose)
        internal
        view
        returns (Parsed memory out)
    {
        _need(tbs, 58);
        if (uint32(bytes4(tbs[0:4])) != MAGIC) revert BadMagic(uint32(bytes4(tbs[0:4])));
        // Both live generations. v4 artifacts predate chain-attested issuance
        // and still parse — supersession is handled at admission (PoP and the
        // chain-issuer pins), not by refusing to read history.
        uint32 wireVersion = uint32(bytes4(tbs[4:8]));
        if (wireVersion != VERSION && wireVersion != VERSION_V4) revert BadVersion(wireVersion);

        out.certHash = FinalChainPrecompiles.sha3_256(tbs);
        out.serial = bytes32(tbs[8:40]);
        out.depth = uint8(tbs[40]);
        out.maxDelegationDepth = uint8(tbs[41]);

        uint64 notBeforeNs = uint64(bytes8(tbs[42:50]));
        uint64 notAfterNs = uint64(bytes8(tbs[50:58]));
        if (out.maxDelegationDepth < out.depth) {
            revert InvalidDepth(out.depth, out.maxDelegationDepth);
        }
        if (notAfterNs != 0 && notAfterNs <= notBeforeNs) {
            revert ValidityInverted(notBeforeNs, notAfterNs);
        }
        out.notBefore = notBeforeNs / NS_PER_MILLISECOND;
        out.notAfter = notAfterNs == 0 ? 0 : notAfterNs / NS_PER_MILLISECOND;

        // Four length-prefixed fields: IssuerDN, SubjectDN, AuthorityKeyId,
        // SubjectKeyId. Every field before them is fixed width, which is the
        // whole reason the schema orders them this way.
        uint256 p = 58;
        uint256 issuerDnLen;
        (p, issuerDnLen) = _skipLengthPrefixed(tbs, p);
        out.issuerDnHash = keccak256(tbs[p - issuerDnLen:p]);
        uint256 subjectDnLen;
        (p, subjectDnLen) = _skipLengthPrefixed(tbs, p);
        out.subjectDn = tbs[p - subjectDnLen:p];
        uint256 akidLen;
        (p, akidLen) = _skipLengthPrefixed(tbs, p);
        out.authorityKeyId = _bytes32At(tbs, p - akidLen, akidLen);
        uint256 skidLen;
        (p, skidLen) = _skipLengthPrefixed(tbs, p);
        uint256 skidStart = p - skidLen;

        _need(tbs, p + 2);
        uint16 keyCount = uint16(bytes2(tbs[p:p + 2]));
        p += 2;
        // AFTER the count word. `SubjectKeyId` is SHA3-256 of the KeyEntry
        // array alone — `encodeTbs` writes `PublicKeyCount` as its own field and
        // `encodePublicKeyBlock` returns only the entries. Hashing the count in
        // produces a digest that is self-consistent and matches no certificate
        // any issuer ever wrote.
        uint256 blockStart = p;

        uint32 previousSort = 0;
        for (uint256 i = 0; i < keyCount; i++) {
            _need(tbs, p + 8);
            uint16 alg = uint16(bytes2(tbs[p:p + 2]));
            uint16 purpose = uint16(bytes2(tbs[p + 2:p + 4]));
            uint32 keyLen = uint32(bytes4(tbs[p + 4:p + 8]));
            p += 8;
            _need(tbs, p + keyLen);

            // Ascending by (purpose, algorithm), duplicates invalid. The schema
            // requires the order so `certHash` is reproducible across
            // implementations; enforcing it here also means a second entry for
            // one slot cannot quietly shadow the first.
            uint32 sortKey = (uint32(purpose) << 16) | uint32(alg);
            if (i > 0) {
                if (sortKey == previousSort) revert DuplicateKey(purpose, alg);
                if (sortKey < previousSort) revert KeysNotSorted();
            }
            previousSort = sortKey;

            // The algorithm is pinned per CLASS, not merely recorded. A
            // transaction slot carrying an access-class key would verify
            // cryptographically and mean something entirely different — an
            // identity key must never authorize a transaction, or splitting the
            // classes buys nothing.
            // Matched on the PAIR, not on the purpose alone. A CA carries two
            // keys under one purpose (`0x0004`) distinguished only by
            // algorithm, so matching on purpose first would find the first of
            // them twice and the second never.
            if (purpose == txPurpose && alg == ALG_ML_DSA_87) {
                if (keyLen != FinalChainPrecompiles.ML_DSA_87_PUBLIC_KEY_LEN) {
                    revert BadKeyLength(alg, keyLen);
                }
                out.transactionKey = tbs[p:p + keyLen];
            } else if (purpose == accessPurpose && alg == ALG_SLH_DSA_SHAKE_256S) {
                if (keyLen != FinalChainPrecompiles.SLH_DSA_SHAKE_256S_PUBLIC_KEY_LEN) {
                    revert BadKeyLength(alg, keyLen);
                }
                out.accessKey = tbs[p:p + keyLen];
            } else if (purpose == kemPurpose && alg == ALG_ML_KEM_1024) {
                out.kemMlKem = tbs[p:p + keyLen];
            } else if (purpose == kemPurpose && alg == ALG_HQC_5) {
                out.kemHqc = tbs[p:p + keyLen];
            } else if (purpose == PURPOSE_ACTIVE_SEAL && alg == ALG_SLH_DSA_SHAKE_256S) {
                if (keyLen != FinalChainPrecompiles.SLH_DSA_SHAKE_256S_PUBLIC_KEY_LEN) {
                    revert BadKeyLength(alg, keyLen);
                }
                out.sealKey = tbs[p:p + keyLen];
            } else if (purpose == PURPOSE_ACTIVE_SEAL) {
                // The seal is hash-based by definition — it exists to stand on
                // the OTHER assumption from the transaction key it co-signs
                // with. A lattice seal would be two signatures on one bet.
                revert WrongAlgorithmForSlot(purpose, alg);
            } else if (purpose == txPurpose || purpose == accessPurpose) {
                // A slot the caller asked for, carrying the wrong scheme. It
                // would verify cryptographically and mean something else
                // entirely — an identity key must never authorize a
                // transaction, or splitting the classes buys nothing.
                revert WrongAlgorithmForSlot(purpose, alg);
            } else if (purpose == kemPurpose) {
                // Same rule for the encapsulation slot. A third KEM appearing
                // under this purpose is a hybrid whose second family nobody
                // agreed on, and admitting it silently is how a pair becomes a
                // trio that one reader honours and another ignores.
                revert WrongAlgorithmForSlot(purpose, alg);
            }

            // NO length check on the KEM keys here, and that is deliberate.
            // The signing slots are checked against a constant because the
            // parser's own callers depend on the length; an encapsulation key
            // is checked by `0x0203` / `0x0207` at the moment it is REGISTERED,
            // where the answer is a well-formedness verdict rather than a
            // parse failure. Two checks of the same thing in two shapes is how
            // one of them ends up weaker and nobody notices which.
            p += keyLen;
        }

        // `SubjectKeyId` is SHA3-256 of the KeyEntry array, count word
        // EXCLUDED — `blockStart` is taken after the count is consumed, for the
        // reason given where it is set. Recomputing it is what turns "these
        // bytes decode" into "the CA signed these exact keys"; the field is
        // inside the TBS, so it is covered by the signatures.
        out.subjectKeyId = FinalChainPrecompiles.sha3_256(tbs[blockStart:p]);
        bytes32 declared = _bytes32At(tbs, skidStart, skidLen);
        if (out.subjectKeyId != declared) revert SubjectKeyIdMismatch(out.subjectKeyId, declared);

        // Both or neither. A stage is issued as a unit, so a certificate
        // carrying one of its two keys is not a partial certificate — it is a
        // certificate for a stage that does not exist.
        if (out.transactionKey.length == 0) revert MissingSlot(txPurpose);
        if (out.accessKey.length == 0) revert MissingSlot(accessPurpose);

        // The encapsulation pair is both-or-neither for the same reason, and
        // the reason is louder here: a hybrid quietly reduced to one family is
        // identical on the wire, so a certificate carrying only the lattice
        // half would seal successfully and silently drop the code-based hedge.
        // Neither is the CA case and the pre-v4 case, both legitimate.
        if ((out.kemMlKem.length == 0) != (out.kemHqc.length == 0)) {
            revert MissingSlot(kemPurpose);
        }

        _need(tbs, p + 2);
        uint16 extCount = uint16(bytes2(tbs[p:p + 2]));
        p += 2;
        for (uint256 i = 0; i < extCount; i++) {
            _need(tbs, p + 7);
            uint16 extType = uint16(bytes2(tbs[p:p + 2]));
            uint32 valueLen = uint32(bytes4(tbs[p + 3:p + 7]));
            p += 7;
            _need(tbs, p + valueLen);
            // The Institution extension's VALUE, kept for the issuer
            // profile's jurisdiction rule. Everything else is skipped as
            // before — extensions are structural to certHash, semantic to
            // whichever consumer knows them.
            if (extType == EXT_INSTITUTION) out.institutionExt = tbs[p:p + valueLen];
            p += valueLen;
        }
        out.tbsLength = p;
    }

    /// @notice Parse a LIVE-stage certificate: `activeTransaction` + `activeAccess`.
    /// @dev `external`, like the other three entry points below: the registry
    /// sits against the EIP-170 ceiling and the TBS parser is its single
    /// largest inlined dependency, so the four doors it actually calls are
    /// DEPLOY-LINKED — the library is one more contract in the plane's fixed
    /// nonce-0 deploy order (doctrine §2 of `arch/final-chain-regenesis.md`),
    /// its address baked immutably into the registry's bytecode. A linked
    /// library is code, not a key: nothing can repoint it after deployment.
    function parseLive(bytes calldata tbs) external view returns (Parsed memory) {
        return parse(tbs, PURPOSE_ACTIVE_TX, PURPOSE_ACTIVE_ACCESS, PURPOSE_ACTIVE_KEM);
    }

    /// @notice Parse a RECOVERY-stage certificate.
    /// @dev The recovery pair authorizes rotating the wallet's own credentials
    /// and NOTHING else — acting as a guardian, an ordinary action for that
    /// account, uses the live access key. Keeping the two stages in separate
    /// certificates is what makes that boundary something a verifier can see.
    function parseRecovery(bytes calldata tbs) external view returns (Parsed memory) {
        return parse(tbs, PURPOSE_RECOVERY_TX, PURPOSE_RECOVERY_ACCESS, PURPOSE_RECOVERY_KEM);
    }

    /// @notice Parse a CA certificate, whose two keys are both cert-signing.
    /// @dev No encapsulation purpose: a CA signs and is never sealed to, so
    /// `PURPOSE_ACTIVE_KEM` is passed as a value the loop can never match. A
    /// CA certificate carrying encapsulation keys would parse them into slots
    /// `_write` then discards, which is a shape worth refusing to have.
    function parseCa(bytes calldata tbs) external view returns (Parsed memory) {
        return parse(tbs, PURPOSE_CERT_SIGNING, PURPOSE_CERT_SIGNING, NO_KEM_PURPOSE);
    }

    /**
     * @notice Verify a CA's dual signature over `tbs`.
     * @dev Both must verify, not either. Two signatures under two different
     * hardness assumptions is the entire reason the schema carries two, and
     * accepting one would collapse that to whichever family breaks first.
     */
    function verifyIssuerSignatures(
        bytes memory tbs,
        bytes memory issuerMlDsaKey,
        bytes memory issuerSlhDsaKey,
        bytes memory mlDsaSignature,
        bytes memory slhDsaSignature
    ) external view returns (bool) {
        return FinalChainPrecompiles.verifyMlDsa87(issuerMlDsaKey, tbs, mlDsaSignature)
            && FinalChainPrecompiles.verifySlhDsa(issuerSlhDsaKey, tbs, slhDsaSignature);
    }

    function _need(bytes calldata tbs, uint256 upto) private pure {
        if (tbs.length < upto) revert Truncated(upto, tbs.length);
    }

    function _skipLengthPrefixed(bytes calldata tbs, uint256 p)
        private
        pure
        returns (uint256 next, uint256 length)
    {
        _need(tbs, p + 4);
        length = uint32(bytes4(tbs[p:p + 4]));
        next = p + 4 + length;
        _need(tbs, next);
    }

    function _bytes32At(bytes calldata tbs, uint256 start, uint256 length)
        private
        pure
        returns (bytes32)
    {
        // A SubjectKeyId that is not 32 bytes is not a SHA3-256 digest, so it
        // cannot match and the comparison will fail — which is the correct
        // outcome and needs no separate error.
        if (length != 32) return bytes32(0);
        return bytes32(tbs[start:start + 32]);
    }
}

contracts/finalchain/FinalChainPrecompiles.sol

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2024-2026 Final DeFi
// Licensed under the Business Source License 1.1 (the "License")
//
// Change Date: 2029-01-01
// Change License: GPL-2.0-or-later
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;

/**
 * @title FinalChainPrecompiles
 * @notice The three primitives Final Chain adds to the EVM, and the only
 *         supported way to reach them.
 *
 * @dev **These exist ONLY on Final Chain (chain id 48359).** They are provided
 * by `final-reth`, the node binary in `FinalBackend/vendor/reth/final/`, and
 * nothing at these addresses on Ethereum, Optimism or any other chain will
 * answer. A contract that calls them must be one that only ever runs here;
 * `assertAvailable` below is the cheap way to fail loudly rather than treat an
 * empty return as a verified signature.
 *
 * The addresses are the FIPS numbers, which is the whole allocation rule —
 * there is no local registry to consult and no way for two implementations to
 * disagree about where a primitive lives:
 *
 * | address | primitive | FIPS |
 * |---|---|---|
 * | `0x…0202` | SHA3-256 | 202 |
 * | `0x…0203` | ML-KEM-1024 key validation | 203 |
 * | `0x…0204` | ML-DSA-87 verify | 204 |
 * | `0x…0205` | SLH-DSA-SHAKE-256s verify | 205 |
 * | `0x…0207` | HQC-5 key validation | 207 |
 *
 * The two KEM addresses VALIDATE keys and do nothing else, for one reason:
 * encapsulation is a SENDER operation and decapsulation needs the secret key,
 * so neither belongs on a chain at all. Checking that a registered public key
 * is well-formed is hardening rather than a dependency, and nothing in this
 * system waits on it.
 *
 * HQC's number is 207. It had none when the KEM pair was chosen, which was the
 * one thing separating it from ML-KEM here — a primitive with no standard
 * number has no address under this rule, and inventing one would have been a
 * local convention masquerading as the global one.
 *
 * **No AEAD precompile, at any number.** The chain must never be able to
 * decrypt an intent, and checking a revealed body against its commitment is a
 * hash compare that `0x0202` already serves.
 *
 * ## Why this library refuses to take a public key from its caller
 *
 * It does take one — the primitives are pure functions and cannot do otherwise.
 * The rule lives one level up, in `FinalPqQuorum`: a key passed as an argument
 * proves nothing, because anyone holding a keypair can produce a valid
 * signature under it. Only a key read from `FinalIdentityRegistry` is evidence
 * about WHO signed. Every call site here must be able to answer "where did this
 * key come from" with "storage", never "calldata".
 *
 * ## `success` is not the answer
 *
 * A `staticcall` to a verifier returns two things and both matter. `success`
 * false means the call was malformed — usually a length bug in the caller — and
 * `success` true with a zero word means the signature did not verify. The
 * helpers below collapse both to `false` for the caller's convenience, which is
 * safe in that direction and only in that direction: treating a failed call as
 * a valid signature would be the whole security of the system.
 */
library FinalChainPrecompiles {
    /// @notice SHA3-256 (FIPS 202). NOT `keccak256`, which is the
    /// pre-standardisation padding and produces a different digest.
    address internal constant SHA3_256 = address(0x0202);
    /// @notice ML-DSA-87 verification (FIPS 204). Transaction-class keys.
    address internal constant ML_DSA_87 = address(0x0204);
    /// @notice SLH-DSA-SHAKE-256s verification (FIPS 205). Access-class keys.
    address internal constant SLH_DSA_SHAKE_256S = address(0x0205);

    /// @notice ML-KEM-1024 encapsulation-key validation (FIPS 203).
    /// @dev VALIDATES; it does not encapsulate. Runs FIPS 203 §7.2's own
    /// encapsulation-key check — the type check and the modulus check — and
    /// nothing else. Encapsulation is a sender operation and decapsulation
    /// needs the secret key, so neither belongs on a chain.
    address internal constant ML_KEM_1024 = address(0x0203);

    /// @notice HQC-5 public-key validation (FIPS 207).
    /// @dev Structural only: the length, and the three padding bits the
    /// encoding leaves beyond `n = 57637`. HQC has no cheap key-validity
    /// predicate and this does not pretend to one.
    address internal constant HQC_5 = address(0x0207);

    /// @notice ML-DSA-87 public key length. Round-3 Dilithium5 shares it.
    uint256 internal constant ML_DSA_87_PUBLIC_KEY_LEN = 2592;
    /// @notice ML-DSA-87 signature length. Round-3 Dilithium5 is 4595.
    uint256 internal constant ML_DSA_87_SIGNATURE_LEN = 4627;
    /// @notice SLH-DSA-SHAKE-256s public key length (`PK.seed ‖ PK.root`).
    uint256 internal constant SLH_DSA_SHAKE_256S_PUBLIC_KEY_LEN = 64;
    /// @notice SLH-DSA-SHAKE-256s signature length. The `f` set is 49,856.
    uint256 internal constant SLH_DSA_SHAKE_256S_SIGNATURE_LEN = 29792;

    /// @notice Thrown when a precompile is absent, i.e. this is not Final Chain
    /// or the node is stock reth rather than `final-reth`.
    error PrecompileUnavailable(address precompile);

    /**
     * @notice Reverts unless all five precompiles answer.
     * @dev Call this from a constructor. A contract whose security rests on PQ
     * verification must not deploy onto a chain that cannot perform it — the
     * failure mode otherwise is a quorum that reaches threshold with zero valid
     * signatures, discovered at the worst possible moment.
     *
     * The probe is SHA3-256 of the empty string, whose value is a published
     * FIPS 202 constant. It cannot be produced by an address with no code
     * (which returns empty) nor by `keccak256` (which gives a different digest
     * for the same input), so it distinguishes "the right precompile" from both
     * "nothing here" and "the wrong hash function".
     */
    function assertAvailable() internal view {
        bytes32 expected = 0xa7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a;
        (bool ok, bytes memory out) = SHA3_256.staticcall("");
        if (!ok || out.length != 32 || bytes32(out) != expected) {
            revert PrecompileUnavailable(SHA3_256);
        }
        // The two signature verifiers are probed by shape rather than by a
        // known-answer vector: a KAT here would put a 29,792-byte signature in
        // this contract's bytecode. A deliberately short input is a
        // *precompile error* by contract, so a FAILED call is the pass and a
        // silent success would mean something else is answering at the address.
        _probeRejectsShortInput(ML_DSA_87);
        _probeRejectsShortInput(SLH_DSA_SHAKE_256S);
        // The two KEM validators are probed the other way round, because they
        // are total by contract: a wrong length is a malformed KEY, which is
        // the question being asked, so they ANSWER rather than error. A
        // one-byte input must therefore come back as a well-formed `false`, and
        // a failed call means nothing is there.
        _probeAnswersFalse(ML_KEM_1024);
        _probeAnswersFalse(HQC_5);
    }

    /**
     * @dev A short input must make the precompile ERROR. The gas budget is the
     * whole subtlety.
     *
     * A reverting CONTRACT refunds the gas it did not use. A precompile that
     * returns an error consumes **everything forwarded to it** — and Solidity
     * forwards 63/64 of what is left by default. Two such probes in a
     * constructor therefore burn all but 1/4096 of the deployment's gas, and
     * the deploy fails with no revert data at all.
     *
     * That is not hypothetical: it is what happened the first time this ran
     * against a real `final-reth`, and no Foundry test could have caught it.
     * A mocked precompile is a contract, and a contract's `require` hands the
     * gas back.
     *
     * 5,000 is generous for a call that fails on a length check before any
     * cryptography runs, and small enough that both probes together are noise
     * against a deployment.
     */
    function _probeRejectsShortInput(address precompile) private view {
        bool ok;
        assembly ("memory-safe") {
            let ptr := mload(0x40)
            mstore8(ptr, 0x00)
            ok := staticcall(5000, precompile, ptr, 0x01, 0x00, 0x00)
        }
        if (ok) revert PrecompileUnavailable(precompile);
    }

    /**
     * @dev A one-byte input must come back as a well-formed zero word.
     *
     * The inverse of `_probeRejectsShortInput`, and the inversion is the point:
     * these two precompiles are TOTAL. Every byte string has an answer to "is
     * this a well-formed key", and for one byte the answer is no. A precompile
     * that errored here would be one that treats a malformed key as a caller
     * bug, which is the opposite of what a registry wants.
     *
     * Gas is bounded for the same reason as the other probe — an erroring
     * precompile consumes everything forwarded — even though the pass case
     * returns normally and refunds.
     */
    function _probeAnswersFalse(address precompile) private view {
        bool ok;
        bytes32 answer;
        assembly ("memory-safe") {
            let ptr := mload(0x40)
            mstore8(ptr, 0x00)
            ok := staticcall(5000, precompile, ptr, 0x01, ptr, 0x20)
            answer := mload(ptr)
        }
        if (!ok || answer != bytes32(0)) revert PrecompileUnavailable(precompile);
    }

    /**
     * @notice Is `encapsulationKey` a well-formed ML-KEM-1024 key?
     *
     * @dev The check a registry owes a sender. A malformed encapsulation key
     * stored on chain is an account whose intents cannot be sealed, and the
     * discovery happens at the first attempt to seal one — on the hybrid path,
     * as a pair silently reduced to one family, which is the failure with no
     * error attached.
     *
     * False rather than reverting on any shape, including the wrong length,
     * because the caller is asking a question and every input has an answer.
     */
    function isWellFormedMlKem1024(bytes memory encapsulationKey) internal view returns (bool) {
        return _validatesKey(ML_KEM_1024, encapsulationKey);
    }

    /// @notice Is `publicKey` a well-formed HQC-5 key?
    /// @dev Structural, and honestly partial — see the precompile. It catches a
    /// truncated key, a key from the wrong parameter set, and a tail carrying
    /// smuggled bytes, which are the three ways this goes wrong in practice.
    function isWellFormedHqc5(bytes memory publicKey) internal view returns (bool) {
        return _validatesKey(HQC_5, publicKey);
    }

    /// @dev A failed CALL is not a false answer. It means nothing is at the
    /// address — this is not Final Chain, or the node is stock reth — and
    /// reading it as "the key is malformed" would silently disable the check on
    /// exactly the deployment where it cannot run.
    function _validatesKey(address precompile, bytes memory key) private view returns (bool) {
        (bool ok, bytes memory out) = precompile.staticcall(key);
        if (!ok || out.length != 32) revert PrecompileUnavailable(precompile);
        return bytes32(out) != bytes32(0);
    }

    /// @notice FIPS 202 SHA3-256 over `data`.
    /// @dev The certificate schema hashes `TBSCertificate`, `SubjectKeyId` and
    /// `AuthorityKeyId` with this, so it is the only function that can check a
    /// `certHash` against the bytes it claims to summarise.
    function sha3_256(bytes memory data) internal view returns (bytes32 digest) {
        (bool ok, bytes memory out) = SHA3_256.staticcall(data);
        if (!ok || out.length != 32) revert PrecompileUnavailable(SHA3_256);
        digest = bytes32(out);
    }

    /// @notice Verify an ML-DSA-87 signature. False on any failure, including
    /// a malformed call.
    function verifyMlDsa87(bytes memory publicKey, bytes memory message, bytes memory signature)
        internal
        view
        returns (bool)
    {
        if (
            publicKey.length != ML_DSA_87_PUBLIC_KEY_LEN
                || signature.length != ML_DSA_87_SIGNATURE_LEN
        ) return false;
        return _verify(ML_DSA_87, publicKey, signature, message);
    }

    /// @notice Verify an SLH-DSA-SHAKE-256s signature. False on any failure.
    function verifySlhDsa(bytes memory publicKey, bytes memory message, bytes memory signature)
        internal
        view
        returns (bool)
    {
        if (
            publicKey.length != SLH_DSA_SHAKE_256S_PUBLIC_KEY_LEN
                || signature.length != SLH_DSA_SHAKE_256S_SIGNATURE_LEN
        ) return false;
        return _verify(SLH_DSA_SHAKE_256S, publicKey, signature, message);
    }

    /// @dev `publicKey ‖ signature ‖ message`, in that order. Both fixed-length
    /// fields come first so the message is unambiguously the remainder — the
    /// same reason the precompile takes no length prefix.
    function _verify(
        address precompile,
        bytes memory publicKey,
        bytes memory signature,
        bytes memory message
    ) private view returns (bool) {
        (bool ok, bytes memory out) =
            precompile.staticcall(abi.encodePacked(publicKey, signature, message));
        return ok && out.length == 32 && bytes32(out) != bytes32(0);
    }
}

contracts/finalchain/FinalChainTime.sol

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2024-2026 Final DeFi
pragma solidity ^0.8.20;

/**
 * @title FinalChainTime
 * @notice **On Final Chain, `block.timestamp` is MILLISECONDS.**
 *
 * @dev Every other EVM chain stamps seconds. This one cannot: it mints a block
 * every 100 ms and Ethereum requires block timestamps to strictly increase, so
 * a second-denominated clock would run out of distinct values ten times over
 * per second. Milliseconds is the deliberate choice, and it is a property of
 * the CHAIN — `final-reth` — not of any contract here.
 *
 * Every duration on this chain is therefore in milliseconds, and this library
 * exists so that is stated in one place instead of assumed in fifteen.
 *
 * ## How this was found, which is the reason for the naming rules below
 *
 * It was not found by the test suite. Foundry's `block.timestamp` is seconds,
 * so all 1249 tests agreed with the contracts and every one of them was wrong
 * about the chain they deploy to. It was found the first time anything
 * exercised a deadline against the real chain — a posted intent, which reverted
 * `DeadlinePassed` against a header whose deadline had been computed from wall
 * time.
 *
 * What was actually broken was worse than a posting. `rotationInitiatedAt` is
 * written from `block.timestamp` and compared against `rotationInitiatedAt +
 * delaySeconds`: a millisecond clock plus a second-denominated delay. The
 * 24-hour default recovery delay elapsed in **86 seconds**, and the two-year
 * dormancy threshold in about seventeen hours. That delay is the thing standing
 * between a stolen recovery key and an account.
 *
 * Nothing had noticed because nothing time-dependent had ever run: `walletCount`
 * is 0, `FinalBundleLog.size` is 0, and no intent had been posted.
 *
 * ## The naming rule
 *
 * A field or constant carrying a duration or an instant on this chain ends in
 * `Ms`. Not decoration — the bug was a field named `delaySeconds` that held
 * milliseconds, and a name that lies is how the next reader reintroduces it.
 * `SECONDS` names are gone from `contracts/finalchain/` and must not come back.
 *
 * Solidity's `hours` / `days` suffixes are still the clearest way to write a
 * duration, so they are written as `24 hours * MS_PER_SECOND` rather than as a
 * literal: the intent stays readable and the unit stays explicit.
 */
library FinalChainTime {
    /// @notice Milliseconds per second. The whole conversion, named once.
    uint64 internal constant MS_PER_SECOND = 1_000;

    /// @notice Milliseconds per nanosecond divisor — the certificate schema
    /// stamps validity in NANOseconds, so a certificate converts down to this
    /// chain's clock rather than up.
    uint64 internal constant NS_PER_MILLISECOND = 1_000_000;

    /// @notice This chain's clock, stated as a function so a caller reads the
    /// unit rather than remembering it.
    /// @dev No arithmetic. It exists to make `FinalChainTime.nowMs()` the thing
    /// people write, which is self-describing where `block.timestamp` is not.
    function nowMs() internal view returns (uint64) {
        return uint64(block.timestamp);
    }
}

contracts/finalchain/FinalIdentityRegistry.sol

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2024-2026 Final DeFi
// Licensed under the Business Source License 1.1 (the "License")
//
// Change Date: 2029-01-01
// Change License: GPL-2.0-or-later
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;

import {FinalCertificate} from "./FinalCertificate.sol";
import {FinalChainTime} from "./FinalChainTime.sol";
import {FinalChainPrecompiles} from "./FinalChainPrecompiles.sol";
import {FinalPqQuorum} from "./FinalPqQuorum.sol";

/**
 * @title FinalIdentityRegistry
 * @notice Who every party in the system IS, on chain, with its certificate.
 *
 * @dev Final Chain only. Every service, every co-signer, every certificate
 * authority and every operator has one record here, and that record carries the
 * party's actual public keys — not commitments to them.
 *
 * ## Why the full key and not a hash
 *
 * A commitment would be a quarter of the storage and would be enough to *check*
 * a key someone hands you. It is not enough to VERIFY A SIGNATURE, because
 * verification needs the key itself, and a key that arrives in calldata proves
 * nothing: anyone holding a keypair can produce a valid signature under it. A
 * quorum built on caller-supplied keys is a quorum of one — whoever built the
 * calldata.
 *
 * So the keys live here in full, `FinalPqQuorum` reads them from storage, and
 * "which key is co-signer 3" has exactly one answer. That question previously
 * had three: an environment variable, an on-chain roster, and a Secret Manager
 * entry, with nothing comparing them. Every configuration failure in this
 * program has been those three disagreeing.
 *
 * ## The certificate is the record, not a pointer to one
 *
 * `certHash` is `SHA3-256(TBSCertificate)` — the certificate's own identity per
 * the v3 schema, and the handle revocation is keyed on. The schema says
 * revocation exists "on Final Chain only"; this is that place.
 *
 `registerWithCertificate` takes the TBS bytes and **reads everything out of
 * them**: the digest, the serial, the key identifiers, the depth pair, the
 * validity window and both public keys. It takes no key arguments at all.
 *
 * That is a correction, and the version it replaces is worth naming because it
 * looked right. It took the TBS *and* the keys, derived `certHash` from the
 * TBS, and never compared the two — so a registrar could bind any certificate
 * to any keypair, and the registry would hold a key the certificate does not
 * contain. Every signature that key produced would then verify against a
 * certificate that never authorised it.
 *
 * ## The root is the first record on this chain, not a file somewhere
 *
 * The schema says Final Chain is the only root CA and that "the root is pinned,
 * not distributed" — chain validation terminates at Final Chain **by identity**,
 * never by finding a self-signed certificate in a local store.
 *
 * `registerRoot` is that pin, and it is the only entry point that accepts a
 * certificate without checking an issuer's signature. It takes a depth-0,
 * self-issued certificate from the bootstrap admin, once. Everything after it
 * is `registerWithCertificate`, which **verifies the issuer's ML-DSA and
 * SLH-DSA signatures on chain, through the precompiles**, against the issuer's
 * own registered keys, and checks that the child's `AuthorityKeyId` is the
 * issuer's `SubjectKeyId` and that the issuer's depth admits it.
 *
 * So there is no path by which a key enters this registry unattested. Not
 * "a registrar should only register certified keys" — a registrar *cannot*
 * register anything else.
 *
 * ## Roles are a bitmask
 *
 * One party is legitimately several things — a co-signer that is also a
 * publisher, an operator that is also a guardian. A single enum would force
 * either duplicate records for one key (two sources of truth about one party)
 * or a role hierarchy nobody agrees on. A mask has neither problem, and a
 * quorum asks "does this account carry ROLE_X" rather than "is this account an
 * X", which is the same distinction the certificate schema draws when it says
 * verifiers check for capabilities and never for types.
 *
 * ## Membership is hybrid-gated
 *
 * Who is in this registry, and with which roles, is the root of every quorum on
 * the chain — so it is the one thing no single key may decide. Once bootstrap
 * is sealed, every membership mutation (register, roles, revoke, an LMS key,
 * the registrar threshold itself) and every state-plane configuration change
 * that routes through {requireRegistrarQuorum} takes a `ROLE_REGISTRAR` quorum
 * whose approvals carry BOTH families: the ML-DSA-87 vote and the SLH-DSA
 * seal. A lattice break cannot then rewrite the roster, and neither can a
 * hash-function break; only both at once.
 *
 * The bootstrap window is the exception, and it is the only one: while it is
 * open the bootstrap admin writes alone, because every roster has to be
 * installed by someone before it can install itself.
 *
 * ## The sender is not the account
 *
 * Final Chain transactions are type 0x46, signed by ML-DSA-87, and the node
 * derives `msg.sender` from the key: `keccak256(0x04 ‖ publicKey)[12:]`. That
 * address pays gas and holds no authority. {accountOfSender} binds it to the
 * identity whose `activeTransaction` key it derives from, so a `msg.sender`
 * gate anywhere on this chain asks {senderHasRole} and resolves to the
 * identity — and a key rotation moves the binding rather than the roster.
 */
/// @dev Domain for a stage's encapsulation commitment. Byte-equal to
/// `FinalWalletFactory.DOMAIN_KEM_BUNDLE` and to `DOMAIN_KEM_BUNDLE_PREIMAGE` in
/// the issuer; three derivations of one word, and a mismatch in any of them is a
/// certificate that verifies nowhere.
bytes32 constant DOMAIN_KEM_BUNDLE = keccak256("FINAL_KEM_BUNDLE_v01");

/// @dev Tree 8's leaf domain — byte-equal to
/// `IdentityRootModule.DOMAIN_IDENTITY_LEAF` on every execution chain.
/// Restated rather than imported because the module lives on other chains and
/// there is no import that would make them one value; the cross-contract
/// parity test pins the pair. The `_PQ_` spelling is historical and FROZEN:
/// the premined vanity certificates were mined against this exact constant,
/// and the leaf it derives is the `certHash` inside every wallet's CREATE2
/// derivation.
bytes32 constant DOMAIN_IDENTITY_LEAF = keccak256("FINAL_IDENTITY_LEAF_PQ_v01");

/// @dev D7 (ruled 2026-09-01): ISSUER records project into tree 8 under their
/// own domain — `DOMAIN_ISSUER_LEAF ‖ certHash ‖ version ‖ issuerTreeRoot` —
/// so an issuer is stapleable for offline licence verification while the
/// distinct domain keeps its leaf out of wallet admission (the gateway folds
/// with the wallet domain, so an issuer leaf can never satisfy
/// `verifyIdentityCert`). `issuerTreeRoot` is a RESERVED word, zero until an
/// issuer's own certificate-tree anchor is wired — the only clean path to
/// offline licence revocation, since the fixed-depth insertion-ordered state
/// trees cannot prove non-inclusion.
bytes32 constant DOMAIN_ISSUER_LEAF = keccak256("FINAL_ISSUER_LEAF_v01");

/// @dev Chain-issuer constants (ruled 2026-09-01, amended same day: C-less).
/// The chain is the issuer but holds no keypair, so every chain-attested
/// certificate carries these two NAMED values in its issuer fields — required
/// by the wire format, verifying nothing, covered by `certHash`. The DN is
/// deliberately env-agnostic AND jurisdiction-silent: the issuer is the
/// worldwide network, not a legal entity, and an env-specific DN would fork
/// `certHash` per environment. Reference implementation:
/// `dashboard/public/fcert.js` (`CHAIN_ISSUER_DN`, `CHAIN_AUTHORITY_KEY_ID`);
/// `docs/developers/certificate-schema.md` § Chain-issuer constants.
bytes32 constant CHAIN_ISSUER_DN_HASH = keccak256("CN=Final Chain,O=Final DeFi");

/// @dev `SHA3-256(utf8("FINAL_CHAIN_AUTHORITY_v01"))` — a DOMAIN constant, not
/// a key digest (the chain has no PublicKeyBlock). Precomputed because the
/// mock SHA3 precompile under Foundry is deliberately not the real function;
/// pinned against `hashlib.sha3_256` and the dashboard's value by test.
/// Zero-length AuthorityKeyId stays reserved for the retired genesis root
/// alone and is admitted nowhere.
bytes32 constant CHAIN_AUTHORITY_KEY_ID =
    0x9a6a5d8139ad2d28957698330aaa691017dba7dc80eb7cbec585239fb680bbab;

/// @notice The identity tree's projection door on `FinalStateTrees`. A narrow
/// interface rather than an import, because the trees contract imports this
/// file — the dependency runs that way and this is the one call that runs the
/// other. Same pattern as `IChainSource` on the trees side.
interface IIdentityLeafSink {
    function syncIdentityLeaves(address[] calldata accounts) external;
}

/// @notice `FinalRevocationLog`'s recording door, same narrow-interface
/// reasoning. `recorded` is read first so a fingerprint someone already
/// recorded permissionlessly cannot revert the registry mutation feeding it.
interface IRevocationRecorder {
    function record(bytes32 signerId) external;
    function recorded(bytes32 signerId) external view returns (bool);
}

contract FinalIdentityRegistry {
    // ---------------------------------------------------------------- roles

    /// @notice May co-sign account-state rounds (tree 1).
    uint256 public constant ROLE_ACCOUNT_COSIGNER = 1 << 0;
    /// @notice May co-sign MMR / bundle-log advances.
    uint256 public constant ROLE_MMR_COSIGNER = 1 << 1;
    /// @notice May publish PHI ledger state (tree 2).
    uint256 public constant ROLE_PHI_PUBLISHER = 1 << 2;
    /// @notice May publish vAsset state (tree 3).
    uint256 public constant ROLE_VASSET_PUBLISHER = 1 << 3;
    /// @notice May publish oracle data (tree 4).
    uint256 public constant ROLE_ORACLE_PUBLISHER = 1 << 4;
    /// @notice May publish settlement / asset registry roots (trees 5 and 6).
    uint256 public constant ROLE_REGISTRY_PUBLISHER = 1 << 5;
    /// @notice May act as a wallet guardian.
    uint256 public constant ROLE_GUARDIAN = 1 << 6;
    /// @notice May submit transactions on behalf of the protocol.
    uint256 public constant ROLE_RELAYER = 1 << 7;
    /// @notice May register and revoke identities once bootstrap is sealed.
    uint256 public constant ROLE_REGISTRAR = 1 << 8;
    /// @notice A certificate authority — the root, or an intermediate under it.
    uint256 public constant ROLE_CERTIFICATE_AUTHORITY = 1 << 9;
    /// @notice May co-sign `FinalSettlementLog` appends — the cross-chain
    /// settlement quorum, the same members whose LMS keys satisfy the
    /// execution chains' settlement set. A role of its own rather than a
    /// second use of `ROLE_REGISTRY_PUBLISHER`: the registries (trees 5/6)
    /// change on listing cadence and settlement leaves release custody, and
    /// one role for both would put the value plane behind the listing roster.
    uint256 public constant ROLE_SETTLEMENT_COSIGNER = 1 << 10;

    // ----------------------------------------------------- action domains

    /// @dev One per membership mutation, so an approval to grant a role can
    /// never be replayed as one to revoke. The registry is its own verifying
    /// contract for these.
    bytes32 public constant DOMAIN_REGISTER_WALLET = keccak256("FINAL_REGISTRY_REGISTER_WALLET_v01");
    bytes32 public constant DOMAIN_REGISTER_ISSUER = keccak256("FINAL_REGISTRY_REGISTER_ISSUER_v01");
    /// @notice The admission proof-of-possession digest domain (schema §v5).
    /// The HOLDER signs `keccak256(abi.encode(domain, chainid, registry,
    /// certHash, recoveryCertHash, gateNonce))` with the live transaction key
    /// (ML-DSA-87) AND the live access key (SLH-DSA-SHAKE-256s) — both
    /// families, in the admission transaction, verified by the precompiles.
    /// Possession lives in the TRANSACTION, never in the artifact.
    bytes32 public constant DOMAIN_IDENTITY_ADMISSION = keccak256("FINAL_IDENTITY_ADMISSION_v01");
    /// @notice Root-plane global certificate revocation (D5).
    bytes32 public constant DOMAIN_REVOKE_CERTIFICATE =
        keccak256("FINAL_REGISTRY_REVOKE_CERTIFICATE_v01");
    /// @notice The ISSUING identity's certificate-revocation digest domain.
    bytes32 public constant DOMAIN_ISSUER_CERT_REVOCATION =
        keccak256("FINAL_ISSUER_CERT_REVOCATION_v01");
    bytes32 public constant DOMAIN_REGISTER_LMS_KEY = keccak256("FINAL_REGISTRY_REGISTER_LMS_KEY_v01");
    bytes32 public constant DOMAIN_SET_ROLES = keccak256("FINAL_REGISTRY_SET_ROLES_v01");
    bytes32 public constant DOMAIN_REVOKE = keccak256("FINAL_REGISTRY_REVOKE_v01");
    bytes32 public constant DOMAIN_SET_REGISTRAR_THRESHOLD =
        keccak256("FINAL_REGISTRY_SET_REGISTRAR_THRESHOLD_v01");

    /// @dev The algorithm id the sender derivation is domain-separated by:
    /// ML-DSA-87, FIPS 204, the only algorithm the transaction envelope admits.
    uint8 private constant ENVELOPE_ALG_ML_DSA_87 = 4;

    // ------------------------------------------------------------- storage

    /**
     * @notice One party's on-chain identity.
     * @dev `version` increments on every mutation and is what a rotation is:
     * the record is replaced, not appended to, and the version is how a reader
     * on another chain knows which of two copies it saw is newer.
     */
    struct Identity {
        /// SHA3-256 of the LIVE certificate's TBS bytes. The revocation handle.
        bytes32 certHash;
        /// SHA3-256 of the RECOVERY certificate's TBS bytes.
        bytes32 recoveryCertHash;
        /// The certificate's 32-byte serial, `16 B entropy ‖ 16 B counter`.
        bytes32 serial;
        /// SHA3-256 of this certificate's public key block. A child names it in
        /// its own `AuthorityKeyId`, which is how the chain links the two.
        bytes32 subjectKeyId;
        /// Capability bitmask. Zero for a registered-but-idle party.
        uint256 roles;
        /// Position on the delegation axis; 0 is the Final Chain root.
        uint8 depth;
        /// Deepest level this key may issue to. `== depth` means it signs no
        /// certificates at all, which is every end entity.
        uint8 maxDelegationDepth;
        /// Seconds since epoch. The schema's TBS is nanoseconds; the conversion
        /// happens off chain because block timestamps are seconds and a
        /// comparison across units is a bug waiting for a leap.
        /// @dev MILLISECONDS — this chain's clock. See `FinalChainTime`.
        uint64 notBefore;
        /// Seconds since epoch, or 0 for "never expires" — which the schema
        /// allows and personal identity certificates use.
        uint64 notAfter;
        /// Monotonic. A rotation that does not advance it is refused.
        uint64 version;
        /// Set by `revoke`. Never unset: a revoked certificate is finished, and
        /// an un-revoke would make every past verification re-openable.
        bool revoked;
        /// Distinguishes "no record" from "a record whose fields are all zero".
        bool registered;
    }

    /**
     * @notice A hash-based (LMS) signing key held by a registered account.
     *
     * The protocol plane's quorums verify LMS, not ML-DSA: an execution chain
     * has no PQ precompiles, so `FinalRootAuthority` checks a keccak hash loop
     * instead (`arch/hash-based-authority.md`). Those keys are the authority
     * over `masterRoot`, and therefore over PQ execution — which makes "who
     * holds signer 0x39bb…?" a question the state plane has to be able to
     * answer, exactly as it answers it for every other key.
     *
     * Recorded against an account that is ALREADY registered, so an LMS key is
     * a capability of a known identity rather than a standalone credential. It
     * inherits that identity's revocation: a revoked account's signer is a
     * revoked signer, with nothing extra to remember to do.
     */
    struct LmsKey {
        /// `I`, hashed into every step of the signature.
        bytes16 keyId;
        /// Merkle tree height. Bound into the fingerprint, because the leaf
        /// commits to node `2^h + q` and a signer who could vary it could vary
        /// the numbering.
        uint8 height;
        /// `T[1]`, the LMS public key.
        bytes32 root;
        /// Monotonic. A rotation that does not advance it is refused, so a
        /// replayed registration cannot reinstate a superseded key.
        uint64 version;
        /// Distinguishes "no key" from "a key whose fields are all zero".
        bool registered;
    }

    /// @notice The LMS signing key for an account, if it holds one.
    /// @dev One slot per (account, chain) — LMS-01. `nextLeaf` on an
    /// authority is a complete single-use counter only while the key it names
    /// signs for ONE chain, so the roster is stored the way it is armed:
    /// the same operator is a different signer on every chain.
    mapping(address account => mapping(uint64 chainId => LmsKey)) private _lmsKey;
    /// @notice Which account a signer fingerprint belongs to. This is the
    /// lookup the whole record exists for: a gateway roster names fingerprints
    /// and nothing else, so without it the keys are unattributable.
    /// @dev What a fingerprint is bound to: the account that holds it and the
    /// chain it signs for — one slot, written once at registration and left in
    /// place when superseded (attribution is history). The chain names the
    /// (account, chain) slot `lmsSignerIsLive` resolves against.
    // NOTE: this contract sits ~13 bytes under EIP-170 (24,563 of 24,576 at
    // the pinned optimizer settings). The next feature here pays for itself
    // in bytecode first — see the LMS-binding merge and the off-chain
    // zero-chain check for what that looks like.
    struct LmsBinding {
        address account;
        uint64 chainId;
    }

    mapping(bytes32 signerId => LmsBinding) private _lmsBinding;

    /// @notice The identity record for an account.
    mapping(address account => Identity) private _identity;
    /// The four slots, verbatim. All four are stored in full because the
    /// precompiles verify against a KEY, not a commitment — and a key that
    /// arrived in calldata proves nothing about who signed.
    ///
    /// A CA has two keys, not four, and they live in the two ACTIVE slots. One
    /// storage shape rather than two, because every reader would otherwise have
    /// to know which kind of party it was looking at before it could look.
    mapping(address account => bytes) private _activeTransactionKey;
    mapping(address account => bytes) private _activeAccessKey;
    mapping(address account => bytes) private _recoveryTransactionKey;
    mapping(address account => bytes) private _recoveryAccessKey;
    /// @notice The seal key — a service's second SLH-DSA-SHAKE-256s key, which
    /// co-signs execution-class quorum decisions. Empty for every identity
    /// whose certificate carries no `PURPOSE_ACTIVE_SEAL` entry: users, CAs.
    mapping(address account => bytes) private _activeSealKey;
    /// @notice Encapsulation keys, per stage. Two algorithms each — ML-KEM-1024
    /// (lattice) and HQC-5 (code-based) — so a break in either family leaves the
    /// other standing, the same reasoning that pairs ML-DSA with SLH-DSA above.
    /// @dev Stored as the RAW keys, like the signing keys, because a registry
    /// that held only commitments could not answer "encapsulate to this party"
    /// without a second lookup somewhere less authoritative.
    mapping(address account => bytes) private _activeKemMlKem;
    mapping(address account => bytes) private _activeKemHqc;
    mapping(address account => bytes) private _recoveryKemMlKem;
    mapping(address account => bytes) private _recoveryKemHqc;
    /// @notice Reverse index. A certificate identifies exactly one account, so
    /// presenting a `certHash` is enough to find who it belongs to.
    mapping(bytes32 certHash => address account) public accountOfCertificate;
    /// @notice Revocation by certificate, independent of the account record.
    /// A certificate stays revoked even if its account is later re-registered
    /// under a new one.
    mapping(bytes32 certHash => bool) public certificateRevoked;
    /// @notice Who revoked a certificate through the ISSUER half of the lane.
    /// Scoped by the verifier: the entry binds only when the recorded revoker
    /// is the certificate's own issuer. Never gates registration.
    mapping(bytes32 certHash => address) public certificateRevokedBy;

    /// @notice Every registered account, in registration order. Small by
    /// construction — this is services and co-signers, not wallets.
    address[] private _accounts;

    /// @notice Bootstrap authority. Zero once `sealBootstrap` has run.
    address public bootstrapAdmin;
    /// @notice Whether registration still accepts the bootstrap admin.
    bool public bootstrapSealed;

    /// @notice Where identity mutations project the tree-8 leaf, same-tx.
    /// Zero only before {wireStatePlane} — the deploy tooling wires it before
    /// the first registration, and the projection is skipped while unset so
    /// the wiring transaction itself can be ordered freely in the bootstrap
    /// window.
    address public stateTrees;
    /// @notice Where the PERMANENT standing losses — revocation and LMS-key
    /// supersession — are recorded, same-tx. Zero only before {wireStatePlane}.
    address public revocationLog;

    /// @notice Sealed `ROLE_REGISTRAR` approvals a membership mutation needs.
    /// @dev Zero until set, and bootstrap cannot be sealed while it is zero or
    /// unreachable: a registry sealed behind a threshold nobody can meet is a
    /// registry nobody can ever write to again.
    uint256 public registrarThreshold;
    /// @notice Replay counter per verifying contract — this registry for its
    /// own mutations, each state-plane contract for its configuration. Bound
    /// into every registrar digest, so an approval is for exactly one action.
    mapping(address caller => uint64) private _gateNonce;
    /// @notice The identity a Final Chain sender belongs to. See the contract
    /// notes: a sender is derived from the `activeTransaction` key and is not
    /// the account.
    mapping(address sender => address account) public accountOfSender;

    // -------------------------------------------------------------- events

    event IdentityRegistered(
        address indexed account, bytes32 indexed certHash, uint256 roles, uint64 version
    );
    event IdentityRolesChanged(address indexed account, uint256 previousRoles, uint256 newRoles);
    event LmsKeyRegistered(
        address indexed account,
        bytes32 indexed signerId,
        uint64 indexed chainId,
        bytes16 keyId,
        uint8 height,
        bytes32 root,
        uint64 version
    );
    event IdentityRevoked(address indexed account, bytes32 indexed certHash);
    /// @notice One revocation-lane entry: `revoker` is `address(0)` for the
    /// root plane, the issuing identity otherwise.
    event CertificateRevoked(bytes32 indexed certHash, address indexed revoker);
    event BootstrapSealed(address indexed sealedBy);
    /// @notice The one-shot state-plane wiring landed.
    event StatePlaneWired(address stateTrees, address revocationLog);
    event RegistrarThresholdSet(uint256 threshold);
    /// @notice A registrar quorum authorized an action. `nonce` is the value
    /// the approvals were made over; the next action needs the next one.
    event RegistrarQuorumApproved(
        address indexed verifyingContract, bytes32 indexed actionDomain, uint64 nonce, uint256 valid
    );

    // -------------------------------------------------------------- errors

    error NotAuthorized(address caller);
    error BootstrapAlreadySealed();
    error UnknownAccount(address account);
    /// @notice A certificate's encapsulation key failed the chain's own
    /// well-formedness check. Names the algorithm, because the pair is stored
    /// together and "one of these two" is not an actionable answer.
    error MalformedEncapsulationKey(address account, uint16 algorithmId);
    error CertificateAlreadyBound(bytes32 certHash, address boundTo);
    error CertificateIsRevoked(bytes32 certHash);
    error VersionNotNewer(uint64 current, uint64 offered);
    error IssuerNotACertificateAuthority(address issuer);
    error IssuerMayNotSign(address issuer, uint8 depth, uint8 maxDelegationDepth);
    error WrongDepth(uint8 got, uint8 want);
    error DelegationWidened(uint8 child, uint8 issuer);
    error AuthorityKeyIdMismatch(bytes32 got, bytes32 want);
    error StagesDisagree(bytes32 liveSerial, bytes32 recoverySerial);
    /// @notice `height` outside 1..24. See `FinalLms.MAX_HEIGHT`.
    error LmsHeightOutOfRange(uint8 height);
    /// @notice A zero root commits to no tree.
    error LmsRootIsZero();
    /// @notice This fingerprint already belongs to a different account.
    error LmsKeyAlreadyBound(bytes32 signerId, address boundTo);
    /// @notice Two identities cannot share a transaction key: the sender it
    /// derives would be attributable to both.
    error SenderAlreadyBound(address sender, address boundTo);
    /// @notice Fewer registrars able to seal than the threshold asks for.
    error RegistrarThresholdUnreachable(uint256 sealable, uint256 threshold);
    error RegistrarThresholdIsZero();
    /// @notice {wireStatePlane} ran already, or was handed a zero address.
    error StatePlaneAlreadyWired();
    error ZeroStatePlane();
    /// @notice The holder's admission proof of possession did not verify —
    /// one family failed, or the digest was built over the wrong nonce.
    error AdmissionProofInvalid(address account);
    /// @notice The certificate does not carry the ruled chain-issuer
    /// AuthorityKeyId — it is not a chain-attested certificate.
    error NotChainAttested(bytes32 authorityKeyId);
    /// @notice The certificate's IssuerDN is not the ruled constant.
    error WrongIssuerDn(bytes32 issuerDnHash);
    /// @notice A chain-attested end entity sits at depth 1 with
    /// `maxDelegationDepth == depth`; anything else is not an end entity.
    error NotAnEndEntity(uint8 depth, uint8 maxDelegationDepth);
    /// @notice An issuer that cannot sign is an end entity wearing a profile.
    error IssuerCannotSign(uint8 depth, uint8 maxDelegationDepth);
    /// @notice Third-party issuers carry a real `NotAfter` (ruling 3) —
    /// expiry is the passive half of their lifecycle.
    error IssuerMustExpire();
    /// @notice An issuer validity window past the ~2-year ceiling (ruling 3).
    error IssuerValidityTooLong(uint64 notBefore, uint64 notAfter);
    /// @notice An institution registration without a real ISO 3166 `C=` in
    /// its subject DN, or with a jurisdiction that does not match its
    /// Institution extension. Only the trust root is jurisdiction-silent.
    error JurisdictionMissing();
    error JurisdictionMismatch();

    // --------------------------------------------------------- constructor

    /**
     * @param admin The bootstrap registrar. Genesis names the chain deployer.
     * @dev The precompile probe is the point of the constructor. This contract
     * is meaningless on a chain that cannot verify PQ signatures, and deploying
     * it there would produce a registry full of keys nothing can check.
     */
    constructor(address admin) {
        FinalChainPrecompiles.assertAvailable();
        bootstrapAdmin = admin;
    }

    // ----------------------------------------------------------- authority

    /**
     * @dev Bootstrap is a real window, not a formality: every roster in this
     * system has to be installed by someone before it can install itself, and
     * pretending otherwise produced the one roster that could not be
     * bootstrapped in `FinalRootAuthority`. It is closed by
     * `sealBootstrap`, which is irreversible.
     *
     * While it is open the admin writes alone. Once it is closed there is no
     * single-caller path left — not for a registrar, not for anyone — and
     * every mutation goes through the sealed registrar quorum.
     */
    function _requireMembershipAuthority(
        bytes32 actionDomain,
        bytes32 payloadDigest,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) private {
        if (!bootstrapSealed && msg.sender == bootstrapAdmin) return;
        _requireRegistrarQuorum(address(this), actionDomain, payloadDigest, anchorBlock, approvals);
    }

    /**
     * @notice The sealed registrar quorum, for the other state-plane contracts.
     * @dev `msg.sender` — the calling contract — is the verifying contract the
     * digest binds and the counter it burns, so an approval collected for the
     * trees' configuration cannot be spent on the bundle log's. The caller
     * decides its own bootstrap exemption before calling; this function knows
     * no caller's admin and applies none.
     *
     * Anyone may SUBMIT such a transaction. Authority is the approvals, not the
     * sender, which is the whole point of the quorum.
     */
    function requireRegistrarQuorum(
        bytes32 actionDomain,
        bytes32 payloadDigest,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireRegistrarQuorum(msg.sender, actionDomain, payloadDigest, anchorBlock, approvals);
    }

    /// @dev The digest is `FinalPqQuorum.digest(verifyingContract, actionDomain,
    /// anchorBlock, keccak256(abi.encode(nonce, payloadDigest)))`; the seal is
    /// required — membership is the hybrid class.
    function _requireRegistrarQuorum(
        address verifyingContract,
        bytes32 actionDomain,
        bytes32 payloadDigest,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) private {
        if (registrarThreshold == 0) revert RegistrarThresholdIsZero();
        uint64 nonce = _gateNonce[verifyingContract];
        _gateNonce[verifyingContract] = nonce + 1;
        bytes32 quorumDigest = FinalPqQuorum.digest(
            verifyingContract, actionDomain, anchorBlock, keccak256(abi.encode(nonce, payloadDigest))
        );
        uint256 valid = FinalPqQuorum.require_(
            this,
            approvals,
            quorumDigest,
            ROLE_REGISTRAR,
            registrarThreshold,
            FinalPqQuorum.ALG_ML_DSA_87,
            anchorBlock,
            true
        );
        emit RegistrarQuorumApproved(verifyingContract, actionDomain, nonce, valid);
    }

    /**
     * @notice Set how many sealed registrar approvals a membership mutation needs.
     * @dev Bootstrap admin while the window is open; the current registrar
     * quorum afterwards, so a registrar set that grows or shrinks can move it.
     * Refuses a threshold the sealable registrars cannot meet, and refuses zero:
     * both are a registry that can never be written to again.
     */
    function setRegistrarThreshold(
        uint256 threshold,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireMembershipAuthority(
            DOMAIN_SET_REGISTRAR_THRESHOLD, keccak256(abi.encode(threshold)), anchorBlock, approvals
        );
        if (threshold == 0) revert RegistrarThresholdIsZero();
        uint256 sealable = sealableMemberCount(ROLE_REGISTRAR);
        if (sealable < threshold) revert RegistrarThresholdUnreachable(sealable, threshold);
        registrarThreshold = threshold;
        emit RegistrarThresholdSet(threshold);
    }

    /// @notice The replay counter the next registrar approval for `caller`
    /// must be made over.
    function gateNonceOf(address caller) external view returns (uint64) {
        return _gateNonce[caller];
    }

    // -------------------------------------------------------- LMS signers

    /**
     * @notice The roster identity of an LMS public key.
     * @dev Byte-identical to `FinalRootAuthority.signerId`. Restated rather
     * than imported because the two live on different chains and there is no
     * import that would make them one value — which is precisely why a test
     * pins them together. A drift here would make every lookup miss while
     * looking perfectly well-formed.
     */
    function lmsSignerId(bytes16 keyId, uint8 height, bytes32 root) public pure returns (bytes32) {
        return keccak256(abi.encode(keyId, height, root));
    }

    /**
     * @notice Record the LMS signing key an already-registered account holds.
     * @dev Membership-gated, same as every other write here.
     *
     * Deliberately NOT a certificate: an LMS key is a capability of an existing
     * identity, not an identity of its own. Binding it to an account means it
     * inherits that account's revocation, so retiring a compromised operator is
     * one action rather than one-per-key-they-hold.
     *
     * @param account Must already be registered and not revoked.
     * @param version Strictly increasing. A rotation that does not advance it
     *   is refused, so a replayed registration cannot reinstate a key the
     *   operator has moved off.
     * @param anchorBlock The block the registrars read the roster at; see
     *   `FinalPqQuorum`. Ignored while bootstrap is open.
     * @param approvals The sealed registrar quorum. Empty while bootstrap is open.
     */
    function registerLmsKey(
        address account,
        uint64 chainId,
        bytes16 keyId,
        uint8 height,
        bytes32 root,
        uint64 version,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireMembershipAuthority(
            DOMAIN_REGISTER_LMS_KEY,
            keccak256(abi.encode(account, chainId, keyId, height, root, version)),
            anchorBlock,
            approvals
        );
        Identity storage id = _identity[account];
        if (!id.registered) revert UnknownAccount(account);
        if (id.revoked) revert CertificateIsRevoked(id.certHash);
        // A zero chain id is a tooling mistake, not an attack: the slot it
        // would occupy is self-consistent and no authority consults it. The
        // publisher refuses it; EIP-170 pressure keeps the check off-chain.
        if (height == 0 || height > 24) revert LmsHeightOutOfRange(height);
        if (root == bytes32(0)) revert LmsRootIsZero();

        // Version lineage is PER (account, chain) — LMS-01 made the same
        // operator a different signer on every chain, so chain B starting at
        // version 1 says nothing about chain A being at version 3.
        LmsKey storage existing = _lmsKey[account][chainId];
        // An empty slot holds version 0, so this alone also refuses a version-0
        // registration — versions start at 1.
        if (version <= existing.version) {
            revert VersionNotNewer(existing.version, version);
        }

        bytes32 signerId = lmsSignerId(keyId, height, root);
        address boundTo = _lmsBinding[signerId].account;
        if (boundTo != address(0) && boundTo != account) {
            revert LmsKeyAlreadyBound(signerId, boundTo);
        }

        // The fingerprint being superseded, captured before the slot moves —
        // `existing` is a storage pointer and reads the NEW key afterwards.
        bytes32 superseded = existing.registered
            ? lmsSignerId(existing.keyId, existing.height, existing.root)
            : bytes32(0);

        // The superseded fingerprint is left bound to this account rather than
        // cleared. It is history: a signature made under the old key was made
        // by this operator, and a lookup that stopped resolving would make that
        // unprovable after the fact.
        _lmsKey[account][chainId] = LmsKey(keyId, height, root, version, true);
        _lmsBinding[signerId] = LmsBinding(account, chainId);
        emit LmsKeyRegistered(account, signerId, chainId, keyId, height, root, version);

        // Supersession is a PERMANENT transition — the old fingerprint stops
        // being this slot's current key and nothing re-registers it (a
        // re-registration of the same material is the same fingerprint, which
        // the guard below leaves alone). Recorded same-tx so the execution
        // chains' suspension lane never depends on someone noticing.
        if (superseded != bytes32(0) && superseded != signerId) {
            _recordRevokedSigner(superseded);
        }
        _projectIdentity(account);
    }

    /// @notice The LMS key an account holds for one chain, if any.
    function lmsKeyOf(address account, uint64 chainId) external view returns (LmsKey memory) {
        return _lmsKey[account][chainId];
    }

    /// @notice What a fingerprint is bound to: the account that registered it
    /// and the chain it signs for. Zeroes for a fingerprint never registered.
    /// @dev The revocation log's permanence gate reads this to find the
    /// (account, chain) SLOT a fingerprint belongs to — the slot's current key
    /// is what separates a superseded fingerprint (permanent, recordable) from
    /// a merely lapsed one (expiry, temporary, refused). Attribution is
    /// history: the binding survives supersession, exactly as the mapping
    /// behind {lmsSignerIsLive} does, because it IS that mapping.
    function lmsBindingOf(bytes32 signerId) external view returns (address account, uint64 chainId) {
        LmsBinding storage binding = _lmsBinding[signerId];
        return (binding.account, binding.chainId);
    }

    /**
     * @notice Is this signer fingerprint held by a live, unrevoked account?
     * @dev The question a verifier actually has. A gateway roster names
     * fingerprints and nothing else, so "is 0x39bb… still good?" is otherwise
     * unanswerable from the state plane.
     */
    function lmsSignerIsLive(bytes32 signerId) external view returns (bool live, address account) {
        LmsBinding storage binding = _lmsBinding[signerId];
        account = binding.account;
        if (account == address(0)) return (false, address(0));
        // `isActive`, not a registered/revoked pair spelled out here. The
        // certificate validity window is part of standing: an expired identity
        // already holds no role, and a signer lookup that disagreed would leave
        // a roster satisfiable by an operator the rest of the registry has
        // stopped honouring. Spelling the condition out a second time is how
        // the two drift apart.
        if (!isActive(account)) return (false, account);
        // The CURRENT key of the fingerprint's own (account, chain) slot, not
        // merely one this account ever held: a superseded fingerprint stays
        // attributable but stops being live, and a rotation on one chain says
        // nothing about the same operator's key on another.
        LmsKey storage k = _lmsKey[account][binding.chainId];
        live = k.registered && lmsSignerId(k.keyId, k.height, k.root) == signerId;
    }

    /// @notice Close the bootstrap window. Irreversible.
    /// @dev Refuses while the registrar quorum is unset or unreachable, because
    /// sealing then would leave a registry nobody can ever write to again. The
    /// count is of registrars that can SEAL — a certificate authority carrying
    /// the role has no seal key and can never contribute an approval.
    function sealBootstrap() external {
        if (msg.sender != bootstrapAdmin) revert NotAuthorized(msg.sender);
        if (bootstrapSealed) revert BootstrapAlreadySealed();
        if (registrarThreshold == 0) revert RegistrarThresholdIsZero();
        uint256 sealable = sealableMemberCount(ROLE_REGISTRAR);
        if (sealable < registrarThreshold) {
            revert RegistrarThresholdUnreachable(sealable, registrarThreshold);
        }
        bootstrapSealed = true;
        bootstrapAdmin = address(0);
        emit BootstrapSealed(msg.sender);
    }

    // ------------------------------------------------- state-plane wiring

    /**
     * @notice Wire the trees and the revocation log, once, inside the
     *         bootstrap window.
     * @dev One-shot because both pointers are TRUST TOPOLOGY: the trees
     * pointer decides where the wallet-creation admission set is written, and
     * the log pointer decides where permanent standing losses are recorded. A
     * re-wireable pointer would be a key over both. It cannot be a constructor
     * argument — both contracts take THIS registry as one — so the deploy
     * tooling calls it in the same nonce-fixed block that deploys them, before
     * any identity is registered.
     */
    function wireStatePlane(address stateTrees_, address revocationLog_) external {
        if (bootstrapSealed || msg.sender != bootstrapAdmin) revert NotAuthorized(msg.sender);
        if (stateTrees != address(0) || revocationLog != address(0)) revert StatePlaneAlreadyWired();
        if (stateTrees_ == address(0) || revocationLog_ == address(0)) revert ZeroStatePlane();
        stateTrees = stateTrees_;
        revocationLog = revocationLog_;
        emit StatePlaneWired(stateTrees_, revocationLog_);
    }

    /// @dev Project `account`'s tree-8 leaf, same-tx. Skipped while the plane
    /// is unwired — the bootstrap-window state the deploy tooling closes
    /// before the first registration — and never otherwise: the leaf value is
    /// derived by the trees contract from THIS registry's post-mutation state,
    /// so there is nothing here to get wrong besides forgetting to call it.
    function _projectIdentity(address account) private {
        address trees = stateTrees;
        if (trees == address(0)) return;
        address[] memory one = new address[](1);
        one[0] = account;
        IIdentityLeafSink(trees).syncIdentityLeaves(one);
    }

    /// @dev Record a PERMANENTLY retired fingerprint, same-tx, unless the log
    /// is unwired or someone already recorded it permissionlessly.
    function _recordRevokedSigner(bytes32 signerId) private {
        address log = revocationLog;
        if (log == address(0)) return;
        if (IRevocationRecorder(log).recorded(signerId)) return;
        IRevocationRecorder(log).record(signerId);
    }

    // -------------------------------------------------------- registration

    /// @notice The holder's admission proof of possession: both live-stage
    /// families over the admission digest (schema §v5). There is no root
    /// keypair and no CA signature any more — the chain admits, and the
    /// "2 signatures at creation" are the HOLDER's, verified by the
    /// precompiles inside this very transaction.
    struct AdmissionProof {
        bytes mlDsaSignature;
        bytes slhDsaSignature;
    }

    /**
     * @notice Register or rotate a Final Wallet identity from its two public
     *         certificates — CHAIN-ATTESTED (schema §v5, ruled 2026-09-01).
     *
     * @param account The wallet address the certificate set derives.
     * @param liveTbs `live.pub.fcert` TBS — `activeTransaction` + `activeAccess`.
     * @param recoveryTbs `recovery.pub.fcert` TBS — the pre-committed recovery pair.
     * @param proof The HOLDER's two signatures over the admission digest —
     *        the live transaction key (ML-DSA-87) and the live access key
     *        (SLH-DSA-SHAKE-256s), verified in the precompiles inside this
     *        transaction. This replaced the CA signature: issuance authority
     *        is the registrar quorum, possession is this proof, and there is
     *        no root keypair anywhere.
     * @param roles Capability bitmask. The one thing the certificates do not
     *        say, because capability is this system's decision.
     * @param version Monotonic. A rotation that does not advance it is refused.
     * @param anchorBlock The block the registrars read the roster at. Ignored
     *        while bootstrap is open.
     * @param approvals The sealed registrar quorum. Empty while bootstrap is
     *        open. The digest binds the account, both certificates' bytes,
     *        the roles and the version.
     *
     * @dev **Both stages, together.** A wallet has four keys in two stages and
     * the recovery pair is PRE-COMMITTED — written at `initialize` from the same
     * certificate set that determined the address, which is why PQ migration
     * takes no key arguments. The two must share a `SerialNumber`: a serial is
     * per certificate SET, so two stages disagreeing are two different wallets.
     *
     * **Chain-attested means pinned, per stage:** the ruled IssuerDN and
     * AuthorityKeyId constants, depth exactly 1 (directly under the chain),
     * and `maxDelegationDepth == depth` (an end entity signs nothing — the
     * same immutable pair `identityTreeLeafOf` discriminates records by).
     */
    function registerWallet(
        address account,
        bytes calldata liveTbs,
        bytes calldata recoveryTbs,
        AdmissionProof calldata proof,
        uint256 roles,
        uint64 version,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external returns (bytes32 certHash) {
        // Read BEFORE the authority check: the quorum path burns this counter
        // inside `_requireRegistrarQuorum`, and the proof must bind the value
        // the round was built over. The bootstrap path burns it explicitly in
        // `_requireAdmissionProof`, so an admission is one-shot in both regimes.
        uint64 admissionNonce = _gateNonce[address(this)];
        _requireMembershipAuthority(
            DOMAIN_REGISTER_WALLET,
            keccak256(
                abi.encode(account, keccak256(liveTbs), keccak256(recoveryTbs), roles, version)
            ),
            anchorBlock,
            approvals
        );

        FinalCertificate.Parsed memory l = FinalCertificate.parseLive(liveTbs);
        FinalCertificate.Parsed memory r = FinalCertificate.parseRecovery(recoveryTbs);
        if (l.serial != r.serial) revert StagesDisagree(l.serial, r.serial);

        _requireChainAttestedEndEntity(l);
        _requireChainAttestedEndEntity(r);
        _requireAdmissionProof(account, l, r.certHash, proof, admissionNonce);

        certHash = l.certHash;
        _write(account, l, r, roles, version, false);
    }

    /**
     * @notice Register or rotate an ISSUER — a third party (or our own
     *         intermediate) that signs certificates OFF-chain with the keys
     *         registered here (D2: the superCA).
     *
     * @param account The issuer's account on this chain.
     * @param tbs The single issuer certificate's TBS: two CERT_SIGNING keys
     *        (ML-DSA-87 + SLH-DSA-SHAKE-256s), no recovery stage — renewing an
     *        issuer is re-issuing, a governance act rather than a key rotation.
     * @param parent The registered parent issuer for a nested intermediate;
     *        `address(0)` for an issuer hanging directly under the chain.
     * @param proof The issuer's OWN two cert-signing keys over the admission
     *        digest (`recoveryCertHash` slot is zero — there is no recovery
     *        stage to bind).
     *
     * @dev Admission is chain-native like any identity: registrar quorum plus
     * the holder's PoP. What the v4 delegation rules said survives verbatim as
     * LINEAGE — a nested issuer's depth, delegation bound and AuthorityKeyId
     * must chain to its registered parent — but no parent SIGNS anything; the
     * chain's admission is the issuance.
     *
     * Ruling 3: a registered issuer always expires (`NotAfter` real, window
     * bounded ~2 years) — the passive liveness touchpoint; renewal re-issues
     * under the same registered keys with a version bump.
     *
     * The jurisdiction rule (ruled 2026-09-01, amended): only the trust root
     * is jurisdiction-silent. An institution MUST carry its real ISO 3166
     * `C=` in its subject DN, matching the `jurisdiction` field of its
     * `0x0102` Institution extension — CA/Browser-Forum practice, enforced at
     * the door because a verifier's legal recourse starts with knowing where
     * an issuer answers for itself.
     */
    function registerIssuer(
        address account,
        bytes calldata tbs,
        address parent,
        AdmissionProof calldata proof,
        uint256 roles,
        uint64 version,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external returns (bytes32 certHash) {
        uint64 admissionNonce = _gateNonce[address(this)];
        _requireMembershipAuthority(
            DOMAIN_REGISTER_ISSUER,
            keccak256(abi.encode(account, keccak256(tbs), parent, roles, version)),
            anchorBlock,
            approvals
        );

        FinalCertificate.Parsed memory c = FinalCertificate.parseCa(tbs);
        // An issuer that cannot sign is an end entity wearing a profile —
        // and an end entity belongs in `registerWallet`.
        if (c.depth == 0 || c.maxDelegationDepth <= c.depth) {
            revert IssuerCannotSign(c.depth, c.maxDelegationDepth);
        }
        if (c.notAfter == 0) revert IssuerMustExpire();
        if (c.notAfter - c.notBefore > MAX_ISSUER_VALIDITY_MS) {
            revert IssuerValidityTooLong(c.notBefore, c.notAfter);
        }
        if (c.issuerDnHash != CHAIN_ISSUER_DN_HASH) revert WrongIssuerDn(c.issuerDnHash);
        _requireLineage(parent, c);
        _requireJurisdiction(c);
        _requireAdmissionProof(account, c, bytes32(0), proof, admissionNonce);

        certHash = c.certHash;
        _write(account, c, c, roles | ROLE_CERTIFICATE_AUTHORITY, version, true);
    }

    /// @notice Ruling 3's validity ceiling for registered issuers, in this
    /// chain's milliseconds: two 366-day years.
    uint64 public constant MAX_ISSUER_VALIDITY_MS = 2 * 366 days * 1000;

    /// @dev The chain-attested end-entity pins, run once per stage.
    function _requireChainAttestedEndEntity(FinalCertificate.Parsed memory c) private pure {
        if (c.authorityKeyId != CHAIN_AUTHORITY_KEY_ID) revert NotChainAttested(c.authorityKeyId);
        if (c.issuerDnHash != CHAIN_ISSUER_DN_HASH) revert WrongIssuerDn(c.issuerDnHash);
        if (c.depth != 1 || c.maxDelegationDepth != c.depth) {
            revert NotAnEndEntity(c.depth, c.maxDelegationDepth);
        }
    }

    /// @dev The v4 delegation rules, surviving as lineage: a nested issuer
    /// chains to a registered, signing-capable parent one level up; a direct
    /// issuer hangs under the chain at depth 1.
    function _requireLineage(address parent, FinalCertificate.Parsed memory c) private view {
        if (parent == address(0)) {
            if (c.authorityKeyId != CHAIN_AUTHORITY_KEY_ID) {
                revert NotChainAttested(c.authorityKeyId);
            }
            if (c.depth != 1) revert WrongDepth(c.depth, 1);
            return;
        }
        Identity storage ca = _identity[parent];
        if (!hasRole(parent, ROLE_CERTIFICATE_AUTHORITY)) {
            revert IssuerNotACertificateAuthority(parent);
        }
        // Delegation is governed by depth, not by a boolean. `Depth <
        // MaxDelegationDepth` permits signing, and a child sits exactly one
        // level down — an issuer cannot skip levels to escape its own bound.
        if (ca.depth >= ca.maxDelegationDepth) {
            revert IssuerMayNotSign(parent, ca.depth, ca.maxDelegationDepth);
        }
        if (c.depth != ca.depth + 1) revert WrongDepth(c.depth, ca.depth + 1);
        if (c.maxDelegationDepth > ca.maxDelegationDepth) {
            revert DelegationWidened(c.maxDelegationDepth, ca.maxDelegationDepth);
        }
        if (c.authorityKeyId != ca.subjectKeyId) {
            revert AuthorityKeyIdMismatch(c.authorityKeyId, ca.subjectKeyId);
        }
    }

    /// @dev The jurisdiction rule: a real ISO 3166 alpha-2 `C=` in the subject
    /// DN, equal to the Institution extension's `jurisdiction` field. The DN
    /// is canonical comma-separated form, so `C=` matches at the start or
    /// right after a comma; the component value is exactly two bytes.
    function _requireJurisdiction(FinalCertificate.Parsed memory c) private pure {
        bytes memory dn = c.subjectDn;
        bytes2 country;
        bool found = false;
        for (uint256 i = 0; i + 4 <= dn.length; i++) {
            if ((i == 0 || dn[i - 1] == ",") && dn[i] == "C" && dn[i + 1] == "=") {
                // Exactly two bytes, then end-of-DN or the next component.
                if (i + 4 < dn.length && dn[i + 4] != ",") revert JurisdictionMissing();
                country = bytes2(bytes.concat(dn[i + 2], dn[i + 3]));
                found = true;
                break;
            }
        }
        if (!found) revert JurisdictionMissing();

        // Institution extension: legalNameLength ‖ legalName ‖
        // registrationNoLength ‖ registrationNo ‖ jurisdictionLength ‖
        // jurisdiction. The jurisdiction must EQUAL the DN's country.
        bytes memory ext = c.institutionExt;
        if (ext.length < 6) revert JurisdictionMissing();
        uint256 q = 2 + (uint256(uint8(ext[0])) << 8 | uint256(uint8(ext[1])));
        if (ext.length < q + 2) revert JurisdictionMissing();
        q += 2 + (uint256(uint8(ext[q])) << 8 | uint256(uint8(ext[q + 1])));
        if (ext.length < q + 2) revert JurisdictionMissing();
        uint256 jLen = uint256(uint8(ext[q])) << 8 | uint256(uint8(ext[q + 1]));
        q += 2;
        if (jLen != 2 || ext.length < q + 2) revert JurisdictionMismatch();
        if (bytes2(bytes.concat(ext[q], ext[q + 1])) != country) revert JurisdictionMismatch();
    }

    /// @dev Verify the holder's PoP: both live-stage families over the
    /// admission digest, in the precompiles, inside this transaction. Burns
    /// the gate nonce on the bootstrap path (the quorum path burned it in
    /// `_requireRegistrarQuorum` already), so an admission is one-shot in
    /// both regimes.
    function _requireAdmissionProof(
        address account,
        FinalCertificate.Parsed memory live,
        bytes32 recoveryCertHash,
        AdmissionProof calldata proof,
        uint64 admissionNonce
    ) private {
        bytes memory message = abi.encodePacked(
            keccak256(
                abi.encode(
                    DOMAIN_IDENTITY_ADMISSION,
                    block.chainid,
                    address(this),
                    live.certHash,
                    recoveryCertHash,
                    admissionNonce
                )
            )
        );
        if (
            !FinalChainPrecompiles.verifyMlDsa87(live.transactionKey, message, proof.mlDsaSignature)
                || !FinalChainPrecompiles.verifySlhDsa(live.accessKey, message, proof.slhDsaSignature)
        ) revert AdmissionProofInvalid(account);
        if (_gateNonce[address(this)] == admissionNonce) {
            _gateNonce[address(this)] = admissionNonce + 1;
        }
    }

    function _write(
        address account,
        FinalCertificate.Parsed memory live,
        FinalCertificate.Parsed memory recovery,
        uint256 roles,
        uint64 version,
        bool isCa
    ) private {
        if (account == address(0)) revert UnknownAccount(account);
        if (certificateRevoked[live.certHash]) revert CertificateIsRevoked(live.certHash);

        address boundTo = accountOfCertificate[live.certHash];
        if (boundTo != address(0) && boundTo != account) {
            revert CertificateAlreadyBound(live.certHash, boundTo);
        }

        Identity storage id = _identity[account];
        if (!id.registered) {
            _accounts.push(account);
            id.registered = true;
        } else {
            if (version <= id.version) revert VersionNotNewer(id.version, version);
            if (id.revoked) revert CertificateIsRevoked(id.certHash);
            // A rotation releases the previous certificate's binding. It is NOT
            // revoked — a superseded certificate and a compromised one are
            // different facts and revocation is the louder of the two.
            if (id.certHash != live.certHash) delete accountOfCertificate[id.certHash];
        }

        id.certHash = live.certHash;
        id.recoveryCertHash = recovery.certHash;
        id.serial = live.serial;
        id.subjectKeyId = live.subjectKeyId;
        id.roles = roles;
        id.depth = live.depth;
        id.maxDelegationDepth = live.maxDelegationDepth;
        id.notBefore = live.notBefore;
        id.notAfter = live.notAfter;
        id.version = version;

        // The sender binding moves with the transaction key. The old sender is
        // released rather than kept: a rotation is the account disowning that
        // key, and a gate that still resolved it would honour a retired key.
        address sender = senderFor(live.transactionKey);
        address senderBoundTo = accountOfSender[sender];
        if (senderBoundTo != address(0) && senderBoundTo != account) {
            revert SenderAlreadyBound(sender, senderBoundTo);
        }
        if (_activeTransactionKey[account].length != 0) {
            address previousSender = senderFor(_activeTransactionKey[account]);
            if (previousSender != sender) delete accountOfSender[previousSender];
        }
        accountOfSender[sender] = account;

        _activeTransactionKey[account] = live.transactionKey;
        _activeAccessKey[account] = live.accessKey;
        // A CA has no recovery pair; the two active slots are all it has.
        _recoveryTransactionKey[account] = isCa ? bytes("") : recovery.transactionKey;
        _recoveryAccessKey[account] = isCa ? bytes("") : recovery.accessKey;
        // Cleared on a rotation to a certificate without one, for the same
        // reason the encapsulation pair is: a stale seal surviving a rotation
        // would let a retired key keep co-signing execution.
        _activeSealKey[account] = isCa ? bytes("") : live.sealKey;

        // The encapsulation pair, validated before it is stored.
        //
        // **The registry is where a sender looks up "encapsulate to this
        // party", so a malformed key here is not a bad record — it is an
        // account nobody can seal an intent to.** The discovery would happen at
        // the first attempt, and on the hybrid path it would happen as a pair
        // silently reduced to one family, which is identical on the wire. The
        // precompiles make it a refusal at registration instead.
        //
        // Neither is a re-implementation of the KEM: `0x0203` runs FIPS 203
        // §7.2's own encapsulation-key check and `0x0207` runs the structural
        // check HQC-5's encoding admits. Encapsulation is a sender operation
        // and decapsulation needs the secret key, so nothing more belongs here.
        //
        // A CA is sealed to by nobody and carries no encapsulation stage, so
        // its slots are cleared rather than checked.
        _storeKemPair(account, isCa, live.kemMlKem, live.kemHqc, true);
        _storeKemPair(account, isCa, recovery.kemMlKem, recovery.kemHqc, false);

        accountOfCertificate[live.certHash] = account;

        emit IdentityRegistered(account, live.certHash, roles, version);
        // Same-tx: a registration or rotation is visible to every execution
        // chain's admission set the moment it is visible here.
        _projectIdentity(account);
    }

    /**
     * @dev Store one stage's encapsulation pair, or clear it.
     *
     * Empty is legitimate and is not the same as absent-and-wrong: a CA has no
     * encapsulation stage, and a certificate issued before v4 carries none.
     * `FinalCertificate.parse` has already refused the half-populated case, so
     * by here the pair is both or neither.
     *
     * Cleared rather than left alone on a rotation to an empty pair. A stale
     * key surviving a rotation is a sender encapsulating to a credential the
     * account has disowned, and the intent then never decrypts — the failure
     * mode with no error attached, and the one this whole pairing exists to
     * avoid.
     */
    function _storeKemPair(address account, bool isCa, bytes memory mlKem, bytes memory hqc, bool isLive)
        private
    {
        if (isCa || mlKem.length == 0) {
            delete (isLive ? _activeKemMlKem : _recoveryKemMlKem)[account];
            delete (isLive ? _activeKemHqc : _recoveryKemHqc)[account];
            return;
        }
        if (!FinalChainPrecompiles.isWellFormedMlKem1024(mlKem)) {
            revert MalformedEncapsulationKey(account, FinalCertificate.ALG_ML_KEM_1024);
        }
        if (!FinalChainPrecompiles.isWellFormedHqc5(hqc)) {
            revert MalformedEncapsulationKey(account, FinalCertificate.ALG_HQC_5);
        }
        if (isLive) {
            _activeKemMlKem[account] = mlKem;
            _activeKemHqc[account] = hqc;
        } else {
            _recoveryKemMlKem[account] = mlKem;
            _recoveryKemHqc[account] = hqc;
        }
    }

    /// @notice Grant or withdraw capabilities without rotating keys.
    /// @dev Separate from registration because the two have different
    /// cadences: a role changes when a service's job changes, a key changes
    /// when it is compromised or aged out. Folding them together would force a
    /// key rotation to express a role change.
    function setRoles(
        address account,
        uint256 roles,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireMembershipAuthority(
            DOMAIN_SET_ROLES, keccak256(abi.encode(account, roles)), anchorBlock, approvals
        );
        Identity storage id = _identity[account];
        if (!id.registered) revert UnknownAccount(account);
        if (id.revoked) revert CertificateIsRevoked(id.certHash);
        uint256 previous = id.roles;
        id.roles = roles;
        _requireRegistrarQuorumReachable();
        emit IdentityRolesChanged(account, previous, roles);
        // Roles are not in the tree-8 leaf, so this rewrites the same value —
        // kept anyway so "every identity mutation projects" has no exceptions
        // to remember.
        _projectIdentity(account);
    }

    /// @dev Once sealed, no mutation may leave the registrar quorum unreachable
    /// — that is the one change nothing could ever undo. Checked after the
    /// write so the count reflects it.
    function _requireRegistrarQuorumReachable() private view {
        if (!bootstrapSealed) return;
        uint256 sealable = sealableMemberCount(ROLE_REGISTRAR);
        if (sealable < registrarThreshold) {
            revert RegistrarThresholdUnreachable(sealable, registrarThreshold);
        }
    }

    /// @notice Revoke an identity and its certificate. Irreversible.
    /// @param chainIds The chains whose LMS-key slots this account holds — the
    /// registrars supply the list (the digest binds it) because a mapping
    /// cannot enumerate its own keys. Each named slot's current fingerprint is
    /// recorded into the revocation log same-tx; a chain with no slot is
    /// skipped, and a fingerprint missed by an incomplete list stays
    /// permanently recordable through the log's permissionless door, since a
    /// revoked account never regains standing.
    /// @dev Clears the roles as well as setting the flag. Both are checked
    /// everywhere, but leaving a revoked record carrying roles invites a future
    /// reader that checks only one of them.
    function revoke(
        address account,
        uint64[] calldata chainIds,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireMembershipAuthority(
            DOMAIN_REVOKE, keccak256(abi.encode(account, chainIds)), anchorBlock, approvals
        );
        Identity storage id = _identity[account];
        if (!id.registered) revert UnknownAccount(account);
        id.revoked = true;
        id.roles = 0;
        certificateRevoked[id.certHash] = true;
        _requireRegistrarQuorumReachable();
        emit IdentityRevoked(account, id.certHash);
        // AFTER the flag lands, so the log's own gate sees the permanent
        // transition it requires.
        for (uint256 i = 0; i < chainIds.length; i++) {
            LmsKey storage k = _lmsKey[account][chainIds[i]];
            if (k.registered) _recordRevokedSigner(lmsSignerId(k.keyId, k.height, k.root));
        }
        _projectIdentity(account);
    }

    /**
     * @notice Root-plane GLOBAL certificate revocation, by `certHash` (D5).
     *
     * @dev The half of the one revocation lane that gates registration and
     * covers break-glass: any certificate — registered, off-chain-issued, or
     * never seen — can be killed by handle under the registrar quorum. When
     * the handle is a registered identity's CURRENT certificate the identity
     * falls with it (flag, roles, same-tx projection), so a break-glass by
     * handle is never weaker than {revoke} — it only skips the LMS-slot
     * enumeration, which stays permanently recordable through the revocation
     * log's permissionless door.
     */
    function revokeCertificate(
        bytes32 certHash,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireMembershipAuthority(
            DOMAIN_REVOKE_CERTIFICATE, keccak256(abi.encode(certHash)), anchorBlock, approvals
        );
        certificateRevoked[certHash] = true;
        address bound = accountOfCertificate[certHash];
        if (bound != address(0)) {
            Identity storage id = _identity[bound];
            if (!id.revoked) {
                id.revoked = true;
                id.roles = 0;
                _requireRegistrarQuorumReachable();
                emit IdentityRevoked(bound, certHash);
                _projectIdentity(bound);
            }
        }
        emit CertificateRevoked(certHash, address(0));
    }

    /**
     * @notice The ISSUING identity's half of the revocation lane: a registered
     * issuer revokes a certificate it signed OFF-chain, by `certHash`.
     *
     * @dev "Sub-issuer and us alike" (D5) — but SCOPED: this records WHO
     * revoked, and a verifier honours the entry only when the revoker is the
     * certificate's own issuer (which the verifier knows — it holds the
     * cert). It deliberately does NOT set the global `certificateRevoked`
     * flag: that flag gates registration, and letting any registered issuer
     * set it for an arbitrary handle would be a griefing lane over other
     * people's certificates.
     *
     * Anyone may SUBMIT; authority is the two signatures — the issuer's
     * registered cert-signing keys over a digest binding this registry, the
     * chain, the handle and the issuer's own gate nonce. One-way: the first
     * revoker of a handle is recorded and a second write is refused, because
     * "revoked twice by two parties" is two facts where the lane models one.
     */
    function revokeIssuedCertificate(
        address issuer,
        bytes32 certHash,
        AdmissionProof calldata proof
    ) external {
        if (!hasRole(issuer, ROLE_CERTIFICATE_AUTHORITY)) {
            revert IssuerNotACertificateAuthority(issuer);
        }
        if (certificateRevokedBy[certHash] != address(0)) revert CertificateIsRevoked(certHash);
        uint64 nonce = _gateNonce[issuer];
        _gateNonce[issuer] = nonce + 1;
        bytes memory message = abi.encodePacked(
            keccak256(
                abi.encode(
                    DOMAIN_ISSUER_CERT_REVOCATION,
                    block.chainid,
                    address(this),
                    issuer,
                    certHash,
                    nonce
                )
            )
        );
        if (
            !FinalChainPrecompiles.verifyMlDsa87(
                _activeTransactionKey[issuer], message, proof.mlDsaSignature
            )
                || !FinalChainPrecompiles.verifySlhDsa(
                    _activeAccessKey[issuer], message, proof.slhDsaSignature
                )
        ) revert AdmissionProofInvalid(issuer);
        certificateRevokedBy[certHash] = issuer;
        emit CertificateRevoked(certHash, issuer);
    }

    // ---------------------------------------------------------------- views

    /// @notice The full identity record. `registered` is the field to branch on.
    function identityOf(address account) external view returns (Identity memory) {
        return _identity[account];
    }

    /// @notice `activeTransaction` — ML-DSA-87. What a quorum verifies against.
    function activeTransactionKeyOf(address account) external view returns (bytes memory) {
        return _activeTransactionKey[account];
    }

    /// @notice `activeAccess` — SLH-DSA-SHAKE-256s. Identity, and guardianship.
    function activeAccessKeyOf(address account) external view returns (bytes memory) {
        return _activeAccessKey[account];
    }

    /// @notice `activeSeal` — SLH-DSA-SHAKE-256s. What `FinalPqQuorum` verifies
    /// an execution-class approval's `seal` against. Empty when the identity
    /// carries no seal, in which case it cannot take part in a sealed quorum.
    function activeSealKeyOf(address account) external view returns (bytes memory) {
        return _activeSealKey[account];
    }

    /// @notice `recoveryTransaction`. Authorizes rotating this account's own
    /// credentials and nothing else. Empty for a CA.
    function recoveryTransactionKeyOf(address account) external view returns (bytes memory) {
        return _recoveryTransactionKey[account];
    }

    /// @notice `recoveryAccess`. Empty for a CA.
    function recoveryAccessKeyOf(address account) external view returns (bytes memory) {
        return _recoveryAccessKey[account];
    }

    /// @notice The four commitments, in the order tree 1's leaf wants them.
    /// @dev keccak, not SHA3 — these feed `FinalWalletFactory.accountStateLeafHash`,
    /// which every other chain verifies with, and that one hashes with keccak.
    function keyCommitments(address account)
        external
        view
        returns (
            bytes32 liveAccess,
            bytes32 liveTransaction,
            bytes32 recoveryAccess,
            bytes32 recoveryTransaction
        )
    {
        liveAccess = keccak256(_activeAccessKey[account]);
        liveTransaction = keccak256(_activeTransactionKey[account]);
        recoveryAccess = keccak256(_recoveryAccessKey[account]);
        recoveryTransaction = keccak256(_recoveryTransactionKey[account]);
    }

    /**
     * @notice The tree-8 leaf `account` currently earns: the execution
     *         chains' identity leaf while the identity stands, zero once it
     *         does not.
     *
     * @dev The leaf VALUE is `keccak256(DOMAIN_IDENTITY_LEAF ‖ serial ‖
     * keysHash)` — byte-identical to `IdentityRootModule.identityLeafHash`,
     * which is also the `certHash` inside the wallet's CREATE2 derivation —
     * with `keysHash` folded exactly as the certificate issuer folds it:
     * `keccak256(activeAccess ‖ activeTransaction ‖ recoveryAccess ‖
     * recoveryTransaction ‖ activeKem ‖ recoveryKem)`, six commitment words
     * packed in slot order (`minePqVanityCerts.cjs` is the reference encoder;
     * the parity test pins this function against the premined fixtures).
     *
     * Zero — the empty slot's own value, unprovable as a leaf because no
     * certificate hashes to it — for anything that must not admit a wallet
     * creation: a revoked identity, one outside its validity window, and any
     * CA. The CA exclusion is structural, not a role read: an end entity has
     * `depth == maxDelegationDepth` (it issues nothing), a CA never does, and
     * the depth pair is immutable per version where roles are not.
     *
     * Lives HERE rather than on `FinalStateTrees` (whose tree 8 consumes it)
     * because every input is this contract's storage and the trees contract
     * sits against EIP-170.
     */
    function identityTreeLeafOf(address account) external view returns (bytes32) {
        Identity storage id = _identity[account];
        if (!id.registered) revert UnknownAccount(account);
        if (id.revoked || !_withinValidity(id)) return bytes32(0);
        if (id.depth != id.maxDelegationDepth) {
            // D7 (ruled 2026-09-01): an ISSUER exists in tree 8 under its own
            // domain, so its record is stapleable for offline licence
            // verification. `certHash` suffices (it covers the whole TBS and
            // the verifier holds the cert), `version` makes supersession move
            // the leaf, and the third word RESERVES the issuer's own
            // certificate-tree anchor — zero until wired. The distinct domain
            // does the wallet-admission exclusion the zero projection used to
            // do; zero-on-revoke above is now load-bearing for both record
            // kinds (a fresh staple is an unrevoked statement).
            return keccak256(
                abi.encodePacked(DOMAIN_ISSUER_LEAF, id.certHash, uint64(id.version), bytes32(0))
            );
        }
        bytes32 liveKem = keccak256(
            abi.encodePacked(DOMAIN_KEM_BUNDLE, _activeKemMlKem[account], _activeKemHqc[account]));
        bytes32 recoveryKem = keccak256(
            abi.encodePacked(DOMAIN_KEM_BUNDLE, _recoveryKemMlKem[account], _recoveryKemHqc[account]));
        bytes32 keysHash = keccak256(
            abi.encodePacked(
                keccak256(_activeAccessKey[account]),
                keccak256(_activeTransactionKey[account]),
                keccak256(_recoveryAccessKey[account]),
                keccak256(_recoveryTransactionKey[account]),
                liveKem,
                recoveryKem
            )
        );
        return keccak256(abi.encodePacked(DOMAIN_IDENTITY_LEAF, id.serial, keysHash));
    }

    /// @notice Per-stage encapsulation commitments, in `AccountStateLeaf` order.
    /// @dev One word per STAGE, over both of that stage's KEM public keys. The
    /// pair is the unit — an account holds both or neither — so committing them
    /// separately would model a state the protocol does not recognise, and every
    /// downstream record would carry two words where one says the same thing.
    ///
    /// An account registered before the encapsulation slots existed hashes the
    /// empty string here rather than reverting: `syncIdentities` must keep
    /// projecting it, and a leaf that cannot be built is a party that cannot be
    /// revoked.
    function kemCommitments(address account)
        external
        view
        returns (bytes32 liveKem, bytes32 recoveryKem)
    {
        liveKem = keccak256(
            abi.encodePacked(DOMAIN_KEM_BUNDLE, _activeKemMlKem[account], _activeKemHqc[account]));
        recoveryKem = keccak256(
            abi.encodePacked(DOMAIN_KEM_BUNDLE, _recoveryKemMlKem[account], _recoveryKemHqc[account]));
    }

    /// @notice The encapsulation keys themselves, for a party composing a message.
    function kemKeysOf(address account)
        external
        view
        returns (bytes memory activeMlKem, bytes memory activeHqc)
    {
        return (_activeKemMlKem[account], _activeKemHqc[account]);
    }

    // ------------------------------------------------------------- senders

    /**
     * @notice The Final Chain sender a transaction key produces.
     * @dev `keccak256(uint8(4) ‖ publicKey)[12:]` — byte-identical to what the
     * node derives from a type-0x46 envelope and to the backend's
     * `pqTransaction.senderOf`. Pure, so a client can compute it from a
     * certificate before the identity is registered.
     */
    function senderFor(bytes memory transactionKey) public pure returns (address) {
        return address(uint160(uint256(keccak256(abi.encodePacked(ENVELOPE_ALG_ML_DSA_87, transactionKey)))));
    }

    /// @notice The sender `account`'s transactions arrive from, or zero for an
    /// account with no transaction key on record.
    function senderOf(address account) external view returns (address) {
        bytes storage key = _activeTransactionKey[account];
        if (key.length == 0) return address(0);
        return senderFor(key);
    }

    /// @notice `hasRole` for a `msg.sender`: resolves the sender to its identity
    /// first. False for a sender no identity claims.
    function senderHasRole(address sender, uint256 roleMask) external view returns (bool) {
        address account = accountOfSender[sender];
        return account != address(0) && hasRole(account, roleMask);
    }

    /// @notice How many accounts carrying `roleMask` also hold a seal key —
    /// the members that can take part in a sealed quorum.
    function sealableMemberCount(uint256 roleMask) public view returns (uint256 sealable) {
        uint256 n = _accounts.length;
        for (uint256 i = 0; i < n; i++) {
            address a = _accounts[i];
            if (hasRole(a, roleMask) && _activeSealKey[a].length != 0) sealable++;
        }
    }

    /// @notice Number of registered accounts.
    function accountCount() external view returns (uint256) {
        return _accounts.length;
    }

    /// @notice Registered account by index, in registration order.
    function accountAt(uint256 index) external view returns (address) {
        return _accounts[index];
    }

    /// @notice Every account carrying every bit in `roleMask`.
    /// @dev A view, so the O(n) scan costs nothing. Callers that need this in a
    /// transaction should pass the member list explicitly instead — see
    /// `FinalPqQuorum`, which takes signers rather than searching for them.
    function accountsWithRole(uint256 roleMask) external view returns (address[] memory found) {
        uint256 n = _accounts.length;
        address[] memory buf = new address[](n);
        uint256 count;
        for (uint256 i = 0; i < n; i++) {
            if (hasRole(_accounts[i], roleMask)) {
                buf[count++] = _accounts[i];
            }
        }
        found = new address[](count);
        for (uint256 i = 0; i < count; i++) {
            found[i] = buf[i];
        }
    }

    /**
     * @notice How many accounts could satisfy a quorum for `roleMask` right now.
     * @dev The number a threshold has to be reachable against. A threshold above
     * it is not a strict quorum, it is a quorum that cannot be met — and the way
     * that presents is an operation that reverts forever with nothing naming the
     * roster as the cause.
     */
    function liveMemberCount(uint256 roleMask) public view returns (uint256 live) {
        uint256 n = _accounts.length;
        for (uint256 i = 0; i < n; i++) {
            if (hasRole(_accounts[i], roleMask)) live++;
        }
    }

    /**
     * @notice Whether `account` currently carries every bit in `roleMask`.
     * @dev Every gate in this system asks this one question, so every gate gets
     * the same answer: registered, not revoked, inside its validity window, and
     * holding the capability. A caller that checked only the role bit would
     * accept an expired certificate.
     *
     * `roleMask == 0` is false. A zero mask asks nothing and must not read as
     * "yes" — that is the shape of an uninitialised configuration variable, and
     * the one reading it should not be a universal pass.
     */
    function hasRole(address account, uint256 roleMask) public view returns (bool) {
        if (roleMask == 0) return false;
        Identity storage id = _identity[account];
        if (!id.registered || id.revoked) return false;
        if (id.roles & roleMask != roleMask) return false;
        return _withinValidity(id);
    }

    /// @notice Whether `account` is registered, unrevoked and in date,
    /// regardless of capability.
    function isActive(address account) public view returns (bool) {
        Identity storage id = _identity[account];
        return id.registered && !id.revoked && _withinValidity(id);
    }

    function _withinValidity(Identity storage id) private view returns (bool) {
        if (id.notBefore != 0 && FinalChainTime.nowMs() < id.notBefore) return false;
        if (id.notAfter != 0 && FinalChainTime.nowMs() >= id.notAfter) return false;
        return true;
    }

}

contracts/finalchain/FinalIntentLog.sol

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.20;

import {FinalChainPrecompiles} from "./FinalChainPrecompiles.sol";
import {FinalChainTime} from "./FinalChainTime.sol";
import {FinalIdentityRegistry} from "./FinalIdentityRegistry.sol";
import {FinalPqQuorum} from "./FinalPqQuorum.sol";
import {FinalStateTrees} from "./FinalStateTrees.sol";

/**
 * @title FinalIntentLog
 * @notice Every intent is posted here before it may be anchored. Ordering is
 *         block order.
 *
 * @dev ## Why this is a log and not a tree
 *
 * Final Chain carries six trees, and they publish in ROUNDS — every root
 * together, one version bump each. That structure is for state that changes
 * slowly and must be proven elsewhere. An intent is neither: it is high
 * cadence, it lives 180 seconds, and putting it in a round-published tree would
 * bump a version on every transaction and age every outstanding proof — the
 * same objection that kept a liveness timestamp out of the tree-1 account leaf.
 *
 * It is not an MMR either. `FinalBundleLog`'s payload already IS the bundle's
 * intent-Merkle root, so a second append-only log over the same leaves would
 * prove, one level earlier, a fact the bundle anchor already proves.
 *
 * What an intent actually needs is ordering, public availability, and a
 * presence check the anchoring quorum can run objectively. Block order gives
 * the first, calldata gives the second, and a mapping gives the third. That is
 * the whole contract.
 *
 * ## What it buys
 *
 * `FinalBundleLog.append` consumes from here, so a bundle cannot be anchored
 * unless every intent in it was posted first — and the execution chains'
 * authorization is that anchor. Final-Chain-first admission stops being a
 * quorum policy and becomes bytecode. Replay protection falls out of the same
 * mapping: a leaf is consumed exactly once, ever.
 *
 * For the user it closes substitution. The intent they composed is ordered
 * publicly before anything executes, so a relayer or a frontend cannot swap it
 * for another — the anchor will only carry what this log holds.
 *
 * ## The body is encrypted and this contract cannot read it
 *
 * `header` is routing metadata: a chain reference, a deadline, KEM ciphertexts,
 * a commitment to the body, and the intent leaf. Account, destination, fee and
 * calldata are all inside `ciphertext`. That is what closes content-based
 * front-running — an operator watching this log sees a chain id and a deadline
 * and cannot build a profitable order from either.
 *
 * There is deliberately no decryption path on chain and no AEAD precompile. The
 * chain checks a hash and nothing else.
 *
 * ## The envelope is STORED, not left in the event log
 *
 * A two-KEM envelope is ~16 KB and keeping it in state is not free. It is kept
 * anyway, for the reason `FinalBundleLog` keeps its payloads: this chain caps a
 * `getLogs` range at 100,000 blocks and mints one every 100 ms, so the log is
 * reachable for **2.8 hours**. A forced-path intent lives **48**.
 *
 * An event-only envelope would therefore be unreadable long before it expired —
 * precisely for the guardians who are meant to review a forced intent, and
 * precisely in the window the forced path exists to cover. That is not a
 * degraded read, it is the escape hatch not working.
 *
 * So the event is for DISCOVERY and carries no bytes; state is for retrieval,
 * over one `eth_call` and no range. Emitting the envelope as well would be
 * ~128k gas of duplication for data the same transaction already stored.
 *
 * Measured in `FinalIntentLog.t.sol`: **11,517,049 gas** for a 16,426-byte
 * envelope, which at a 60M block gas limit is **5 intents per block** — 50 a
 * second at 100 ms. That is the throughput ceiling on this path, and it is
 * dominated by HQC-5's 14,421-byte ciphertext.
 *
 * Two levers if it ever binds. SSTORE2-style data contracts are ~3x cheaper per
 * byte, and would need chunking because a two-recipient envelope exceeds
 * EIP-170's 24,576. Pruning consumed envelopes is the other, and it trades the
 * record for the space — a decision, not a cleanup.
 *
 * **The bond becomes load-bearing when the RPC opens, not before.** Posting is
 * permissionless in this contract and the base fee is 1 wei, so the only thing
 * standing between an attacker and 60M gas a block of permanent state is that
 * Final Chain's RPC is fronted by a default-deny Cloud Armor allowlist and
 * reaches operators only. That is a perimeter, not a mechanism, and it is the
 * one that has to be removed for users to post their own intents. Nothing here
 * rate-limits; the bond is what is supposed to, and this is why it cannot ship
 * after the RPC does.
 *
 * ## Approve and cancel, without naming an account
 *
 * An approval-gated intent executes only after a second on-chain act, made
 * after the user decrypts the posted envelope and reads what the CHAIN holds —
 * the read-back that closes execution of what the user has not seen. Who may
 * approve is a **per-intent, wallet-generated ML-DSA-87 key**, committed in the
 * plaintext header: the commitment names a key, never an account, which is what
 * answers "a per-intent veto has to say who may veto, and the account is
 * encrypted". A substituted commitment strands the intent — the execution
 * signature bound into the leaf still authorizes every field — and cancel is
 * the same key, any time before consumption.
 *
 * `approve` also DELIVERS the executor section: the content key and the
 * account's KEM public keys, sealed to the executor set. Before approval the
 * executor holds ciphertext it cannot open; that is cryptography, not policy.
 * An AUTO_APPROVE posting (header flag, bit 0) skips the gate for automation
 * and scheduling and carries its executor section at posting — flipping the
 * flag skips the second look and can never change what executes.
 *
 * The account-wide veto stays `FinalAccountLedger`'s freeze; auto-approved
 * intents have no per-intent cancel because they commit to no approval key.
 *
 * ## Tree 7 is the search structure; the MMR is the record
 *
 * Every transition — posted, approved, cancelled, consumed — is mirrored into
 * `FinalStateTrees.TREE_INTENTS` through `treeWriter[7]`, keyed by a RING over
 * the posting sequence (`seq mod 2^20`), so slots recycle and the tree is an
 * index with a ~1M-posting retention window rather than a permanent record.
 * Backends enumerate it instead of scanning logs; proofs against it are proofs
 * about the current window.
 */
contract FinalIntentLog {
    /// @notice Where roles are resolved. Immutable.
    ///
    /// @dev The only thing this log needs a registry for is authorizing
    ///      `setConsumer`, and that could have been a deployer check. It is the
    ///      registry instead so the wiring falls under the SAME bootstrap window
    ///      as `FinalStateTrees.setTreeWriter` and `FinalBundleLog.configure` —
    ///      one rule for who may wire Final Chain together, rather than three.
    FinalIdentityRegistry public immutable registry;

    /// @notice Where every status transition is mirrored (tree 7). Immutable:
    /// a movable index would be an index whose history can be swapped.
    FinalStateTrees public immutable trees;

    /// @dev Header layout, fixed offsets. Must match `encodeHeader` in
    ///      `FinalBackend/src/intents/intentEnvelope.js` — the cross-repo parity
    ///      suite pins it. Length-prefixed and canonical precisely so these
    ///      offsets exist: a JSON header would have no stable position to read.
    ///
    ///        0         version           uint8   = 2 | 3 (v3 salts recipientsHash; offsets identical)
    ///        1         flags             uint8   bit0 AUTO_APPROVE; rest zero
    ///        2..33     targetChainRef    bytes32
    ///        34..41    executeNotBefore  uint64  ms; 0 = immediate
    ///        42..49    deadline          uint64  ms
    ///        50..81    bodyCommitment    bytes32
    ///        82..113   intentLeaf        bytes32
    ///        114..145  approvalKeyCommit bytes32 zero iff AUTO_APPROVE
    ///        146..147  kemSet            uint16
    ///        148..149  kemKeyVersion     uint16
    ///        150..181  recipientsHash    bytes32
    ///        182..     recipients, executorSection, bond
    uint256 private constant OFF_FLAGS = 1;
    uint256 private constant OFF_TARGET_CHAIN_REF = 2;
    uint256 private constant OFF_EXECUTE_NOT_BEFORE = 34;
    uint256 private constant OFF_DEADLINE = 42;
    uint256 private constant OFF_BODY_COMMITMENT = 50;
    uint256 private constant OFF_INTENT_LEAF = 82;
    uint256 private constant OFF_APPROVAL_KEY_COMMIT = 114;
    /// @dev The whole fixed prefix, including `recipientsHash` — which this
    ///      contract never reads. Requiring it anyway is the cheap half of
    ///      "well-formed": a header truncated after the leaf would parse
    ///      perfectly here and then fail to decrypt for its recipient. The
    ///      contract reads nothing past offset 145.
    uint256 private constant HEADER_MIN_BYTES = 182;

    /// @notice The oldest envelope wire version this contract reads.
    uint8 public constant HEADER_VERSION = 2;

    /// @notice The newest. v3 (B6) keeps every offset and salts `recipientsHash`
    ///         — a field this contract never reads — with a holder-derived value,
    ///         so a wallet's postings stop sharing a fingerprint in the clear.
    ///         Both versions parse identically here; the header's own AAD is what
    ///         tells them apart for the recipients.
    uint8 public constant HEADER_VERSION_MAX = 3;

    /// @notice Header flag bit 0: skip the approval gate.
    /// @dev Poster-controlled routing, honestly scoped: flipping it skips the
    /// user's second look and can never change what executes — the user's own
    /// signature over the intent fields, bound into the leaf, remains the only
    /// execution authorization.
    uint8 public constant FLAG_AUTO_APPROVE = 0x01;

    // ---------------------------------------------------- approval domains

    /// @dev Distinct per action, so an approval can never be replayed as a
    /// cancellation or vice versa. Both digests bind this chain and this log.
    bytes32 public constant DOMAIN_INTENT_APPROVE = keccak256("FINAL_INTENT_APPROVE_v01");
    bytes32 public constant DOMAIN_INTENT_CANCEL = keccak256("FINAL_INTENT_CANCEL_v01");

    // ------------------------------------------------------ tree-7 mirror

    /// @dev The ring key's domain. Named in `FinalStateTrees`' key.* family
    /// because that is the space it lives in; computed HERE because the log is
    /// the writer and the backend mirrors this function, not the tree.
    bytes32 public constant DOMAIN_INTENT_KEY = keccak256("FinalStateTrees.key.intent.v01");
    /// @dev The status leaf's domain.
    bytes32 public constant DOMAIN_INTENT_STATUS_LEAF = keccak256("FINAL_INTENT_STATUS_LEAF_v01");
    /// @notice Ring size: keys recycle at `CAPACITY`, so the tree can never
    /// fill however long the chain runs. Pinned equal to
    /// `FinalStateTrees.CAPACITY` by test.
    uint64 public constant INTENT_SLOT_RING = uint64(1) << 20;

    /// @notice The tree this log writes. Restated from `FinalStateTrees`
    /// because a contract-type constant is not reachable here; pinned equal to
    /// `trees.TREE_INTENTS()` by test.
    uint8 public constant TREE_INTENTS_ID = 7;
    /// @notice The branch the intent ring lives in — `FinalStateTrees.BRANCH_MAIN`,
    ///         pinned by test. Branch 0 of tree 7 is the log's configuration.
    uint8 public constant BRANCH_MAIN_ID = 1;

    /// @notice Tree-7 status values.
    uint8 public constant STATUS_POSTED = 1;
    uint8 public constant STATUS_APPROVED = 2;
    uint8 public constant STATUS_CONSUMED = 3;
    uint8 public constant STATUS_CANCELLED = 4;

    /// @dev Registrar-quorum actions, verified by the registry with this log as
    /// the verifying contract.
    bytes32 public constant ACTION_SET_CONSUMER = keccak256("FINAL_INTENT_LOG_SET_CONSUMER_v01");
    bytes32 public constant ACTION_SEED_SEQUENCE = keccak256("FINAL_INTENT_LOG_SEED_SEQUENCE_v01");
    bytes32 public constant ACTION_SET_BOND_POLICY = keccak256("FINAL_INTENT_LOG_SET_BOND_POLICY_v01");

    /**
     * @notice The longest an intent may stay consumable past its earliest
     *         execution moment.
     *
     * @dev A cap rather than an exact value because every lane shares this log,
     *      and an uncapped deadline would let a posting sit consumable forever —
     *      which is a replay window dressed as a long-lived intent. An immediate
     *      intent (`executeNotBefore == 0`) gets exactly this from `now`; a
     *      scheduled one gets it from its own start.
     */
    /// @dev MILLISECONDS, like every duration on this chain. Its predecessor
    ///      was once 48 hours read against a millisecond clock — 48 SECONDS —
    ///      so a header whose deadline a client computed from wall time was
    ///      refused before it could ever be posted.
    uint64 public constant EXECUTION_WINDOW = 48 hours * FinalChainTime.MS_PER_SECOND;

    /// @notice How far ahead `executeNotBefore` may sit. The scheduling
    /// horizon: chosen at signing, because the deadline is inside the signed
    /// intent and cannot be extended afterwards.
    uint64 public constant MAX_SCHEDULE_HORIZON = 30 days * FinalChainTime.MS_PER_SECOND;

    /// @notice How far ahead of `executeNotBefore` a scheduled posting may be
    ///         CONSUMED when its target chain has no lead of its own (W1, ruled
    ///         2026-09-03: 90 seconds, per target chain). Admission — the
    ///         co-signer round and the append that consumes the leaf — runs
    ///         before T, so the fleet can compose and broadcast the wrapper to
    ///         land in the first target-chain block at or after T rather than
    ///         minutes late. The holder's veto (`cancel`) closes at consume,
    ///         i.e. no earlier than T − lead: the lead is the last call for a
    ///         cancel. An immediate posting (`executeNotBefore == 0`) has no
    ///         lead. Per chain: a row in tree 7's CONFIGURATION branch
    ///         (`FinalStateTrees.setConfig`, key `configKey(CONFIG_SCHEDULE_LEAD_MS,
    ///         targetChainRef)`) overrides this default (`scheduleLeadMsOf`),
    ///         explicit zero included — the first tenant of the config branch.
    uint64 public constant DEFAULT_SCHEDULE_LEAD_MS = 90 * FinalChainTime.MS_PER_SECOND;

    /// @notice The config-row NAME of a target chain's schedule lead, in
    ///         milliseconds: `trees.configKey(CONFIG_SCHEDULE_LEAD_MS, targetChainRef)`
    ///         → one word holding the lead. Written under the configuration
    ///         authority (bootstrap admin, then the registrar quorum) — an
    ///         operational parameter, not a trust boundary — and PROVABLE like
    ///         every other row of the plane, which a table here was not.
    bytes32 public constant CONFIG_SCHEDULE_LEAD_MS = keccak256("FinalIntentLog.config.scheduleLeadMs.v01");

    struct Posted {
        /// @dev Zero means never posted. Non-zero and `consumedAt == 0` means open.
        uint64 postedAt;
        /// @dev From the header, in MILLISECONDS. Before this, `consume` refuses.
        uint64 executeNotBefore;
        /// @dev From the header, in MILLISECONDS. After this, `consume` refuses.
        uint64 deadline;
        /// @dev Block timestamp of the anchoring append. Non-zero means spent.
        uint64 consumedAt;
        /// @dev `keccak256(body)` — the leaf↔body binding. With the reveal
        ///      schema gone it is checked OFF-chain: the execution chain's
        ///      public calldata against this word.
        bytes32 bodyCommitment;
        /// @dev `keccak256` of the per-intent approval public key; zero iff
        ///      AUTO_APPROVE. Names a key, never an account.
        bytes32 approvalKeyCommit;
        /// @dev From the header; kept for the tree-7 status leaf.
        bytes32 targetChainRef;
        /// @dev When `approve` verified. Non-zero means approved.
        uint64 approvedAt;
        /// @dev When `cancel` verified. Non-zero means dead: `consume` refuses.
        uint64 cancelledAt;
        /// @dev Posting sequence — the tree-7 ring position (`seq mod 2^20`).
        uint64 seq;
        /// @dev Header flags, verbatim.
        uint8 flags;
    }

    /// @dev The bytes, kept apart from `Posted` on purpose. `consume` runs on
    ///      the anchoring path and reads only the packed record; it must never
    ///      pay to walk 16 KB it does not look at.
    struct Envelope {
        /// @dev Canonical header, exactly as posted.
        bytes header;
        /// @dev The sealed body. This contract cannot read it and never tries.
        bytes ciphertext;
        /// @dev The content key and the account's KEM public keys, sealed to
        ///      the executor set. Delivered by `approve` — so before approval
        ///      the executor holds ciphertext it cannot open — or present from
        ///      posting on an AUTO_APPROVE intent. Opaque here: this contract
        ///      stores it and never parses it.
        bytes executorSection;
    }

    /// @notice Keyed by the intent LEAF, not by an envelope id.
    ///
    /// @dev The leaf is what `FinalBundleLog` folds and what the execution
    ///      chain's gateway recomputes, so keying on it makes the presence check
    ///      exact. It also makes duplicate protection land on the right thing: a
    ///      second header carrying a leaf already posted is the same intent
    ///      offered twice, and is refused.
    mapping(bytes32 leaf => Posted) public postedOf;

    /// @notice The full envelope, retrievable for as long as the chain exists.
    mapping(bytes32 leaf => Envelope) private _envelopeOf;

    /// @notice Postings ever made. The next intent takes this as its `seq`.
    uint64 public postSeq;
    /// @notice The leaf posted at sequence `seq` — the enumeration the backend
    ///         walks (`postSeq` is the count) instead of a `getLogs` range.
    mapping(uint64 => bytes32) public leafAt;

    // ─────────────────────────── the per-vertex bond ───────────────────────────
    //
    // **Encrypting the account removes attribution, and attribution is what
    // rate-limiting runs on.** Posting is permissionless at a 1 wei base fee and
    // state spam does not age out the way calldata spam does, so the thing
    // holding it off today is the default-deny allowlist on this chain's RPC —
    // a perimeter, and precisely the one that has to come down for users to post
    // their own intents. A bond charges the spammer regardless of identity,
    // needs no new cryptography, and is the natural unit for operators paid per
    // unit of work.
    //
    // Refunded on valid execution, forfeited on a vertex that was never
    // anchored. Both are PULL: `consume` runs inside the anchoring append and
    // must not be able to fail, or be delayed, because of where a refund was
    // going.
    //
    // **Not in the header.** The header's `<authenticator>` field stays a
    // free-form reference for off-chain accounting and is deliberately not the
    // bond itself. Moving the bond into the header would be an encoding change
    // rippling through every reader and the cross-repo offset parity, and a
    // bond is not worth one when native value keyed by leaf says the same
    // thing.
    //
    // **Zero is the shipped state**, which is today's behaviour exactly. Arming
    // it is the step that must precede opening the RPC, not follow it.

    struct Bond {
        /// @dev Who paid, and who a refund goes back to. Not the intent's
        /// account — that is encrypted and this contract cannot know it — which
        /// is the whole reason a bond works here and a per-account quota does
        /// not.
        address payer;
        /// @dev Wei paid. Stored rather than re-read from `bondWei`, because the
        /// rate can move between posting and settlement and a refund of
        /// something other than what was paid is a fee nobody agreed to.
        /// `uint88` covers 309 million ether and packs the struct into one slot.
        uint88 amount;
        /// @dev Paid out or forfeited. Set before the transfer.
        bool settled;
    }

    /// @notice The bond posted with each intent, if any.
    mapping(bytes32 leaf => Bond) public bondOf;

    /// @notice What `post` requires. Zero disables the bond entirely.
    uint256 public bondWei;

    /// @notice Where a forfeited bond goes.
    /// @dev Zero means BURN — the value stays in this contract and nothing can
    /// move it. That is the safe default rather than an oversight: a forfeit
    /// destination is a revenue stream, and one set by accident is worse than
    /// one that does not exist. Arming a bond without naming a destination is
    /// refused, so this can never be reached by forgetting.
    address public bondForfeitTo;

    /// @notice The bond rate, or its destination, changed.
    event BondPolicySet(uint256 bondWei, address forfeitTo);
    /// @notice A bond was posted alongside an intent.
    event BondPosted(bytes32 indexed leaf, address indexed payer, uint256 amount);
    /// @notice A bond was returned after the intent was anchored.
    event BondRefunded(bytes32 indexed leaf, address indexed payer, uint256 amount);
    /// @notice A bond was forfeited: the intent expired without being anchored.
    event BondForfeited(bytes32 indexed leaf, address indexed payer, uint256 amount);

    error BondMismatch(uint256 required, uint256 supplied);
    error NoBond(bytes32 leaf);
    error BondAlreadySettled(bytes32 leaf);
    error BondNotRefundable(bytes32 leaf);
    error BondNotForfeitable(bytes32 leaf, uint64 deadline);
    error BondTransferFailed(address to, uint256 amount);
    error BondNeedsAForfeitDestination();

    /// @notice The one contract allowed to consume. Set once.
    address public consumer;

    /// @notice Discovery only. The envelope itself is in state — read it with
    ///         `envelopeOf`, which needs no `getLogs` range.
    event IntentPosted(
        bytes32 indexed leaf,
        bytes32 indexed targetChainRef,
        uint64 executeNotBefore,
        uint64 deadline,
        uint8 flags,
        uint256 headerBytes,
        uint256 ciphertextBytes
    );
    /// @notice The user read the posted intent back and approved it. Carries
    ///         the executor section's size — the bytes that let the executor
    ///         decrypt from here on.
    event IntentApproved(bytes32 indexed leaf, uint256 executorSectionBytes);
    /// @notice The approval key revoked the intent. Terminal.
    event IntentCancelled(bytes32 indexed leaf);
    /// @notice Consumed by an anchoring append.
    event IntentConsumed(bytes32 indexed leaf, address indexed by);
    event ConsumerSet(address indexed consumer);
    /// @notice A target chain's schedule lead was configured (W1).

    error HeaderTooShort(uint256 length);
    error UnsupportedHeaderVersion(uint8 version);
    error DeadlinePassed(uint64 deadline, uint64 nowMs);
    error DeadlineTooFar(uint64 deadline, uint64 limit);
    error ZeroLeaf();
    error AlreadyPosted(bytes32 leaf);
    error NotPosted(bytes32 leaf);
    error AlreadyConsumed(bytes32 leaf, uint64 at);
    error Expired(bytes32 leaf, uint64 deadline);
    /// @notice Header flag bits beyond the ones this version defines.
    error UnsupportedFlags(uint8 flags);
    /// @notice An AUTO_APPROVE header must commit to no approval key.
    error AutoApproveTakesNoCommitment(bytes32 approvalKeyCommit);
    /// @notice An approval-gated header must commit to one.
    error ApprovalKeyCommitRequired();
    /// @notice `executeNotBefore` sits beyond the scheduling horizon.
    error ScheduleTooFar(uint64 executeNotBefore, uint64 limit);
    /// @notice The presented key does not hash to the header's commitment.
    error WrongApprovalKey(bytes32 expected, bytes32 got);
    /// @notice The signature did not verify under the committed key.
    error ApprovalSignatureInvalid(bytes32 leaf);
    error AlreadyApproved(bytes32 leaf, uint64 at);
    /// @notice AUTO_APPROVE intents commit to no key: nothing to approve or
    /// cancel per-intent. Their veto is the account-wide freeze.
    error NoApprovalKey(bytes32 leaf);
    error IntentIsCancelled(bytes32 leaf, uint64 at);
    /// @notice `executeNotBefore` has not arrived. Scheduling is a chain rule.
    error TooEarly(bytes32 leaf, uint64 executeNotBefore, uint64 nowMs);
    /// @notice Neither approved nor AUTO_APPROVE: the user has not read it back.
    error NotApproved(bytes32 leaf);
    error NotConsumer(address caller);
    error ConsumerUnset();
    error ConsumerAlreadySet(address current);
    error ZeroConsumer();
    error NotAuthorized(address caller);

    /**
     * @dev No precompile of its own to call — this contract verifies no
     *      signature and reads no PQ key, it hashes and compares. The probe is
     *      still here because the claim "these deploy to Final Chain and nowhere
     *      else" should be enforced rather than documented, and a posting log on
     *      an execution chain would be a place intents could be posted that no
     *      anchor will ever read.
     */
    constructor(FinalIdentityRegistry registry_, FinalStateTrees trees_) {
        FinalChainPrecompiles.assertAvailable();
        registry = registry_;
        trees = trees_;
    }

    /// @notice A fresh log took over the previous log's sequence.
    event SequenceSeeded(uint64 postSeq);
    /// @notice The sequence can be seeded only into a log that holds nothing.
    error NotFresh();

    /**
     * @notice Take over the previous log's `postSeq`, so every walker's cursor
     *         (`postSeq` is the count; the fleet walks it, never a `getLogs`
     *         range) stays ahead of nothing and behind everything new. Slots
     *         below the seed are the old log's. NO-WIPE redeploy, ruled
     *         2026-09-03. Configuration authority; only while empty.
     */
    function seedSequence(
        uint64 postSeq_,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireConfigurationAuthority(
            ACTION_SEED_SEQUENCE, keccak256(abi.encode(postSeq_)), anchorBlock, approvals
        );
        if (postSeq != 0) revert NotFresh();
        postSeq = postSeq_;
        emit SequenceSeeded(postSeq_);
    }

    /**
     * @dev The configuration gate: the registry's bootstrap admin alone while
     * its window is open, the sealed `ROLE_REGISTRAR` quorum afterwards — the
     * same window and quorum the registry and the trees use. `approvals` is
     * empty during bootstrap.
     */
    function _requireConfigurationAuthority(
        bytes32 actionDomain,
        bytes32 payloadDigest,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) private {
        if (!registry.bootstrapSealed() && msg.sender == registry.bootstrapAdmin()) return;
        registry.requireRegistrarQuorum(actionDomain, payloadDigest, anchorBlock, approvals);
    }

    /**
     * @notice Name the contract allowed to consume postings.
     *
     * @dev One-way and one-shot, like `FinalStateTrees.treeWriter`. Deployment
     *      is circular — the bundle log needs this address in its constructor —
     *      so this is set afterwards, and it can never be moved: a consumer that
     *      could be repointed would be a consumer that could be replaced with
     *      one that does not check.
     *
     *      Authorized, and it has to be. Left open it would be a front-run away
     *      from permanent: an attacker naming their own contract first would
     *      brick the PQ lane with no recovery, because one-shot cuts both ways.
     */
    function setConsumer(
        address newConsumer,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireConfigurationAuthority(
            ACTION_SET_CONSUMER, keccak256(abi.encode(newConsumer)), anchorBlock, approvals
        );
        if (consumer != address(0)) revert ConsumerAlreadySet(consumer);
        if (newConsumer == address(0)) revert ZeroConsumer();
        consumer = newConsumer;
        emit ConsumerSet(newConsumer);
    }

    /// @notice The lead a scheduled posting for `targetChainRef` gets: the
    ///         config-branch row when one exists (explicit zero included),
    ///         else `DEFAULT_SCHEDULE_LEAD_MS`. Capped at the schedule horizon:
    ///         a longer lead would make every scheduled posting consumable at
    ///         post time. The fleet reads this so the watch and the contract
    ///         agree on when an attempt may begin.
    function scheduleLeadMsOf(bytes32 targetChainRef) public view returns (uint64) {
        (bytes32 value, bool present) =
            trees.configValue(TREE_INTENTS_ID, trees.configKey(CONFIG_SCHEDULE_LEAD_MS, targetChainRef));
        if (!present) return DEFAULT_SCHEDULE_LEAD_MS;
        uint256 lead = uint256(value);
        return lead > MAX_SCHEDULE_HORIZON ? MAX_SCHEDULE_HORIZON : uint64(lead);
    }

    /**
     * @notice Post an intent. Permissionless.
     *
     * @dev Permissionless on purpose. Gating posting on an identity would
     *      reintroduce exactly the attribution the encryption removes, so what
     *      bounds a spammer is the BOND rather than a quota: `msg.value` must
     *      equal `bondWei`, and it comes back only if the intent is anchored.
     *
     *      While `bondWei` is zero — the shipped state — this is what it has
     *      always been, and `msg.value` must then be zero too. Accepting a
     *      payment the contract has no rule for would be accepting value it
     *      cannot refund.
     *
     * @param header Canonical envelope header. Parsed, never stored.
     * @param ciphertext The sealed body. Emitted, never stored, never read.
     * @return leaf The intent leaf this posting is keyed on.
     */
    function post(bytes calldata header, bytes calldata ciphertext)
        external
        payable
        returns (bytes32 leaf)
    {
        if (header.length < HEADER_MIN_BYTES) revert HeaderTooShort(header.length);
        uint8 version = uint8(header[0]);
        if (version < HEADER_VERSION || version > HEADER_VERSION_MAX) {
            revert UnsupportedHeaderVersion(version);
        }
        uint8 flags = uint8(header[OFF_FLAGS]);
        if (flags & ~FLAG_AUTO_APPROVE != 0) revert UnsupportedFlags(flags);

        bytes32 targetChainRef = bytes32(header[OFF_TARGET_CHAIN_REF:OFF_TARGET_CHAIN_REF + 32]);
        uint64 executeNotBefore =
            uint64(bytes8(header[OFF_EXECUTE_NOT_BEFORE:OFF_EXECUTE_NOT_BEFORE + 8]));
        uint64 deadline = uint64(bytes8(header[OFF_DEADLINE:OFF_DEADLINE + 8]));
        bytes32 bodyCommitment = bytes32(header[OFF_BODY_COMMITMENT:OFF_BODY_COMMITMENT + 32]);
        leaf = bytes32(header[OFF_INTENT_LEAF:OFF_INTENT_LEAF + 32]);
        bytes32 approvalKeyCommit =
            bytes32(header[OFF_APPROVAL_KEY_COMMIT:OFF_APPROVAL_KEY_COMMIT + 32]);

        // The commitment and the flag are one decision stated twice, so the two
        // must agree: an auto intent with a commitment would look cancellable
        // and not be gated, and a gated one without a commitment could never be
        // approved by anyone — dead on arrival, wearing a live intent's shape.
        bool auto_ = flags & FLAG_AUTO_APPROVE != 0;
        if (auto_ && approvalKeyCommit != bytes32(0)) {
            revert AutoApproveTakesNoCommitment(approvalKeyCommit);
        }
        if (!auto_ && approvalKeyCommit == bytes32(0)) revert ApprovalKeyCommitRequired();

        if (leaf == bytes32(0)) revert ZeroLeaf();
        if (postedOf[leaf].postedAt != 0) revert AlreadyPosted(leaf);

        // Scheduling is a contract rule. An immediate intent gets the window
        // from now; a scheduled one gets it from its own start, and the start
        // itself is bounded by the horizon — chosen at signing, because the
        // deadline is inside the signed intent and cannot be extended after.
        uint64 nowMs = FinalChainTime.nowMs();
        if (deadline <= nowMs) revert DeadlinePassed(deadline, nowMs);
        if (executeNotBefore == 0) {
            uint64 limit = nowMs + EXECUTION_WINDOW;
            if (deadline > limit) revert DeadlineTooFar(deadline, limit);
        } else {
            uint64 horizon = nowMs + MAX_SCHEDULE_HORIZON;
            if (executeNotBefore > horizon) revert ScheduleTooFar(executeNotBefore, horizon);
            uint64 limit = executeNotBefore + EXECUTION_WINDOW;
            if (deadline > limit) revert DeadlineTooFar(deadline, limit);
        }

        // The bond scales with lifetime. A month-long posting occupies state
        // and attention a 48-hour one does not, and a flat bond makes
        // long-lived spam the cheap kind. Ceiling division, so a single extra
        // millisecond of a new window costs a whole unit.
        uint256 required = bondWei;
        if (required != 0) {
            required = required
                * ((uint256(deadline) - nowMs + EXECUTION_WINDOW - 1) / EXECUTION_WINDOW);
        }
        if (required > type(uint88).max) revert BondMismatch(type(uint88).max, required);
        if (msg.value != required) revert BondMismatch(required, msg.value);

        uint64 seq = postSeq;
        postSeq = seq + 1;
        leafAt[seq] = leaf;

        postedOf[leaf] = Posted({
            postedAt: nowMs,
            executeNotBefore: executeNotBefore,
            deadline: deadline,
            consumedAt: 0,
            bodyCommitment: bodyCommitment,
            approvalKeyCommit: approvalKeyCommit,
            targetChainRef: targetChainRef,
            approvedAt: 0,
            cancelledAt: 0,
            seq: seq,
            flags: flags
        });
        _envelopeOf[leaf] = Envelope({header: header, ciphertext: ciphertext, executorSection: ""});

        if (required != 0) {
            bondOf[leaf] = Bond({payer: msg.sender, amount: uint88(required), settled: false});
            emit BondPosted(leaf, msg.sender, required);
        }

        _writeStatus(leaf, postedOf[leaf], STATUS_POSTED);
        emit IntentPosted(
            leaf, targetChainRef, executeNotBefore, deadline, flags, header.length, ciphertext.length
        );
    }

    /**
     * @notice Set the bond rate and where a forfeit goes.
     *
     * @dev Same gate as `setConsumer`, and unlike it this is NOT one-shot: a
     * bond is a price and a price that could never move would be a parameter
     * chosen once, before the traffic it is meant to bound existed.
     *
     * Arming a non-zero bond without a forfeit destination is refused. Zero
     * means burn, which is a real choice — but it has to be made rather than
     * arrived at by leaving a field unset, because the difference is an
     * accumulating balance nobody can ever move.
     *
     * Changing the rate does not touch bonds already posted. Each stores what
     * was actually paid, so a rate rise cannot retroactively underpay a refund
     * and a cut cannot leave one over-funded.
     */
    function setBondPolicy(
        uint256 newBondWei,
        address forfeitTo,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireConfigurationAuthority(
            ACTION_SET_BOND_POLICY, keccak256(abi.encode(newBondWei, forfeitTo)), anchorBlock, approvals
        );
        if (newBondWei != 0 && forfeitTo == address(0)) {
            // Only when ARMING. Setting the rate back to zero with no
            // destination is disarming, which needs no destination.
            revert BondNeedsAForfeitDestination();
        }
        if (newBondWei > type(uint88).max) revert BondMismatch(type(uint88).max, newBondWei);
        bondWei = newBondWei;
        bondForfeitTo = forfeitTo;
        emit BondPolicySet(newBondWei, forfeitTo);
    }

    /**
     * @notice Return a bond whose intent was anchored.
     *
     * @dev Permissionless to CALL and fixed in destination: the money goes to
     * the recorded payer whoever asks. That is what lets a relayer sweep on a
     * user's behalf without being able to redirect anything.
     *
     * A pull rather than a push inside `consume`. `consume` runs in the
     * anchoring append, and a refund there would put an arbitrary payer's
     * `receive` on the path of every bundle — one contract that reverts, or
     * burns the gas, and the anchor fails for reasons that have nothing to do
     * with the bundle.
     *
     * `consumedAt != 0` is the whole test. Consumption means `FinalBundleLog`
     * folded this leaf into an anchored bundle, which is exactly "valid
     * execution" — there is no later outcome for the chain to wait on, because
     * from here the execution chains authorize against the anchor.
     */
    function claimBond(bytes32 leaf) external {
        Bond storage b = bondOf[leaf];
        if (b.payer == address(0)) revert NoBond(leaf);
        if (b.settled) revert BondAlreadySettled(leaf);
        if (postedOf[leaf].consumedAt == 0) revert BondNotRefundable(leaf);

        b.settled = true;
        uint256 amount = b.amount;
        address payer = b.payer;
        emit BondRefunded(leaf, payer, amount);
        _send(payer, amount);
    }

    /**
     * @notice Forfeit the bond on an intent that expired without being anchored.
     *
     * @dev Permissionless, and gated on the DEADLINE rather than on a judgement.
     * An intent past its deadline can never be consumed — `consume` refuses one
     * — so "expired and unconsumed" is a terminal, objective state, and anyone
     * may say so.
     *
     * That is the half of the bond that actually bounds spam. A refund on
     * success only makes an honest posting free; the cost of a vertex that goes
     * nowhere is what a spammer pays.
     */
    function forfeitBond(bytes32 leaf) external {
        Bond storage b = bondOf[leaf];
        if (b.payer == address(0)) revert NoBond(leaf);
        if (b.settled) revert BondAlreadySettled(leaf);
        Posted storage p = postedOf[leaf];
        if (p.consumedAt != 0) revert BondNotForfeitable(leaf, p.deadline);
        if (FinalChainTime.nowMs() <= p.deadline) revert BondNotForfeitable(leaf, p.deadline);

        b.settled = true;
        uint256 amount = b.amount;
        address to = bondForfeitTo;
        emit BondForfeited(leaf, b.payer, amount);
        // A zero destination BURNS: the value stays here and nothing can move
        // it. Deliberate, and the reason `setBondPolicy` refuses to arm a bond
        // without a destination unless somebody chose this one.
        if (to != address(0)) _send(to, amount);
    }

    /// @dev Checks-effects-interactions is done by the caller — `settled` is set
    /// before this runs — so a reentrant call finds the bond already spent.
    ///
    /// Full gas rather than a stipend, because a payer may legitimately be a
    /// contract. The consequence to accept: a payer whose `receive` reverts
    /// cannot be refunded and the bond is stuck. That is their own contract's
    /// behaviour, and the alternative — swallowing the failure — would mark the
    /// bond settled while the money stayed here.
    function _send(address to, uint256 amount) private {
        (bool ok,) = to.call{value: amount}("");
        if (!ok) revert BondTransferFailed(to, amount);
    }

    /**
     * @notice Approve a posted intent: the read-back that authorizes execution.
     *
     * @dev Anyone may SUBMIT this — the authority is the signature and the
     *      sender only pays gas, the same standing the guardian lanes give
     *      their relay. The key arrives in calldata and proves something only
     *      because it is checked against the commitment the header made at
     *      posting: storage-anchored evidence, exactly the rule `FinalPqQuorum`
     *      states for quorum keys.
     *
     *      The digest binds this chain, this log, the leaf AND the executor
     *      section, so an approval cannot be replayed here or elsewhere, and
     *      the section it delivered cannot be swapped for another under the
     *      same signature.
     *
     * @param publicKey The per-intent ML-DSA-87 public key (2592 B).
     * @param signature Over the 32-byte approval digest, verbatim (4627 B).
     * @param executorSection The content key and the account's KEM public
     *        keys, sealed to the executor set. Stored beside the envelope;
     *        this contract never parses it.
     */
    function approve(
        bytes32 leaf,
        bytes calldata publicKey,
        bytes calldata signature,
        bytes calldata executorSection
    ) external {
        Posted storage p = _requireActionable(leaf);
        if (p.approvedAt != 0) revert AlreadyApproved(leaf, p.approvedAt);
        uint64 nowMs = FinalChainTime.nowMs();
        if (nowMs > p.deadline) revert Expired(leaf, p.deadline);

        bytes32 digest = keccak256(
            abi.encode(
                DOMAIN_INTENT_APPROVE, block.chainid, address(this), leaf, keccak256(executorSection)
            )
        );
        _requireApprovalKey(p, publicKey, signature, digest, leaf);

        p.approvedAt = nowMs;
        _envelopeOf[leaf].executorSection = executorSection;
        _writeStatus(leaf, p, STATUS_APPROVED);
        emit IntentApproved(leaf, executorSection.length);
    }

    /**
     * @notice Revoke a posted intent. Terminal, and allowed AFTER approval:
     *         a user may change their mind at any point before execution.
     *
     * @dev No deadline check — cancelling an expired intent is harmless and
     *      refusing it would fail a retry for nothing. Only consumption closes
     *      the door, because consumption is execution.
     */
    function cancel(bytes32 leaf, bytes calldata publicKey, bytes calldata signature) external {
        Posted storage p = _requireActionable(leaf);

        bytes32 digest =
            keccak256(abi.encode(DOMAIN_INTENT_CANCEL, block.chainid, address(this), leaf));
        _requireApprovalKey(p, publicKey, signature, digest, leaf);

        p.cancelledAt = FinalChainTime.nowMs();
        _writeStatus(leaf, p, STATUS_CANCELLED);
        emit IntentCancelled(leaf);
    }

    /// @dev Posted, not consumed, not cancelled — the states in which the
    /// approval key still has anything to say.
    function _requireActionable(bytes32 leaf) private view returns (Posted storage p) {
        p = postedOf[leaf];
        if (p.postedAt == 0) revert NotPosted(leaf);
        if (p.consumedAt != 0) revert AlreadyConsumed(leaf, p.consumedAt);
        if (p.cancelledAt != 0) revert IntentIsCancelled(leaf, p.cancelledAt);
    }

    /// @dev The committed key, and a valid ML-DSA-87 signature under it over
    /// the 32-byte digest verbatim — the same convention every quorum approval
    /// follows. An AUTO_APPROVE posting committed to nothing and has nothing
    /// to approve or cancel; its veto is the account-wide freeze.
    function _requireApprovalKey(
        Posted storage p,
        bytes calldata publicKey,
        bytes calldata signature,
        bytes32 digest,
        bytes32 leaf
    ) private view {
        bytes32 commit = p.approvalKeyCommit;
        if (commit == bytes32(0)) revert NoApprovalKey(leaf);
        bytes32 got = keccak256(publicKey);
        if (got != commit) revert WrongApprovalKey(commit, got);
        if (!FinalChainPrecompiles.verifyMlDsa87(publicKey, abi.encodePacked(digest), signature)) {
            revert ApprovalSignatureInvalid(leaf);
        }
    }

    // ------------------------------------------------------ tree-7 mirror

    /// @notice The tree-7 key for posting sequence `seq` — a RING, so slots
    ///         recycle at `INTENT_SLOT_RING` and the tree can never fill.
    /// @dev Mirrored by the backend byte for byte; pinned cross-repo.
    function intentSlotKey(uint64 seq) public pure returns (bytes32) {
        return keccak256(
            abi.encodePacked(DOMAIN_INTENT_KEY, bytes32(uint256(seq % INTENT_SLOT_RING)))
        );
    }

    /// @notice The tree-7 status leaf. Everything in it is public log state —
    ///         nothing account-linked, so the tree adds searchability without
    ///         touching unlinkability. Expiry is derived from `deadline` by the
    ///         reader, never written.
    function intentStatusLeaf(
        bytes32 intentLeaf,
        uint8 status,
        uint64 postedAt,
        uint64 executeNotBefore,
        uint64 deadline,
        bytes32 targetChainRef,
        bytes32 bodyCommitment,
        uint64 seq
    ) public pure returns (bytes32) {
        return keccak256(
            bytes.concat(
                DOMAIN_INTENT_STATUS_LEAF,
                abi.encode(
                    intentLeaf,
                    status,
                    postedAt,
                    executeNotBefore,
                    deadline,
                    targetChainRef,
                    bodyCommitment,
                    seq
                )
            )
        );
    }

    /// @dev One transition, one write. The log is `treeWriter[7]`, and the
    /// tree-1 argument applies verbatim: everything this mirrors was already
    /// verified here, so no quorum belongs on top.
    function _writeStatus(bytes32 leaf, Posted storage p, uint8 status) private {
        bytes32[] memory keys = new bytes32[](1);
        bytes32[] memory statusLeaves = new bytes32[](1);
        keys[0] = intentSlotKey(p.seq);
        statusLeaves[0] = intentStatusLeaf(
            leaf,
            status,
            p.postedAt,
            p.executeNotBefore,
            p.deadline,
            p.targetChainRef,
            p.bodyCommitment,
            p.seq
        );
        trees.setLeavesAsWriter(TREE_INTENTS_ID, BRANCH_MAIN_ID, keys, statusLeaves);
    }

    /**
     * @notice Consume a posted intent as part of an anchoring append.
     *
     * @dev Called by `FinalBundleLog` in the same transaction as the append, so
     *      there is no window in which a bundle is anchored and its intents are
     *      not yet spent.
     *
     *      Consumption is permanent and is the replay gate. The execution chain
     *      keeps its own consumed-seqId set, which covers a bundle being
     *      re-executed; this covers an intent being re-anchored into a second
     *      bundle, which that set does not see.
     */
    function consume(bytes32 leaf) external {
        // Checked before the comparison, not folded into it. An unset consumer
        // is `address(0)`, and `msg.sender != consumer` would then be FALSE for
        // a caller of `address(0)` — so an unwired log would consume for the one
        // caller nobody can be, which is the kind of "unreachable" that stops
        // being unreachable the moment something else changes.
        address c = consumer;
        if (c == address(0)) revert ConsumerUnset();
        if (msg.sender != c) revert NotConsumer(msg.sender);
        Posted storage p = postedOf[leaf];
        if (p.postedAt == 0) revert NotPosted(leaf);
        if (p.consumedAt != 0) revert AlreadyConsumed(leaf, p.consumedAt);
        if (p.cancelledAt != 0) revert IntentIsCancelled(leaf, p.cancelledAt);
        uint64 nowMs = FinalChainTime.nowMs();
        if (nowMs > p.deadline) revert Expired(leaf, p.deadline);
        // Scheduling is enforced HERE, which is what makes the funding and fee
        // re-checks execution-time checks for free: admission cannot happen
        // before the moment the intent named — less the lead, so the wrapper
        // that follows admission can land AT that moment rather than minutes
        // after it (`SCHEDULE_LEAD_MS`).
        if (nowMs + _scheduleLeadMs(p) < p.executeNotBefore) {
            revert TooEarly(leaf, p.executeNotBefore, nowMs);
        }
        // The read-back gate. Neither approved nor auto means the user has not
        // seen on chain what is about to execute.
        if (p.approvedAt == 0 && p.flags & FLAG_AUTO_APPROVE == 0) revert NotApproved(leaf);
        p.consumedAt = nowMs;
        _writeStatus(leaf, p, STATUS_CONSUMED);
        emit IntentConsumed(leaf, msg.sender);
    }

    /// @notice The stored envelope. One `eth_call`, no range, any age.
    ///
    /// @dev A getter rather than a public mapping so the three parts come back
    ///      in one call — a caller pairing a header from one read with a
    ///      ciphertext from another would be pairing across a posting that
    ///      landed between them.
    function envelopeOf(bytes32 leaf)
        external
        view
        returns (bytes memory header, bytes memory ciphertext, bytes memory executorSection)
    {
        Envelope storage e = _envelopeOf[leaf];
        return (e.header, e.ciphertext, e.executorSection);
    }

    /// @notice Is `leaf` posted, unspent, uncancelled and unexpired right now?
    function isOpen(bytes32 leaf) external view returns (bool) {
        Posted storage p = postedOf[leaf];
        return p.postedAt != 0 && p.consumedAt == 0 && p.cancelledAt == 0
            && FinalChainTime.nowMs() <= p.deadline;
    }

    /// @notice Would `consume` accept `leaf` right now? Open, due, and either
    ///         approved or AUTO_APPROVE — the work-queue predicate, so a solver
    ///         asks the same question the contract answers.
    function isConsumable(bytes32 leaf) external view returns (bool) {
        Posted storage p = postedOf[leaf];
        uint64 nowMs = FinalChainTime.nowMs();
        return p.postedAt != 0 && p.consumedAt == 0 && p.cancelledAt == 0 && nowMs <= p.deadline
            && nowMs + _scheduleLeadMs(p) >= p.executeNotBefore
            && (p.approvedAt != 0 || p.flags & FLAG_AUTO_APPROVE != 0);
    }

    /// @dev The lead `consume` grants a posting: its target chain's
    ///      (`scheduleLeadMsOf`) for a scheduled one, nothing for an immediate
    ///      one (there is no moment to lead).
    function _scheduleLeadMs(Posted storage p) private view returns (uint64) {
        return p.executeNotBefore == 0 ? 0 : scheduleLeadMsOf(p.targetChainRef);
    }
}

contracts/finalchain/FinalPqQuorum.sol

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2024-2026 Final DeFi
// Licensed under the Business Source License 1.1 (the "License")
//
// Change Date: 2029-01-01
// Change License: GPL-2.0-or-later
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;

import {FinalChainPrecompiles} from "./FinalChainPrecompiles.sol";
import {FinalIdentityRegistry} from "./FinalIdentityRegistry.sol";

/**
 * @title FinalPqQuorum
 * @notice K-of-N approval where the signatures are post-quantum and the chain
 *         is what checks them.
 *
 * @dev This library is the reason Final Chain exists in this design.
 *
 * `FinalBackend/src/pq/credential.js` carries a rule it had to enforce in code
 * because nothing else could: **a surface whose signature is verified on chain
 * cannot be PQ.** A co-signer approval reaching `FinalRootAuthority` is checked
 * by ECDSA/ERC-1271 in Solidity, so a PQ co-signer would produce approvals the
 * contract cannot read, and the quorum would stop reaching threshold with
 * nothing in any log naming the cause. `PQ_SURFACE` and `assertBackendVerified`
 * exist to keep anyone from crossing that line by accident.
 *
 * Here the line is gone. The precompiles verify ML-DSA-87 and
 * SLH-DSA-SHAKE-256s natively, so a quorum can be PQ *and* on chain, and
 * "the backend says these four signatures verified" becomes "these four
 * signatures verify, and any node re-derives that independently".
 *
 * ## Three rules, each closing a specific hole
 *
 * 1. **Keys come from the registry, never from calldata.** A key passed as an
 *    argument proves nothing — anyone with a keypair can sign under it. This is
 *    the difference between a 4-of-5 quorum and a 1-of-1 held by whoever built
 *    the transaction.
 *
 * 2. **Signers strictly ascending.** One comparison per entry rejects duplicates
 *    outright, so a single member cannot supply four approvals and satisfy a
 *    threshold of four. The alternative — an O(n²) seen-check — is the same
 *    guarantee with more ways to get it wrong.
 *
 * 3. **The digest binds chain id and verifying contract.** Without both, an
 *    approval collected for one contract is replayable against another with the
 *    same payload shape, and an approval from the test chain is replayable on
 *    the production one. These co-signers hold one key across environments.
 *
 * ## Which algorithm
 *
 * The stack splits its keys by hardness assumption, not by convenience:
 * ML-DSA-87 (lattice) signs transactions, SLH-DSA-SHAKE-256s (hash-based) signs
 * identity. Two families, so one cryptanalytic result cannot take both.
 *
 * So an action inherits the class of what it authorizes. Advancing a state root
 * is operational and high-cadence: transaction class. Registering or revoking
 * an identity is the thing the access class exists for. `ALG_ANY` is available
 * and should be used sparingly — accepting either means a break in one family
 * takes the quorum.
 *
 * An action that authorizes EXECUTION takes both: the ML-DSA-87 approval and a
 * `seal`, an SLH-DSA-SHAKE-256s signature over the same digest by the member's
 * `activeSeal` key. Neither family alone can then move funds, and the seal key
 * is its own slot — never the access key — so the process that seals cannot
 * also rotate the identity it seals for.
 *
 * Every digest binds an `anchorBlock`: the block at which the members read
 * tree 1 to decide who is in the round. Binding it means every approval in a
 * round was made against ONE roster view, and the window in `require_` means a
 * view older than `ANCHOR_WINDOW` blocks is refused rather than honoured.
 *
 * The practical cost is worth stating: an SLH-DSA signature is 29,792 bytes, so
 * a 4-of-5 access-class quorum is ~119 KB of calldata. That is affordable here
 * only because this is our own chain. Do not carry this pattern to a chain
 * where it is not.
 */
library FinalPqQuorum {
    /// @notice ML-DSA-87 — FIPS 204. Algorithm ids are the FIPS numbers: the
    /// same ids `FinalCertificate` and the backend registry use, and the numbers
    /// the precompile addresses end in (`0x0204`).
    uint8 internal constant ALG_ML_DSA_87 = 4;
    /// @notice SLH-DSA-SHAKE-256s — FIPS 205 (`0x0205`).
    uint8 internal constant ALG_SLH_DSA_SHAKE_256S = 5;
    /// @notice Either scheme is acceptable for this action.
    uint8 internal constant ALG_ANY = 0;

    /// @notice How far behind the chain head an approval's anchor may sit.
    /// @dev Members evaluate roster membership against tree 1 AT the anchor
    /// block. 600 blocks is ten minutes at the chain's one-second cadence —
    /// generous against a round that takes seconds, and short enough that a
    /// roster rotated away is refused rather than counted.
    uint64 internal constant ANCHOR_WINDOW = 600;

    /// @dev Domain separator for every quorum digest. Distinct from any
    /// EIP-712 domain in the stack: these are not typed-data signatures and
    /// must not be confusable with one.
    bytes32 internal constant DOMAIN_PQ_QUORUM = keccak256("FINAL_CHAIN_PQ_QUORUM_v01");

    /// @notice One member's approval.
    struct Approval {
        /// The member's account, which is also the key it is looked up by.
        address signer;
        /// `ALG_ML_DSA_87` or `ALG_SLH_DSA_SHAKE_256S`.
        uint8 algorithm;
        /// Over the 32-byte digest from `digest()`, verbatim. Both schemes
        /// hash internally, so the digest is not re-hashed before signing.
        bytes signature;
        /// SLH-DSA-SHAKE-256s over the same digest, by the member's `activeSeal`
        /// key. Required where the action authorizes execution; empty otherwise.
        bytes seal;
    }

    error ThresholdNotMet(uint256 valid, uint256 required);
    error SignersNotAscending(address previous, address next);
    error SignerLacksRole(address signer, uint256 roleMask);
    error WrongAlgorithm(address signer, uint8 got, uint8 required);
    error BadSignature(address signer, uint8 algorithm);
    error BadSeal(address signer);
    error AnchorAhead(uint64 anchorBlock, uint256 blockNumber);
    error AnchorStale(uint64 anchorBlock, uint256 blockNumber);
    error ThresholdIsZero();

    /**
     * @notice The message every member of this quorum signs.
     * @param verifyingContract The contract consuming the approvals. Binding it
     *        stops an approval collected for one contract being replayed
     *        against another with the same payload shape.
     * @param actionDomain What is being authorized — a per-action constant, so
     *        an approval for "advance the accounts tree" cannot be replayed as
     *        one for "revoke an identity".
     * @param anchorBlock The Final Chain block the members read tree 1 at to
     *        decide the roster. Bound here so every approval in a round names
     *        the same view; checked against `ANCHOR_WINDOW` by `require_`.
     * @param payloadDigest The action's own committed content. Callers MUST
     *        include a nonce or a monotonic counter in it; nothing here can
     *        tell a replay of round 7 from a fresh round 7.
     */
    function digest(
        address verifyingContract,
        bytes32 actionDomain,
        uint64 anchorBlock,
        bytes32 payloadDigest
    ) internal view returns (bytes32) {
        return keccak256(
            abi.encode(
                DOMAIN_PQ_QUORUM,
                block.chainid,
                verifyingContract,
                actionDomain,
                anchorBlock,
                payloadDigest
            )
        );
    }

    /**
     * @notice Reverts unless at least `threshold` distinct members holding
     *         `roleMask` have signed `quorumDigest`.
     * @param registry Where public keys and roles come from. Not a parameter
     *        for flexibility — a parameter so the caller's own immutable
     *        registry address is what is used, rather than one from calldata.
     * @param requiredAlgorithm `ALG_ANY` to accept either scheme.
     * @param anchorBlock The anchor the digest was built over. Refused if it is
     *        ahead of this block or more than `ANCHOR_WINDOW` behind it.
     * @param requireSeal Whether every approval must also carry a valid `seal`
     *        by the member's `activeSeal` key — the execution class.
     * @return valid The number of approvals that verified, which is at least
     *         `threshold` if this returns at all.
     *
     * @dev Every failure reverts with the offending signer named. A quorum that
     * silently skipped bad approvals and counted the rest would let a
     * misconfigured co-signer sit broken indefinitely: the threshold would keep
     * being met by the others and nothing would say one member had stopped
     * contributing. That is exactly the failure this program has already had,
     * in `fanOut`, where a per-chain advance failure was recorded and execution
     * continued.
     */
    function require_(
        FinalIdentityRegistry registry,
        Approval[] calldata approvals,
        bytes32 quorumDigest,
        uint256 roleMask,
        uint256 threshold,
        uint8 requiredAlgorithm,
        uint64 anchorBlock,
        bool requireSeal
    ) internal view returns (uint256 valid) {
        if (threshold == 0) revert ThresholdIsZero();
        if (anchorBlock > block.number) revert AnchorAhead(anchorBlock, block.number);
        if (block.number - anchorBlock > ANCHOR_WINDOW) revert AnchorStale(anchorBlock, block.number);

        bytes memory message = abi.encodePacked(quorumDigest);
        address previous = address(0);

        uint256 n = approvals.length;
        for (uint256 i = 0; i < n; i++) {
            Approval calldata a = approvals[i];

            // Strictly ascending. `address(0)` as the initial value works
            // because it can never be a registered signer.
            if (a.signer <= previous) revert SignersNotAscending(previous, a.signer);
            previous = a.signer;

            if (!registry.hasRole(a.signer, roleMask)) revert SignerLacksRole(a.signer, roleMask);

            if (requiredAlgorithm != ALG_ANY && a.algorithm != requiredAlgorithm) {
                revert WrongAlgorithm(a.signer, a.algorithm, requiredAlgorithm);
            }

            if (!_verify(registry, a, message)) revert BadSignature(a.signer, a.algorithm);
            if (requireSeal && !_verifySeal(registry, a, message)) revert BadSeal(a.signer);

            valid++;
        }

        if (valid < threshold) revert ThresholdNotMet(valid, threshold);
    }

    /// @notice Non-reverting form, for views and for callers that want to
    /// report rather than refuse.
    function count(
        FinalIdentityRegistry registry,
        Approval[] calldata approvals,
        bytes32 quorumDigest,
        uint256 roleMask,
        uint8 requiredAlgorithm,
        uint64 anchorBlock,
        bool requireSeal
    ) internal view returns (uint256 valid) {
        if (anchorBlock > block.number || block.number - anchorBlock > ANCHOR_WINDOW) return 0;
        bytes memory message = abi.encodePacked(quorumDigest);
        address previous = address(0);
        uint256 n = approvals.length;
        for (uint256 i = 0; i < n; i++) {
            Approval calldata a = approvals[i];
            if (a.signer <= previous) return valid;
            previous = a.signer;
            if (!registry.hasRole(a.signer, roleMask)) continue;
            if (requiredAlgorithm != ALG_ANY && a.algorithm != requiredAlgorithm) continue;
            if (!_verify(registry, a, message)) continue;
            if (requireSeal && !_verifySeal(registry, a, message)) continue;
            valid++;
        }
    }

    /// @dev The seal: SLH-DSA-SHAKE-256s by the member's `activeSeal` key over
    /// the same digest. A member with no seal key on record cannot seal, and an
    /// approval with no seal bytes is not one.
    function _verifySeal(
        FinalIdentityRegistry registry,
        Approval calldata a,
        bytes memory message
    ) private view returns (bool) {
        bytes memory key = registry.activeSealKeyOf(a.signer);
        if (key.length == 0 || a.seal.length == 0) return false;
        return FinalChainPrecompiles.verifySlhDsa(key, message, a.seal);
    }

    function _verify(
        FinalIdentityRegistry registry,
        Approval calldata a,
        bytes memory message
    ) private view returns (bool) {
        // The LIVE pair, always. The recovery pair authorizes rotating this
        // account's own credentials and NOTHING else — a quorum that accepted
        // it would hand the recovery keys everyday authority, which is exactly
        // the separation the two stages exist to draw.
        if (a.algorithm == ALG_ML_DSA_87) {
            return FinalChainPrecompiles.verifyMlDsa87(
                registry.activeTransactionKeyOf(a.signer), message, a.signature
            );
        }
        if (a.algorithm == ALG_SLH_DSA_SHAKE_256S) {
            return FinalChainPrecompiles.verifySlhDsa(
                registry.activeAccessKeyOf(a.signer), message, a.signature
            );
        }
        // Any other id is a refusal, never a default — including the KEM ids
        // (3, 7) and the reserved FN-DSA id (6), none of which is a signature
        // scheme this quorum verifies.
        return false;
    }
}

contracts/finalchain/FinalStateTrees.sol

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2024-2026 Final DeFi
// Licensed under the Business Source License 1.1 (the "License")
//
// Change Date: 2029-01-01
// Change License: GPL-2.0-or-later
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;

import {FinalIdentityRegistry} from "./FinalIdentityRegistry.sol";
import {FinalChainTime} from "./FinalChainTime.sol";
import {FinalPqQuorum} from "./FinalPqQuorum.sol";

/// @notice The one question `syncIdentities` asks the asset registry.
/// @dev An interface rather than an import of `FinalAssetRegistry`, which
///      imports this file: the registry is tree 6's writer and holds the trees
///      as an immutable, so the dependency runs that way and this is the one
///      read that runs the other.
interface IChainSource {
    function enabledChainRefs() external view returns (bytes32[] memory);
}

/// @notice The one question {FinalStateTrees.syncSlotKeyLeaves} asks the
///         slot-key registry: the leaf value for one member's slot — the
///         registry's own verdict, zero when the slot holds nothing usable.
interface ISlotKeySource {
    function slotKeyLeafOf(address member, uint64 slotIndex) external view returns (bytes32);
}

/// @notice The one question {FinalStateTrees.syncEndpointLeaves} asks the
///         endpoint registry: the leaf value for one tunnel endpoint — the
///         registry's own verdict (certificate hash, status, expiry, region),
///         zero when nothing is registered under the id.
interface IEndpointSource {
    function endpointLeafOf(bytes32 endpointId) external view returns (bytes32);
}

/**
 * @title FinalStateTrees
 * @notice The eight trees. Final Chain's state plane, and the source of truth
 *         every other chain projects from.
 *
 * @dev One tree per domain, because they change at unrelated cadences and a
 * combined tree invalidates every outstanding proof on every tick:
 *
 * | # | tree | holds | cadence |
 * |---|---|---|---|
 * | 1 | accounts | every Final Wallet's public state | per rotation / creation |
 * | 2 | phi | the PHI record: per (wallet, chain) balances, the lock, exposures | per publisher round |
 * | 3 | vasset | issued vAsset supply and backing, per (asset, chain) | per settlement |
 * | 4 | oracle | published prices and their inputs | ~10 s; 1 s for morph and fee assets |
 * | 5 | settlement | chain and asset registry roots | rarely |
 * | 6 | allowlist | assets, chains, policy, price sources, DEX deployments | rarely |
 * | 7 | intents | intent status, ring-keyed over the posting sequence | per posting |
 * | 8 | identity | the wallet-creation admission set, projected from the registry | per identity mutation |
 *
 * ## The hash shape is not a choice
 *
 * Leaves hash as `keccak256(0x00 ‖ leaf)` and internal nodes as
 * `keccak256(0x01 ‖ lo ‖ hi)` with the pair sorted. That is
 * `FinalMerkle.verifyTaggedSortedProof`, verbatim, which is what
 * `FinalWalletFactory.syncAccountState` and `FinalSettlement` already run on
 * every supported chain. A proof produced here is consumed there with no
 * translation and no contract change, and tree 1's leaf preimage is exactly
 * `FinalWalletFactory.accountStateLeafHash` — same fields, same order, the
 * `deployedChains` table `abi.encode`d like every other field.
 *
 * Getting this wrong is not a compile error anywhere. It is a root every chain
 * silently rejects, with nothing pointing at the cause.
 *
 * ## Positional slots under a sorted-pair tree
 *
 * Sorted pairs make a proof position-agnostic, which is why it carries no
 * direction bits. That does not stop the TREE from being positional, and here
 * it is: every key gets a permanent slot, so a single leaf update is `DEPTH`
 * hashes instead of a rebuild over every leaf. The verifier neither knows nor
 * needs to know that a slot exists.
 *
 * ## Branches (2026-09-04)
 *
 * The slot space of every tree is cut into `BRANCH_COUNT` branches by the top
 * `BRANCH_BITS` of the slot: a branch is a subtree with a permanent place, its
 * root is one internal node, and a leaf's path to the tree root passes through
 * it. Branches hold what belongs to the same domain but not to the same rows
 * — branch 0 is the owning service's CONFIGURATION on every tree, tree 8 adds
 * the owner → wallets index and the co-signers' slot keys beside the admission
 * set — and they are chosen over more trees because a branch shares its
 * tree's authority doors and writer, while a tree would need its own. A leaf
 * proves against its branch root with `BRANCH_DEPTH` siblings, against the
 * tree root with `DEPTH`, against the round root with `ROUND_DEPTH`: one path,
 * cut at three heights, one verifier.
 *
 * ## Rounds, and why the live roots are not the product
 *
 * `setLeaves` moves a tree. It does not publish one. A consumer that fetched
 * eight roots one at a time would get a price proof from one moment and a
 * roster proof from another, and something delisted in between would still
 * verify.
 *
 * `publishRound` snapshots all eight together, and folds them into ONE round
 * root — the tree roots as the level-`DEPTH` nodes of a depth-`ROUND_DEPTH`
 * tree, tree `t` at position `t` — so a single word commits to the whole
 * plane and any leaf in it proves against that word with four more siblings.
 * A round is the unit a consumer pins, and it is the only thing this contract
 * promises is contemporaneous. The execution chains keep anchoring per-tree
 * roots (identity, account state, registry roots): those must move at their
 * own cadence, not at the oracle's.
 */
contract FinalStateTrees {
    // ---------------------------------------------------------------- trees

    /// @notice Every Final Wallet's public state. The source of truth other
    /// chains copy through `syncAccountState`.
    uint8 public constant TREE_ACCOUNTS = 1;
    /// @notice The PHI record, per `(wallet, chain)`: balances, the lock, its
    /// terms, the exposures carved from it and the accrual between reconciliations.
    uint8 public constant TREE_PHI = 2;
    /// @notice vAsset supply and backing.
    uint8 public constant TREE_VASSET = 3;
    /// @notice Oracle prices and their inputs.
    uint8 public constant TREE_ORACLE = 4;
    /// @notice Settlement chain and asset registry roots.
    uint8 public constant TREE_SETTLEMENT = 5;
    /// @notice Which assets and chains are supported.
    uint8 public constant TREE_ALLOWLIST = 6;
    /// @notice Intent status, keyed by a RING over the posting sequence.
    /// @dev The search structure beside `FinalBundleLog`'s permanent record.
    /// Written only by `FinalIntentLog` through `treeWriter[7]` — the tree-1
    /// argument verbatim: the log verified the bond, the commitment, the
    /// approval and the consume itself, and a service quorum on top would be a
    /// censorship point over posting. Slots are permanent and intents are
    /// unbounded flow, so the log recycles keys modulo `CAPACITY`: the tree is
    /// an index with a ~1M-posting retention window, never the record.
    uint8 public constant TREE_INTENTS = 7;
    /// @notice The wallet-creation admission set — the identity leaves
    /// (`keccak256(DOMAIN_IDENTITY_LEAF ‖ serial ‖ keysHash)`) every execution
    /// chain's gateway verifies certificates against.
    /// @dev The root the gateways anchor as `currentIdentityRoot` — a
    /// CONTINUOUS root over this tree replaces the cold-set snapshot the
    /// retired `publishIdentityRoot.cjs` ceremony folded off-chain. Two
    /// feeders, one per identity plane, and NO quorum door for either:
    ///
    /// - SERVICE identities: {syncIdentityLeaves}, the permissionless
    ///   projection of `FinalIdentityRegistry`'s own verdict — the registry
    ///   calls it same-tx on every identity mutation, and anyone may call it
    ///   to retire a leaf whose standing lapsed by TIME (expiry moves no
    ///   registry storage, so only a projection pass can zero it).
    /// - USER identities: `treeWriter[8]` — `FinalAccountLedger`, which
    ///   computes the leaf from the genesis certificate fields it verified
    ///   under its opener quorum and writes it once at `openAccount`. A user
    ///   admission leaf is permanent by construction: the certificate IS the
    ///   address, rotation never changes it, and a post-rotation creation on
    ///   a new chain reads PUBLISHED account state (WAL-02), never the
    ///   certificate's genesis keys.
    ///
    /// A quorum of service signatures must not be able to state an identity
    /// neither ruler decided, so `setLeaves` refuses this tree outright.
    uint8 public constant TREE_IDENTITY = 8;
    /// @notice Count, for iteration. Trees are 1-indexed; 0 is not a tree.
    uint8 public constant TREE_COUNT = 8;

    /// @notice 2^24 slots per tree, laid out as 16 BRANCHES of 2^20: the top
    /// `BRANCH_BITS` of a slot name the branch, the rest its position in it.
    /// A million rows per branch is far past where this design gets replaced
    /// by Final Chain proper. Raising any of this later is a migration, not a
    /// parameter change: the depth is in every root.
    uint256 public constant DEPTH = 24;
    uint256 public constant BRANCH_BITS = 4;
    uint8 public constant BRANCH_COUNT = 16;
    /// @notice Height of a branch: a leaf proves against its branch root with
    /// this many siblings.
    uint256 public constant BRANCH_DEPTH = DEPTH - BRANCH_BITS;
    /// @notice Slots per branch.
    uint256 public constant BRANCH_CAPACITY = 1 << BRANCH_DEPTH;
    /// @notice Slots per tree, all branches together.
    uint256 public constant CAPACITY = 1 << DEPTH;
    /// @notice The round root is a tree over the tree roots — position `t`
    /// holds tree `t`'s root, positions 0 and 9..15 the empty tree — folded
    /// with the same node hash. It is literally the root of a depth-28 tree
    /// whose level-24 nodes are the eight tree roots, which is what lets one
    /// path prove a leaf against it.
    uint256 public constant FOREST_BITS = 4;
    uint256 public constant ROUND_DEPTH = DEPTH + FOREST_BITS;

    /// @notice Branch 0 of EVERY tree: the configuration of the service that
    /// owns the tree — key → one word, the VALUE stored so a contract on this
    /// chain reads it directly (`configValue`), the hash in the tree so it is
    /// provable wherever a round root is. Written only by {setConfig} under
    /// the configuration authority; every other door refuses the branch.
    uint8 public constant BRANCH_CONFIG = 0;
    /// @notice Branch 1 of every tree: the domain's own rows — accounts, PHI
    /// records, vAssets, prices, registry roots, the allowlist, the intent
    /// ring, the identity admission set.
    uint8 public constant BRANCH_MAIN = 1;
    /// @notice Tree 8, branch 2: the owner → wallets index. Key = the owner
    /// (`ownerIndexKeyFor`), leaf = {ownerIndexLeafHash} over the ledger's
    /// `walletsByOwner(owner)`. Written by tree 8's writer, the ledger, beside
    /// every open and every owner transfer — the tree is the search structure,
    /// the ledger holds the readable array it proves.
    uint8 public constant BRANCH_OWNER_INDEX = 2;
    /// @notice Tree 8, branch 3: the co-signers' per-slot KEM publics — a RING
    /// of `SLOT_KEY_RING` positions per member, projected from
    /// `slotKeySource` by {syncSlotKeyLeaves} exactly as identities are.
    uint8 public constant BRANCH_SLOT_KEYS = 3;
    /// @notice Tree 8, branch 4: the tunnel endpoints — the Final Node
    /// identities a wallet's FNP session terminates at. Key = the endpoint id
    /// (`endpointKeyFor`, the certificate's subject key id), leaf = the
    /// endpoint registry's verdict, projected from `endpointSource` by
    /// {syncEndpointLeaves} exactly as slot keys are. An execution chain never
    /// parses an endpoint certificate; it anchors this tree's root and a client
    /// proves the leaf against it (user ruling 2026-09-05).
    uint8 public constant BRANCH_ENDPOINTS = 4;
    /// @notice Slot-key positions per member. A slot index wraps modulo this,
    /// so the branch is an index over the recent slots and never fills; 1024
    /// members × 1024 positions is the branch exactly.
    uint64 public constant SLOT_KEY_RING = 1024;

    /// @dev Must equal `FinalWalletFactory.DOMAIN_ACCOUNT_STATE_LEAF`. Pinned
    /// by the cross-repo parity test; a field reordered on one side and not the
    /// other is a root every chain rejects.
    ///
    /// `v02`: `deployedChains` became the `(chainRef, account)` table. A v01
    /// leaf and a v02 leaf never share a domain, so a proof built against the
    /// retired plane cannot verify against this one by accident.
    bytes32 public constant DOMAIN_ACCOUNT_STATE_LEAF =
        keccak256("FINAL_ACCOUNT_STATE_LEAF_v02");

    bytes32 private constant ACTION_SET_LEAVES = keccak256("FinalStateTrees.setLeaves.v01");
    /// @dev Registrar-quorum actions, verified by the registry with this
    /// contract as the verifying contract. See `FinalIdentityRegistry.requireRegistrarQuorum`.
    bytes32 public constant ACTION_CONFIGURE_TREE = keccak256("FINAL_STATE_TREES_CONFIGURE_TREE_v01");
    bytes32 public constant ACTION_SET_TREE_WRITER = keccak256("FINAL_STATE_TREES_SET_TREE_WRITER_v01");
    bytes32 public constant ACTION_SET_CHAIN_SOURCE = keccak256("FINAL_STATE_TREES_SET_CHAIN_SOURCE_v01");
    bytes32 public constant ACTION_SET_SLOT_KEY_SOURCE = keccak256("FINAL_STATE_TREES_SET_SLOT_KEY_SOURCE_v01");
    bytes32 public constant ACTION_SET_ENDPOINT_SOURCE = keccak256("FINAL_STATE_TREES_SET_ENDPOINT_SOURCE_v01");
    bytes32 public constant ACTION_SEED_COUNTERS = keccak256("FINAL_STATE_TREES_SEED_COUNTERS_v01");
    bytes32 public constant ACTION_SET_TYPED_WRITER = keccak256("FINAL_STATE_TREES_SET_TYPED_WRITER_v01");
    bytes32 public constant ACTION_SET_CONFIG = keccak256("FINAL_STATE_TREES_SET_CONFIG_v01");

    /// @dev Key domains. Both are full-width hashes rather than the packed
    /// address they came from, which matters: an address key occupies only the
    /// low 160 bits, so a hashed key colliding with one needs ~2^96 work rather
    /// than a full collision. That is expensive but not comfortable, and the
    /// consequence would be a service identity landing in a wallet's slot.
    bytes32 private constant DOMAIN_ACCOUNT_KEY = keccak256("FinalStateTrees.key.account.v01");
    bytes32 private constant DOMAIN_IDENTITY_TREE_KEY = keccak256("FinalStateTrees.key.identity.v01");
    /// @dev Tree 8, branches 2 and 3, and branch 0 of every tree. Each is its
    ///      own domain so a key can never land in another branch's slot by
    ///      construction — `_set` refuses a key whose slot sits in a different
    ///      branch, and the domain is what makes that refusal unreachable.
    bytes32 private constant DOMAIN_OWNER_INDEX_KEY = keccak256("FinalStateTrees.key.ownerIndex.v01");
    bytes32 private constant DOMAIN_SLOT_KEY = keccak256("FinalStateTrees.key.slotKey.v01");
    bytes32 private constant DOMAIN_ENDPOINT_KEY = keccak256("FinalStateTrees.key.endpoint.v01");
    bytes32 private constant DOMAIN_CONFIG_KEY = keccak256("FinalStateTrees.key.config.v01");

    /// @notice Leaf domains for the owner index (tree 8, branch 2) and for
    /// configuration rows (branch 0 of every tree). The config leaf binds the
    /// tree too, so the same row in two trees is two different leaves.
    bytes32 public constant DOMAIN_OWNER_INDEX_LEAF = keccak256("FINAL_OWNER_INDEX_LEAF_v01");
    bytes32 public constant DOMAIN_CONFIG_LEAF = keccak256("FINAL_CONFIG_LEAF_v01");

    // -------------------------------------------------------------- storage

    /// @notice The registry every signer is resolved through. Immutable so the
    /// quorum can never be pointed at a registry supplied in calldata.
    FinalIdentityRegistry public immutable registry;

    /// @notice Approvals required per tree.
    ///
    /// @dev Per-tree and not a scalar, because each tree is gated by a
    ///      DIFFERENT role — account co-signers, PHI, vAsset and oracle
    ///      publishers, registry publishers — so K is a property of that
    ///      tree's roster, not of the contract. All six read 2 today; that is
    ///      a deploy-time default, not an invariant, and collapsing them would
    ///      put the oracle roster's quorum on the account co-signers'.
    ///
    ///      The VALUE is a full word: it is a quantity compared against a live
    ///      member count, and every other threshold in the system is `uint256`.
    ///      The KEY is `uint8` because that is what a tree id is here — six
    ///      `uint8` constants, every parameter, every event, every error,
    ///      `_assertTree`, and the ten sibling mappings below. Widening it
    ///      would buy nothing (a narrow key is padded to 32 bytes before
    ///      hashing, so the slot is identical) and cost the getter's selector
    ///      on a contract that is live on both Final Chains.
    mapping(uint8 treeId => uint256) public threshold;
    /// @notice Role a signer must hold to write to a tree.
    mapping(uint8 treeId => uint256) public writerRole;

    /// Raw (untagged) leaf value by tree and slot.
    mapping(uint8 => mapping(uint256 => bytes32)) private _leaf;
    /// Internal nodes, levels 1..DEPTH. Level 0 is derived from `_leaf`.
    mapping(uint8 => mapping(uint256 => mapping(uint256 => bytes32))) private _node;
    /// Empty-subtree hash per level, computed once at construction — up to
    /// the round root's height, since the forest's empty positions are empty
    /// trees.
    bytes32[ROUND_DEPTH + 1] private _zero;

    /// Permanent slot for a key, 1-based so 0 means unassigned. The slot's top
    /// `BRANCH_BITS` are the branch the key lives in.
    mapping(uint8 => mapping(bytes32 => uint256)) private _slotPlusOne;
    /// The key a slot was handed to — the reverse of `_slotPlusOne`, so any
    /// branch enumerates on chain (`keyAt` over `0 .. branchSlotsUsed`) with
    /// no log window. One extra word per NEW key, never per update.
    mapping(uint8 => mapping(uint256 => bytes32)) private _keyAt;
    /// @notice Slots handed out per tree, all branches together.
    mapping(uint8 => uint256) public slotsUsed;
    /// Slots handed out per branch — the next position in it.
    mapping(uint8 => mapping(uint8 => uint256)) private _branchSlotsUsed;
    /// The VALUE behind a configuration row (branch 0), by tree and key.
    mapping(uint8 => mapping(bytes32 => bytes32)) private _configValue;

    /// @notice Live root per tree. Moves on every `setLeaves`.
    mapping(uint8 treeId => bytes32) public liveRoot;
    /// @notice Writes applied per tree, for change detection between rounds.
    mapping(uint8 treeId => uint64) public treeVersion;

    /// @notice A contemporaneous snapshot of all eight roots, and the one
    /// round root that folds them.
    struct Round {
        bytes32[TREE_COUNT + 1] roots;
        bytes32 roundRoot;
        uint64 blockNumber;
        uint64 timestamp;
    }

    /// @notice Published rounds, 1-indexed. Round 0 is "nothing published".
    mapping(uint64 => Round) private _rounds;
    /// @notice Highest published round.
    uint64 public round;
    /// Tree versions as of the last published round.
    mapping(uint8 => uint64) private _publishedVersion;

    /// @notice Per-tree nonce, bound into every quorum digest.
    mapping(uint8 treeId => uint64) public nonce;

    /**
     * @notice A CONTRACT allowed to write one tree without a quorum.
     *
     * @dev Exactly one per tree, and today exactly one exists: tree 1's is
     * `FinalAccountLedger`.
     *
     * This looks like a hole and is the opposite. The quorum on `setLeaves`
     * exists because a tree's writer is otherwise one key deciding what the
     * chain states. A writer contract is not a key — its rules are its
     * bytecode, it has no owner and no proxy, and tree 1's writer authorizes
     * every change by verifying the ACCOUNT HOLDER'S own post-quantum signature
     * in this chain's precompiles. That is strictly stronger evidence than a
     * K-of-N of our own services attesting to what they read.
     *
     * Keeping the quorum on top of it would be actively worse: our fleet could
     * then withhold approval from a user rotating a stolen key, which is a
     * censorship power over the exact operation the account plane exists to
     * make possible.
     *
     * The writer is set on the same bootstrap window as `configureTree` and can
     * be moved by a registrar afterwards — an immutable pointer would mean a
     * ledger upgrade abandons the tree it writes.
     */
    mapping(uint8 treeId => address) public treeWriter;

    /**
     * @notice Where `syncIdentities` reads the chain set from — the asset
     *         registry, which is also tree 6's writer.
     *
     * @dev A service identity is a Final Wallet whose address is the same on
     * every EVM chain, so its tree-1 `deployedChains` table is derivable: one
     * `(chainRef, itself)` row per chain the registry has enabled. The table
     * is DERIVED from state rather than supplied by the caller precisely so
     * that `syncIdentities` can stay permissionless — a caller-chosen table
     * would let anyone grant a service identity on a chain of their choosing.
     *
     * Unset (zero) means services carry an empty table and exist on Final
     * Chain alone, which is what a plane looks like before its registry is
     * seeded. Same configuration gate as `setTreeWriter`, because pointing this
     * at a different contract changes what every service leaf says.
     */
    address public chainSource;
    /// @notice Where {syncSlotKeyLeaves} reads the co-signers' slot keys from
    ///         — the slot-key registry, whose verdict tree 8's branch 3
    ///         projects. Same configuration gate as `chainSource`; unset means
    ///         the branch cannot be written.
    address public slotKeySource;
    /// @notice The endpoint registry whose verdict tree 8's branch 4 projects.
    address public endpointSource;
    /// @notice The one contract admitted to {writeTyped}: `FinalStateRecords`,
    ///         which holds the preimages behind trees 2, 3 and 4 and computes
    ///         their keys and hashes. Same configuration gate as `treeWriter`.
    address public typedWriter;

    // --------------------------------------------------------------- events

    event LeavesSet(uint8 indexed treeId, uint256 count, bytes32 newRoot, uint64 treeVersion);
    event RoundPublished(uint64 indexed round, uint64 blockNumber, uint64 timestamp);
    event TreeConfigured(uint8 indexed treeId, uint256 writerRole, uint256 threshold);
    event TreeWriterSet(uint8 indexed treeId, address writer);
    event ChainSourceSet(address source);
    event SlotKeySourceSet(address source);
    event EndpointSourceSet(address source);
    /// @notice A fresh plane took over the previous plane's counters.
    event CountersSeeded(uint64 round, uint64[] versions);
    event TypedWriterSet(address writer);
    event ConfigSet(uint8 indexed treeId, bytes32 indexed key, bytes32 value);

    // --------------------------------------------------------------- errors

    error UnknownTree(uint8 treeId);
    error LengthMismatch(uint256 keys, uint256 leaves);
    error BranchFull(uint8 treeId, uint8 branch);
    error UnknownBranch(uint8 branch);
    /// @notice A key already holds a slot in another branch of this tree.
    error BranchMismatch(uint8 treeId, bytes32 key, uint8 have, uint8 want);
    /// @notice Branch 0 is written by `setConfig` alone.
    error ConfigBranchReserved(uint8 treeId);
    error SlotKeySourceUnset();
    error EndpointSourceUnset();
    /// @notice Counters can be seeded only into a plane that has published nothing.
    error NotFresh();
    error VersionCountMismatch(uint256 given);
    error TreeNotConfigured(uint8 treeId);
    error NothingToPublish();
    error UnknownKey(uint8 treeId, bytes32 key);
    error NotAuthorized(address caller);
    error NoRounds();
    error ThresholdUnreachable(uint8 treeId, uint256 live, uint256 required);
    /// @notice Trees 7 and 8 take no quorum writes — only their writer
    /// contract (and, for tree 8, the registry projection).
    error WriterOnlyTree(uint8 treeId);
    /// @notice `setLeaves` was called on a tree that has a typed writer.
    /// @dev Trees 2, 3 and 4 keep the leaf's preimage beside its hash so a
    ///      consumer can read the VALUE. An untyped write sets the hash and
    ///      cannot set the preimage — the pair would disagree, and the stored
    ///      value would look authoritative while committing to nothing. The
    ///      typed entrypoint is not a convenience over this one; it is the
    ///      only door.
    error TypedTreeOnly(uint8 treeId);
    /// @notice A `deployedChains` row names the zero chain or the zero account,
    ///         or repeats a chain. A table with either proves nothing about
    ///         where the account exists.
    error InvalidChainAccount(bytes32 chainRef, bytes32 account);

    // ---------------------------------------------------------- constructor

    /**
     * @param registry_ The identity registry. Every signer, key and role is
     *        resolved through it.
     * @dev The empty-subtree table is built here rather than as constants
     * because it depends on the tagging, and a constant table that drifted from
     * the tagging would produce roots nothing can verify — silently, since both
     * sides would still be self-consistent.
     */
    constructor(FinalIdentityRegistry registry_) {
        registry = registry_;

        // Level 0: the tagged hash of an empty (zero) leaf.
        _zero[0] = keccak256(abi.encodePacked(bytes1(0x00), bytes32(0)));
        for (uint256 l = 0; l < ROUND_DEPTH; l++) {
            // Both children equal, so the sort is a no-op and the order is
            // irrelevant — which is the only reason this table is one value per
            // level rather than one per position.
            _zero[l + 1] = keccak256(abi.encodePacked(bytes1(0x01), _zero[l], _zero[l]));
        }

        for (uint8 t = 1; t <= TREE_COUNT; t++) {
            liveRoot[t] = _zero[DEPTH];
        }
    }

    // ------------------------------------------------------- configuration

    /**
     * @dev The configuration gate: the registry's bootstrap admin alone while
     * its window is open, the sealed `ROLE_REGISTRAR` quorum afterwards. The
     * same window the registry uses, for the same reason — every roster has to
     * be installed by someone before it can install itself — and the same
     * quorum, because a threshold is membership by another name: whoever can
     * set K to one owns the tree.
     */
    function _requireConfigurationAuthority(
        bytes32 actionDomain,
        bytes32 payloadDigest,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) private {
        if (!registry.bootstrapSealed() && msg.sender == registry.bootstrapAdmin()) return;
        registry.requireRegistrarQuorum(actionDomain, payloadDigest, anchorBlock, approvals);
    }

    /**
     * @notice Set which role may write a tree and how many approvals it needs.
     * @param k Approvals a write needs; `0` leaves the tree unconfigured.
     * @param anchorBlock The registrars' roster anchor. Ignored during bootstrap.
     * @param approvals The sealed registrar quorum. Empty during bootstrap.
     */
    function configureTree(
        uint8 treeId,
        uint256 role,
        uint256 k,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _assertTree(treeId);
        _requireConfigurationAuthority(
            ACTION_CONFIGURE_TREE, keccak256(abi.encode(treeId, role, k)), anchorBlock, approvals
        );
        // Refuse a threshold nobody can meet. Register the members first; that
        // ordering is the point, not an inconvenience. A 4-of-5 configured
        // against three registered co-signers is a tree that reverts on every
        // write, and the revert names the threshold rather than the roster.
        if (k != 0) {
            uint256 live = registry.liveMemberCount(role);
            if (live < k) revert ThresholdUnreachable(treeId, live, k);
        }
        writerRole[treeId] = role;
        threshold[treeId] = k;
        emit TreeConfigured(treeId, role, k);
    }

    /**
     * @notice Point a tree at the contract allowed to write it directly.
     * @dev Same gate as `configureTree`, for the same reason. Setting it to the
     * zero address removes the path entirely and leaves the tree quorum-only.
     */
    function setTreeWriter(
        uint8 treeId,
        address writer,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _assertTree(treeId);
        _requireConfigurationAuthority(
            ACTION_SET_TREE_WRITER, keccak256(abi.encode(treeId, writer)), anchorBlock, approvals
        );
        treeWriter[treeId] = writer;
        emit TreeWriterSet(treeId, writer);
    }

    /**
     * @notice Point `syncIdentities` at the contract that knows the chain set.
     * @dev Same gate as `setTreeWriter`. Zero removes the source, after which
     * service leaves carry an empty `deployedChains` table.
     */
    function setChainSource(
        address source,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireConfigurationAuthority(
            ACTION_SET_CHAIN_SOURCE, keccak256(abi.encode(source)), anchorBlock, approvals
        );
        chainSource = source;
        emit ChainSourceSet(source);
    }

    /// @notice Point tree 8's branch 3 at the slot-key registry it projects.
    function setSlotKeySource(
        address source,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireConfigurationAuthority(
            ACTION_SET_SLOT_KEY_SOURCE, keccak256(abi.encode(source)), anchorBlock, approvals
        );
        slotKeySource = source;
        emit SlotKeySourceSet(source);
    }

    /// @notice Point tree 8's branch 4 at the endpoint registry it projects.
    function setEndpointSource(
        address source,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireConfigurationAuthority(
            ACTION_SET_ENDPOINT_SOURCE, keccak256(abi.encode(source)), anchorBlock, approvals
        );
        endpointSource = source;
        emit EndpointSourceSet(source);
    }

    /**
     * @notice Take over the previous plane's counters — one `treeVersion` per
     *         tree (index = treeId, 0 unused) and the published `round` — so a
     *         redeploy is monotonic for every consumer that compares them
     *         (rings, explorers, the round feed). NO-WIPE redeploy, ruled
     *         2026-09-03. Past rounds' roots stay on the old plane:
     *         `roundRootAt` below the seed answers zero.
     * @dev Configuration authority (bootstrap admin before the seal, registrar
     *      quorum after), and only while this plane has published nothing.
     */
    function seedCounters(
        uint64[] calldata versions,
        uint64 round_,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireConfigurationAuthority(
            ACTION_SEED_COUNTERS, keccak256(abi.encode(versions, round_)), anchorBlock, approvals
        );
        if (versions.length != TREE_COUNT + 1) revert VersionCountMismatch(versions.length);
        if (round != 0) revert NotFresh();
        for (uint8 t = 1; t <= TREE_COUNT; t++) {
            if (treeVersion[t] != 0) revert NotFresh();
        }
        for (uint8 t = 1; t <= TREE_COUNT; t++) {
            treeVersion[t] = versions[t];
        }
        round = round_;
        emit CountersSeeded(round_, versions);
    }

    /// @notice Install the records contract that writes the typed trees.
    function setTypedWriter(
        address writer,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireConfigurationAuthority(
            ACTION_SET_TYPED_WRITER, keccak256(abi.encode(writer)), anchorBlock, approvals
        );
        typedWriter = writer;
        emit TypedWriterSet(writer);
    }

    /**
     * @notice Write configuration rows into a tree's branch 0.
     * @param treeId The tree whose owning service the rows configure.
     * @param keys `configKey(name, sub)` per row.
     * @param values One word per row — a duration, a count, an address, a
     *        flag; the reader knows the shape from the name.
     *
     * @dev The configuration authority, not the tree's writer or quorum: a
     * tree's writer states what its domain verified, its quorum attests to
     * what it read, and neither is the authority over how the service that
     * feeds it is configured. The value is stored beside the hash so a
     * contract on this chain reads it in one call (`configValue`) while the
     * same row is provable off chain against the round root — one source for
     * the fleet, the contracts and the explorer, where the fleet's environment
     * used to be a second one.
     */
    function setConfig(
        uint8 treeId,
        bytes32[] calldata keys,
        bytes32[] calldata values,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _assertTree(treeId);
        if (keys.length != values.length || keys.length == 0) revert LengthMismatch(keys.length, values.length);
        _requireConfigurationAuthority(
            ACTION_SET_CONFIG, keccak256(abi.encode(treeId, keys, values)), anchorBlock, approvals
        );
        for (uint256 i = 0; i < keys.length; i++) {
            _configValue[treeId][keys[i]] = values[i];
            _set(treeId, BRANCH_CONFIG, keys[i], configLeafHash(treeId, keys[i], values[i]));
            emit ConfigSet(treeId, keys[i], values[i]);
        }
        _bump(treeId, keys.length);
    }

    // ------------------------------------------------------------- writing

    /**
     * @notice Write leaves into one branch of one tree under a PQ quorum.
     * @param treeId Which tree.
     * @param branch Which branch — never 0, which `setConfig` alone writes.
     * @param keys Domain keys — a wallet address for accounts, an asset id for
     *        the allowlist, whatever identifies a row in that domain. Each gets
     *        a permanent slot in the branch on first write.
     * @param leaves The raw (untagged) leaf values.
     * @param approvals At least `threshold[treeId]` of them, ascending by signer.
     *
     * @dev The digest binds the tree, its nonce, and the full batch. Binding the
     * nonce is what stops the same approved batch being replayed: without it,
     * an approval to set a price is an approval to set that price again at any
     * later block, which for an oracle is the whole attack.
     *
     * ML-DSA-87 is required rather than accepted. These are operational,
     * high-cadence writes — the transaction class — and leaving the choice open
     * would mean a break in either scheme takes the tree.
     */
    function setLeaves(
        uint8 treeId,
        uint8 branch,
        bytes32[] calldata keys,
        bytes32[] calldata leaves,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _assertTree(treeId);
        _assertDataBranch(treeId, branch);
        if (treeId == TREE_PHI || treeId == TREE_VASSET || treeId == TREE_ORACLE) {
            revert TypedTreeOnly(treeId);
        }
        // Trees 7 and 8 have their own rulers and NO quorum path at all: an
        // intent's status is what `FinalIntentLog` verified, an identity is
        // what the registry or the ledger verified, and no set of service
        // signatures can make a different answer true.
        if (treeId == TREE_INTENTS || treeId == TREE_IDENTITY) revert WriterOnlyTree(treeId);
        if (keys.length != leaves.length) revert LengthMismatch(keys.length, leaves.length);
        uint256 k = threshold[treeId];
        if (k == 0) revert TreeNotConfigured(treeId);

        uint64 n = nonce[treeId];
        FinalPqQuorum.require_(
            registry,
            approvals,
            FinalPqQuorum.digest(
                address(this),
                ACTION_SET_LEAVES,
                anchorBlock,
                keccak256(abi.encode(treeId, branch, n, keys, leaves))
            ),
            writerRole[treeId],
            k,
            FinalPqQuorum.ALG_ML_DSA_87,
            anchorBlock,
            false
        );
        nonce[treeId] = n + 1;

        for (uint256 i = 0; i < keys.length; i++) {
            _set(treeId, branch, keys[i], leaves[i]);
        }

        _bump(treeId, keys.length);
    }

    /// @notice One chain an account exists on, and as what.
    /// @dev `chainRef` is the registry's CAIP-derived chain reference — the one
    ///      identifier that names an EVM chain and a non-EVM one alike — and
    ///      `account` is the wallet's account there, in that chain's own account
    ///      space (an EVM address right-aligned, a 32-byte key filling the
    ///      width). Field-for-field with `IWalletTypes.ChainAccount`.
    struct ChainAccount {
        bytes32 chainRef;
        bytes32 account;
    }

    /// @notice `FinalWalletFactory.AccountStateLeaf`, field for field.
    struct AccountStateLeaf {
        address wallet;
        bytes32 liveAccess;
        bytes32 liveTransaction;
        bytes32 recoveryAccess;
        bytes32 recoveryTransaction;
        /// @dev Active-stage encapsulation commitment and its pre-committed
        /// successor. Field-for-field with `FinalWalletFactory.AccountStateLeaf`;
        /// a field added on one side and not the other is a root every execution
        /// chain rejects, with nothing pointing at the cause.
        bytes32 liveKem;
        bytes32 recoveryKem;
        address owner;
        bool pqEnabled;
        bool frozen;
        /// @dev The chains this account exists on, and its account on each —
        /// including chains whose accounts are not EVM addresses. Decided HERE
        /// (set by the holder through the ledger) and enforced there: an
        /// execution chain refuses to create the account unless the table has a
        /// row for it, and a settlement toward a chain with no row is refused at
        /// the source. This is what a zero beneficiary resolves through; it
        /// replaced a bitmask over registry slots that could only say "may
        /// exist", never "as what".
        ChainAccount[] deployedChains;
        /// @dev Per-chain dormancy verdict, one bit per asset-registry chain
        /// slot. Keeps the slot space the bitmask had.
        uint32 dormantChains;
        uint64 version;
    }

    /**
     * @notice Write account state into tree 1 from the typed leaf.
     * @dev The typed form exists so the leaf preimage is built HERE rather than
     * by whoever assembles the calldata. Tree 1 is the source of truth for every
     * other chain, and `syncAccountState` will accept any 32 bytes that carry a
     * valid proof — so if the publisher chose the preimage, the publisher could
     * write an account state that no wallet record on this chain agrees with,
     * and the proof would still verify everywhere.
     *
     * **Sealed.** Tree 1 is membership: a leaf here is who an account is, on
     * every chain. So the round takes the hybrid class — each approval carries
     * the ML-DSA-87 vote AND the member's SLH-DSA seal — where the other trees
     * take the transaction class alone. A lattice break rewrites a price; it
     * does not rewrite an account.
     */
    function setAccountStates(
        AccountStateLeaf[] calldata leaves,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        uint256 k = threshold[TREE_ACCOUNTS];
        if (k == 0) revert TreeNotConfigured(TREE_ACCOUNTS);

        bytes32[] memory keys = new bytes32[](leaves.length);
        bytes32[] memory hashes = new bytes32[](leaves.length);
        for (uint256 i = 0; i < leaves.length; i++) {
            keys[i] = accountKeyFor(leaves[i].wallet);
            hashes[i] = accountStateLeafHash(leaves[i]);
        }

        uint64 n = nonce[TREE_ACCOUNTS];
        FinalPqQuorum.require_(
            registry,
            approvals,
            FinalPqQuorum.digest(
                address(this),
                ACTION_SET_LEAVES,
                anchorBlock,
                keccak256(abi.encode(TREE_ACCOUNTS, n, keys, hashes))
            ),
            writerRole[TREE_ACCOUNTS],
            k,
            FinalPqQuorum.ALG_ML_DSA_87,
            anchorBlock,
            true
        );
        nonce[TREE_ACCOUNTS] = n + 1;

        for (uint256 i = 0; i < leaves.length; i++) {
            _set(TREE_ACCOUNTS, BRANCH_MAIN, keys[i], hashes[i]);
        }

        _bump(TREE_ACCOUNTS, leaves.length);
    }

    /**
     * @notice Write account state into tree 1 from the contract that owns it.
     * @dev No quorum, and no nonce burned: `treeWriter[1]` is the ledger, and
     * the ledger already verified the holder's own signature before it called
     * here. See {treeWriter} for why adding a service quorum on top would be a
     * censorship power rather than a safeguard.
     *
     * Typed, exactly as `setAccountStates` is: the preimage is built HERE, so
     * even the writer contract cannot publish a leaf whose meaning no record on
     * this chain agrees with.
     */
    function setAccountStatesAsWriter(AccountStateLeaf[] calldata leaves) external {
        if (msg.sender != treeWriter[TREE_ACCOUNTS]) revert NotAuthorized(msg.sender);
        for (uint256 i = 0; i < leaves.length; i++) {
            _set(TREE_ACCOUNTS, BRANCH_MAIN, accountKeyFor(leaves[i].wallet), accountStateLeafHash(leaves[i]));
        }
        _bump(TREE_ACCOUNTS, leaves.length);
    }

    /**
     * @notice Write raw leaves into any tree from the contract that owns it.
     * @dev The generic sibling of {setAccountStatesAsWriter}, for a tree whose
     * writer is a contract rather than a service quorum. Same authorization —
     * `treeWriter[treeId]` and nothing else — and the same reasoning: the
     * writer has already verified whatever its domain requires, and layering a
     * quorum on top of a contract's own rules is a censorship power rather
     * than a safeguard.
     *
     * UNTYPED, unlike the account path, and that is the trade. Tree 1's
     * preimage is built here so even the ledger cannot publish a leaf whose
     * meaning no record agrees with; a generic writer supplies its own hash,
     * so the leaf means whatever that contract says it means. Acceptable only
     * because the writer is a specific contract this chain's operators
     * installed — its rules are its bytecode, it has no owner and no proxy —
     * and NOT acceptable for a role-gated key. Point `treeWriter` at a
     * contract, never at an EOA.
     */
    function setLeavesAsWriter(uint8 treeId, uint8 branch, bytes32[] calldata keys, bytes32[] calldata leaves)
        external
    {
        if (msg.sender != treeWriter[treeId]) revert NotAuthorized(msg.sender);
        _assertDataBranch(treeId, branch);
        if (keys.length != leaves.length) revert LengthMismatch(keys.length, leaves.length);
        for (uint256 i = 0; i < keys.length; i++) {
            _set(treeId, branch, keys[i], leaves[i]);
        }
        _bump(treeId, keys.length);
    }

    /// @notice The leaf hash `FinalWalletFactory.accountStateLeafHash` computes.
    /// @dev Identical `abi.encode`, identical field order, identical domain.
    /// Pinned against the factory by test. `deployedChains` rides through
    /// `abi.encode` like every other field — head offset, then length and
    /// rows — so the table is committed whole and in order.
    function accountStateLeafHash(AccountStateLeaf memory leaf) public pure returns (bytes32) {
        _assertChainAccounts(leaf.deployedChains);
        return keccak256(
            abi.encode(
                DOMAIN_ACCOUNT_STATE_LEAF,
                leaf.wallet,
                leaf.liveAccess,
                leaf.liveTransaction,
                leaf.recoveryAccess,
                leaf.recoveryTransaction,
                leaf.liveKem,
                leaf.recoveryKem,
                leaf.owner,
                leaf.pqEnabled,
                leaf.frozen,
                leaf.deployedChains,
                leaf.dormantChains,
                leaf.version
            )
        );
    }

    /// @dev A well-formed table: no zero chain, no zero account, no chain twice.
    ///      Checked where the leaf is hashed so no door — quorum, writer
    ///      contract, identity projection — can publish a table a resolver
    ///      would read two ways.
    function _assertChainAccounts(ChainAccount[] memory rows) private pure {
        for (uint256 i = 0; i < rows.length; i++) {
            if (rows[i].chainRef == bytes32(0) || rows[i].account == bytes32(0)) {
                revert InvalidChainAccount(rows[i].chainRef, rows[i].account);
            }
            for (uint256 j = 0; j < i; j++) {
                if (rows[j].chainRef == rows[i].chainRef) {
                    revert InvalidChainAccount(rows[i].chainRef, rows[i].account);
                }
            }
        }
    }

    /// @notice The account `wallet`'s published table names on `chainRef`, or
    ///         zero if it has no row there.
    /// @dev A convenience over `accountStateLeafHash`'s input for readers on
    /// this chain; execution chains answer the same question from their synced
    /// record (`FinalWalletFactory.addressOn`).
    function accountOn(AccountStateLeaf memory leaf, bytes32 chainRef) public pure returns (bytes32) {
        for (uint256 i = 0; i < leaf.deployedChains.length; i++) {
            if (leaf.deployedChains[i].chainRef == chainRef) return leaf.deployedChains[i].account;
        }
        return bytes32(0);
    }

    /**
     * @notice The typed trees' write door — `FinalStateRecords` alone.
     * @dev The quorum, the nonce and the write, shared by every typed record.
     * The records contract computed the keys and hashes from the structs it
     * stores; this contract admits nobody else to trees 2, 3 and 4
     * (`setLeaves` refuses them), so the value there can never drift from
     * the commitment here.
     *
     * The digest is byte-identical to `setLeaves`' over the same keys and
     * hashes, deliberately: the typed entrypoints choose the PREIMAGE, not the
     * authorization. A member recomputes one digest whichever door the batch
     * came through, and there is no second approval shape to get wrong.
     */
    function writeTyped(
        uint8 treeId,
        bytes32[] memory keys,
        bytes32[] memory hashes,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        if (msg.sender != typedWriter) revert NotAuthorized(msg.sender);
        uint256 k = threshold[treeId];
        if (k == 0) revert TreeNotConfigured(treeId);

        uint64 n = nonce[treeId];
        FinalPqQuorum.require_(
            registry,
            approvals,
            FinalPqQuorum.digest(
                address(this),
                ACTION_SET_LEAVES,
                anchorBlock,
                keccak256(abi.encode(treeId, n, keys, hashes))
            ),
            writerRole[treeId],
            k,
            FinalPqQuorum.ALG_ML_DSA_87,
            anchorBlock,
            false
        );
        nonce[treeId] = n + 1;

        for (uint256 i = 0; i < keys.length; i++) {
            _set(treeId, BRANCH_MAIN, keys[i], hashes[i]);
        }

        _bump(treeId, keys.length);
    }

    /**
     * @notice Snapshot every tree's root into a new round.
     * @dev Permissionless, deliberately. Every root being snapshotted was
     * already authorized by its tree's quorum, so this adds no authority — it
     * only fixes a moment. Requiring a signature would put a liveness
     * dependency in front of publication for no security gain.
     *
     * A round that would change nothing is refused, so the round number cannot
     * be advanced by anyone with gas to spend.
     */
    function publishRound() external returns (uint64 published) {
        bool changed;
        for (uint8 t = 1; t <= TREE_COUNT; t++) {
            if (treeVersion[t] != _publishedVersion[t]) {
                changed = true;
                break;
            }
        }
        if (!changed) revert NothingToPublish();

        published = round + 1;
        Round storage r = _rounds[published];
        for (uint8 t = 1; t <= TREE_COUNT; t++) {
            r.roots[t] = liveRoot[t];
            _publishedVersion[t] = treeVersion[t];
        }
        r.roundRoot = _foldForest(_forestLeaves(r.roots));
        r.blockNumber = uint64(block.number);
        // MILLISECONDS, like every instant on this chain.
        r.timestamp = FinalChainTime.nowMs();
        round = published;
        emit RoundPublished(published, r.blockNumber, r.timestamp);
    }

    // ---------------------------------------------------------------- views

    /// @notice Every root from one round. Index by the `TREE_*` constants;
    /// index 0 is unused.
    function rootsAt(uint64 which) external view returns (bytes32[TREE_COUNT + 1] memory) {
        return _rounds[which].roots;
    }

    /// @notice One tree's root at one round.
    function rootAt(uint64 which, uint8 treeId) external view returns (bytes32) {
        _assertTree(treeId);
        return _rounds[which].roots[treeId];
    }

    /// @notice The one word that commits to every tree at one round.
    function roundRootAt(uint64 which) external view returns (bytes32) {
        return _rounds[which].roundRoot;
    }

    /**
     * @notice The `FOREST_BITS` siblings that take a tree's root at one round
     *         up to that round's root — appended to `proofFor`, they make a
     *         leaf provable against `roundRootAt(which)` by the same verifier.
     */
    function roundProofFor(uint64 which, uint8 treeId) external view returns (bytes32[] memory path) {
        _assertTree(treeId);
        if (which == 0 || which > round) revert NoRounds();
        bytes32[] memory level = _forestLeaves(_rounds[which].roots);
        path = new bytes32[](FOREST_BITS);
        uint256 idx = treeId;
        uint256 n = level.length;
        for (uint256 l = 0; l < FOREST_BITS; l++) {
            path[l] = level[idx ^ 1];
            n >>= 1;
            for (uint256 i = 0; i < n; i++) {
                level[i] = _pair(level[2 * i], level[2 * i + 1]);
            }
            idx >>= 1;
        }
    }

    /// @notice The latest round's roots, with the block it was taken at.
    function latestRound()
        external
        view
        returns (uint64 which, bytes32[TREE_COUNT + 1] memory roots, uint64 blockNumber, uint64 timestamp)
    {
        which = round;
        if (which == 0) revert NoRounds();
        Round storage r = _rounds[which];
        return (which, r.roots, r.blockNumber, r.timestamp);
    }

    /// @notice The raw leaf stored for a key, and whether it has a slot.
    function leafOf(uint8 treeId, bytes32 key) external view returns (bytes32 leaf, bool present) {
        uint256 s = _slotPlusOne[treeId][key];
        if (s == 0) return (bytes32(0), false);
        return (_leaf[treeId][s - 1], true);
    }

    /// @notice The permanent slot for a key. Reverts if it has none. The
    /// slot's top `BRANCH_BITS` are its branch.
    function slotOf(uint8 treeId, bytes32 key) public view returns (uint256) {
        uint256 s = _slotPlusOne[treeId][key];
        if (s == 0) revert UnknownKey(treeId, key);
        return s - 1;
    }

    /// @notice The key a slot was handed to, or zero if it is still free —
    /// the enumeration every branch offers: slots `branch << BRANCH_DEPTH`
    /// through `+ branchSlotsUsed(treeId, branch) - 1`.
    function keyAt(uint8 treeId, uint256 slot) external view returns (bytes32) {
        return _keyAt[treeId][slot];
    }

    /// @notice Slots handed out in one branch.
    function branchSlotsUsed(uint8 treeId, uint8 branch) external view returns (uint256) {
        return _branchSlotsUsed[treeId][branch];
    }

    /// @notice One branch's root: the level-`BRANCH_DEPTH` node at its position.
    function branchRoot(uint8 treeId, uint8 branch) external view returns (bytes32) {
        _assertTree(treeId);
        _assertBranch(branch);
        return _nodeAt(treeId, BRANCH_DEPTH, branch);
    }

    /// @notice The first `BRANCH_DEPTH` siblings of `proofFor` — a proof
    /// against the leaf's branch root rather than the tree root.
    function branchProofFor(uint8 treeId, bytes32 key) external view returns (bytes32[] memory) {
        _assertTree(treeId);
        return _path(treeId, slotOf(treeId, key), BRANCH_DEPTH);
    }

    /// @notice A configuration row's value, and whether the row exists.
    function configValue(uint8 treeId, bytes32 key) external view returns (bytes32 value, bool present) {
        present = _slotPlusOne[treeId][key] != 0;
        value = _configValue[treeId][key];
    }

    /// @notice The branch-0 key of a configuration row: a name the owning
    /// service defines, and a sub-key (a chain reference, an asset, zero).
    function configKey(bytes32 name, bytes32 sub) public pure returns (bytes32) {
        return keccak256(abi.encode(DOMAIN_CONFIG_KEY, name, sub));
    }

    /// @notice The leaf a configuration row hashes to.
    function configLeafHash(uint8 treeId, bytes32 key, bytes32 value) public pure returns (bytes32) {
        return keccak256(abi.encode(DOMAIN_CONFIG_LEAF, treeId, key, value));
    }

    /// @notice The tree-8 branch-2 key an owner occupies.
    function ownerIndexKeyFor(address owner) public pure returns (bytes32) {
        return keccak256(abi.encode(DOMAIN_OWNER_INDEX_KEY, owner));
    }

    /// @notice The owner-index leaf: a commitment to the ledger's ordered
    /// `walletsByOwner(owner)`.
    function ownerIndexLeafHash(address owner, address[] memory wallets) public pure returns (bytes32) {
        return keccak256(abi.encode(DOMAIN_OWNER_INDEX_LEAF, owner, wallets));
    }

    /// @notice The tree-8 branch-3 key of one member's slot — a ring position.
    function slotKeyFor(address member, uint64 slotIndex) public pure returns (bytes32) {
        return keccak256(abi.encode(DOMAIN_SLOT_KEY, member, slotIndex % SLOT_KEY_RING));
    }

    /**
     * @notice Project slot keys into tree 8's branch 3 — the co-signers'
     *         per-slot KEM publics the private option seals to.
     * @dev Permissionless, for {syncIdentityLeaves}' reason: the leaf VALUE
     * is `slotKeySource`'s own verdict (the registry verified the member's
     * signature when the key was published, and answers zero once the slot's
     * window has passed), so this adds no authority and only projects. The
     * registry calls it same-tx on publication; anyone may call it to retire a
     * slot that lapsed by time.
     */
    function syncSlotKeyLeaves(address member, uint64[] calldata slotIndexes) external {
        address source = slotKeySource;
        if (source == address(0)) revert SlotKeySourceUnset();
        for (uint256 i = 0; i < slotIndexes.length; i++) {
            _set(
                TREE_IDENTITY,
                BRANCH_SLOT_KEYS,
                slotKeyFor(member, slotIndexes[i]),
                ISlotKeySource(source).slotKeyLeafOf(member, slotIndexes[i])
            );
        }
        _bump(TREE_IDENTITY, slotIndexes.length);
    }

    /// @notice The tree-8 branch-4 key of one tunnel endpoint.
    function endpointKeyFor(bytes32 endpointId) public pure returns (bytes32) {
        return keccak256(abi.encode(DOMAIN_ENDPOINT_KEY, endpointId));
    }

    /**
     * @notice Project tunnel endpoints into tree 8's branch 4.
     * @dev Permissionless, for {syncSlotKeyLeaves}' reason: the leaf VALUE is
     * `endpointSource`'s own verdict — the registry admitted the certificate
     * under the registrar quorum with the holder's proof of possession, and
     * answers the revoked status once it is revoked — so this adds no authority
     * and only projects. The registry calls it same-tx on registration and
     * revocation; anyone may call it to re-project.
     */
    function syncEndpointLeaves(bytes32[] calldata endpointIds) external {
        address source = endpointSource;
        if (source == address(0)) revert EndpointSourceUnset();
        for (uint256 i = 0; i < endpointIds.length; i++) {
            _set(
                TREE_IDENTITY,
                BRANCH_ENDPOINTS,
                endpointKeyFor(endpointIds[i]),
                IEndpointSource(source).endpointLeafOf(endpointIds[i])
            );
        }
        _bump(TREE_IDENTITY, endpointIds.length);
    }

    /**
     * @notice The sibling path for a key, ready for
     *         `FinalMerkle.verifyTaggedSortedProof` on any chain.
     * @dev A view, so the backend fetches a proof with one `eth_call` instead of
     * rebuilding the tree off chain. Rebuilding is where a divergence between
     * what the chain holds and what a service believes it holds would come
     * from, and this removes the second implementation entirely.
     */
    function proofFor(uint8 treeId, bytes32 key) external view returns (bytes32[] memory) {
        _assertTree(treeId);
        return _path(treeId, slotOf(treeId, key), DEPTH);
    }

    /// @notice The empty-subtree hash at a level. Level `DEPTH` is the root of
    /// a tree with nothing in it.
    function emptyRoot(uint256 level) external view returns (bytes32) {
        return _zero[level];
    }

    /// @notice The tree-1 key a wallet occupies.
    function accountKeyFor(address wallet) public pure returns (bytes32) {
        return keccak256(abi.encode(DOMAIN_ACCOUNT_KEY, wallet));
    }

    /**
     * @notice Copy a registered identity into tree 1 as an account-state leaf.
     * @dev Services are Final Wallets, so a service's leaf is the SAME leaf a
     * user's wallet gets — `FinalWalletFactory.AccountStateLeaf`, four key
     * commitments and all. There is no second shape and no second domain,
     * which is what lets every chain that already consumes account state
     * consume a co-signer's identity with no contract change.
     *
     * `owner` is the account itself: a service wallet is its own owner, having
     * no separate holder to speak for it.
     *
     * Permissionless, and for the same reason `publishRound` is: every fact it
     * writes was already authorized when it entered the registry, so this adds
     * no authority and only projects. Gating it would put a liveness dependency
     * in front of publishing a revocation, which is the one thing that must
     * never wait.
     */
    function syncIdentities(address[] calldata accounts) external {
        // One table for the batch: a service is its own canonical address on
        // every enabled chain, so the rows differ only in `account`.
        bytes32[] memory chainRefs = _enabledChainRefs();
        for (uint256 i = 0; i < accounts.length; i++) {
            address who = accounts[i];
            FinalIdentityRegistry.Identity memory id = registry.identityOf(who);
            if (!id.registered) revert UnknownKey(TREE_ACCOUNTS, accountKeyFor(who));
            (bytes32 la, bytes32 lt, bytes32 ra, bytes32 rt) = registry.keyCommitments(who);
            (bytes32 lk, bytes32 rk) = registry.kemCommitments(who);
            ChainAccount[] memory table = new ChainAccount[](chainRefs.length);
            for (uint256 c = 0; c < chainRefs.length; c++) {
                table[c] = ChainAccount({chainRef: chainRefs[c], account: bytes32(uint256(uint160(who)))});
            }
            AccountStateLeaf memory leaf = AccountStateLeaf({
                wallet: who,
                liveAccess: la,
                liveTransaction: lt,
                recoveryAccess: ra,
                recoveryTransaction: rt,
                liveKem: lk,
                recoveryKem: rk,
                // A service reaches every chain the registry has enabled, at
                // its own address, and is never dormant: dormancy measures an
                // ABSENT holder, and these identities have no holder to be
                // absent.
                deployedChains: table,
                dormantChains: 0,
                owner: who,
                // Every identity here is PQ by construction — there is no other
                // kind of key in this registry.
                pqEnabled: true,
                // Revocation is a leaf that CHANGES, not one that disappears.
                // A consumer holding an old proof gets a stale `false`, which is
                // why the round is the thing to pin.
                frozen: id.revoked,
                version: id.version
            });
            _set(TREE_ACCOUNTS, BRANCH_MAIN, accountKeyFor(who), accountStateLeafHash(leaf));
        }
        _bump(TREE_ACCOUNTS, accounts.length);
    }

    /// @notice The tree-8 slot key an identity occupies.
    function identityKeyFor(address account) public pure returns (bytes32) {
        return keccak256(abi.encode(DOMAIN_IDENTITY_TREE_KEY, account));
    }

    /**
     * @notice Project identities into tree 8 — the wallet-creation admission
     *         set whose live root every execution chain anchors as its
     *         `currentIdentityRoot`.
     *
     * @dev The leaf VALUE is the registry's own verdict —
     * `FinalIdentityRegistry.identityTreeLeafOf`: the execution chains'
     * identity leaf while the identity stands, zero once it does not. Derived
     * there rather than here because every input (serial, the six key
     * commitments, standing, the CA depth pair) is registry storage, and this
     * contract sits against EIP-170 while the registry does not.
     *
     * Permissionless, for exactly {syncIdentities}' reason: every fact
     * written here was authorized when it entered the registry, so this adds
     * no authority and only projects. The registry itself calls it same-tx on
     * every identity mutation (register, rotate, roles, revoke, LMS-key ops),
     * which is what makes the root CONTINUOUS; the open door additionally lets
     * anyone retire a leaf whose standing lapsed by TIME — expiry moves no
     * registry storage, so no mutation hook can ever fire for it.
     *
     * There is no quorum door and no writer seat (both raw doors refuse this
     * tree), so the strongest thing any caller can do here is copy the
     * registry's own verdict.
     */
    function syncIdentityLeaves(address[] calldata accounts) external {
        for (uint256 i = 0; i < accounts.length; i++) {
            _set(TREE_IDENTITY, BRANCH_MAIN, identityKeyFor(accounts[i]), registry.identityTreeLeafOf(accounts[i]));
        }
        _bump(TREE_IDENTITY, accounts.length);
    }

    /**
     * @notice Per-tree quorum health: can each configured tree still be written?
     * @dev A threshold above the live member count is not a strict quorum, it is
     * a tree that reverts forever with nothing naming the roster as the cause.
     * `configureTree` refuses to create that state, but revocation can arrive at
     * it later — revocation must never be blocked on quorum arithmetic, so the
     * check has to be something monitoring reads rather than something the
     * contract enforces after the fact.
     */
    function quorumHealth()
        external
        view
        returns (uint256[] memory live, uint256[] memory required, bool[] memory ok)
    {
        live = new uint256[](TREE_COUNT + 1);
        required = new uint256[](TREE_COUNT + 1);
        ok = new bool[](TREE_COUNT + 1);
        for (uint8 t = 1; t <= TREE_COUNT; t++) {
            required[t] = threshold[t];
            live[t] = required[t] == 0 ? 0 : registry.liveMemberCount(writerRole[t]);
            ok[t] = required[t] == 0 || live[t] >= required[t];
        }
    }

    // -------------------------------------------------------------- internal

    /// @dev The enabled chain references `chainSource` knows, or none if it is
    ///      unset. Read through the narrow interface so this contract need not
    ///      import the registry that imports it.
    function _enabledChainRefs() private view returns (bytes32[] memory) {
        address source = chainSource;
        if (source == address(0)) return new bytes32[](0);
        return IChainSource(source).enabledChainRefs();
    }

    function _assertTree(uint8 treeId) private pure {
        if (treeId == 0 || treeId > TREE_COUNT) revert UnknownTree(treeId);
    }

    function _assertBranch(uint8 branch) private pure {
        if (branch >= BRANCH_COUNT) revert UnknownBranch(branch);
    }

    /// @dev A branch a quorum or a writer may write: any but the config branch.
    function _assertDataBranch(uint8 treeId, uint8 branch) private pure {
        _assertBranch(branch);
        if (branch == BRANCH_CONFIG) revert ConfigBranchReserved(treeId);
    }

    /// @dev Version + event, the tail of every write door.
    function _bump(uint8 treeId, uint256 count) private {
        uint64 v = treeVersion[treeId] + 1;
        treeVersion[treeId] = v;
        emit LeavesSet(treeId, count, liveRoot[treeId], v);
    }

    /// @dev `keccak256(0x01 ‖ lo ‖ hi)`, the pair sorted — the one node hash.
    function _pair(bytes32 a, bytes32 b) private pure returns (bytes32) {
        (bytes32 lo, bytes32 hi) = a < b ? (a, b) : (b, a);
        return keccak256(abi.encodePacked(bytes1(0x01), lo, hi));
    }

    /// @dev The sibling path from a slot up `height` levels.
    function _path(uint8 treeId, uint256 idx, uint256 height) private view returns (bytes32[] memory path) {
        path = new bytes32[](height);
        for (uint256 l = 0; l < height; l++) {
            path[l] = _nodeAt(treeId, l, idx ^ 1);
            idx >>= 1;
        }
    }

    /// @dev The forest's leaves: the tree roots at their positions, the
    ///      empty tree at the rest.
    function _forestLeaves(bytes32[TREE_COUNT + 1] memory roots) private view returns (bytes32[] memory level) {
        level = new bytes32[](1 << FOREST_BITS);
        for (uint256 p = 0; p < level.length; p++) {
            level[p] = (p >= 1 && p <= TREE_COUNT) ? roots[p] : _zero[DEPTH];
        }
    }

    /// @dev Fold a power-of-two level to its root, in place.
    function _foldForest(bytes32[] memory level) private pure returns (bytes32) {
        for (uint256 n = level.length; n > 1; n >>= 1) {
            for (uint256 i = 0; i < n / 2; i++) {
                level[i] = _pair(level[2 * i], level[2 * i + 1]);
            }
        }
        return level[0];
    }

    function _set(uint8 treeId, uint8 branch, bytes32 key, bytes32 leaf) private {
        uint256 s = _slotPlusOne[treeId][key];
        uint256 idx;
        if (s == 0) {
            uint256 used = _branchSlotsUsed[treeId][branch];
            if (used >= BRANCH_CAPACITY) revert BranchFull(treeId, branch);
            idx = (uint256(branch) << BRANCH_DEPTH) | used;
            _branchSlotsUsed[treeId][branch] = used + 1;
            slotsUsed[treeId] += 1;
            _slotPlusOne[treeId][key] = idx + 1;
            _keyAt[treeId][idx] = key;
        } else {
            idx = s - 1;
            uint8 have = uint8(idx >> BRANCH_DEPTH);
            if (have != branch) revert BranchMismatch(treeId, key, have, branch);
        }

        _leaf[treeId][idx] = leaf;

        bytes32 cursor = keccak256(abi.encodePacked(bytes1(0x00), leaf));
        for (uint256 l = 0; l < DEPTH; l++) {
            cursor = _pair(cursor, _nodeAt(treeId, l, idx ^ 1));
            idx >>= 1;
            _node[treeId][l + 1][idx] = cursor;
        }
        liveRoot[treeId] = cursor;
    }

    /// @dev Level 0 is derived from the leaf store rather than duplicated into
    /// `_node`, so there is one place a leaf lives and no way for the two to
    /// disagree. Unset positions fall through to the empty-subtree hash.
    function _nodeAt(uint8 treeId, uint256 level, uint256 index) private view returns (bytes32) {
        if (level == 0) {
            return keccak256(abi.encodePacked(bytes1(0x00), _leaf[treeId][index]));
        }
        bytes32 v = _node[treeId][level][index];
        return v == bytes32(0) ? _zero[level] : v;
    }
}

contracts/utils/FinalBundleTree.sol

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.20;

/// @notice Empty-bundle guard. File-level so every caller reverts with the same
///         selector — `PqAnchorModule` and `PaymasterModule` on the execution
///         chain, `FinalBundleLog` on Final Chain.
error EmptyBundleTree();

/**
 * @title FinalBundleTree
 * @notice The fold from a bundle's intent leaves to its root, in one place.
 *
 * @dev Two chains compute this root and they must agree exactly. The gateway
 *      folds it from the intents it is about to dispatch (`PqAnchorModule`);
 *      `FinalBundleLog` folds it from the leaves being anchored, so that the
 *      payload it stores is RECOMPUTED rather than asserted by whoever
 *      assembled the transaction. A divergence between the two would not be a
 *      failed verification — it would be a bundle that anchors on Final Chain
 *      and can never execute, or worse.
 *
 *      So the fold lives here rather than being written twice. There is a third
 *      copy in `FinalBackend/src/mmr/pqBundle.js`, kept honest by the cross-repo
 *      parity suite.
 *
 *      The padding discipline is the whole point of the odd-tail branch: the
 *      `POS_LIFT` tag is what makes `Root([a,b,c]) != Root([a,b,c,c])`. A
 *      self-paired tail would let a bundle be re-presented with its last intent
 *      duplicated under the same root.
 */
library FinalBundleTree {
    /// @dev Position tag for an interior node. Distinct from the leaf tag so a
    ///      node hash can never be reinterpreted as a leaf.
    uint8 internal constant POS_NODE = 0x01;
    /// @dev Position tag for the lift of an odd trailing element.
    uint8 internal constant POS_LIFT = 0x02;

    /// @dev Domain separator for both interior forms.
    bytes32 internal constant DOMAIN_PQ_NODE = keccak256("FINAL_PQ_BUNDLE_NODE_v01");

    /**
     * @notice Fold a leaf layer to its root.
     *
     * @dev Takes a layer rather than a bundle so a mixed bundle's PQ **subset**
     *      can be folded — Final Chain never saw the pre-PQ rows and cannot
     *      commit to leaves it did not build.
     *
     *      Mutates nothing the caller can observe: `leaves` is read on the first
     *      pass and every later layer is freshly allocated.
     */
    function foldLeaves(bytes32[] memory leaves) internal pure returns (bytes32 root) {
        if (leaves.length == 0) revert EmptyBundleTree();
        bytes32[] memory layer = leaves;
        while (layer.length > 1) {
            uint256 srcLen = layer.length;
            uint256 pairCount = srcLen >> 1;
            uint256 oddTail = srcLen & 1;
            bytes32[] memory next = new bytes32[](pairCount + oddTail);
            for (uint256 i = 0; i < pairCount; i++) {
                next[i] = keccak256(
                    abi.encodePacked(POS_NODE, DOMAIN_PQ_NODE, layer[i << 1], layer[(i << 1) | 1])
                );
            }
            if (oddTail == 1) {
                next[pairCount] = keccak256(
                    abi.encodePacked(POS_LIFT, DOMAIN_PQ_NODE, layer[srcLen - 1])
                );
            }
            layer = next;
        }
        return layer[0];
    }
}

contracts/utils/FinalMmr.sol

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2024-2026 Final DeFi
// Licensed under the Business Source License 1.1 (the "License")
//
// Change Date: 2029-01-01
// Change License: GPL-2.0-or-later
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;

/**
 * @title FinalMmr
 * @notice Production append-only-log (Merkle-Mountain-Range) **inclusion**
 * verification — positional and domain-separated, replacing the legacy
 * sorted-pair fold whose loss of position re-opened leaf/node second-preimage
 * ambiguity (FinalWallet audit F-02 follow-up). The algorithm is the RFC 6962 /
 * Trillian inclusion decomposition, which verifies a leaf against the
 * Merkle-Tree-Hash of an arbitrarily-sized (not just power-of-two) append-only
 * log — i.e. an MMR over the log's perfect-subtree "peaks".
 *
 * ## Canonical layout (the off-chain Final-Chain MMR MUST match this exactly)
 *
 * - **leaf**: `keccak256(0x00 ‖ domain ‖ payload)`
 * - **node**: `keccak256(0x01 ‖ domain ‖ left ‖ right)`   (positional: `left`
 *   is always the lower-index child)
 *
 * The 1-byte `0x00`/`0x01` tags are the load-bearing safety property: a 64-byte
 * internal node can never be reinterpreted as a leaf preimage (and vice-versa),
 * closing the second-preimage class that an untagged or sorted construction
 * leaves open. `domain` is the caller's commitment-space separator (e.g.
 * `DOMAIN_PQ_MMR`) so two distinct logs that share this library cannot have a
 * proof from one accepted by the other.
 *
 * ## Proof shape: `(index, size, proof[])`
 *
 * - `index` — 0-based position of the leaf in the log.
 * - `size`  — total leaf count the `root` commits to.
 * - `proof` — the audit path. Its structure is **fully determined** by
 *   `(index, size)`:
 *     - `inner  = bitLength(index ^ (size - 1))` positional siblings (combined
 *       left/right per the corresponding bit of `index`), followed by
 *     - `border = popcount(index >> inner)` perfect-subtree peak hashes, each
 *       folded in as a left sibling (the right-to-left peak bag).
 *   Because the length and the left/right schedule are derived from
 *   `(index, size)` — never from the prover — a malformed, padded, or reshaped
 *   proof cannot silently validate: `computeRoot` reverts on a length mismatch
 *   and an out-of-range index.
 *
 * Pure library, no storage, no external calls. Hashing uses
 * `abi.encodePacked` (same construction shape as
 * `PqAnchorModule._computeBundleMerkleRoot`) for auditability over inline-asm
 * micro-optimization — the cost is one inclusion proof per bundle (~log₂N
 * hashes), not a per-intent hot path.
 */
library FinalMmr {
    /// @dev Leaf domain tag. Prepended before `domain` + payload.
    uint8 internal constant LEAF_TAG = 0x00;
    /// @dev Internal-node domain tag. Prepended before `domain` + (left,right).
    uint8 internal constant NODE_TAG = 0x01;

    /// @notice Leaf index is not strictly less than the committed `size`.
    error MmrIndexOutOfRange(uint256 index, uint256 size);
    /// @notice `proof.length` does not equal the `(index, size)` decomposition.
    error MmrProofLengthMismatch(uint256 expected, uint256 actual);
    /// @notice `size` is zero — an empty log commits to no leaves.
    error MmrEmptyLog();

    /// @notice Domain-separated MMR leaf hash: `keccak256(0x00 ‖ domain ‖ payload)`.
    /// @param domain Commitment-space separator (e.g. `DOMAIN_PQ_MMR`).
    /// @param payload The value being committed as a leaf (e.g. a bundle root).
    /// @return leaf The tagged leaf hash.
    function hashLeaf(bytes32 domain, bytes32 payload) internal pure returns (bytes32 leaf) {
        return keccak256(abi.encodePacked(LEAF_TAG, domain, payload));
    }

    /// @dev Domain-separated, positional internal node: `keccak256(0x01 ‖ domain ‖ left ‖ right)`.
    function _hashNode(bytes32 domain, bytes32 left, bytes32 right) private pure returns (bytes32 node) {
        return keccak256(abi.encodePacked(NODE_TAG, domain, left, right));
    }

    /// @notice Recompute the append-only-log root implied by a leaf's inclusion proof.
    /// @dev Reverts (never silently returns a wrong root) on an out-of-range index,
    /// an empty log, or a proof whose length does not match the canonical
    /// `(index, size)` decomposition. The caller compares the result to the
    /// trusted anchor.
    /// @param leaf The already-`hashLeaf`-tagged leaf.
    /// @param index 0-based leaf position.
    /// @param size Total committed leaf count.
    /// @param domain Commitment-space separator (must match the leaf's domain).
    /// @param proof Audit path (`inner` positional siblings then `border` peaks).
    /// @return root The recomputed log root.
    function computeRoot(
        bytes32 leaf,
        uint256 index,
        uint256 size,
        bytes32 domain,
        bytes32[] memory proof
    ) internal pure returns (bytes32 root) {
        if (size == 0) revert MmrEmptyLog();
        if (index >= size) revert MmrIndexOutOfRange(index, size);

        // RFC 6962 / Trillian decomposition. `inner` is the number of levels
        // at which the node still has a right subtree within the (possibly
        // imperfect) tree; `border` is the number of perfect-subtree peaks to
        // the left that must be bagged in afterwards.
        uint256 inner = bitLength(index ^ (size - 1));
        uint256 border = popcount(index >> inner);
        uint256 expectedLen = inner + border;
        if (proof.length != expectedLen) revert MmrProofLengthMismatch(expectedLen, proof.length);

        bytes32 res = leaf;
        // Phase 1 — `inner` positional combines. The i-th bit of `index`
        // decides whether the running hash is the left (bit 0) or right (bit 1)
        // child at that level.
        for (uint256 i = 0; i < inner; ) {
            bytes32 sibling = proof[i];
            if ((index >> i) & 1 == 0) {
                res = _hashNode(domain, res, sibling); // running hash is the left child
            } else {
                res = _hashNode(domain, sibling, res); // running hash is the right child
            }
            unchecked { ++i; }
        }
        // Phase 2 — `border` peak bag. Each remaining sibling is a completed
        // perfect-subtree peak to the LEFT, folded right-to-left.
        for (uint256 i = inner; i < expectedLen; ) {
            res = _hashNode(domain, proof[i], res);
            unchecked { ++i; }
        }
        return res;
    }

    /// @notice Verify `leaf` is the `index`-th of `size` leaves committed by `root`.
    /// @dev Convenience wrapper over `computeRoot`. The leaf must already be
    /// `hashLeaf`-tagged. Returns `false` only when the recomputed root differs;
    /// structurally invalid proofs revert inside `computeRoot` (fail-closed).
    function verifyInclusion(
        bytes32 leaf,
        uint256 index,
        uint256 size,
        bytes32 domain,
        bytes32[] memory proof,
        bytes32 root
    ) internal pure returns (bool) {
        return computeRoot(leaf, index, size, domain, proof) == root;
    }

    /// @notice Verify inclusion of a raw `payload` (hashes the leaf internally).
    function verifyInclusionOfPayload(
        bytes32 payload,
        uint256 index,
        uint256 size,
        bytes32 domain,
        bytes32[] memory proof,
        bytes32 root
    ) internal pure returns (bool) {
        return computeRoot(hashLeaf(domain, payload), index, size, domain, proof) == root;
    }

    /// @notice Bit length of `x` (position of the highest set bit + 1); `0` for `x == 0`.
    /// @dev `internal` rather than `private` so `FinalBundleLog` can derive a
    /// proof's shape with the SAME arithmetic the verifier derives it with.
    /// Two copies of this decomposition is a proof built to one shape and
    /// checked against another, which reverts on the gateway naming neither.
    function bitLength(uint256 x) internal pure returns (uint256 n) {
        while (x != 0) {
            x >>= 1;
            unchecked { ++n; }
        }
    }

    /// @notice Population count (number of set bits) of `x`.
    /// @dev `internal` for the same reason as {bitLength}.
    function popcount(uint256 x) internal pure returns (uint256 c) {
        while (x != 0) {
            unchecked {
                c += x & 1;
                x >>= 1;
            }
        }
    }
}

abi

[
  {
    "type": "constructor",
    "inputs": [
      {
        "name": "registry_",
        "type": "address",
        "internalType": "contract FinalIdentityRegistry"
      },
      {
        "name": "intentLog_",
        "type": "address",
        "internalType": "contract FinalIntentLog"
      }
    ],
    "stateMutability": "nonpayable"
  },
  {
    "type": "function",
    "name": "ACTION_CONFIGURE",
    "inputs": [],
    "outputs": [
      {
        "name": "",
        "type": "bytes32",
        "internalType": "bytes32"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "ACTION_SEED",
    "inputs": [],
    "outputs": [
      {
        "name": "",
        "type": "bytes32",
        "internalType": "bytes32"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "ACTION_SEED_PAYLOADS",
    "inputs": [],
    "outputs": [
      {
        "name": "",
        "type": "bytes32",
        "internalType": "bytes32"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "DOMAIN",
    "inputs": [],
    "outputs": [
      {
        "name": "",
        "type": "bytes32",
        "internalType": "bytes32"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "append",
    "inputs": [
      {
        "name": "termsLeaves",
        "type": "bytes32[]",
        "internalType": "bytes32[]"
      },
      {
        "name": "bundles",
        "type": "bytes32[][]",
        "internalType": "bytes32[][]"
      },
      {
        "name": "anchorBlock",
        "type": "uint64",
        "internalType": "uint64"
      },
      {
        "name": "approvals",
        "type": "tuple[]",
        "internalType": "struct FinalPqQuorum.Approval[]",
        "components": [
          {
            "name": "signer",
            "type": "address",
            "internalType": "address"
          },
          {
            "name": "algorithm",
            "type": "uint8",
            "internalType": "uint8"
          },
          {
            "name": "signature",
            "type": "bytes",
            "internalType": "bytes"
          },
          {
            "name": "seal",
            "type": "bytes",
            "internalType": "bytes"
          }
        ]
      }
    ],
    "outputs": [
      {
        "name": "firstIndex",
        "type": "uint256",
        "internalType": "uint256"
      }
    ],
    "stateMutability": "nonpayable"
  },
  {
    "type": "function",
    "name": "appendPayloads",
    "inputs": [
      {
        "name": "payloads",
        "type": "bytes32[]",
        "internalType": "bytes32[]"
      },
      {
        "name": "anchorBlock",
        "type": "uint64",
        "internalType": "uint64"
      },
      {
        "name": "approvals",
        "type": "tuple[]",
        "internalType": "struct FinalPqQuorum.Approval[]",
        "components": [
          {
            "name": "signer",
            "type": "address",
            "internalType": "address"
          },
          {
            "name": "algorithm",
            "type": "uint8",
            "internalType": "uint8"
          },
          {
            "name": "signature",
            "type": "bytes",
            "internalType": "bytes"
          },
          {
            "name": "seal",
            "type": "bytes",
            "internalType": "bytes"
          }
        ]
      }
    ],
    "outputs": [
      {
        "name": "firstIndex",
        "type": "uint256",
        "internalType": "uint256"
      }
    ],
    "stateMutability": "nonpayable"
  },
  {
    "type": "function",
    "name": "bundleAt",
    "inputs": [
      {
        "name": "leafIndex",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "atSize",
        "type": "uint256",
        "internalType": "uint256"
      }
    ],
    "outputs": [
      {
        "name": "payload",
        "type": "bytes32",
        "internalType": "bytes32"
      },
      {
        "name": "proof",
        "type": "bytes32[]",
        "internalType": "bytes32[]"
      },
      {
        "name": "root_",
        "type": "bytes32",
        "internalType": "bytes32"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "configure",
    "inputs": [
      {
        "name": "role",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "k",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "anchorBlock",
        "type": "uint64",
        "internalType": "uint64"
      },
      {
        "name": "approvals",
        "type": "tuple[]",
        "internalType": "struct FinalPqQuorum.Approval[]",
        "components": [
          {
            "name": "signer",
            "type": "address",
            "internalType": "address"
          },
          {
            "name": "algorithm",
            "type": "uint8",
            "internalType": "uint8"
          },
          {
            "name": "signature",
            "type": "bytes",
            "internalType": "bytes"
          },
          {
            "name": "seal",
            "type": "bytes",
            "internalType": "bytes"
          }
        ]
      }
    ],
    "outputs": [],
    "stateMutability": "nonpayable"
  },
  {
    "type": "function",
    "name": "head",
    "inputs": [],
    "outputs": [
      {
        "name": "root_",
        "type": "bytes32",
        "internalType": "bytes32"
      },
      {
        "name": "size_",
        "type": "uint256",
        "internalType": "uint256"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "indexOf",
    "inputs": [
      {
        "name": "payload",
        "type": "bytes32",
        "internalType": "bytes32"
      }
    ],
    "outputs": [
      {
        "name": "index",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "found",
        "type": "bool",
        "internalType": "bool"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "intentLog",
    "inputs": [],
    "outputs": [
      {
        "name": "",
        "type": "address",
        "internalType": "contract FinalIntentLog"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "nonce",
    "inputs": [],
    "outputs": [
      {
        "name": "",
        "type": "uint64",
        "internalType": "uint64"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "payloadAt",
    "inputs": [
      {
        "name": "index",
        "type": "uint256",
        "internalType": "uint256"
      }
    ],
    "outputs": [
      {
        "name": "",
        "type": "bytes32",
        "internalType": "bytes32"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "payloadsBetween",
    "inputs": [
      {
        "name": "from",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "to",
        "type": "uint256",
        "internalType": "uint256"
      }
    ],
    "outputs": [
      {
        "name": "out",
        "type": "bytes32[]",
        "internalType": "bytes32[]"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "peaks",
    "inputs": [],
    "outputs": [
      {
        "name": "",
        "type": "bytes32[]",
        "internalType": "bytes32[]"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "peaksAt",
    "inputs": [
      {
        "name": "atSize",
        "type": "uint256",
        "internalType": "uint256"
      }
    ],
    "outputs": [
      {
        "name": "bag",
        "type": "bytes32[]",
        "internalType": "bytes32[]"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "proofAt",
    "inputs": [
      {
        "name": "leafIndex",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "atSize",
        "type": "uint256",
        "internalType": "uint256"
      }
    ],
    "outputs": [
      {
        "name": "proof",
        "type": "bytes32[]",
        "internalType": "bytes32[]"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "registry",
    "inputs": [],
    "outputs": [
      {
        "name": "",
        "type": "address",
        "internalType": "contract FinalIdentityRegistry"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "root",
    "inputs": [],
    "outputs": [
      {
        "name": "",
        "type": "bytes32",
        "internalType": "bytes32"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "rootAt",
    "inputs": [
      {
        "name": "atSize",
        "type": "uint256",
        "internalType": "uint256"
      }
    ],
    "outputs": [
      {
        "name": "",
        "type": "bytes32",
        "internalType": "bytes32"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "seed",
    "inputs": [
      {
        "name": "peaks_",
        "type": "bytes32[]",
        "internalType": "bytes32[]"
      },
      {
        "name": "size_",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "anchorBlock",
        "type": "uint64",
        "internalType": "uint64"
      },
      {
        "name": "approvals",
        "type": "tuple[]",
        "internalType": "struct FinalPqQuorum.Approval[]",
        "components": [
          {
            "name": "signer",
            "type": "address",
            "internalType": "address"
          },
          {
            "name": "algorithm",
            "type": "uint8",
            "internalType": "uint8"
          },
          {
            "name": "signature",
            "type": "bytes",
            "internalType": "bytes"
          },
          {
            "name": "seal",
            "type": "bytes",
            "internalType": "bytes"
          }
        ]
      }
    ],
    "outputs": [],
    "stateMutability": "nonpayable"
  },
  {
    "type": "function",
    "name": "seedPayloads",
    "inputs": [
      {
        "name": "payloads",
        "type": "bytes32[]",
        "internalType": "bytes32[]"
      },
      {
        "name": "anchorBlock",
        "type": "uint64",
        "internalType": "uint64"
      },
      {
        "name": "approvals",
        "type": "tuple[]",
        "internalType": "struct FinalPqQuorum.Approval[]",
        "components": [
          {
            "name": "signer",
            "type": "address",
            "internalType": "address"
          },
          {
            "name": "algorithm",
            "type": "uint8",
            "internalType": "uint8"
          },
          {
            "name": "signature",
            "type": "bytes",
            "internalType": "bytes"
          },
          {
            "name": "seal",
            "type": "bytes",
            "internalType": "bytes"
          }
        ]
      }
    ],
    "outputs": [],
    "stateMutability": "nonpayable"
  },
  {
    "type": "function",
    "name": "size",
    "inputs": [],
    "outputs": [
      {
        "name": "",
        "type": "uint256",
        "internalType": "uint256"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "threshold",
    "inputs": [],
    "outputs": [
      {
        "name": "",
        "type": "uint256",
        "internalType": "uint256"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "writerRole",
    "inputs": [],
    "outputs": [
      {
        "name": "",
        "type": "uint256",
        "internalType": "uint256"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "event",
    "name": "BundleAppended",
    "inputs": [
      {
        "name": "index",
        "type": "uint256",
        "indexed": true,
        "internalType": "uint256"
      },
      {
        "name": "payload",
        "type": "bytes32",
        "indexed": true,
        "internalType": "bytes32"
      },
      {
        "name": "root",
        "type": "bytes32",
        "indexed": false,
        "internalType": "bytes32"
      },
      {
        "name": "size",
        "type": "uint256",
        "indexed": false,
        "internalType": "uint256"
      }
    ],
    "anonymous": false
  },
  {
    "type": "event",
    "name": "LogConfigured",
    "inputs": [
      {
        "name": "writerRole",
        "type": "uint256",
        "indexed": false,
        "internalType": "uint256"
      },
      {
        "name": "threshold",
        "type": "uint256",
        "indexed": false,
        "internalType": "uint256"
      }
    ],
    "anonymous": false
  },
  {
    "type": "event",
    "name": "Seeded",
    "inputs": [
      {
        "name": "size",
        "type": "uint256",
        "indexed": false,
        "internalType": "uint256"
      },
      {
        "name": "peaks",
        "type": "uint256",
        "indexed": false,
        "internalType": "uint256"
      }
    ],
    "anonymous": false
  },
  {
    "type": "event",
    "name": "SeededPayloads",
    "inputs": [
      {
        "name": "size",
        "type": "uint256",
        "indexed": false,
        "internalType": "uint256"
      }
    ],
    "anonymous": false
  },
  {
    "type": "error",
    "name": "AnchorAhead",
    "inputs": [
      {
        "name": "anchorBlock",
        "type": "uint64",
        "internalType": "uint64"
      },
      {
        "name": "blockNumber",
        "type": "uint256",
        "internalType": "uint256"
      }
    ]
  },
  {
    "type": "error",
    "name": "AnchorStale",
    "inputs": [
      {
        "name": "anchorBlock",
        "type": "uint64",
        "internalType": "uint64"
      },
      {
        "name": "blockNumber",
        "type": "uint256",
        "internalType": "uint256"
      }
    ]
  },
  {
    "type": "error",
    "name": "BadSeal",
    "inputs": [
      {
        "name": "signer",
        "type": "address",
        "internalType": "address"
      }
    ]
  },
  {
    "type": "error",
    "name": "BadSignature",
    "inputs": [
      {
        "name": "signer",
        "type": "address",
        "internalType": "address"
      },
      {
        "name": "algorithm",
        "type": "uint8",
        "internalType": "uint8"
      }
    ]
  },
  {
    "type": "error",
    "name": "EmptyBatch",
    "inputs": []
  },
  {
    "type": "error",
    "name": "EmptyBundleLeaves",
    "inputs": [
      {
        "name": "bundleIndex",
        "type": "uint256",
        "internalType": "uint256"
      }
    ]
  },
  {
    "type": "error",
    "name": "EmptyBundleTree",
    "inputs": []
  },
  {
    "type": "error",
    "name": "LogNotConfigured",
    "inputs": []
  },
  {
    "type": "error",
    "name": "MmrEmptyLog",
    "inputs": []
  },
  {
    "type": "error",
    "name": "MmrIndexOutOfRange",
    "inputs": [
      {
        "name": "index",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "size",
        "type": "uint256",
        "internalType": "uint256"
      }
    ]
  },
  {
    "type": "error",
    "name": "NotAuthorized",
    "inputs": [
      {
        "name": "caller",
        "type": "address",
        "internalType": "address"
      }
    ]
  },
  {
    "type": "error",
    "name": "NotFresh",
    "inputs": []
  },
  {
    "type": "error",
    "name": "PeaksMismatch",
    "inputs": [
      {
        "name": "expected",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "given",
        "type": "uint256",
        "internalType": "uint256"
      }
    ]
  },
  {
    "type": "error",
    "name": "PrecompileUnavailable",
    "inputs": [
      {
        "name": "precompile",
        "type": "address",
        "internalType": "address"
      }
    ]
  },
  {
    "type": "error",
    "name": "ProofShapeMismatch",
    "inputs": [
      {
        "name": "expected",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "walked",
        "type": "uint256",
        "internalType": "uint256"
      }
    ]
  },
  {
    "type": "error",
    "name": "SignerLacksRole",
    "inputs": [
      {
        "name": "signer",
        "type": "address",
        "internalType": "address"
      },
      {
        "name": "roleMask",
        "type": "uint256",
        "internalType": "uint256"
      }
    ]
  },
  {
    "type": "error",
    "name": "SignersNotAscending",
    "inputs": [
      {
        "name": "previous",
        "type": "address",
        "internalType": "address"
      },
      {
        "name": "next",
        "type": "address",
        "internalType": "address"
      }
    ]
  },
  {
    "type": "error",
    "name": "SizeAhead",
    "inputs": [
      {
        "name": "asked",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "held",
        "type": "uint256",
        "internalType": "uint256"
      }
    ]
  },
  {
    "type": "error",
    "name": "TermsLeafCountMismatch",
    "inputs": [
      {
        "name": "termsLeaves",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "bundles",
        "type": "uint256",
        "internalType": "uint256"
      }
    ]
  },
  {
    "type": "error",
    "name": "ThresholdIsZero",
    "inputs": []
  },
  {
    "type": "error",
    "name": "ThresholdNotMet",
    "inputs": [
      {
        "name": "valid",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "required",
        "type": "uint256",
        "internalType": "uint256"
      }
    ]
  },
  {
    "type": "error",
    "name": "ThresholdUnreachable",
    "inputs": [
      {
        "name": "live",
        "type": "uint256",
        "internalType": "uint256"
      },
      {
        "name": "required",
        "type": "uint256",
        "internalType": "uint256"
      }
    ]
  },
  {
    "type": "error",
    "name": "WrongAlgorithm",
    "inputs": [
      {
        "name": "signer",
        "type": "address",
        "internalType": "address"
      },
      {
        "name": "got",
        "type": "uint8",
        "internalType": "uint8"
      },
      {
        "name": "required",
        "type": "uint8",
        "internalType": "uint8"
      }
    ]
  },
  {
    "type": "error",
    "name": "ZeroBundleRoot",
    "inputs": []
  },
  {
    "type": "error",
    "name": "ZeroIntentLog",
    "inputs": []
  },
  {
    "type": "error",
    "name": "ZeroTermsLeaf",
    "inputs": [
      {
        "name": "bundleIndex",
        "type": "uint256",
        "internalType": "uint256"
      }
    ]
  }
]

odczyt kontraktu

bajtkod · 9,427 bajtów

0x6080806040526004361015610012575f80fd5b5f3560e01c90816304fedb2f14611597575080631a30f07914611579578063205a094e1461155f5780632441c09b1461104157806342cde4e81461102457806348d316ac14610fd457806352a9674b14610f9a5780635bb8a95114610f7c57806362984f8814610ce65780636ce6041714610cb35780636f4ce56a14610c8757806372f56b2c14610c4d5780637b10399914610c095780638f7dcfa314610be5578063949d225d14610bc857806394bc4e96146108a5578063affed0e01461087f578063b19f480514610845578063c0131f5914610801578063ca2869a0146107db578063cba57358146107bf578063cba8bdf714610382578063ebb3eedb146102d6578063ebf0c717146102b95763f47f54cb1461012f575f80fd5b346102b55761013d3661165e565b9392919082156102a657600154918215610297576001600160401b03916102506102569260025495858716936006549a8b8b6101a78c61019960405193849260208401968d88526040850152606080850152608084019161198a565b03601f1981018352826116f8565b51902060405160208101917fd850f5df47b124511e8e6ec99cf1a0beaf7c6237eff0a31305ce53d85f31267583524660408301523060608301527f5985b2aa0699a556c4b84df321b016abe612f656b53dfdb4741aaa1912f686b460808301528a871660a083015260c082015260c0815261022360e0826116f8565b519020905f54927f0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d540611c48565b506119ae565b16906001600160401b031916176002555f5b81811061027a57602084604051908152f35b8061029161028b60019385876119cc565b35611edd565b01610268565b6382d4481f60e01b5f5260045ffd5b63c2e5347d60e01b5f5260045ffd5b5f80fd5b346102b5575f3660031901126102b5576020600754604051908152f35b346102b5576102e4366115cf565b60065480821161036c57508082116103565761030861030383836116de565b611744565b91805b82811061032c5760405160208082528190610328908201876115e5565b0390f35b806001915f52600460205260405f205461034f61034985846116de565b87611797565b520161030b565b906388c73b2960e01b5f5260045260245260445ffd5b90635b8d5fdb60e11b5f5260045260245260445ffd5b346102b55760803660031901126102b5576004356001600160401b0381116102b5576103b2903690600401611618565b602435906103be611648565b6064356001600160401b0381116102b5576103dd903690600401611618565b6040516328305db160e21b81527f0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d5406001600160a01b03169290602081600481875afa90811561067b575f91610790575b50801561072a575b61058c575b5050505060065461057d575f82805b610559575081810361054457505f8060ff5b60018086831c16146104e1575b801561049057801561047c575f190161045b565b634e487b7160e01b5f52601160045260245ffd5b7f67f9b61bf7b39fd24dd60467083f89ea77979db358db2804069474590a36c035604086868160065581155f146104d3575f5b60075582519182526020820152a1005b6104dc82611b22565b6104c3565b906104ed8385886119cc565b35156105355761052f9061050b61050385611be3565b9486896119cc565b35835f52600360205260405f2082851c5f5260205260405f20556001831b906116eb565b90610468565b634425ca1360e01b5f5260045ffd5b63ecc9b8ed60e01b5f5260045260245260445ffd5b60018082161461056d575b60011c80610449565b9061057790611be3565b90610564565b63dc63d81f60e01b5f5260045ffd5b6040516020810190604082526105be816105aa606082018a8d61198a565b8a604083015203601f1981018352826116f8565b519020833b156102b55790826001600160401b039593926040519687956322f3f44760e11b875260848701927f405bbda3343b6e69c32fb7eafff8f0a1e55a5ee2ec35458b3abc776b2668195260048901526024880152166044860152608060648601525260a4830160a060048460051b8601010192825f90607e19813603015b8383106106865750505050505091815f818582965003925af1801561067b5761066b575b80808061043a565b5f610675916116f8565b83610663565b6040513d5f823e3d90fd5b60a3198a8803018552949650929491939092918635828112156102b557830180356001600160a01b038116908190036102b557825260208101359160ff83168093036102b55761071860209282600195858095015261070a6106ff6106ee6040850185611a13565b608060408601526080850191611a44565b926060810190611a13565b916060818503910152611a44565b9801960193019091889695949261063f565b5060405163f5778b0360e01b8152602081600481875afa90811561067b575f91610761575b506001600160a01b0316331415610435565b610783915060203d602011610789575b61077b81836116f8565b8101906119f4565b8861074f565b503d610771565b6107b2915060203d6020116107b8575b6107aa81836116f8565b8101906119dc565b8861042d565b503d6107a0565b346102b5575f3660031901126102b55760205f54604051908152f35b346102b55760203660031901126102b55760206107f9600435611b22565b604051908152f35b346102b5575f3660031901126102b5576040517f000000000000000000000000f2b3161ed308717da082ee706cfdd27048e0d62d6001600160a01b03168152602090f35b346102b5575f3660031901126102b55760206040517f27c91cbb7cc32319dd47788e8b096cc02ee8ccca641645266d7046769a12fbc38152f35b346102b5575f3660031901126102b55760206001600160401b0360025416604051908152f35b346102b55760803660031901126102b5576024356004356108c4611648565b6064356001600160401b0381116102b5576108e3903690600401611618565b6040516328305db160e21b81527f0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d5406001600160a01b0316939290602081600481885afa90811561067b575f91610ba9575b508015610b53575b610a01575b50505082610983575b7fbfc08a458e488f0e56f7ff4bfe317bed1ba5d3f7ef5a2bda241528695f6fcdf360408385815f558060015582519182526020820152a1005b60206024916040519283809263342f616360e01b82528660048301525afa90811561067b575f916109cf575b508281101561094a579050633770da3360e11b5f5260045260245260445ffd5b90506020813d6020116109f9575b816109ea602093836116f8565b810103126102b55751836109af565b3d91506109dd565b604051602081019086825287604082015260408152610a216060826116f8565b519020843b156102b55790826001600160401b0394926040519586946322f3f44760e11b865260848601927f27c91cbb7cc32319dd47788e8b096cc02ee8ccca641645266d7046769a12fbc360048801526024870152166044850152608060648501525260a4820160a060048560051b8501010193825f90607e19813603015b838310610adb5750505050505080825f9350038183865af1801561067b57610acb575b8080610941565b5f610ad5916116f8565b83610ac4565b60a3198989030185529496939550919390928635828112156102b557830180356001600160a01b038116908190036102b557825260208101359160ff83168093036102b557610b4260209282600195858095015261070a6106ff6106ee6040850185611a13565b980196019301909187959492610aa1565b5060405163f5778b0360e01b8152602081600481885afa90811561067b575f91610b8a575b506001600160a01b031633141561093c565b610ba3915060203d6020116107895761077b81836116f8565b87610b78565b610bc2915060203d6020116107b8576107aa81836116f8565b87610934565b346102b5575f3660031901126102b5576020600654604051908152f35b346102b5575f3660031901126102b557604060075460065482519182526020820152f35b346102b5575f3660031901126102b5576040517f0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d5406001600160a01b03168152602090f35b346102b5575f3660031901126102b55760206040517f405bbda3343b6e69c32fb7eafff8f0a1e55a5ee2ec35458b3abc776b266819528152f35b346102b55760203660031901126102b5576040610ca5600435611af8565b825191825215156020820152f35b346102b55760203660031901126102b557610328610cd2600435611a64565b6040519182916020835260208301906115e5565b346102b557610cf43661165e565b6040516328305db160e21b81529394937f0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d5406001600160a01b03169290602081600481875afa90811561067b575f91610f5d575b508015610f07575b610db2575b5050505060065461057d5781156102a6575f5b828110610d9b577f1c295873c1ce4ce2ac720f43d6909e66b931b42e9246b862278eba9624c0bf05602084604051908152a1005b80610dac61028b60019386866119cc565b01610d67565b604051602081019060208252610dd081610199604082018b8b61198a565b519020833b156102b55790826001600160401b039593926040519687956322f3f44760e11b875260848701927f9abdf9961fd14fd177480eccbad16b2d7f231898b2d763c8e8b50364d8b3b17160048901526024880152166044860152608060648601525260a4830160a060048460051b8601010192825f90607e19813603015b838310610e8d5750505050505091815f818582965003925af1801561067b57610e7d575b808080610d54565b5f610e87916116f8565b82610e75565b60a3198a8803018552949650929491939092918635828112156102b557830180356001600160a01b038116908190036102b557825260208101359160ff83168093036102b557610ef560209282600195858095015261070a6106ff6106ee6040850185611a13565b98019601930190918896959492610e51565b5060405163f5778b0360e01b8152602081600481875afa90811561067b575f91610f3e575b506001600160a01b0316331415610d4f565b610f57915060203d6020116107895761077b81836116f8565b87610f2c565b610f76915060203d6020116107b8576107aa81836116f8565b87610d47565b346102b5575f3660031901126102b557610328610cd2600654611a64565b346102b5575f3660031901126102b55760206040517fb66ca34dc0d9a9daa6230aee35894330ccfa7e4eaa29a198577eed0b26a412058152f35b346102b557610fe2366115cf565b610ffe610ff8610ff283856117ab565b936116c3565b91611b22565b61101a60405193849384526060602085015260608401906115e5565b9060408301520390f35b346102b5575f3660031901126102b5576020600154604051908152f35b346102b55760803660031901126102b5576004356001600160401b0381116102b557611071903690600401611618565b906024356001600160401b0381116102b557611091903690600401611618565b61109c939193611648565b936064356001600160401b0381116102b5576110bc903690600401611618565b9583156102a6578385036115485760015480156102975760029795979693965492600654966040516001600160401b03861660208201528860408201526080606082015261110e60a082018c8961198a565b601f19828203016080830152888152602081019060208a60051b820101918c915f5b8c81106114dc575050505090611156816111dd979695949303601f1981018352826116f8565b6020815191012060405160208101917fd850f5df47b124511e8e6ec99cf1a0beaf7c6237eff0a31305ce53d85f31267583524660408301523060608301527f2e1c2ff2f9bb13fd926fe3e8b209f98e6c873bb259534a2148ca355409247cba60808301526001600160401b03871660a083015260c082015260c0815261022360e0826116f8565b506001600160401b036111f18183166119ae565b67ffffffffffffffff199092169116176002555f947f000000000000000000000000f2b3161ed308717da082ee706cfdd27048e0d62d6001600160a01b03165b838710156114d1578660051b860135601e19873603018112156102b55786018035906001600160401b0382116102b557602001908060051b360382136102b55780156114be576112828985876119cc565b35156114ab576001810180821161047c5761129c90611744565b916112a88a86886119cc565b356112b284611776565b525f5b82811061143457505050805115611425575b805160018111156113f9578060011c9060018116926112e961030385856116eb565b935f5b8481106113795750600114611304575b5050506112c7565b5f19820191821161047c576113709161131c91611797565b516040516020810191600160f91b83527fc976f483968b324bd57de8efa226478a3634db61776dacd4da866f8fa37c0fd560218301526041820152604181526113666061826116f8565b5190209183611797565b528880806112fc565b80600191821b6113968361138d8388611797565b51921786611797565b516040519060208201928560f81b84527fc976f483968b324bd57de8efa226478a3634db61776dacd4da866f8fa37c0fd5602184015260418301526061820152606181526113e56081826116f8565b5190206113f28289611797565b52016112ec565b50966114176114116001939699989598979497611776565b51611edd565b019592949194939093611231565b634f297b6160e11b5f5260045ffd5b61143f8184846119cc565b35853b156102b5576040519063af6f8c1b60e01b825260048201525f81602481838a5af1801561067b5761149b575b5061147a8184846119cc565b3590600181019182821161047c5761149460019387611797565b52016112b5565b5f6114a5916116f8565b8b61146e565b886322566cfd60e01b5f5260045260245ffd5b8863c9cdeff560e01b5f5260045260245ffd5b602085604051908152f35b909192939c9e9c601f9e9b9e19838203018452601e198c360301853512156102b5578b85350190602082359201916001600160401b0381116102b5578060051b360383136102b557611534602092839260019561198a565b9601940191019e9c9e9d9a9d919091611130565b8385635b2d642360e11b5f5260045260245260445ffd5b346102b557610328610cd2611573366115cf565b906117ab565b346102b55760203660031901126102b55760206107f96004356116c3565b346102b5575f3660031901126102b557807f9abdf9961fd14fd177480eccbad16b2d7f231898b2d763c8e8b50364d8b3b17160209252f35b60409060031901126102b5576004359060243590565b90602080835192838152019201905f5b8181106116025750505090565b82518452602093840193909201916001016115f5565b9181601f840112156102b5578235916001600160401b0383116102b5576020808501948460051b0101116102b557565b604435906001600160401b03821682036102b557565b9060606003198301126102b5576004356001600160401b0381116102b5578261168991600401611618565b929092916024356001600160401b03811681036102b55791604435906001600160401b0382116102b5576116bf91600401611618565b9091565b6006548082101561035657505f52600460205260405f205490565b9190820391821161047c57565b9190820180921161047c57565b90601f801991011681019081106001600160401b0382111761171957604052565b634e487b7160e01b5f52604160045260245ffd5b6001600160401b0381116117195760051b60200190565b9061174e8261172d565b61175b60405191826116f8565b828152809261176c601f199161172d565b0190602036910137565b8051156117835760200190565b634e487b7160e01b5f52603260045260245ffd5b80518210156117835760209160051b010190565b919060065480821161036c5750801561197b5780831015611965575f1981019080821161047c57906117f56103036117e4838718611bf1565b6117ef87821c611c0a565b906116eb565b9390915f835f945b61182757505050508251808203611812575050565b63383613b560e01b5f5260045260245260445ffd5b90919293600185188281105f14611874578392916001949185925f52600360205260405f20905f5260205260405f2054611861828b611797565b5201945b831c9392918201911c806117fd565b82819692961461188a575b509060019291611865565b839591951b61189981866116de565b6118a561030382611c0a565b905f92906118b281611bf1565b805b611918575050505f19820191821161047c576118d08282611797565b5191805b6118f85750506001939291818592506118ed828b611797565b52019490919261187f565b5f19019182906119129061190c8385611797565b51612048565b926118d4565b5f190160018083831c161461192e575b806118b4565b6001819395825f52600360205260405f2087841c5f5260205260405f20546119568288611797565b5201946001821b019250611928565b826388c73b2960e01b5f5260045260245260445ffd5b635bf77f6760e01b5f5260045ffd5b81835290916001600160fb1b0383116102b55760209260051b809284830137010190565b6001600160401b036001911601906001600160401b03821161047c57565b91908110156117835760051b0190565b908160209103126102b5575180151581036102b55790565b908160209103126102b557516001600160a01b03811681036102b55790565b9035601e19823603018112156102b55701602081359101916001600160401b0382116102b55781360383136102b557565b908060209392818452848401375f828201840152601f01601f1916010190565b90600654808311611ae25750611a7c61030383611c0a565b915f5f91611a8981611bf1565b805b611a955750505050565b5f190160018083831c1614611aab575b80611a8b565b6001819493825f52600360205260405f2085841c5f5260205260405f2054611ad3828a611797565b5201926001821b019350611aa5565b82635b8d5fdb60e11b5f5260045260245260445ffd5b5f52600560205260405f20548015611b1b575f19810190811161047c5790600190565b505f905f90565b60065480821161036c57508015611bde57611b3f61030382611c0a565b5f915f90611b4c81611bf1565b805b611b91575050505f19820191821161047c57611b6a8282611797565b5191805b611b7757505090565b5f1901918290611b8b9061190c8385611797565b92611b6e565b5f190160018083831c1614611ba7575b80611b4e565b6001819395825f52600360205260405f2087841c5f5260205260405f2054611bcf8288611797565b5201946001821b019250611ba1565b505f90565b5f19811461047c5760010190565b90815f925b611bfd5750565b6001928301921c80611bf6565b90815f925b611c165750565b9160018316019160011c80611c0f565b356001600160a01b03811681036102b55790565b3560ff811681036102b55790565b92939195965f978615611ece576001600160401b0316438111611eb857610258611c7282436116de565b11611ea2575060405194602086015260208552611c906040866116f8565b5f955f985b888a1015611e78578960051b840135607e19853603018112156102b557840197611cbe89611c26565b6001600160a01b039182169116811015611e4c5750611cdc88611c26565b97611ce681611c26565b604051632e4bfa5160e11b81526001600160a01b0391821660048201526024810188905290602090829060449082908c165afa90811561067b575f91611e2e575b5015611e075760208101600460ff611d3e83611c3a565b1603611dd557611d4f89838a612183565b15611da25750611d608882896122c8565b15611d795750611d71600191611be3565b990198611c95565b611d8290611c26565b63c082266360e01b5f9081526001600160a01b0391909116600452602490fd5b611db6611db060ff93611c26565b91611c3a565b9063bbf82ba360e01b5f5260018060a01b03166004521660245260445ffd5b611de3611db060ff93611c26565b9063587548c360e11b5f5260018060a01b031660045216602452600460445260645ffd5b611e118691611c26565b63ae8bb03960e01b5f5260018060a01b031660045260245260445ffd5b611e46915060203d81116107b8576107aa81836116f8565b5f611d27565b611e5589611c26565b6311641feb60e21b5f9081526004929092526001600160a01b0316602452604490fd5b98509550955050505050808310611e8c5750565b826305bc216760e51b5f5260045260245260445ffd5b630ed38fd160e41b5f526004524360245260445ffd5b637b51505560e01b5f526004524360245260445ffd5b631fc460bf60e11b5f5260045ffd5b801561053557600654805f5260046020528160405f2055815f52600560205260405f20541561202b575b60405160208101905f82527fb66ca34dc0d9a9daa6230aee35894330ccfa7e4eaa29a198577eed0b26a41205602182015283604182015260418152611f4d6061826116f8565b5190205f8281527f3617319a054d772f909f7c479a2cebe5066e836a939412e32403c99029b92eff602052604081208290559082905b600180831614611fd6575050507f1585fcb2f8b662b0e77609aa657994b280f417bea79b40989d135efaf884054860406001830180600655611fc481611b22565b908160075582519182526020820152a3565b825f52600360205260405f20905f1983019083821161047c57600192612005925f5260205260405f2054612048565b91811c920190815f52600360205260405f20835f526020528060405f2055919091611f83565b6001810180821161047c57825f52600560205260405f2055611f07565b90604051906020820192600160f81b84527fb66ca34dc0d9a9daa6230aee35894330ccfa7e4eaa29a198577eed0b26a41205602184015260418301526061820152606181526120986081826116f8565b51902090565b6001600160401b03811161171957601f01601f191660200190565b6020818303126102b5578051906001600160401b0382116102b5570181601f820112156102b5578051906120ec8261209e565b926120fa60405194856116f8565b828452602083830101116102b557815f9260208093018386015e8301015290565b903590601e19813603018212156102b557018035906001600160401b0382116102b5576020019181360383136102b557565b9291926121598261209e565b9161216760405193846116f8565b8294818452818301116102b5578281602093845f960137010152565b9160208201600460ff61219583611c3a565b16146122475760ff6121a8600592611c3a565b16146121b5575050505f90565b5f6121bf83611c26565b604051639e5adaeb60e01b81526001600160a01b0391821660048201529485916024918391165afa91821561067b57612218935f9361221b575b5061220b81604061221293019061211b565b369161214d565b9161244f565b90565b61221291935061223f61220b913d805f833e61223781836116f8565b8101906120b9565b9391506121f9565b505f61225283611c26565b60405163b7af85d760e01b81526001600160a01b0391821660048201529485916024918391165afa91821561067b57612218935f936122a4575b5061220b81604061229e93019061211b565b91612366565b61229e9193506122c061220b913d805f833e61223781836116f8565b93915061228c565b90915f6122d484611c26565b60405163ad84ad1360e01b81526001600160a01b0391821660048201529384916024918391165afa91821561067b575f9261234a575b508151158015612334575b61232d5761221261220b84606061221896019061211b565b5050505f90565b50612342606084018461211b565b905015612315565b61235f9192503d805f833e61223781836116f8565b905f61230a565b610a20815114801590612442575b61232d5760206123c75f948286958160405195869481808701998051918291018b5e8601908282018b8152815193849201905e010190878252805192839101825e0185815203601f1981018352826116f8565b51906102045afa3d1561243b573d6123de8161209e565b906123ec60405192836116f8565b81523d5f602083013e5b8161242f575b81612405575090565b905060208151910151906020811061241e575b50151590565b5f199060200360031b1b165f612418565b805160201491506123fc565b60606123f6565b5061121383511415612374565b60408151148015906124c6575b61232d5760206124af5f948286958160405195869481808701998051918291018b5e8601908282018b8152815193849201905e010190878252805192839101825e0185815203601f1981018352826116f8565b51906102055afa3d1561243b573d6123de8161209e565b506174608351141561245c56
Brak ogona metadanych CBOR — ten bajtkod zbudowano z wyłączonym cbor_metadata, ustawieniem, które nasze kontrakty przypinają dla niezmienności adresów CREATE2.

deasemblacja (pierwsze 4,000 operacji)

pcopoperand
0000PUSH10x80
0002DUP1
0003PUSH10x40
0005MSTORE
0006PUSH10x04
0008CALLDATASIZE
0009LT
000aISZERO
000bPUSH20x0012
000eJUMPI
000fPUSH0
0010DUP1
0011REVERT
0012JUMPDEST
0013PUSH0
0014CALLDATALOAD
0015PUSH10xe0
0017SHR
0018SWAP1
0019DUP2
001aPUSH40x04fedb2f
001fEQ
0020PUSH20x1597
0023JUMPI
0024POP
0025DUP1
0026PUSH40x1a30f079
002bEQ
002cPUSH20x1579
002fJUMPI
0030DUP1
0031PUSH40x205a094e
0036EQ
0037PUSH20x155f
003aJUMPI
003bDUP1
003cPUSH40x2441c09b
0041EQ
0042PUSH20x1041
0045JUMPI
0046DUP1
0047PUSH40x42cde4e8
004cEQ
004dPUSH20x1024
0050JUMPI
0051DUP1
0052PUSH40x48d316ac
0057EQ
0058PUSH20x0fd4
005bJUMPI
005cDUP1
005dPUSH40x52a9674b
0062EQ
0063PUSH20x0f9a
0066JUMPI
0067DUP1
0068PUSH40x5bb8a951
006dEQ
006ePUSH20x0f7c
0071JUMPI
0072DUP1
0073PUSH40x62984f88
0078EQ
0079PUSH20x0ce6
007cJUMPI
007dDUP1
007ePUSH40x6ce60417
0083EQ
0084PUSH20x0cb3
0087JUMPI
0088DUP1
0089PUSH40x6f4ce56a
008eEQ
008fPUSH20x0c87
0092JUMPI
0093DUP1
0094PUSH40x72f56b2c
0099EQ
009aPUSH20x0c4d
009dJUMPI
009eDUP1
009fPUSH40x7b103999
00a4EQ
00a5PUSH20x0c09
00a8JUMPI
00a9DUP1
00aaPUSH40x8f7dcfa3
00afEQ
00b0PUSH20x0be5
00b3JUMPI
00b4DUP1
00b5PUSH40x949d225d
00baEQ
00bbPUSH20x0bc8
00beJUMPI
00bfDUP1
00c0PUSH40x94bc4e96
00c5EQ
00c6PUSH20x08a5
00c9JUMPI
00caDUP1
00cbPUSH40xaffed0e0
00d0EQ
00d1PUSH20x087f
00d4JUMPI
00d5DUP1
00d6PUSH40xb19f4805
00dbEQ
00dcPUSH20x0845
00dfJUMPI
00e0DUP1
00e1PUSH40xc0131f59
00e6EQ
00e7PUSH20x0801
00eaJUMPI
00ebDUP1
00ecPUSH40xca2869a0
00f1EQ
00f2PUSH20x07db
00f5JUMPI
00f6DUP1
00f7PUSH40xcba57358
00fcEQ
00fdPUSH20x07bf
0100JUMPI
0101DUP1
0102PUSH40xcba8bdf7
0107EQ
0108PUSH20x0382
010bJUMPI
010cDUP1
010dPUSH40xebb3eedb
0112EQ
0113PUSH20x02d6
0116JUMPI
0117DUP1
0118PUSH40xebf0c717
011dEQ
011ePUSH20x02b9
0121JUMPI
0122PUSH40xf47f54cb
0127EQ
0128PUSH20x012f
012bJUMPI
012cPUSH0
012dDUP1
012eREVERT
012fJUMPDEST
0130CALLVALUE
0131PUSH20x02b5
0134JUMPI
0135PUSH20x013d
0138CALLDATASIZE
0139PUSH20x165e
013cJUMP
013dJUMPDEST
013eSWAP4
013fSWAP3
0140SWAP2
0141SWAP1
0142DUP3
0143ISZERO
0144PUSH20x02a6
0147JUMPI
0148PUSH10x01
014aSLOAD
014bSWAP2
014cDUP3
014dISZERO
014ePUSH20x0297
0151JUMPI
0152PUSH10x01
0154PUSH10x01
0156PUSH10x40
0158SHL
0159SUB
015aSWAP2
015bPUSH20x0250
015ePUSH20x0256
0161SWAP3
0162PUSH10x02
0164SLOAD
0165SWAP6
0166DUP6
0167DUP8
0168AND
0169SWAP4
016aPUSH10x06
016cSLOAD
016dSWAP11
016eDUP12
016fDUP12
0170PUSH20x01a7
0173DUP13
0174PUSH20x0199
0177PUSH10x40
0179MLOAD
017aSWAP4
017bDUP5
017cSWAP3
017dPUSH10x20
017fDUP5
0180ADD
0181SWAP7
0182DUP14
0183DUP9
0184MSTORE
0185PUSH10x40
0187DUP6
0188ADD
0189MSTORE
018aPUSH10x60
018cDUP1
018dDUP6
018eADD
018fMSTORE
0190PUSH10x80
0192DUP5
0193ADD
0194SWAP2
0195PUSH20x198a
0198JUMP
0199JUMPDEST
019aSUB
019bPUSH10x1f
019dNOT
019eDUP2
019fADD
01a0DUP4
01a1MSTORE
01a2DUP3
01a3PUSH20x16f8
01a6JUMP
01a7JUMPDEST
01a8MLOAD
01a9SWAP1
01aaKECCAK256
01abPUSH10x40
01adMLOAD
01aePUSH10x20
01b0DUP2
01b1ADD
01b2SWAP2
01b3PUSH320xd850f5df47b124511e8e6ec99cf1a0beaf7c6237eff0a31305ce53d85f312675
01d4DUP4
01d5MSTORE
01d6CHAINID
01d7PUSH10x40
01d9DUP4
01daADD
01dbMSTORE
01dcADDRESS
01ddPUSH10x60
01dfDUP4
01e0ADD
01e1MSTORE
01e2PUSH320x5985b2aa0699a556c4b84df321b016abe612f656b53dfdb4741aaa1912f686b4
0203PUSH10x80
0205DUP4
0206ADD
0207MSTORE
0208DUP11
0209DUP8
020aAND
020bPUSH10xa0
020dDUP4
020eADD
020fMSTORE
0210PUSH10xc0
0212DUP3
0213ADD
0214MSTORE
0215PUSH10xc0
0217DUP2
0218MSTORE
0219PUSH20x0223
021cPUSH10xe0
021eDUP3
021fPUSH20x16f8
0222JUMP
0223JUMPDEST
0224MLOAD
0225SWAP1
0226KECCAK256
0227SWAP1
0228PUSH0
0229SLOAD
022aSWAP3
022bPUSH320x0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d540
024cPUSH20x1c48
024fJUMP
0250JUMPDEST
0251POP
0252PUSH20x19ae
0255JUMP
0256JUMPDEST
0257AND
0258SWAP1
0259PUSH10x01
025bPUSH10x01
025dPUSH10x40
025fSHL
0260SUB
0261NOT
0262AND
0263OR
0264PUSH10x02
0266SSTORE
0267PUSH0
0268JUMPDEST
0269DUP2
026aDUP2
026bLT
026cPUSH20x027a
026fJUMPI
0270PUSH10x20
0272DUP5
0273PUSH10x40
0275MLOAD
0276SWAP1
0277DUP2
0278MSTORE
0279RETURN
027aJUMPDEST
027bDUP1
027cPUSH20x0291
027fPUSH20x028b
0282PUSH10x01
0284SWAP4
0285DUP6
0286DUP8
0287PUSH20x19cc
028aJUMP
028bJUMPDEST
028cCALLDATALOAD
028dPUSH20x1edd
0290JUMP
0291JUMPDEST
0292ADD
0293PUSH20x0268
0296JUMP
0297JUMPDEST
0298PUSH40x82d4481f
029dPUSH10xe0
029fSHL
02a0PUSH0
02a1MSTORE
02a2PUSH10x04
02a4PUSH0
02a5REVERT
02a6JUMPDEST
02a7PUSH40xc2e5347d
02acPUSH10xe0
02aeSHL
02afPUSH0
02b0MSTORE
02b1PUSH10x04
02b3PUSH0
02b4REVERT
02b5JUMPDEST
02b6PUSH0
02b7DUP1
02b8REVERT
02b9JUMPDEST
02baCALLVALUE
02bbPUSH20x02b5
02beJUMPI
02bfPUSH0
02c0CALLDATASIZE
02c1PUSH10x03
02c3NOT
02c4ADD
02c5SLT
02c6PUSH20x02b5
02c9JUMPI
02caPUSH10x20
02ccPUSH10x07
02ceSLOAD
02cfPUSH10x40
02d1MLOAD
02d2SWAP1
02d3DUP2
02d4MSTORE
02d5RETURN
02d6JUMPDEST
02d7CALLVALUE
02d8PUSH20x02b5
02dbJUMPI
02dcPUSH20x02e4
02dfCALLDATASIZE
02e0PUSH20x15cf
02e3JUMP
02e4JUMPDEST
02e5PUSH10x06
02e7SLOAD
02e8DUP1
02e9DUP3
02eaGT
02ebPUSH20x036c
02eeJUMPI
02efPOP
02f0DUP1
02f1DUP3
02f2GT
02f3PUSH20x0356
02f6JUMPI
02f7PUSH20x0308
02faPUSH20x0303
02fdDUP4
02feDUP4
02ffPUSH20x16de
0302JUMP
0303JUMPDEST
0304PUSH20x1744
0307JUMP
0308JUMPDEST
0309SWAP2
030aDUP1
030bJUMPDEST
030cDUP3
030dDUP2
030eLT
030fPUSH20x032c
0312JUMPI
0313PUSH10x40
0315MLOAD
0316PUSH10x20
0318DUP1
0319DUP3
031aMSTORE
031bDUP2
031cSWAP1
031dPUSH20x0328
0320SWAP1
0321DUP3
0322ADD
0323DUP8
0324PUSH20x15e5
0327JUMP
0328JUMPDEST
0329SUB
032aSWAP1
032bRETURN
032cJUMPDEST
032dDUP1
032ePUSH10x01
0330SWAP2
0331PUSH0
0332MSTORE
0333PUSH10x04
0335PUSH10x20
0337MSTORE
0338PUSH10x40
033aPUSH0
033bKECCAK256
033cSLOAD
033dPUSH20x034f
0340PUSH20x0349
0343DUP6
0344DUP5
0345PUSH20x16de
0348JUMP
0349JUMPDEST
034aDUP8
034bPUSH20x1797
034eJUMP
034fJUMPDEST
0350MSTORE
0351ADD
0352PUSH20x030b
0355JUMP
0356JUMPDEST
0357SWAP1
0358PUSH40x88c73b29
035dPUSH10xe0
035fSHL
0360PUSH0
0361MSTORE
0362PUSH10x04
0364MSTORE
0365PUSH10x24
0367MSTORE
0368PUSH10x44
036aPUSH0
036bREVERT
036cJUMPDEST
036dSWAP1
036ePUSH40x5b8d5fdb
0373PUSH10xe1
0375SHL
0376PUSH0
0377MSTORE
0378PUSH10x04
037aMSTORE
037bPUSH10x24
037dMSTORE
037ePUSH10x44
0380PUSH0
0381REVERT
0382JUMPDEST
0383CALLVALUE
0384PUSH20x02b5
0387JUMPI
0388PUSH10x80
038aCALLDATASIZE
038bPUSH10x03
038dNOT
038eADD
038fSLT
0390PUSH20x02b5
0393JUMPI
0394PUSH10x04
0396CALLDATALOAD
0397PUSH10x01
0399PUSH10x01
039bPUSH10x40
039dSHL
039eSUB
039fDUP2
03a0GT
03a1PUSH20x02b5
03a4JUMPI
03a5PUSH20x03b2
03a8SWAP1
03a9CALLDATASIZE
03aaSWAP1
03abPUSH10x04
03adADD
03aePUSH20x1618
03b1JUMP
03b2JUMPDEST
03b3PUSH10x24
03b5CALLDATALOAD
03b6SWAP1
03b7PUSH20x03be
03baPUSH20x1648
03bdJUMP
03beJUMPDEST
03bfPUSH10x64
03c1CALLDATALOAD
03c2PUSH10x01
03c4PUSH10x01
03c6PUSH10x40
03c8SHL
03c9SUB
03caDUP2
03cbGT
03ccPUSH20x02b5
03cfJUMPI
03d0PUSH20x03dd
03d3SWAP1
03d4CALLDATASIZE
03d5SWAP1
03d6PUSH10x04
03d8ADD
03d9PUSH20x1618
03dcJUMP
03ddJUMPDEST
03dePUSH10x40
03e0MLOAD
03e1PUSH40x28305db1
03e6PUSH10xe2
03e8SHL
03e9DUP2
03eaMSTORE
03ebPUSH320x0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d540
040cPUSH10x01
040ePUSH10x01
0410PUSH10xa0
0412SHL
0413SUB
0414AND
0415SWAP3
0416SWAP1
0417PUSH10x20
0419DUP2
041aPUSH10x04
041cDUP2
041dDUP8
041eGAS
041fSTATICCALL
0420SWAP1
0421DUP2
0422ISZERO
0423PUSH20x067b
0426JUMPI
0427PUSH0
0428SWAP2
0429PUSH20x0790
042cJUMPI
042dJUMPDEST
042ePOP
042fDUP1
0430ISZERO
0431PUSH20x072a
0434JUMPI
0435JUMPDEST
0436PUSH20x058c
0439JUMPI
043aJUMPDEST
043bPOP
043cPOP
043dPOP
043ePOP
043fPUSH10x06
0441SLOAD
0442PUSH20x057d
0445JUMPI
0446PUSH0
0447DUP3
0448DUP1
0449JUMPDEST
044aPUSH20x0559
044dJUMPI
044ePOP
044fDUP2
0450DUP2
0451SUB
0452PUSH20x0544
0455JUMPI
0456POP
0457PUSH0
0458DUP1
0459PUSH10xff
045bJUMPDEST
045cPUSH10x01
045eDUP1
045fDUP7
0460DUP4
0461SHR
0462AND
0463EQ
0464PUSH20x04e1
0467JUMPI
0468JUMPDEST
0469DUP1
046aISZERO
046bPUSH20x0490
046eJUMPI
046fDUP1
0470ISZERO
0471PUSH20x047c
0474JUMPI
0475PUSH0
0476NOT
0477ADD
0478PUSH20x045b
047bJUMP
047cJUMPDEST
047dPUSH40x4e487b71
0482PUSH10xe0
0484SHL
0485PUSH0
0486MSTORE
0487PUSH10x11
0489PUSH10x04
048bMSTORE
048cPUSH10x24
048ePUSH0
048fREVERT
0490JUMPDEST
0491PUSH320x67f9b61bf7b39fd24dd60467083f89ea77979db358db2804069474590a36c035
04b2PUSH10x40
04b4DUP7
04b5DUP7
04b6DUP2
04b7PUSH10x06
04b9SSTORE
04baDUP2
04bbISZERO
04bcPUSH0
04bdEQ
04bePUSH20x04d3
04c1JUMPI
04c2PUSH0
04c3JUMPDEST
04c4PUSH10x07
04c6SSTORE
04c7DUP3
04c8MLOAD
04c9SWAP2
04caDUP3
04cbMSTORE
04ccPUSH10x20
04ceDUP3
04cfADD
04d0MSTORE
04d1LOG1
04d2STOP
04d3JUMPDEST
04d4PUSH20x04dc
04d7DUP3
04d8PUSH20x1b22
04dbJUMP
04dcJUMPDEST
04ddPUSH20x04c3
04e0JUMP
04e1JUMPDEST
04e2SWAP1
04e3PUSH20x04ed
04e6DUP4
04e7DUP6
04e8DUP9
04e9PUSH20x19cc
04ecJUMP
04edJUMPDEST
04eeCALLDATALOAD
04efISZERO
04f0PUSH20x0535
04f3JUMPI
04f4PUSH20x052f
04f7SWAP1
04f8PUSH20x050b
04fbPUSH20x0503
04feDUP6
04ffPUSH20x1be3
0502JUMP
0503JUMPDEST
0504SWAP5
0505DUP7
0506DUP10
0507PUSH20x19cc
050aJUMP
050bJUMPDEST
050cCALLDATALOAD
050dDUP4
050ePUSH0
050fMSTORE
0510PUSH10x03
0512PUSH10x20
0514MSTORE
0515PUSH10x40
0517PUSH0
0518KECCAK256
0519DUP3
051aDUP6
051bSHR
051cPUSH0
051dMSTORE
051ePUSH10x20
0520MSTORE
0521PUSH10x40
0523PUSH0
0524KECCAK256
0525SSTORE
0526PUSH10x01
0528DUP4
0529SHL
052aSWAP1
052bPUSH20x16eb
052eJUMP
052fJUMPDEST
0530SWAP1
0531PUSH20x0468
0534JUMP
0535JUMPDEST
0536PUSH40x4425ca13
053bPUSH10xe0
053dSHL
053ePUSH0
053fMSTORE
0540PUSH10x04
0542PUSH0
0543REVERT
0544JUMPDEST
0545PUSH40xecc9b8ed
054aPUSH10xe0
054cSHL
054dPUSH0
054eMSTORE
054fPUSH10x04
0551MSTORE
0552PUSH10x24
0554MSTORE
0555PUSH10x44
0557PUSH0
0558REVERT
0559JUMPDEST
055aPUSH10x01
055cDUP1
055dDUP3
055eAND
055fEQ
0560PUSH20x056d
0563JUMPI
0564JUMPDEST
0565PUSH10x01
0567SHR
0568DUP1
0569PUSH20x0449
056cJUMP
056dJUMPDEST
056eSWAP1
056fPUSH20x0577
0572SWAP1
0573PUSH20x1be3
0576JUMP
0577JUMPDEST
0578SWAP1
0579PUSH20x0564
057cJUMP
057dJUMPDEST
057ePUSH40xdc63d81f
0583PUSH10xe0
0585SHL
0586PUSH0
0587MSTORE
0588PUSH10x04
058aPUSH0
058bREVERT
058cJUMPDEST
058dPUSH10x40
058fMLOAD
0590PUSH10x20
0592DUP2
0593ADD
0594SWAP1
0595PUSH10x40
0597DUP3
0598MSTORE
0599PUSH20x05be
059cDUP2
059dPUSH20x05aa
05a0PUSH10x60
05a2DUP3
05a3ADD
05a4DUP11
05a5DUP14
05a6PUSH20x198a
05a9JUMP
05aaJUMPDEST
05abDUP11
05acPUSH10x40
05aeDUP4
05afADD
05b0MSTORE
05b1SUB
05b2PUSH10x1f
05b4NOT
05b5DUP2
05b6ADD
05b7DUP4
05b8MSTORE
05b9DUP3
05baPUSH20x16f8
05bdJUMP
05beJUMPDEST
05bfMLOAD
05c0SWAP1
05c1KECCAK256
05c2DUP4
05c3EXTCODESIZE
05c4ISZERO
05c5PUSH20x02b5
05c8JUMPI
05c9SWAP1
05caDUP3
05cbPUSH10x01
05cdPUSH10x01
05cfPUSH10x40
05d1SHL
05d2SUB
05d3SWAP6
05d4SWAP4
05d5SWAP3
05d6PUSH10x40
05d8MLOAD
05d9SWAP7
05daDUP8
05dbSWAP6
05dcPUSH40x22f3f447
05e1PUSH10xe1
05e3SHL
05e4DUP8
05e5MSTORE
05e6PUSH10x84
05e8DUP8
05e9ADD
05eaSWAP3
05ebPUSH320x405bbda3343b6e69c32fb7eafff8f0a1e55a5ee2ec35458b3abc776b26681952
060cPUSH10x04
060eDUP10
060fADD
0610MSTORE
0611PUSH10x24
0613DUP9
0614ADD
0615MSTORE
0616AND
0617PUSH10x44
0619DUP7
061aADD
061bMSTORE
061cPUSH10x80
061ePUSH10x64
0620DUP7
0621ADD
0622MSTORE
0623MSTORE
0624PUSH10xa4
0626DUP4
0627ADD
0628PUSH10xa0
062aPUSH10x04
062cDUP5
062dPUSH10x05
062fSHL
0630DUP7
0631ADD
0632ADD
0633ADD
0634SWAP3
0635DUP3
0636PUSH0
0637SWAP1
0638PUSH10x7e
063aNOT
063bDUP2
063cCALLDATASIZE
063dSUB
063eADD
063fJUMPDEST
0640DUP4
0641DUP4
0642LT
0643PUSH20x0686
0646JUMPI
0647POP
0648POP
0649POP
064aPOP
064bPOP
064cPOP
064dSWAP2
064eDUP2
064fPUSH0
0650DUP2
0651DUP6
0652DUP3
0653SWAP7
0654POP
0655SUB
0656SWAP3
0657GAS
0658CALL
0659DUP1
065aISZERO
065bPUSH20x067b
065eJUMPI
065fPUSH20x066b
0662JUMPI
0663JUMPDEST
0664DUP1
0665DUP1
0666DUP1
0667PUSH20x043a
066aJUMP
066bJUMPDEST
066cPUSH0
066dPUSH20x0675
0670SWAP2
0671PUSH20x16f8
0674JUMP
0675JUMPDEST
0676DUP4
0677PUSH20x0663
067aJUMP
067bJUMPDEST
067cPUSH10x40
067eMLOAD
067fRETURNDATASIZE
0680PUSH0
0681DUP3
0682RETURNDATACOPY
0683RETURNDATASIZE
0684SWAP1
0685REVERT
0686JUMPDEST
0687PUSH10xa3
0689NOT
068aDUP11
068bDUP9
068cSUB
068dADD
068eDUP6
068fMSTORE
0690SWAP5
0691SWAP7
0692POP
0693SWAP3
0694SWAP5
0695SWAP2
0696SWAP4
0697SWAP1
0698SWAP3
0699SWAP2
069aDUP7
069bCALLDATALOAD
069cDUP3
069dDUP2
069eSLT
069fISZERO
06a0PUSH20x02b5
06a3JUMPI
06a4DUP4
06a5ADD
06a6DUP1
06a7CALLDATALOAD
06a8PUSH10x01
06aaPUSH10x01
06acPUSH10xa0
06aeSHL
06afSUB
06b0DUP2
06b1AND
06b2SWAP1
06b3DUP2
06b4SWAP1
06b5SUB
06b6PUSH20x02b5
06b9JUMPI
06baDUP3
06bbMSTORE
06bcPUSH10x20
06beDUP2
06bfADD
06c0CALLDATALOAD
06c1SWAP2
06c2PUSH10xff
06c4DUP4
06c5AND
06c6DUP1
06c7SWAP4
06c8SUB
06c9PUSH20x02b5
06ccJUMPI
06cdPUSH20x0718
06d0PUSH10x20
06d2SWAP3
06d3DUP3
06d4PUSH10x01
06d6SWAP6
06d7DUP6
06d8DUP1
06d9SWAP6
06daADD
06dbMSTORE
06dcPUSH20x070a
06dfPUSH20x06ff
06e2PUSH20x06ee
06e5PUSH10x40
06e7DUP6
06e8ADD
06e9DUP6
06eaPUSH20x1a13
06edJUMP
06eeJUMPDEST
06efPUSH10x80
06f1PUSH10x40
06f3DUP7
06f4ADD
06f5MSTORE
06f6PUSH10x80
06f8DUP6
06f9ADD
06faSWAP2
06fbPUSH20x1a44
06feJUMP
06ffJUMPDEST
0700SWAP3
0701PUSH10x60
0703DUP2
0704ADD
0705SWAP1
0706PUSH20x1a13
0709JUMP
070aJUMPDEST
070bSWAP2
070cPUSH10x60
070eDUP2
070fDUP6
0710SUB
0711SWAP2
0712ADD
0713MSTORE
0714PUSH20x1a44
0717JUMP
0718JUMPDEST
0719SWAP9
071aADD
071bSWAP7
071cADD
071dSWAP4
071eADD
071fSWAP1
0720SWAP2
0721DUP9
0722SWAP7
0723SWAP6
0724SWAP5
0725SWAP3
0726PUSH20x063f
0729JUMP
072aJUMPDEST
072bPOP
072cPUSH10x40
072eMLOAD
072fPUSH40xf5778b03
0734PUSH10xe0
0736SHL
0737DUP2
0738MSTORE
0739PUSH10x20
073bDUP2
073cPUSH10x04
073eDUP2
073fDUP8
0740GAS
0741STATICCALL
0742SWAP1
0743DUP2
0744ISZERO
0745PUSH20x067b
0748JUMPI
0749PUSH0
074aSWAP2
074bPUSH20x0761
074eJUMPI
074fJUMPDEST
0750POP
0751PUSH10x01
0753PUSH10x01
0755PUSH10xa0
0757SHL
0758SUB
0759AND
075aCALLER
075bEQ
075cISZERO
075dPUSH20x0435
0760JUMP
0761JUMPDEST
0762PUSH20x0783
0765SWAP2
0766POP
0767PUSH10x20
0769RETURNDATASIZE
076aPUSH10x20
076cGT
076dPUSH20x0789
0770JUMPI
0771JUMPDEST
0772PUSH20x077b
0775DUP2
0776DUP4
0777PUSH20x16f8
077aJUMP
077bJUMPDEST
077cDUP2
077dADD
077eSWAP1
077fPUSH20x19f4
0782JUMP
0783JUMPDEST
0784DUP9
0785PUSH20x074f
0788JUMP
0789JUMPDEST
078aPOP
078bRETURNDATASIZE
078cPUSH20x0771
078fJUMP
0790JUMPDEST
0791PUSH20x07b2
0794SWAP2
0795POP
0796PUSH10x20
0798RETURNDATASIZE
0799PUSH10x20
079bGT
079cPUSH20x07b8
079fJUMPI
07a0JUMPDEST
07a1PUSH20x07aa
07a4DUP2
07a5DUP4
07a6PUSH20x16f8
07a9JUMP
07aaJUMPDEST
07abDUP2
07acADD
07adSWAP1
07aePUSH20x19dc
07b1JUMP
07b2JUMPDEST
07b3DUP9
07b4PUSH20x042d
07b7JUMP
07b8JUMPDEST
07b9POP
07baRETURNDATASIZE
07bbPUSH20x07a0
07beJUMP
07bfJUMPDEST
07c0CALLVALUE
07c1PUSH20x02b5
07c4JUMPI
07c5PUSH0
07c6CALLDATASIZE
07c7PUSH10x03
07c9NOT
07caADD
07cbSLT
07ccPUSH20x02b5
07cfJUMPI
07d0PUSH10x20
07d2PUSH0
07d3SLOAD
07d4PUSH10x40
07d6MLOAD
07d7SWAP1
07d8DUP2
07d9MSTORE
07daRETURN
07dbJUMPDEST
07dcCALLVALUE
07ddPUSH20x02b5
07e0JUMPI
07e1PUSH10x20
07e3CALLDATASIZE
07e4PUSH10x03
07e6NOT
07e7ADD
07e8SLT
07e9PUSH20x02b5
07ecJUMPI
07edPUSH10x20
07efPUSH20x07f9
07f2PUSH10x04
07f4CALLDATALOAD
07f5PUSH20x1b22
07f8JUMP
07f9JUMPDEST
07faPUSH10x40
07fcMLOAD
07fdSWAP1
07feDUP2
07ffMSTORE
0800RETURN
0801JUMPDEST
0802CALLVALUE
0803PUSH20x02b5
0806JUMPI
0807PUSH0
0808CALLDATASIZE
0809PUSH10x03
080bNOT
080cADD
080dSLT
080ePUSH20x02b5
0811JUMPI
0812PUSH10x40
0814MLOAD
0815PUSH320x000000000000000000000000f2b3161ed308717da082ee706cfdd27048e0d62d
0836PUSH10x01
0838PUSH10x01
083aPUSH10xa0
083cSHL
083dSUB
083eAND
083fDUP2
0840MSTORE
0841PUSH10x20
0843SWAP1
0844RETURN
0845JUMPDEST
0846CALLVALUE
0847PUSH20x02b5
084aJUMPI
084bPUSH0
084cCALLDATASIZE
084dPUSH10x03
084fNOT
0850ADD
0851SLT
0852PUSH20x02b5
0855JUMPI
0856PUSH10x20
0858PUSH10x40
085aMLOAD
085bPUSH320x27c91cbb7cc32319dd47788e8b096cc02ee8ccca641645266d7046769a12fbc3
087cDUP2
087dMSTORE
087eRETURN
087fJUMPDEST
0880CALLVALUE
0881PUSH20x02b5
0884JUMPI
0885PUSH0
0886CALLDATASIZE
0887PUSH10x03
0889NOT
088aADD
088bSLT
088cPUSH20x02b5
088fJUMPI
0890PUSH10x20
0892PUSH10x01
0894PUSH10x01
0896PUSH10x40
0898SHL
0899SUB
089aPUSH10x02
089cSLOAD
089dAND
089ePUSH10x40
08a0MLOAD
08a1SWAP1
08a2DUP2
08a3MSTORE
08a4RETURN
08a5JUMPDEST
08a6CALLVALUE
08a7PUSH20x02b5
08aaJUMPI
08abPUSH10x80
08adCALLDATASIZE
08aePUSH10x03
08b0NOT
08b1ADD
08b2SLT
08b3PUSH20x02b5
08b6JUMPI
08b7PUSH10x24
08b9CALLDATALOAD
08baPUSH10x04
08bcCALLDATALOAD
08bdPUSH20x08c4
08c0PUSH20x1648
08c3JUMP
08c4JUMPDEST
08c5PUSH10x64
08c7CALLDATALOAD
08c8PUSH10x01
08caPUSH10x01
08ccPUSH10x40
08ceSHL
08cfSUB
08d0DUP2
08d1GT
08d2PUSH20x02b5
08d5JUMPI
08d6PUSH20x08e3
08d9SWAP1
08daCALLDATASIZE
08dbSWAP1
08dcPUSH10x04
08deADD
08dfPUSH20x1618
08e2JUMP
08e3JUMPDEST
08e4PUSH10x40
08e6MLOAD
08e7PUSH40x28305db1
08ecPUSH10xe2
08eeSHL
08efDUP2
08f0MSTORE
08f1PUSH320x0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d540
0912PUSH10x01
0914PUSH10x01
0916PUSH10xa0
0918SHL
0919SUB
091aAND
091bSWAP4
091cSWAP3
091dSWAP1
091ePUSH10x20
0920DUP2
0921PUSH10x04
0923DUP2
0924DUP9
0925GAS
0926STATICCALL
0927SWAP1
0928DUP2
0929ISZERO
092aPUSH20x067b
092dJUMPI
092ePUSH0
092fSWAP2
0930PUSH20x0ba9
0933JUMPI
0934JUMPDEST
0935POP
0936DUP1
0937ISZERO
0938PUSH20x0b53
093bJUMPI
093cJUMPDEST
093dPUSH20x0a01
0940JUMPI
0941JUMPDEST
0942POP
0943POP
0944POP
0945DUP3
0946PUSH20x0983
0949JUMPI
094aJUMPDEST
094bPUSH320xbfc08a458e488f0e56f7ff4bfe317bed1ba5d3f7ef5a2bda241528695f6fcdf3
096cPUSH10x40
096eDUP4
096fDUP6
0970DUP2
0971PUSH0
0972SSTORE
0973DUP1
0974PUSH10x01
0976SSTORE
0977DUP3
0978MLOAD
0979SWAP2
097aDUP3
097bMSTORE
097cPUSH10x20
097eDUP3
097fADD
0980MSTORE
0981LOG1
0982STOP
0983JUMPDEST
0984PUSH10x20
0986PUSH10x24
0988SWAP2
0989PUSH10x40
098bMLOAD
098cSWAP3
098dDUP4
098eDUP1
098fSWAP3
0990PUSH40x342f6163
0995PUSH10xe0
0997SHL
0998DUP3
0999MSTORE
099aDUP7
099bPUSH10x04
099dDUP4
099eADD
099fMSTORE
09a0GAS
09a1STATICCALL
09a2SWAP1
09a3DUP2
09a4ISZERO
09a5PUSH20x067b
09a8JUMPI
09a9PUSH0
09aaSWAP2
09abPUSH20x09cf
09aeJUMPI
09afJUMPDEST
09b0POP
09b1DUP3
09b2DUP2
09b3LT
09b4ISZERO
09b5PUSH20x094a
09b8JUMPI
09b9SWAP1
09baPOP
09bbPUSH40x3770da33
09c0PUSH10xe1
09c2SHL
09c3PUSH0
09c4MSTORE
09c5PUSH10x04
09c7MSTORE
09c8PUSH10x24
09caMSTORE
09cbPUSH10x44
09cdPUSH0
09ceREVERT
09cfJUMPDEST
09d0SWAP1
09d1POP
09d2PUSH10x20
09d4DUP2
09d5RETURNDATASIZE
09d6PUSH10x20
09d8GT
09d9PUSH20x09f9
09dcJUMPI
09ddJUMPDEST
09deDUP2
09dfPUSH20x09ea
09e2PUSH10x20
09e4SWAP4
09e5DUP4
09e6PUSH20x16f8
09e9JUMP
09eaJUMPDEST
09ebDUP2
09ecADD
09edSUB
09eeSLT
09efPUSH20x02b5
09f2JUMPI
09f3MLOAD
09f4DUP4
09f5PUSH20x09af
09f8JUMP
09f9JUMPDEST
09faRETURNDATASIZE
09fbSWAP2
09fcPOP
09fdPUSH20x09dd
0a00JUMP
0a01JUMPDEST
0a02PUSH10x40
0a04MLOAD
0a05PUSH10x20
0a07DUP2
0a08ADD
0a09SWAP1
0a0aDUP7
0a0bDUP3
0a0cMSTORE
0a0dDUP8
0a0ePUSH10x40
0a10DUP3
0a11ADD
0a12MSTORE
0a13PUSH10x40
0a15DUP2
0a16MSTORE
0a17PUSH20x0a21
0a1aPUSH10x60
0a1cDUP3
0a1dPUSH20x16f8
0a20JUMP
0a21JUMPDEST
0a22MLOAD
0a23SWAP1
0a24KECCAK256
0a25DUP5
0a26EXTCODESIZE
0a27ISZERO
0a28PUSH20x02b5
0a2bJUMPI
0a2cSWAP1
0a2dDUP3
0a2ePUSH10x01
0a30PUSH10x01
0a32PUSH10x40
0a34SHL
0a35SUB
0a36SWAP5
0a37SWAP3
0a38PUSH10x40
0a3aMLOAD
0a3bSWAP6
0a3cDUP7
0a3dSWAP5
0a3ePUSH40x22f3f447
0a43PUSH10xe1
0a45SHL
0a46DUP7
0a47MSTORE
0a48PUSH10x84
0a4aDUP7
0a4bADD
0a4cSWAP3
0a4dPUSH320x27c91cbb7cc32319dd47788e8b096cc02ee8ccca641645266d7046769a12fbc3
0a6ePUSH10x04
0a70DUP9
0a71ADD
0a72MSTORE
0a73PUSH10x24
0a75DUP8
0a76ADD
0a77MSTORE
0a78AND
0a79PUSH10x44
0a7bDUP6
0a7cADD
0a7dMSTORE
0a7ePUSH10x80
0a80PUSH10x64
0a82DUP6
0a83ADD
0a84MSTORE
0a85MSTORE
0a86PUSH10xa4
0a88DUP3
0a89ADD
0a8aPUSH10xa0
0a8cPUSH10x04
0a8eDUP6
0a8fPUSH10x05
0a91SHL
0a92DUP6
0a93ADD
0a94ADD
0a95ADD
0a96SWAP4
0a97DUP3
0a98PUSH0
0a99SWAP1
0a9aPUSH10x7e
0a9cNOT
0a9dDUP2
0a9eCALLDATASIZE
0a9fSUB
0aa0ADD
0aa1JUMPDEST
0aa2DUP4
0aa3DUP4
0aa4LT
0aa5PUSH20x0adb
0aa8JUMPI
0aa9POP
0aaaPOP
0aabPOP
0aacPOP
0aadPOP
0aaePOP
0aafDUP1
0ab0DUP3
0ab1PUSH0
0ab2SWAP4
0ab3POP
0ab4SUB
0ab5DUP2
0ab6DUP4
0ab7DUP7
0ab8GAS
0ab9CALL
0abaDUP1
0abbISZERO
0abcPUSH20x067b
0abfJUMPI
0ac0PUSH20x0acb
0ac3JUMPI
0ac4JUMPDEST
0ac5DUP1
0ac6DUP1
0ac7PUSH20x0941
0acaJUMP
0acbJUMPDEST
0accPUSH0
0acdPUSH20x0ad5
0ad0SWAP2
0ad1PUSH20x16f8
0ad4JUMP
0ad5JUMPDEST
0ad6DUP4
0ad7PUSH20x0ac4
0adaJUMP
0adbJUMPDEST
0adcPUSH10xa3
0adeNOT
0adfDUP10
0ae0DUP10
0ae1SUB
0ae2ADD
0ae3DUP6
0ae4MSTORE
0ae5SWAP5
0ae6SWAP7
0ae7SWAP4
0ae8SWAP6
0ae9POP
0aeaSWAP2
0aebSWAP4
0aecSWAP1
0aedSWAP3
0aeeDUP7
0aefCALLDATALOAD
0af0DUP3
0af1DUP2
0af2SLT
0af3ISZERO
0af4PUSH20x02b5
0af7JUMPI
0af8DUP4
0af9ADD
0afaDUP1
0afbCALLDATALOAD
0afcPUSH10x01
0afePUSH10x01
0b00PUSH10xa0
0b02SHL
0b03SUB
0b04DUP2
0b05AND
0b06SWAP1
0b07DUP2
0b08SWAP1
0b09SUB
0b0aPUSH20x02b5
0b0dJUMPI
0b0eDUP3
0b0fMSTORE
0b10PUSH10x20
0b12DUP2
0b13ADD
0b14CALLDATALOAD
0b15SWAP2
0b16PUSH10xff
0b18DUP4
0b19AND
0b1aDUP1
0b1bSWAP4
0b1cSUB
0b1dPUSH20x02b5
0b20JUMPI
0b21PUSH20x0b42
0b24PUSH10x20
0b26SWAP3
0b27DUP3
0b28PUSH10x01
0b2aSWAP6
0b2bDUP6
0b2cDUP1
0b2dSWAP6
0b2eADD
0b2fMSTORE
0b30PUSH20x070a
0b33PUSH20x06ff
0b36PUSH20x06ee
0b39PUSH10x40
0b3bDUP6
0b3cADD
0b3dDUP6
0b3ePUSH20x1a13
0b41JUMP
0b42JUMPDEST
0b43SWAP9
0b44ADD
0b45SWAP7
0b46ADD
0b47SWAP4
0b48ADD
0b49SWAP1
0b4aSWAP2
0b4bDUP8
0b4cSWAP6
0b4dSWAP5
0b4eSWAP3
0b4fPUSH20x0aa1
0b52JUMP
0b53JUMPDEST
0b54POP
0b55PUSH10x40
0b57MLOAD
0b58PUSH40xf5778b03
0b5dPUSH10xe0
0b5fSHL
0b60DUP2
0b61MSTORE
0b62PUSH10x20
0b64DUP2
0b65PUSH10x04
0b67DUP2
0b68DUP9
0b69GAS
0b6aSTATICCALL
0b6bSWAP1
0b6cDUP2
0b6dISZERO
0b6ePUSH20x067b
0b71JUMPI
0b72PUSH0
0b73SWAP2
0b74PUSH20x0b8a
0b77JUMPI
0b78JUMPDEST
0b79POP
0b7aPUSH10x01
0b7cPUSH10x01
0b7ePUSH10xa0
0b80SHL
0b81SUB
0b82AND
0b83CALLER
0b84EQ
0b85ISZERO
0b86PUSH20x093c
0b89JUMP
0b8aJUMPDEST
0b8bPUSH20x0ba3
0b8eSWAP2
0b8fPOP
0b90PUSH10x20
0b92RETURNDATASIZE
0b93PUSH10x20
0b95GT
0b96PUSH20x0789
0b99JUMPI
0b9aPUSH20x077b
0b9dDUP2
0b9eDUP4
0b9fPUSH20x16f8
0ba2JUMP
0ba3JUMPDEST
0ba4DUP8
0ba5PUSH20x0b78
0ba8JUMP
0ba9JUMPDEST
0baaPUSH20x0bc2
0badSWAP2
0baePOP
0bafPUSH10x20
0bb1RETURNDATASIZE
0bb2PUSH10x20
0bb4GT
0bb5PUSH20x07b8
0bb8JUMPI
0bb9PUSH20x07aa
0bbcDUP2
0bbdDUP4
0bbePUSH20x16f8
0bc1JUMP
0bc2JUMPDEST
0bc3DUP8
0bc4PUSH20x0934
0bc7JUMP
0bc8JUMPDEST
0bc9CALLVALUE
0bcaPUSH20x02b5
0bcdJUMPI
0bcePUSH0
0bcfCALLDATASIZE
0bd0PUSH10x03
0bd2NOT
0bd3ADD
0bd4SLT
0bd5PUSH20x02b5
0bd8JUMPI
0bd9PUSH10x20
0bdbPUSH10x06
0bddSLOAD
0bdePUSH10x40
0be0MLOAD
0be1SWAP1
0be2DUP2
0be3MSTORE
0be4RETURN
0be5JUMPDEST
0be6CALLVALUE
0be7PUSH20x02b5
0beaJUMPI
0bebPUSH0
0becCALLDATASIZE
0bedPUSH10x03
0befNOT
0bf0ADD
0bf1SLT
0bf2PUSH20x02b5
0bf5JUMPI
0bf6PUSH10x40
0bf8PUSH10x07
0bfaSLOAD
0bfbPUSH10x06
0bfdSLOAD
0bfeDUP3
0bffMLOAD
0c00SWAP2
0c01DUP3
0c02MSTORE
0c03PUSH10x20
0c05DUP3
0c06ADD
0c07MSTORE
0c08RETURN
0c09JUMPDEST
0c0aCALLVALUE
0c0bPUSH20x02b5
0c0eJUMPI
0c0fPUSH0
0c10CALLDATASIZE
0c11PUSH10x03
0c13NOT
0c14ADD
0c15SLT
0c16PUSH20x02b5
0c19JUMPI
0c1aPUSH10x40
0c1cMLOAD
0c1dPUSH320x0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d540
0c3ePUSH10x01
0c40PUSH10x01
0c42PUSH10xa0
0c44SHL
0c45SUB
0c46AND
0c47DUP2
0c48MSTORE
0c49PUSH10x20
0c4bSWAP1
0c4cRETURN
0c4dJUMPDEST
0c4eCALLVALUE
0c4fPUSH20x02b5
0c52JUMPI
0c53PUSH0
0c54CALLDATASIZE
0c55PUSH10x03
0c57NOT
0c58ADD
0c59SLT
0c5aPUSH20x02b5
0c5dJUMPI
0c5ePUSH10x20
0c60PUSH10x40
0c62MLOAD
0c63PUSH320x405bbda3343b6e69c32fb7eafff8f0a1e55a5ee2ec35458b3abc776b26681952
0c84DUP2
0c85MSTORE
0c86RETURN
0c87JUMPDEST
0c88CALLVALUE
0c89PUSH20x02b5
0c8cJUMPI
0c8dPUSH10x20
0c8fCALLDATASIZE
0c90PUSH10x03
0c92NOT
0c93ADD
0c94SLT
0c95PUSH20x02b5
0c98JUMPI
0c99PUSH10x40
0c9bPUSH20x0ca5
0c9ePUSH10x04
0ca0CALLDATALOAD
0ca1PUSH20x1af8
0ca4JUMP
0ca5JUMPDEST
0ca6DUP3
0ca7MLOAD
0ca8SWAP2
0ca9DUP3
0caaMSTORE
0cabISZERO
0cacISZERO
0cadPUSH10x20
0cafDUP3
0cb0ADD
0cb1MSTORE
0cb2RETURN
0cb3JUMPDEST
0cb4CALLVALUE
0cb5PUSH20x02b5
0cb8JUMPI
0cb9PUSH10x20
0cbbCALLDATASIZE
0cbcPUSH10x03
0cbeNOT
0cbfADD
0cc0SLT
0cc1PUSH20x02b5
0cc4JUMPI
0cc5PUSH20x0328
0cc8PUSH20x0cd2
0ccbPUSH10x04
0ccdCALLDATALOAD
0ccePUSH20x1a64
0cd1JUMP
0cd2JUMPDEST
0cd3PUSH10x40
0cd5MLOAD
0cd6SWAP2
0cd7DUP3
0cd8SWAP2
0cd9PUSH10x20
0cdbDUP4
0cdcMSTORE
0cddPUSH10x20
0cdfDUP4
0ce0ADD
0ce1SWAP1
0ce2PUSH20x15e5
0ce5JUMP
0ce6JUMPDEST
0ce7CALLVALUE
0ce8PUSH20x02b5
0cebJUMPI
0cecPUSH20x0cf4
0cefCALLDATASIZE
0cf0PUSH20x165e
0cf3JUMP
0cf4JUMPDEST
0cf5PUSH10x40
0cf7MLOAD
0cf8PUSH40x28305db1
0cfdPUSH10xe2
0cffSHL
0d00DUP2
0d01MSTORE
0d02SWAP4
0d03SWAP5
0d04SWAP4
0d05PUSH320x0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d540
0d26PUSH10x01
0d28PUSH10x01
0d2aPUSH10xa0
0d2cSHL
0d2dSUB
0d2eAND
0d2fSWAP3
0d30SWAP1
0d31PUSH10x20
0d33DUP2
0d34PUSH10x04
0d36DUP2
0d37DUP8
0d38GAS
0d39STATICCALL
0d3aSWAP1
0d3bDUP2
0d3cISZERO
0d3dPUSH20x067b
0d40JUMPI
0d41PUSH0
0d42SWAP2
0d43PUSH20x0f5d
0d46JUMPI
0d47JUMPDEST
0d48POP
0d49DUP1
0d4aISZERO
0d4bPUSH20x0f07
0d4eJUMPI
0d4fJUMPDEST
0d50PUSH20x0db2
0d53JUMPI
0d54JUMPDEST
0d55POP
0d56POP
0d57POP
0d58POP
0d59PUSH10x06
0d5bSLOAD
0d5cPUSH20x057d
0d5fJUMPI
0d60DUP2
0d61ISZERO
0d62PUSH20x02a6
0d65JUMPI
0d66PUSH0
0d67JUMPDEST
0d68DUP3
0d69DUP2
0d6aLT
0d6bPUSH20x0d9b
0d6eJUMPI
0d6fPUSH320x1c295873c1ce4ce2ac720f43d6909e66b931b42e9246b862278eba9624c0bf05
0d90PUSH10x20
0d92DUP5
0d93PUSH10x40
0d95MLOAD
0d96SWAP1
0d97DUP2
0d98MSTORE
0d99LOG1
0d9aSTOP
0d9bJUMPDEST
0d9cDUP1
0d9dPUSH20x0dac
0da0PUSH20x028b
0da3PUSH10x01
0da5SWAP4
0da6DUP7
0da7DUP7
0da8PUSH20x19cc
0dabJUMP
0dacJUMPDEST
0dadADD
0daePUSH20x0d67
0db1JUMP
0db2JUMPDEST
0db3PUSH10x40
0db5MLOAD
0db6PUSH10x20
0db8DUP2
0db9ADD
0dbaSWAP1
0dbbPUSH10x20
0dbdDUP3
0dbeMSTORE
0dbfPUSH20x0dd0
0dc2DUP2
0dc3PUSH20x0199
0dc6PUSH10x40
0dc8DUP3
0dc9ADD
0dcaDUP12
0dcbDUP12
0dccPUSH20x198a
0dcfJUMP
0dd0JUMPDEST
0dd1MLOAD
0dd2SWAP1
0dd3KECCAK256
0dd4DUP4
0dd5EXTCODESIZE
0dd6ISZERO
0dd7PUSH20x02b5
0ddaJUMPI
0ddbSWAP1
0ddcDUP3
0dddPUSH10x01
0ddfPUSH10x01
0de1PUSH10x40
0de3SHL
0de4SUB
0de5SWAP6
0de6SWAP4
0de7SWAP3
0de8PUSH10x40
0deaMLOAD
0debSWAP7
0decDUP8
0dedSWAP6
0deePUSH40x22f3f447
0df3PUSH10xe1
0df5SHL
0df6DUP8
0df7MSTORE
0df8PUSH10x84
0dfaDUP8
0dfbADD
0dfcSWAP3
0dfdPUSH320x9abdf9961fd14fd177480eccbad16b2d7f231898b2d763c8e8b50364d8b3b171
0e1ePUSH10x04
0e20DUP10
0e21ADD
0e22MSTORE
0e23PUSH10x24
0e25DUP9
0e26ADD
0e27MSTORE
0e28AND
0e29PUSH10x44
0e2bDUP7
0e2cADD
0e2dMSTORE
0e2ePUSH10x80
0e30PUSH10x64
0e32DUP7
0e33ADD
0e34MSTORE
0e35MSTORE
0e36PUSH10xa4
0e38DUP4
0e39ADD
0e3aPUSH10xa0
0e3cPUSH10x04
0e3eDUP5
0e3fPUSH10x05
0e41SHL
0e42DUP7
0e43ADD
0e44ADD
0e45ADD
0e46SWAP3
0e47DUP3
0e48PUSH0
0e49SWAP1
0e4aPUSH10x7e
0e4cNOT
0e4dDUP2
0e4eCALLDATASIZE
0e4fSUB
0e50ADD
0e51JUMPDEST
0e52DUP4
0e53DUP4
0e54LT
0e55PUSH20x0e8d
0e58JUMPI
0e59POP
0e5aPOP
0e5bPOP
0e5cPOP
0e5dPOP
0e5ePOP
0e5fSWAP2
0e60DUP2
0e61PUSH0
0e62DUP2
0e63DUP6
0e64DUP3
0e65SWAP7
0e66POP
0e67SUB
0e68SWAP3
0e69GAS
0e6aCALL
0e6bDUP1
0e6cISZERO
0e6dPUSH20x067b
0e70JUMPI
0e71PUSH20x0e7d
0e74JUMPI
0e75JUMPDEST
0e76DUP1
0e77DUP1
0e78DUP1
0e79PUSH20x0d54
0e7cJUMP
0e7dJUMPDEST
0e7ePUSH0
0e7fPUSH20x0e87
0e82SWAP2
0e83PUSH20x16f8
0e86JUMP
0e87JUMPDEST
0e88DUP3
0e89PUSH20x0e75
0e8cJUMP
0e8dJUMPDEST
0e8ePUSH10xa3
0e90NOT
0e91DUP11
0e92DUP9
0e93SUB
0e94ADD
0e95DUP6
0e96MSTORE
0e97SWAP5
0e98SWAP7
0e99POP
0e9aSWAP3
0e9bSWAP5
0e9cSWAP2
0e9dSWAP4
0e9eSWAP1
0e9fSWAP3
0ea0SWAP2
0ea1DUP7
0ea2CALLDATALOAD
0ea3DUP3
0ea4DUP2
0ea5SLT
0ea6ISZERO
0ea7PUSH20x02b5
0eaaJUMPI
0eabDUP4
0eacADD
0eadDUP1
0eaeCALLDATALOAD
0eafPUSH10x01
0eb1PUSH10x01
0eb3PUSH10xa0
0eb5SHL
0eb6SUB
0eb7DUP2
0eb8AND
0eb9SWAP1
0ebaDUP2
0ebbSWAP1
0ebcSUB
0ebdPUSH20x02b5
0ec0JUMPI
0ec1DUP3
0ec2MSTORE
0ec3PUSH10x20
0ec5DUP2
0ec6ADD
0ec7CALLDATALOAD
0ec8SWAP2
0ec9PUSH10xff
0ecbDUP4
0eccAND
0ecdDUP1
0eceSWAP4
0ecfSUB
0ed0PUSH20x02b5
0ed3JUMPI
0ed4PUSH20x0ef5
0ed7PUSH10x20
0ed9SWAP3
0edaDUP3
0edbPUSH10x01
0eddSWAP6
0edeDUP6
0edfDUP1
0ee0SWAP6
0ee1ADD
0ee2MSTORE
0ee3PUSH20x070a
0ee6PUSH20x06ff
0ee9PUSH20x06ee
0eecPUSH10x40
0eeeDUP6
0eefADD
0ef0DUP6
0ef1PUSH20x1a13
0ef4JUMP
0ef5JUMPDEST
0ef6SWAP9
0ef7ADD
0ef8SWAP7
0ef9ADD
0efaSWAP4
0efbADD
0efcSWAP1
0efdSWAP2
0efeDUP9
0effSWAP7
0f00SWAP6
0f01SWAP5
0f02SWAP3
0f03PUSH20x0e51
0f06JUMP
0f07JUMPDEST
0f08POP
0f09PUSH10x40
0f0bMLOAD
0f0cPUSH40xf5778b03
0f11PUSH10xe0
0f13SHL
0f14DUP2
0f15MSTORE
0f16PUSH10x20
0f18DUP2
0f19PUSH10x04
0f1bDUP2
0f1cDUP8
0f1dGAS
0f1eSTATICCALL
0f1fSWAP1
0f20DUP2
0f21ISZERO
0f22PUSH20x067b
0f25JUMPI
0f26PUSH0
0f27SWAP2
0f28PUSH20x0f3e
0f2bJUMPI
0f2cJUMPDEST
0f2dPOP
0f2ePUSH10x01
0f30PUSH10x01
0f32PUSH10xa0
0f34SHL
0f35SUB
0f36AND
0f37CALLER
0f38EQ
0f39ISZERO
0f3aPUSH20x0d4f
0f3dJUMP
0f3eJUMPDEST
0f3fPUSH20x0f57
0f42SWAP2
0f43POP
0f44PUSH10x20
0f46RETURNDATASIZE
0f47PUSH10x20
0f49GT
0f4aPUSH20x0789
0f4dJUMPI
0f4ePUSH20x077b
0f51DUP2
0f52DUP4
0f53PUSH20x16f8
0f56JUMP
0f57JUMPDEST
0f58DUP8
0f59PUSH20x0f2c
0f5cJUMP
0f5dJUMPDEST
0f5ePUSH20x0f76
0f61SWAP2
0f62POP
0f63PUSH10x20
0f65RETURNDATASIZE
0f66PUSH10x20
0f68GT
0f69PUSH20x07b8
0f6cJUMPI
0f6dPUSH20x07aa
0f70DUP2
0f71DUP4
0f72PUSH20x16f8
0f75JUMP
0f76JUMPDEST
0f77DUP8
0f78PUSH20x0d47
0f7bJUMP
0f7cJUMPDEST
0f7dCALLVALUE
0f7ePUSH20x02b5
0f81JUMPI
0f82PUSH0
0f83CALLDATASIZE
0f84PUSH10x03
0f86NOT
0f87ADD
0f88SLT
0f89PUSH20x02b5
0f8cJUMPI
0f8dPUSH20x0328
0f90PUSH20x0cd2
0f93PUSH10x06
0f95SLOAD
0f96PUSH20x1a64
0f99JUMP
0f9aJUMPDEST
0f9bCALLVALUE
0f9cPUSH20x02b5
0f9fJUMPI
0fa0PUSH0
0fa1CALLDATASIZE
0fa2PUSH10x03
0fa4NOT
0fa5ADD
0fa6SLT
0fa7PUSH20x02b5
0faaJUMPI
0fabPUSH10x20
0fadPUSH10x40
0fafMLOAD
0fb0PUSH320xb66ca34dc0d9a9daa6230aee35894330ccfa7e4eaa29a198577eed0b26a41205
0fd1DUP2
0fd2MSTORE
0fd3RETURN
0fd4JUMPDEST
0fd5CALLVALUE
0fd6PUSH20x02b5
0fd9JUMPI
0fdaPUSH20x0fe2
0fddCALLDATASIZE
0fdePUSH20x15cf
0fe1JUMP
0fe2JUMPDEST
0fe3PUSH20x0ffe
0fe6PUSH20x0ff8
0fe9PUSH20x0ff2
0fecDUP4
0fedDUP6
0feePUSH20x17ab
0ff1JUMP
0ff2JUMPDEST
0ff3SWAP4
0ff4PUSH20x16c3
0ff7JUMP
0ff8JUMPDEST
0ff9SWAP2
0ffaPUSH20x1b22
0ffdJUMP
0ffeJUMPDEST
0fffPUSH20x101a
1002PUSH10x40
1004MLOAD
1005SWAP4
1006DUP5
1007SWAP4
1008DUP5
1009MSTORE
100aPUSH10x60
100cPUSH10x20
100eDUP6
100fADD
1010MSTORE
1011PUSH10x60
1013DUP5
1014ADD
1015SWAP1
1016PUSH20x15e5
1019JUMP
101aJUMPDEST
101bSWAP1
101cPUSH10x40
101eDUP4
101fADD
1020MSTORE
1021SUB
1022SWAP1
1023RETURN
1024JUMPDEST
1025CALLVALUE
1026PUSH20x02b5
1029JUMPI
102aPUSH0
102bCALLDATASIZE
102cPUSH10x03
102eNOT
102fADD
1030SLT
1031PUSH20x02b5
1034JUMPI
1035PUSH10x20
1037PUSH10x01
1039SLOAD
103aPUSH10x40
103cMLOAD
103dSWAP1
103eDUP2
103fMSTORE
1040RETURN
1041JUMPDEST
1042CALLVALUE
1043PUSH20x02b5
1046JUMPI
1047PUSH10x80
1049CALLDATASIZE
104aPUSH10x03
104cNOT
104dADD
104eSLT
104fPUSH20x02b5
1052JUMPI
1053PUSH10x04
1055CALLDATALOAD
1056PUSH10x01
1058PUSH10x01
105aPUSH10x40
105cSHL
105dSUB
105eDUP2
105fGT
1060PUSH20x02b5
1063JUMPI
1064PUSH20x1071
1067SWAP1
1068CALLDATASIZE
1069SWAP1
106aPUSH10x04
106cADD
106dPUSH20x1618
1070JUMP
1071JUMPDEST
1072SWAP1
1073PUSH10x24
1075CALLDATALOAD
1076PUSH10x01
1078PUSH10x01
107aPUSH10x40
107cSHL
107dSUB
107eDUP2
107fGT
1080PUSH20x02b5
1083JUMPI
1084PUSH20x1091
1087SWAP1
1088CALLDATASIZE
1089SWAP1
108aPUSH10x04
108cADD
108dPUSH20x1618
1090JUMP
1091JUMPDEST
1092PUSH20x109c
1095SWAP4
1096SWAP2
1097SWAP4
1098PUSH20x1648
109bJUMP
109cJUMPDEST
109dSWAP4
109ePUSH10x64
10a0CALLDATALOAD
10a1PUSH10x01
10a3PUSH10x01
10a5PUSH10x40
10a7SHL
10a8SUB
10a9DUP2
10aaGT
10abPUSH20x02b5
10aeJUMPI
10afPUSH20x10bc
10b2SWAP1
10b3CALLDATASIZE
10b4SWAP1
10b5PUSH10x04
10b7ADD
10b8PUSH20x1618
10bbJUMP
10bcJUMPDEST
10bdSWAP6
10beDUP4
10bfISZERO
10c0PUSH20x02a6
10c3JUMPI
10c4DUP4
10c5DUP6
10c6SUB
10c7PUSH20x1548
10caJUMPI
10cbPUSH10x01
10cdSLOAD
10ceDUP1
10cfISZERO
10d0PUSH20x0297
10d3JUMPI
10d4PUSH10x02
10d6SWAP8
10d7SWAP6
10d8SWAP8
10d9SWAP7
10daSWAP4
10dbSWAP7
10dcSLOAD
10ddSWAP3
10dePUSH10x06
10e0SLOAD
10e1SWAP7
10e2PUSH10x40
10e4MLOAD
10e5PUSH10x01
10e7PUSH10x01
10e9PUSH10x40
10ebSHL
10ecSUB
10edDUP7
10eeAND
10efPUSH10x20
10f1DUP3
10f2ADD
10f3MSTORE
10f4DUP9
10f5PUSH10x40
10f7DUP3
10f8ADD
10f9MSTORE
10faPUSH10x80
10fcPUSH10x60
10feDUP3
10ffADD
1100MSTORE
1101PUSH20x110e
1104PUSH10xa0
1106DUP3
1107ADD
1108DUP13
1109DUP10
110aPUSH20x198a
110dJUMP
110eJUMPDEST
110fPUSH10x1f
1111NOT
1112DUP3
1113DUP3
1114SUB
1115ADD
1116PUSH10x80
1118DUP4
1119ADD
111aMSTORE
111bDUP9
111cDUP2
111dMSTORE
111ePUSH10x20
1120DUP2
1121ADD
1122SWAP1
1123PUSH10x20
1125DUP11
1126PUSH10x05
1128SHL
1129DUP3
112aADD
112bADD
112cSWAP2
112dDUP13
112eSWAP2
112fPUSH0
1130JUMPDEST
1131DUP13
1132DUP2
1133LT
1134PUSH20x14dc
1137JUMPI
1138POP
1139POP
113aPOP
113bPOP
113cSWAP1
113dPUSH20x1156
1140DUP2
1141PUSH20x11dd
1144SWAP8
1145SWAP7
1146SWAP6
1147SWAP5
1148SWAP4
1149SUB
114aPUSH10x1f
114cNOT
114dDUP2
114eADD
114fDUP4
1150MSTORE
1151DUP3
1152PUSH20x16f8
1155JUMP
1156JUMPDEST
1157PUSH10x20
1159DUP2
115aMLOAD
115bSWAP2
115cADD
115dKECCAK256
115ePUSH10x40
1160MLOAD
1161PUSH10x20
1163DUP2
1164ADD
1165SWAP2
1166PUSH320xd850f5df47b124511e8e6ec99cf1a0beaf7c6237eff0a31305ce53d85f312675
1187DUP4
1188MSTORE
1189CHAINID
118aPUSH10x40
118cDUP4
118dADD
118eMSTORE
118fADDRESS
1190PUSH10x60
1192DUP4
1193ADD
1194MSTORE
1195PUSH320x2e1c2ff2f9bb13fd926fe3e8b209f98e6c873bb259534a2148ca355409247cba
11b6PUSH10x80
11b8DUP4
11b9ADD
11baMSTORE
11bbPUSH10x01
11bdPUSH10x01
11bfPUSH10x40
11c1SHL
11c2SUB
11c3DUP8
11c4AND
11c5PUSH10xa0
11c7DUP4
11c8ADD
11c9MSTORE
11caPUSH10xc0
11ccDUP3
11cdADD
11ceMSTORE
11cfPUSH10xc0
11d1DUP2
11d2MSTORE
11d3PUSH20x0223
11d6PUSH10xe0
11d8DUP3
11d9PUSH20x16f8
11dcJUMP
11ddJUMPDEST
11dePOP
11dfPUSH10x01
11e1PUSH10x01
11e3PUSH10x40
11e5SHL
11e6SUB
11e7PUSH20x11f1
11eaDUP2
11ebDUP4
11ecAND
11edPUSH20x19ae
11f0JUMP
11f1JUMPDEST
11f2PUSH80xffffffffffffffff
11fbNOT
11fcSWAP1
11fdSWAP3
11feAND
11ffSWAP2
1200AND
1201OR
1202PUSH10x02
1204SSTORE
1205PUSH0
1206SWAP5
1207PUSH320x000000000000000000000000f2b3161ed308717da082ee706cfdd27048e0d62d
1228PUSH10x01
122aPUSH10x01
122cPUSH10xa0
122eSHL
122fSUB
1230AND
1231JUMPDEST
1232DUP4
1233DUP8
1234LT
1235ISZERO
1236PUSH20x14d1
1239JUMPI
123aDUP7
123bPUSH10x05
123dSHL
123eDUP7
123fADD
1240CALLDATALOAD
1241PUSH10x1e
1243NOT
1244DUP8
1245CALLDATASIZE
1246SUB
1247ADD
1248DUP2
1249SLT
124aISZERO
124bPUSH20x02b5
124eJUMPI
124fDUP7
1250ADD
1251DUP1
1252CALLDATALOAD
1253SWAP1
1254PUSH10x01
1256PUSH10x01
1258PUSH10x40
125aSHL
125bSUB
125cDUP3
125dGT
125ePUSH20x02b5
1261JUMPI
1262PUSH10x20
1264ADD
1265SWAP1
1266DUP1
1267PUSH10x05
1269SHL
126aCALLDATASIZE
126bSUB
126cDUP3
126dSGT
126ePUSH20x02b5
1271JUMPI
1272DUP1
1273ISZERO
1274PUSH20x14be
1277JUMPI
1278PUSH20x1282
127bDUP10
127cDUP6
127dDUP8
127ePUSH20x19cc
1281JUMP
1282JUMPDEST
1283CALLDATALOAD
1284ISZERO
1285PUSH20x14ab
1288JUMPI
1289PUSH10x01
128bDUP2
128cADD
128dDUP1
128eDUP3
128fGT
1290PUSH20x047c
1293JUMPI
1294PUSH20x129c
1297SWAP1
1298PUSH20x1744
129bJUMP
129cJUMPDEST
129dSWAP2
129ePUSH20x12a8
12a1DUP11
12a2DUP7
12a3DUP9
12a4PUSH20x19cc
12a7JUMP
12a8JUMPDEST
12a9CALLDATALOAD
12aaPUSH20x12b2
12adDUP5
12aePUSH20x1776
12b1JUMP
12b2JUMPDEST
12b3MSTORE
12b4PUSH0
12b5JUMPDEST
12b6DUP3
12b7DUP2
12b8LT
12b9PUSH20x1434
12bcJUMPI
12bdPOP
12bePOP
12bfPOP
12c0DUP1
12c1MLOAD
12c2ISZERO
12c3PUSH20x1425
12c6JUMPI
12c7JUMPDEST
12c8DUP1
12c9MLOAD
12caPUSH10x01
12ccDUP2
12cdGT
12ceISZERO
12cfPUSH20x13f9
12d2JUMPI
12d3DUP1
12d4PUSH10x01
12d6SHR
12d7SWAP1
12d8PUSH10x01
12daDUP2
12dbAND
12dcSWAP3
12ddPUSH20x12e9
12e0PUSH20x0303
12e3DUP6
12e4DUP6
12e5PUSH20x16eb
12e8JUMP
12e9JUMPDEST
12eaSWAP4
12ebPUSH0
12ecJUMPDEST
12edDUP5
12eeDUP2
12efLT
12f0PUSH20x1379
12f3JUMPI
12f4POP
12f5PUSH10x01
12f7EQ
12f8PUSH20x1304
12fbJUMPI
12fcJUMPDEST
12fdPOP
12fePOP
12ffPOP
1300PUSH20x12c7
1303JUMP
1304JUMPDEST
1305PUSH0
1306NOT
1307DUP3
1308ADD
1309SWAP2
130aDUP3
130bGT
130cPUSH20x047c
130fJUMPI
1310PUSH20x1370
1313SWAP2
1314PUSH20x131c
1317SWAP2
1318PUSH20x1797
131bJUMP
131cJUMPDEST
131dMLOAD
131ePUSH10x40
1320MLOAD
1321PUSH10x20
1323DUP2
1324ADD
1325SWAP2
1326PUSH10x01
1328PUSH10xf9
132aSHL
132bDUP4
132cMSTORE
132dPUSH320xc976f483968b324bd57de8efa226478a3634db61776dacd4da866f8fa37c0fd5
134ePUSH10x21
1350DUP4
1351ADD
1352MSTORE
1353PUSH10x41
1355DUP3
1356ADD
1357MSTORE
1358PUSH10x41
135aDUP2
135bMSTORE
135cPUSH20x1366
135fPUSH10x61
1361DUP3
1362PUSH20x16f8
1365JUMP
1366JUMPDEST
1367MLOAD
1368SWAP1
1369KECCAK256
136aSWAP2
136bDUP4
136cPUSH20x1797
136fJUMP
1370JUMPDEST
1371MSTORE
1372DUP9
1373DUP1
1374DUP1
1375PUSH20x12fc
1378JUMP
1379JUMPDEST
137aDUP1
137bPUSH10x01
137dSWAP2
137eDUP3
137fSHL
1380PUSH20x1396
1383DUP4
1384PUSH20x138d
1387DUP4
1388DUP9
1389PUSH20x1797
138cJUMP
138dJUMPDEST
138eMLOAD
138fSWAP3
1390OR
1391DUP7
1392PUSH20x1797
1395JUMP
1396JUMPDEST
1397MLOAD
1398PUSH10x40
139aMLOAD
139bSWAP1
139cPUSH10x20
139eDUP3
139fADD
13a0SWAP3
13a1DUP6
13a2PUSH10xf8
13a4SHL
13a5DUP5
13a6MSTORE
13a7PUSH320xc976f483968b324bd57de8efa226478a3634db61776dacd4da866f8fa37c0fd5
13c8PUSH10x21
13caDUP5
13cbADD
13ccMSTORE
13cdPUSH10x41
13cfDUP4
13d0ADD
13d1MSTORE
13d2PUSH10x61
13d4DUP3
13d5ADD
13d6MSTORE
13d7PUSH10x61
13d9DUP2
13daMSTORE
13dbPUSH20x13e5
13dePUSH10x81
13e0DUP3
13e1PUSH20x16f8
13e4JUMP
13e5JUMPDEST
13e6MLOAD
13e7SWAP1
13e8KECCAK256
13e9PUSH20x13f2
13ecDUP3
13edDUP10
13eePUSH20x1797
13f1JUMP
13f2JUMPDEST
13f3MSTORE
13f4ADD
13f5PUSH20x12ec
13f8JUMP
13f9JUMPDEST
13faPOP
13fbSWAP7
13fcPUSH20x1417
13ffPUSH20x1411
1402PUSH10x01
1404SWAP4
1405SWAP7
1406SWAP10
1407SWAP9
1408SWAP6
1409SWAP9
140aSWAP8
140bSWAP5
140cSWAP8
140dPUSH20x1776
1410JUMP
1411JUMPDEST
1412MLOAD
1413PUSH20x1edd
1416JUMP
1417JUMPDEST
1418ADD
1419SWAP6
141aSWAP3
141bSWAP5
141cSWAP2
141dSWAP5
141eSWAP4
141fSWAP1
1420SWAP4
1421PUSH20x1231
1424JUMP
1425JUMPDEST
1426PUSH40x4f297b61
142bPUSH10xe1
142dSHL
142ePUSH0
142fMSTORE
1430PUSH10x04
1432PUSH0
1433REVERT
1434JUMPDEST
1435PUSH20x143f
1438DUP2
1439DUP5
143aDUP5
143bPUSH20x19cc
143eJUMP
143fJUMPDEST
1440CALLDATALOAD
1441DUP6
1442EXTCODESIZE
1443ISZERO
1444PUSH20x02b5
1447JUMPI
1448PUSH10x40
144aMLOAD
144bSWAP1
144cPUSH40xaf6f8c1b
1451PUSH10xe0
1453SHL
1454DUP3
1455MSTORE
1456PUSH10x04
1458DUP3
1459ADD
145aMSTORE
145bPUSH0
145cDUP2
145dPUSH10x24
145fDUP2
1460DUP4
1461DUP11
1462GAS
1463CALL
1464DUP1
1465ISZERO
1466PUSH20x067b
1469JUMPI
146aPUSH20x149b
146dJUMPI
146eJUMPDEST
146fPOP
1470PUSH20x147a
1473DUP2
1474DUP5
1475DUP5
1476PUSH20x19cc
1479JUMP
147aJUMPDEST
147bCALLDATALOAD
147cSWAP1
147dPUSH10x01
147fDUP2
1480ADD
1481SWAP2
1482DUP3
1483DUP3
1484GT
1485PUSH20x047c
1488JUMPI
1489PUSH20x1494
148cPUSH10x01
148eSWAP4
148fDUP8
1490PUSH20x1797
1493JUMP
1494JUMPDEST
1495MSTORE
1496ADD
1497PUSH20x12b5
149aJUMP
149bJUMPDEST
149cPUSH0
149dPUSH20x14a5
14a0SWAP2
14a1PUSH20x16f8
14a4JUMP
14a5JUMPDEST
14a6DUP12
14a7PUSH20x146e
14aaJUMP
14abJUMPDEST
14acDUP9
14adPUSH40x22566cfd
14b2PUSH10xe0
14b4SHL
14b5PUSH0
14b6MSTORE
14b7PUSH10x04
14b9MSTORE
14baPUSH10x24
14bcPUSH0
14bdREVERT
14beJUMPDEST
14bfDUP9
14c0PUSH40xc9cdeff5
14c5PUSH10xe0
14c7SHL
14c8PUSH0
14c9MSTORE
14caPUSH10x04
14ccMSTORE
14cdPUSH10x24
14cfPUSH0
14d0REVERT
14d1JUMPDEST
14d2PUSH10x20
14d4DUP6
14d5PUSH10x40
14d7MLOAD
14d8SWAP1
14d9DUP2
14daMSTORE
14dbRETURN
14dcJUMPDEST
14ddSWAP1
14deSWAP2
14dfSWAP3
14e0SWAP4
14e1SWAP13
14e2SWAP15
14e3SWAP13
14e4PUSH10x1f
14e6SWAP15
14e7SWAP12
14e8SWAP15
14e9NOT
14eaDUP4
14ebDUP3
14ecSUB
14edADD
14eeDUP5
14efMSTORE
14f0PUSH10x1e
14f2NOT
14f3DUP13
14f4CALLDATASIZE
14f5SUB
14f6ADD
14f7DUP6
14f8CALLDATALOAD
14f9SLT
14faISZERO
14fbPUSH20x02b5
14feJUMPI
14ffDUP12
1500DUP6
1501CALLDATALOAD
1502ADD
1503SWAP1
1504PUSH10x20
1506DUP3
1507CALLDATALOAD
1508SWAP3
1509ADD
150aSWAP2
150bPUSH10x01
150dPUSH10x01
150fPUSH10x40
1511SHL
1512SUB
1513DUP2
1514GT
1515PUSH20x02b5
1518JUMPI
1519DUP1
151aPUSH10x05
151cSHL
151dCALLDATASIZE
151eSUB
151fDUP4
1520SGT
1521PUSH20x02b5
1524JUMPI
1525PUSH20x1534
1528PUSH10x20
152aSWAP3
152bDUP4
152cSWAP3
152dPUSH10x01
152fSWAP6
1530PUSH20x198a
1533JUMP
1534JUMPDEST
1535SWAP7
1536ADD
1537SWAP5
1538ADD
1539SWAP2
153aADD
153bSWAP15
153cSWAP13
153dSWAP15
153eSWAP14
153fSWAP11
1540SWAP14
1541SWAP2
1542SWAP1
1543SWAP2
1544PUSH20x1130
1547JUMP
1548JUMPDEST
1549DUP4
154aDUP6
154bPUSH40x5b2d6423
1550PUSH10xe1
1552SHL
1553PUSH0
1554MSTORE
1555PUSH10x04
1557MSTORE
1558PUSH10x24
155aMSTORE
155bPUSH10x44
155dPUSH0
155eREVERT
155fJUMPDEST
1560CALLVALUE
1561PUSH20x02b5
1564JUMPI
1565PUSH20x0328
1568PUSH20x0cd2
156bPUSH20x1573
156eCALLDATASIZE
156fPUSH20x15cf
1572JUMP
1573JUMPDEST
1574SWAP1
1575PUSH20x17ab
1578JUMP
1579JUMPDEST
157aCALLVALUE
157bPUSH20x02b5
157eJUMPI
157fPUSH10x20
1581CALLDATASIZE
1582PUSH10x03
1584NOT
1585ADD
1586SLT
1587PUSH20x02b5
158aJUMPI
158bPUSH10x20
158dPUSH20x07f9
1590PUSH10x04
1592CALLDATALOAD
1593PUSH20x16c3
1596JUMP
1597JUMPDEST
1598CALLVALUE
1599PUSH20x02b5
159cJUMPI
159dPUSH0
159eCALLDATASIZE
159fPUSH10x03
15a1NOT
15a2ADD
15a3SLT
15a4PUSH20x02b5
15a7JUMPI
15a8DUP1
15a9PUSH320x9abdf9961fd14fd177480eccbad16b2d7f231898b2d763c8e8b50364d8b3b171
15caPUSH10x20
15ccSWAP3
15cdMSTORE
15ceRETURN
15cfJUMPDEST
15d0PUSH10x40
15d2SWAP1
15d3PUSH10x03
15d5NOT
15d6ADD
15d7SLT
15d8PUSH20x02b5
15dbJUMPI
15dcPUSH10x04
15deCALLDATALOAD
15dfSWAP1
15e0PUSH10x24
15e2CALLDATALOAD
15e3SWAP1
15e4JUMP
15e5JUMPDEST
15e6SWAP1
15e7PUSH10x20
15e9DUP1
15eaDUP4
15ebMLOAD
15ecSWAP3
15edDUP4
15eeDUP2
15efMSTORE
15f0ADD
15f1SWAP3
15f2ADD
15f3SWAP1
15f4PUSH0
15f5JUMPDEST
15f6DUP2
15f7DUP2
15f8LT
15f9PUSH20x1602
15fcJUMPI
15fdPOP
15fePOP
15ffPOP
1600SWAP1
1601JUMP
1602JUMPDEST
1603DUP3
1604MLOAD
1605DUP5
1606MSTORE
1607PUSH10x20
1609SWAP4
160aDUP5
160bADD
160cSWAP4
160dSWAP1
160eSWAP3
160fADD
1610SWAP2
1611PUSH10x01
1613ADD
1614PUSH20x15f5
1617JUMP
1618JUMPDEST
1619SWAP2
161aDUP2
161bPUSH10x1f
161dDUP5
161eADD
161fSLT
1620ISZERO
1621PUSH20x02b5
1624JUMPI
1625DUP3
1626CALLDATALOAD
1627SWAP2
1628PUSH10x01
162aPUSH10x01
162cPUSH10x40
162eSHL
162fSUB
1630DUP4
1631GT
1632PUSH20x02b5
1635JUMPI
1636PUSH10x20
1638DUP1
1639DUP6
163aADD
163bSWAP5
163cDUP5
163dPUSH10x05
163fSHL
1640ADD
1641ADD
1642GT
1643PUSH20x02b5
1646JUMPI
1647JUMP
1648JUMPDEST
1649PUSH10x44
164bCALLDATALOAD
164cSWAP1
164dPUSH10x01
164fPUSH10x01
1651PUSH10x40
1653SHL
1654SUB
1655DUP3
1656AND
1657DUP3
1658SUB
1659PUSH20x02b5
165cJUMPI
165dJUMP
165eJUMPDEST
165fSWAP1
1660PUSH10x60
1662PUSH10x03
1664NOT
1665DUP4
1666ADD
1667SLT
1668PUSH20x02b5
166bJUMPI
166cPUSH10x04
166eCALLDATALOAD
166fPUSH10x01
1671PUSH10x01
1673PUSH10x40
1675SHL
1676SUB
1677DUP2
1678GT
1679PUSH20x02b5
167cJUMPI
167dDUP3
167ePUSH20x1689
1681SWAP2
1682PUSH10x04
1684ADD
1685PUSH20x1618
1688JUMP
1689JUMPDEST
168aSWAP3
168bSWAP1
168cSWAP3
168dSWAP2
168ePUSH10x24
1690CALLDATALOAD
1691PUSH10x01
1693PUSH10x01
1695PUSH10x40
1697SHL
1698SUB
1699DUP2
169aAND
169bDUP2
169cSUB
169dPUSH20x02b5
16a0JUMPI
16a1SWAP2
16a2PUSH10x44
16a4CALLDATALOAD
16a5SWAP1
16a6PUSH10x01
16a8PUSH10x01
16aaPUSH10x40
16acSHL
16adSUB
16aeDUP3
16afGT
16b0PUSH20x02b5
16b3JUMPI
16b4PUSH20x16bf
16b7SWAP2
16b8PUSH10x04
16baADD
16bbPUSH20x1618
16beJUMP
16bfJUMPDEST
16c0SWAP1
16c1SWAP2
16c2JUMP
16c3JUMPDEST
16c4PUSH10x06
16c6SLOAD
16c7DUP1
16c8DUP3
16c9LT
16caISZERO
16cbPUSH20x0356
16ceJUMPI
16cfPOP
16d0PUSH0
16d1MSTORE
16d2PUSH10x04
16d4PUSH10x20
16d6MSTORE
16d7PUSH10x40
16d9PUSH0
16daKECCAK256
16dbSLOAD
16dcSWAP1
16ddJUMP
16deJUMPDEST
16dfSWAP2
16e0SWAP1
16e1DUP3
16e2SUB
16e3SWAP2
16e4DUP3
16e5GT
16e6PUSH20x047c
16e9JUMPI
16eaJUMP
16ebJUMPDEST
16ecSWAP2
16edSWAP1
16eeDUP3
16efADD
16f0DUP1
16f1SWAP3
16f2GT
16f3PUSH20x047c
16f6JUMPI
16f7JUMP
16f8JUMPDEST
16f9SWAP1
16faPUSH10x1f
16fcDUP1
16fdNOT
16feSWAP2
16ffADD
1700AND
1701DUP2
1702ADD
1703SWAP1
1704DUP2
1705LT
1706PUSH10x01
1708PUSH10x01
170aPUSH10x40
170cSHL
170dSUB
170eDUP3
170fGT
1710OR
1711PUSH20x1719
1714JUMPI
1715PUSH10x40
1717MSTORE
1718JUMP
1719JUMPDEST
171aPUSH40x4e487b71
171fPUSH10xe0
1721SHL
1722PUSH0
1723MSTORE
1724PUSH10x41
1726PUSH10x04
1728MSTORE
1729PUSH10x24
172bPUSH0
172cREVERT
172dJUMPDEST
172ePUSH10x01
1730PUSH10x01
1732PUSH10x40
1734SHL
1735SUB
1736DUP2
1737GT
1738PUSH20x1719
173bJUMPI
173cPUSH10x05
173eSHL
173fPUSH10x20
1741ADD
1742SWAP1
1743JUMP
1744JUMPDEST
1745SWAP1
1746PUSH20x174e
1749DUP3
174aPUSH20x172d
174dJUMP
174eJUMPDEST
174fPUSH20x175b
1752PUSH10x40
1754MLOAD
1755SWAP2
1756DUP3
1757PUSH20x16f8
175aJUMP
175bJUMPDEST
175cDUP3
175dDUP2
175eMSTORE
175fDUP1
1760SWAP3
1761PUSH20x176c
1764PUSH10x1f
1766NOT
1767SWAP2
1768PUSH20x172d
176bJUMP
176cJUMPDEST
176dADD
176eSWAP1
176fPUSH10x20
1771CALLDATASIZE
1772SWAP2
1773ADD
1774CALLDATACOPY
1775JUMP
1776JUMPDEST
1777DUP1
1778MLOAD
1779ISZERO
177aPUSH20x1783
177dJUMPI
177ePUSH10x20
1780ADD
1781SWAP1
1782JUMP
1783JUMPDEST
1784PUSH40x4e487b71
1789PUSH10xe0
178bSHL
178cPUSH0
178dMSTORE
178ePUSH10x32
1790PUSH10x04
1792MSTORE
1793PUSH10x24
1795PUSH0
1796REVERT
1797JUMPDEST
1798DUP1
1799MLOAD
179aDUP3
179bLT
179cISZERO
179dPUSH20x1783
17a0JUMPI
17a1PUSH10x20
17a3SWAP2
17a4PUSH10x05
17a6SHL
17a7ADD
17a8ADD
17a9SWAP1
17aaJUMP
17abJUMPDEST
17acSWAP2
17adSWAP1
17aePUSH10x06
17b0SLOAD
17b1DUP1
17b2DUP3
17b3GT
17b4PUSH20x036c
17b7JUMPI
17b8POP
17b9DUP1
17baISZERO
17bbPUSH20x197b
17beJUMPI
17bfDUP1
17c0DUP4
17c1LT
17c2ISZERO
17c3PUSH20x1965
17c6JUMPI
17c7PUSH0
17c8NOT
17c9DUP2
17caADD
17cbSWAP1
17ccDUP1
17cdDUP3
17ceGT
17cfPUSH20x047c
17d2JUMPI
17d3SWAP1
17d4PUSH20x17f5
17d7PUSH20x0303
17daPUSH20x17e4
17ddDUP4
17deDUP8
17dfXOR
17e0PUSH20x1bf1
17e3JUMP
17e4JUMPDEST
17e5PUSH20x17ef
17e8DUP8
17e9DUP3
17eaSHR
17ebPUSH20x1c0a
17eeJUMP
17efJUMPDEST
17f0SWAP1
17f1PUSH20x16eb
17f4JUMP
17f5JUMPDEST
17f6SWAP4
17f7SWAP1
17f8SWAP2
17f9PUSH0
17faDUP4
17fbPUSH0
17fcSWAP5
17fdJUMPDEST
17fePUSH20x1827
1801JUMPI
1802POP
1803POP
1804POP
1805POP
1806DUP3
1807MLOAD
1808DUP1
1809DUP3
180aSUB
180bPUSH20x1812
180eJUMPI
180fPOP
1810POP
1811JUMP
1812JUMPDEST
1813PUSH40x383613b5
1818PUSH10xe0
181aSHL
181bPUSH0
181cMSTORE
181dPUSH10x04
181fMSTORE
1820PUSH10x24
1822MSTORE
1823PUSH10x44
1825PUSH0
1826REVERT
1827JUMPDEST
1828SWAP1
1829SWAP2
182aSWAP3
182bSWAP4
182cPUSH10x01
182eDUP6
182fXOR
1830DUP3
1831DUP2
1832LT
1833PUSH0
1834EQ
1835PUSH20x1874
1838JUMPI
1839DUP4
183aSWAP3
183bSWAP2
183cPUSH10x01
183eSWAP5
183fSWAP2
1840DUP6
1841SWAP3
1842PUSH0
1843MSTORE
1844PUSH10x03
1846PUSH10x20
1848MSTORE
1849PUSH10x40
184bPUSH0
184cKECCAK256
184dSWAP1
184ePUSH0
184fMSTORE
1850PUSH10x20
1852MSTORE
1853PUSH10x40
1855PUSH0
1856KECCAK256
1857SLOAD
1858PUSH20x1861
185bDUP3
185cDUP12
185dPUSH20x1797
1860JUMP
1861JUMPDEST
1862MSTORE
1863ADD
1864SWAP5
1865JUMPDEST
1866DUP4
1867SHR
1868SWAP4
1869SWAP3
186aSWAP2
186bDUP3
186cADD
186dSWAP2
186eSHR
186fDUP1
1870PUSH20x17fd
1873JUMP
1874JUMPDEST
1875DUP3
1876DUP2
1877SWAP7
1878SWAP3
1879SWAP7
187aEQ
187bPUSH20x188a
187eJUMPI
187fJUMPDEST
1880POP
1881SWAP1
1882PUSH10x01
1884SWAP3
1885SWAP2
1886PUSH20x1865
1889JUMP
188aJUMPDEST
188bDUP4
188cSWAP6
188dSWAP2
188eSWAP6
188fSHL
1890PUSH20x1899
1893DUP2
1894DUP7
1895PUSH20x16de
1898JUMP
1899JUMPDEST
189aPUSH20x18a5
189dPUSH20x0303
18a0DUP3
18a1PUSH20x1c0a
18a4JUMP
18a5JUMPDEST
18a6SWAP1
18a7PUSH0
18a8SWAP3
18a9SWAP1
18aaPUSH20x18b2
18adDUP2
18aePUSH20x1bf1
18b1JUMP
18b2JUMPDEST
18b3DUP1
18b4JUMPDEST
18b5PUSH20x1918
18b8JUMPI
18b9POP
18baPOP
18bbPOP
18bcPUSH0
18bdNOT
18beDUP3
18bfADD
18c0SWAP2
18c1DUP3
18c2GT
18c3PUSH20x047c
18c6JUMPI
18c7PUSH20x18d0
18caDUP3
18cbDUP3
18ccPUSH20x1797
18cfJUMP
18d0JUMPDEST
18d1MLOAD
18d2SWAP2
18d3DUP1
18d4JUMPDEST
18d5PUSH20x18f8
18d8JUMPI
18d9POP
18daPOP
18dbPUSH10x01
18ddSWAP4
18deSWAP3
18dfSWAP2
18e0DUP2
18e1DUP6
18e2SWAP3
18e3POP
18e4PUSH20x18ed
18e7DUP3
18e8DUP12
18e9PUSH20x1797
18ecJUMP
18edJUMPDEST
18eeMSTORE
18efADD
18f0SWAP5
18f1SWAP1
18f2SWAP2
18f3SWAP3
18f4PUSH20x187f
18f7JUMP
18f8JUMPDEST
18f9PUSH0
18faNOT
18fbADD
18fcSWAP2
18fdDUP3
18feSWAP1
18ffPUSH20x1912
1902SWAP1
1903PUSH20x190c
1906DUP4
1907DUP6
1908PUSH20x1797
190bJUMP
190cJUMPDEST
190dMLOAD
190ePUSH20x2048
1911JUMP
1912JUMPDEST
1913SWAP3
1914PUSH20x18d4