# NIST Randomness on ETC

A draw whose winner comes from a NIST Randomness Beacon pulse, checked against NIST's own RSA signature inside the EVM. The organizer commits to a pulse index before entries close, and NIST reveals it.

**Alpha, unaudited.** NistBeacon and NistDraw are examples. Nobody has audited them. Nothing here comes with a guarantee. [Alpha, No Guarantees](https://etchd.app/docs/trust#alpha) says what that means.

## Why

A contract that picks a winner from its own block data hands the choice to whoever mines the block. The NIST beacon publishes a signed 512-bit random value every minute, chained to the one before, and the signature is RSA with a 4096-bit key. ETC has had the modexp precompile at `0x05` under EIP-2565 pricing since Magneto, so a contract can check that signature itself, and the draw needs no oracle and no etchd operator.

NistDraw is the smallest useful shape of that. The organizer deploys it with a pulse index and a closing time. People call `enter` until `closesAt`. Once NIST has published the chosen pulse, anyone calls `draw` with its fields and signature, and the contract picks `entrants[keccak256(outputValue) % n]`. Etchd records every pulse in branch 4, so a pulse stays available from a calendar or a mirrored day after NIST's own API moves on.

## What It Trusts

The draw trusts NIST alone. NistBeacon pins NIST's certificate in its code, by its SHA-512 id `5b6386e6…` and its 4096-bit modulus with exponent 65537, and refuses a pulse that names any other certificate. Nothing in it trusts an etchd operator. An etchd nist-pulse record carries the same bytes and an anchor time, and the CLI takes them from a CAR, but the contract checks NIST's signature whoever supplies them.

The pinned certificate is valid from 2026-10-21 to 2027-03-20. When NIST rotates its key, a new NistBeacon with the new certificate is needed, about 1.4 M gas to deploy. A draw whose pulse is signed under a certificate its beacon does not pin can never complete, and neither can one whose chain NIST resets before the chosen pulse. The organizer then deploys a new draw. The demo entries carry no value, which keeps that case cheap.

The draw also rests on ETC's honest majority. `draw` refuses a pulse whose NIST timestamp is earlier than `closesAt + 300`, so a pulse that was public while entries were open is refused even when a miner sets `block.timestamp` a few minutes behind. A miner who outruns about 23 honest blocks, five minutes of ETC, could still include a late `enter` after the pulse is out, and ETC has had 51% reorgs. A draw with value behind it picks a wider margin.

`draw` accepts any `statusCode`. NIST sets bits for the first pulse of a chain and for an interval longer than the period, and neither makes a pulse unsafe for a draw. Fixture pulse 1996051 carries status 6, and the draw test accepts it.

## How the Contract Checks It

We worked out what NIST signs from real pulses and the certificates they name. Every string and byte string in `fields` carries a u32be length, and the whole serialization is 807 bytes for a seven-digit pulse index.

```text
fields = str(uri) str(version) u32(cipherSuite) u32(period) bytes(certificateId) u64(chainIndex) u64(pulseIndex)
         str(timeStamp) bytes(localRandomValue) bytes(external.sourceId) u32(external.statusCode) bytes(external.value)
         bytes(previous) bytes(hour) bytes(day) bytes(month) bytes(year) bytes(precommitmentValue) u32(statusCode)
signature   = RSASSA-PKCS1-v1_5 with SHA-512 over fields, 4096-bit key, e = 65537
              EM = 00 01 FF..FF 00 || 3051300d060960864801650304020305000440 || sha512(fields)
outputValue = sha512(fields || signatureValue)        the signature raw, with no length prefix
certificateId = sha512(DER certificate)
```

`NistBeacon.verify(fields, signature)` walks it in this order.

1. The signature is exactly 512 bytes, else `BadSignature`, and read big-endian it is strictly below the modulus, a compare of 16 words, else `SignatureNotReduced`.
2. The fields parse. The version is `2.0`, `cipherSuite` is 0, `certificateId` is the pinned id, `chainIndex` is 2, the timestamp is 24 characters, every length fits, and nothing follows `statusCode`. Anything else is `BadPulse`.
3. SHA-512 runs in EVM assembly, since ETC has no SHA-512 precompile, at about 70,000 gas per 128-byte block. The six full blocks of `fields` are compressed once and both digests finish from that state, `sha512(fields)` and `sha512(fields || signature)`, 12 compressions in place of 18.
4. The modexp precompile raises the signature to 65537 mod the pinned modulus, and the result must equal the PKCS#1 v1.5 encoding of `sha512(fields)` byte for byte, else `BadSignature`.
5. It returns the pulse index, the timestamp as Unix seconds, `statusCode` and `outputValue`. `NistDraw.draw` then requires the pulse index chosen at deploy and a timestamp at or after `closesAt + 300`, else `WrongPulse`, and picks the winner.

Why the check against the modulus. Modexp reduces its base mod n, so without it `s + n` verifies as well as `s` whenever `s + n` still fits in 512 bytes, that is whenever `s < 2^4096 - n`. The pinned modulus starts `0xca`, so that holds for about 27% of pulses. `outputValue` is hashed over the caller's bytes, so `s + n` would give an output NIST never published, and the caller of the permissionless `draw` could pick whichever of two winners suits them. The same rule sits in `checkNistSignature` in [@etchd/core](https://github.com/classix-dev/etchd/blob/main/packages/core/src/sources/parsers/nist.ts), so neither a capture nor the CLI hands a contract the second form.

Two certificate oddities came out of the fixtures. Pulses up to 1996050 name certificate `87f27f43…`, a 2048-bit certificate whose key does not verify them, and they verify under the 4096-bit key of `5b6386e6…`, which pulses name from 1996051 on. Pulse 1996051's `previous` equals 1996050's output across the change, at 13:32 and 13:34 UTC on 2026-10-23, with one pulse missing in between. NistBeacon follows the id a pulse names, so it refuses 1996050 and earlier. Pulse 1700000 names `528943a5…` and verifies under that certificate, so NIST has changed keys before.

## Gas

Measured on Anvil with Shanghai rules and chain id 61 in [nist.test.ts](https://github.com/classix-dev/etchd/blob/main/packages/contract/test/nist.test.ts). Each figure is a whole transaction. The modexp call alone estimates at about 22,600 gas on ETC mainnet RPCs, against 21,845 by the EIP-2565 formula and 49,971 by the older EIP-198 rule. The block gas limit is 8,000,000, so a draw takes an eighth of a block and fits with no fallback.

| Call | Gas |
| --- | --- |
| NistBeacon.verify of one real pulse, 1,319 bytes of calldata | 897,814 |
| NistDraw.draw with three entrants | 975,839 |

A fallback we did not need would hash SHA-512 in chunks across calls with the midstate in storage, which keeps the trust in NIST alone and costs about 30% more. Checking the RSA signature over a digest the caller supplies is no fallback, since the caller would choose the digest.

## Figures

The contract hashes the NIST pulse fields with SHA-512 in EVM code, since ETC has no SHA-512 precompile. It refuses a signature that is not below NIST's modulus, raises the signature to 65537 through the modexp precompile at 0x05, and checks the result against the padded hash. The pulse's outputValue is the SHA-512 of the fields and the signature, and its keccak256 modulo the number of entrants picks the winner.

## The Source

Compiled with solc 0.8.38, optimizer 200 and EVM version shanghai, with the settings in [packages/contract/verify](https://github.com/classix-dev/etchd/tree/main/packages/contract/verify). This is [NistDraw.sol](https://github.com/classix-dev/etchd/blob/main/packages/contract/src/examples/NistDraw.sol), which holds NistBeacon and NistDraw.

NistDraw.sol

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.38;

import {Sha512} from "./Sha512.sol";

/// @title NIST beacon checker
/// @author Classix
/// @notice Checks a NIST Randomness Beacon 2.0 pulse of chain 2 against NIST's own RSA signature, with the key pinned in
///         the code. Nothing here trusts an etchd operator. An etchd nist-pulse record carries the same bytes.
/// @dev A NIST key rotation needs a new NistBeacon. The signature must be below the modulus, since MODEXP reduces its
///      base and `s + n` would otherwise verify too and give an outputValue NIST never published.
contract NistBeacon {
    /// SHA-512 of the DER certificate, as the pulse's certificateId field names it.
    bytes32 public constant CERT_ID_HI = bytes32(hex"5b6386e6ac8a993bac8d99d6549deda44af82f2fa4a0e9c0025b6474ab0cbfca");
    bytes32 public constant CERT_ID_LO = bytes32(hex"b67bfff264fda41aac003b0c9ee7fb77d2938dc0f286fb73cc8fa79864ec0c18");
    /// The 4096-bit RSA modulus of that certificate, exponent 65537. Valid 2026-10-21 to 2027-03-20.
    bytes internal constant MODULUS =
        hex"ca0112757321c9285fb9547033f683d458529b3aaa795e246c88b59466c3327dbaf3647cd45c7c07c352f853be4a7d61f40cff11ee6807c9fa17aaf8753b146d3bc6094e31d3bb4887f8984e59199efdb9d21cabeb3fb44052a85c181064c5f899d72a08523be3764c503f90f0d0d3d4bdd104f94258f0ffb34a8a393d8711c0c5ddae50626eb7a86ab22fd8600448d7c63606932d0e5d74464645c9223ab7fd9149869bf32146755cbcffff59dd38e395d41d15c06e590e3be95d3afbf0d78c0ce1e48156625c743f63b920a4c7b5f23b3568b9a49d48578bb5e7ee379a60d1b5a3e3ce8eea0007b24880b26f493f850f0527be9d4291cc226df15fb3b5f08b9d892234b5d4320d609273846cc4c6bf1c0e3bd5b7bb589fa3e239241a8bdf9c6a9cf4a8cb8eb5454f7b86dc92090c964e2112dcb23b6d94fe703176dd5fc5ab9de3dfea1a3a870bdfd0061233594e34876c64cb5206c1d1fc8643c5e2e828ede2f3b5ed66322dc61123dc7a582c90adc6156d1b0dfe06d43acccfa0a4ec5dc216b4e748c1743e9a5ba8bb93cfbe726fa350db1c0c2e0a7be2a7206202c7ba7f9acb7ec5dbde4e8850197592e94b8519edabd7514d5e4e91b0c22ddee601912bac71cd4f450d01f6c51d2a32340149dd3f19a2cf67e871fe87163cb5c8412fe7dfb90a087b045e0ec0565011644616da18e005ed7017b4f8f1838c0433c83547";
    uint64 public constant CHAIN_INDEX = 2;

    struct Pulse {
        uint64 pulseIndex;
        uint64 time; // timeStamp as Unix seconds
        uint32 statusCode;
        bytes32 outHi; // outputValue = sha512(fields || signature)
        bytes32 outLo;
    }

    error BadPulse(uint256 offset);
    error BadSignature();
    error SignatureNotReduced();

    /// `fields` is the pulse serialized as NIST signs it (every field up to and including statusCode, each string and
    /// byte string with a u32be length), `signature` the 512-byte signatureValue.
    function verify(bytes calldata fields, bytes calldata signature) external view returns (Pulse memory p) {
        if (signature.length != 512) revert BadSignature();
        if (!belowModulus(signature)) revert SignatureNotReduced();
        uint256 i = skip(fields, 0); // uri
        i = expectString(fields, i, "2.0"); // version
        if (u32(fields, i) != 0) revert BadPulse(i); // cipherSuite 0, SHA-512 and RSA PKCS#1 v1.5
        i += 8; // cipherSuite, period
        if (u32(fields, i) != 64 || bytes32(fields[i + 4:i + 36]) != CERT_ID_HI || bytes32(fields[i + 36:i + 68]) != CERT_ID_LO) revert BadPulse(i);
        i += 68;
        if (uint64(bytes8(fields[i:i + 8])) != CHAIN_INDEX) revert BadPulse(i);
        p.pulseIndex = uint64(bytes8(fields[i + 8:i + 16]));
        i += 16;
        if (u32(fields, i) != 24) revert BadPulse(i);
        p.time = isoSeconds(fields[i + 4:i + 28]);
        i += 28;
        i = skip(fields, i); // localRandomValue
        i = skip(fields, i); // external.sourceId
        i += 4; // external.statusCode
        for (uint256 k; k < 7; k++) i = skip(fields, i); // external.value, previous, hour, day, month, year, precommitmentValue
        p.statusCode = u32(fields, i);
        if (i + 4 != fields.length) revert BadPulse(i);
        (bytes32 hHi, bytes32 hLo, bytes32 oHi, bytes32 oLo) = Sha512.hashPrefixAndWhole(fields, signature);
        if (!rsaOk(signature, hHi, hLo)) revert BadSignature();
        (p.outHi, p.outLo) = (oHi, oLo);
    }

    /// @notice SHA-512 of `data` as two 32-byte halves, the library verify uses, exposed so its test vectors can run.
    function sha512(bytes calldata data) external pure returns (bytes32 hi, bytes32 lo) {
        return Sha512.hash(data);
    }

    /// The signature read big-endian is strictly below MODULUS, compared one 32-byte word at a time.
    function belowModulus(bytes calldata signature) private pure returns (bool) {
        bytes memory n = MODULUS;
        for (uint256 k; k < 16; k++) {
            uint256 a = uint256(bytes32(signature[k * 32:k * 32 + 32]));
            uint256 b;
            assembly ("memory-safe") {
                b := mload(add(add(n, 32), mul(k, 32)))
            }
            if (a != b) return a < b;
        }
        return false;
    }

    /// RSASSA-PKCS1-v1_5 with SHA-512: signature^65537 mod n must be 00 01 FF..FF 00 || DigestInfo || hash.
    function rsaOk(bytes calldata signature, bytes32 hHi, bytes32 hLo) private view returns (bool) {
        bytes memory input = bytes.concat(bytes32(uint256(512)), bytes32(uint256(3)), bytes32(uint256(512)), signature, hex"010001", MODULUS);
        bytes memory em = new bytes(512);
        bool ok;
        assembly ("memory-safe") {
            ok := staticcall(gas(), 0x05, add(input, 32), mload(input), add(em, 32), 512)
        }
        if (!ok) return false;
        bytes memory want = new bytes(512);
        want[0] = 0x00;
        want[1] = 0x01;
        for (uint256 j = 2; j < 428; j++) want[j] = 0xff;
        bytes memory tail = bytes.concat(hex"003051300d060960864801650304020305000440", hHi, hLo);
        for (uint256 j; j < 84; j++) want[428 + j] = tail[j];
        return keccak256(em) == keccak256(want);
    }

    function u32(bytes calldata b, uint256 i) private pure returns (uint32) {
        if (i + 4 > b.length) revert BadPulse(i);
        return uint32(bytes4(b[i:i + 4]));
    }

    function skip(bytes calldata b, uint256 i) private pure returns (uint256) {
        uint256 n = u32(b, i);
        if (i + 4 + n > b.length) revert BadPulse(i);
        return i + 4 + n;
    }

    function expectString(bytes calldata b, uint256 i, bytes memory s) private pure returns (uint256) {
        if (u32(b, i) != s.length || keccak256(b[i + 4:i + 4 + s.length]) != keccak256(s)) revert BadPulse(i);
        return i + 4 + s.length;
    }

    /// "yyyy-mm-ddThh:mm:ss.000Z" as Unix seconds, by the days-from-civil rule.
    function isoSeconds(bytes calldata s) private pure returns (uint64) {
        uint256 y = num(s, 0, 4);
        uint256 m = num(s, 5, 2);
        uint256 d = num(s, 8, 2);
        if (m <= 2) y -= 1;
        uint256 era = y / 400;
        uint256 yoe = y - era * 400;
        uint256 doy = (153 * (m > 2 ? m - 3 : m + 9) + 2) / 5 + d - 1;
        uint256 doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
        uint256 days_ = era * 146097 + doe - 719468;
        return uint64(days_ * 86400 + num(s, 11, 2) * 3600 + num(s, 14, 2) * 60 + num(s, 17, 2));
    }

    function num(bytes calldata s, uint256 from, uint256 len) private pure returns (uint256 v) {
        for (uint256 k; k < len; k++) {
            uint8 c = uint8(s[from + k]);
            if (c < 48 || c > 57) revert BadPulse(from);
            v = v * 10 + (c - 48);
        }
    }
}

/// @title NIST draw
/// @author Classix
/// @notice A draw whose winner comes from a NIST pulse chosen before entries closed. The pulse index is fixed at deploy,
///         and the pulse must carry a NIST timestamp at least MARGIN seconds after closesAt, so nobody could know its
///         value while entries were open, even with block times a few minutes behind. Any statusCode is accepted.
/// @dev If NIST rotates its certificate or restarts chain 2 before the chosen pulse, the draw can never complete, and
///      the organizer deploys a new one.
contract NistDraw {
    uint64 public constant MARGIN = 300;

    NistBeacon public immutable beacon;
    uint64 public immutable pulseIndex;
    uint64 public immutable closesAt;
    address[] public entrants;
    address public winner;
    bytes32 public outHi;
    bytes32 public outLo;

    event Entered(address who, uint256 n);
    event Drawn(uint64 pulseIndex, address winner, uint256 n);

    error Closed();
    error NotYet();
    error WrongPulse(uint64 pulseIndex, uint64 time);
    error AlreadyDrawn();
    error NoEntrants();

    constructor(NistBeacon beacon_, uint64 pulseIndex_, uint64 closesAt_) {
        beacon = beacon_;
        pulseIndex = pulseIndex_;
        closesAt = closesAt_;
    }

    function enter() external {
        if (block.timestamp >= closesAt) revert Closed();
        entrants.push(msg.sender);
        emit Entered(msg.sender, entrants.length);
    }

    function draw(bytes calldata fields, bytes calldata signature) external returns (address) {
        if (winner != address(0)) revert AlreadyDrawn();
        if (block.timestamp < closesAt) revert NotYet();
        uint256 n = entrants.length;
        if (n == 0) revert NoEntrants();
        NistBeacon.Pulse memory p = beacon.verify(fields, signature);
        if (p.pulseIndex != pulseIndex || p.time < closesAt + MARGIN) revert WrongPulse(p.pulseIndex, p.time);
        (outHi, outLo) = (p.outHi, p.outLo);
        winner = entrants[uint256(keccak256(abi.encodePacked(p.outHi, p.outLo))) % n];
        emit Drawn(p.pulseIndex, winner, n);
        return winner;
    }
}
```

And the library, [Sha512.sol](https://github.com/classix-dev/etchd/blob/main/packages/contract/src/examples/Sha512.sol).

Sha512.sol

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.38;

/// @title SHA-512 in EVM assembly
/// @author Classix
/// @notice SHA-512 (FIPS 180-4). ETC has no SHA-512 precompile, so NIST pulses are hashed here.
library Sha512 {
    bytes internal constant K =
        hex"428a2f98d728ae227137449123ef65cdb5c0fbcfec4d3b2fe9b5dba58189dbbc3956c25bf348b53859f111f1b605d019923f82a4af194f9bab1c5ed5da6d8118d807aa98a303024212835b0145706fbe243185be4ee4b28c550c7dc3d5ffb4e272be5d74f27b896f80deb1fe3b1696b19bdc06a725c71235c19bf174cf692694e49b69c19ef14ad2efbe4786384f25e30fc19dc68b8cd5b5240ca1cc77ac9c652de92c6f592b02754a7484aa6ea6e4835cb0a9dcbd41fbd476f988da831153b5983e5152ee66dfaba831c66d2db43210b00327c898fb213fbf597fc7beef0ee4c6e00bf33da88fc2d5a79147930aa72506ca6351e003826f142929670a0e6e7027b70a8546d22ffc2e1b21385c26c9264d2c6dfc5ac42aed53380d139d95b3df650a73548baf63de766a0abb3c77b2a881c2c92e47edaee692722c851482353ba2bfe8a14cf10364a81a664bbc423001c24b8b70d0f89791c76c51a30654be30d192e819d6ef5218d69906245565a910f40e35855771202a106aa07032bbd1b819a4c116b8d2d0c81e376c085141ab532748774cdf8eeb9934b0bcb5e19b48a8391c0cb3c5c95a634ed8aa4ae3418acb5b9cca4f7763e373682e6ff3d6b2b8a3748f82ee5defb2fc78a5636f43172f6084c87814a1f0ab728cc702081a6439ec90befffa23631e28a4506cebde82bde9bef9a3f7b2c67915c67178f2e372532bca273eceea26619cd186b8c721c0c207eada7dd6cde0eb1ef57d4f7fee6ed17806f067aa72176fba0a637dc5a2c898a6113f9804bef90dae1b710b35131c471b28db77f523047d8432caab7b40c724933c9ebe0a15c9bebc431d67c49c100d4c4cc5d4becb3e42b6597f299cfc657e2a5fcb6fab3ad6faec6c44198c4a475817";

    /// The digest of `data`, as two 32-byte halves.
    function hash(bytes memory data) internal pure returns (bytes32, bytes32) {
        uint256[8] memory s = initial();
        uint256 full = data.length / 128;
        blocks(s, data, 0, full);
        return finish(s, data, full * 128, data.length - full * 128, "", data.length);
    }

    /// sha512(a) and sha512(a || b) together. The full blocks of `a` are compressed once and shared.
    function hashPrefixAndWhole(bytes memory a, bytes memory b)
        internal
        pure
        returns (bytes32 aHi, bytes32 aLo, bytes32 abHi, bytes32 abLo)
    {
        uint256[8] memory s = initial();
        uint256 full = a.length / 128;
        blocks(s, a, 0, full);
        uint256[8] memory u;
        for (uint256 i; i < 8; i++) u[i] = s[i];
        (aHi, aLo) = finish(s, a, full * 128, a.length - full * 128, "", a.length);
        (abHi, abLo) = finish(u, a, full * 128, a.length - full * 128, b, a.length + b.length);
    }

    function initial() private pure returns (uint256[8] memory s) {
        s = [
            uint256(0x6a09e667f3bcc908), 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1,
            0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179
        ];
    }

    /// Pads data[from:from+len] || extra as the end of a message of `total` bytes, compresses it and returns the digest.
    function finish(uint256[8] memory s, bytes memory data, uint256 from, uint256 len, bytes memory extra, uint256 total)
        private
        pure
        returns (bytes32 hi, bytes32 lo)
    {
        uint256 n = len + extra.length;
        uint256 padded = ((n + 17 + 127) / 128) * 128;
        bytes memory buf = new bytes(padded);
        assembly ("memory-safe") {
            let dst := add(buf, 32)
            let src := add(add(data, 32), from)
            for { let i := 0 } lt(i, len) { i := add(i, 32) } { mstore(add(dst, i), mload(add(src, i))) }
            let e := add(extra, 32)
            let el := mload(extra)
            for { let i := 0 } lt(i, el) { i := add(i, 32) } { mstore(add(add(dst, len), i), mload(add(e, i))) }
            // Clear whatever the word copies wrote past n, then the 0x80 byte and the bit length.
            for { let i := n } lt(i, padded) { i := add(i, 1) } { mstore8(add(dst, i), 0) }
            mstore8(add(dst, n), 0x80)
            let lp := add(dst, sub(padded, 32))
            mstore(lp, or(and(mload(lp), shl(128, not(0))), mul(total, 8)))
        }
        blocks(s, buf, 0, padded / 128);
        hi = bytes32((s[0] << 192) | (s[1] << 128) | (s[2] << 64) | s[3]);
        lo = bytes32((s[4] << 192) | (s[5] << 128) | (s[6] << 64) | s[7]);
    }

    /// Compresses `count` 128-byte blocks of data starting at byte `from` into s.
    function blocks(uint256[8] memory s, bytes memory data, uint256 from, uint256 count) private pure {
        bytes memory k = K;
        uint256[80] memory w;
        for (uint256 j; j < count; j++) {
            schedule(w, data, from + j * 128);
            rounds(s, w, k);
        }
    }

    function schedule(uint256[80] memory w, bytes memory data, uint256 offset) private pure {
        assembly ("memory-safe") {
            let src := add(add(data, 32), offset)
            for { let t := 0 } lt(t, 16) { t := add(t, 1) } {
                mstore(add(w, mul(t, 32)), shr(192, mload(add(src, mul(t, 8)))))
            }
            for { let t := 16 } lt(t, 80) { t := add(t, 1) } {
                let x := mload(add(w, mul(sub(t, 15), 32)))
                let x2 := or(x, shl(64, x))
                let y := mload(add(w, mul(sub(t, 2), 32)))
                let y2 := or(y, shl(64, y))
                let s0 := xor(xor(shr(1, x2), shr(8, x2)), shr(7, x))
                let s1 := xor(xor(shr(19, y2), shr(61, y2)), shr(6, y))
                mstore(
                    add(w, mul(t, 32)),
                    and(
                        add(add(add(mload(add(w, mul(sub(t, 16), 32))), and(s0, 0xffffffffffffffff)), mload(add(w, mul(sub(t, 7), 32)))), and(s1, 0xffffffffffffffff)),
                        0xffffffffffffffff
                    )
                )
            }
        }
    }

    function rounds(uint256[8] memory s, uint256[80] memory w, bytes memory k) private pure {
        assembly ("memory-safe") {
            let a := mload(s)
            let b := mload(add(s, 32))
            let c := mload(add(s, 64))
            let d := mload(add(s, 96))
            let e := mload(add(s, 128))
            let f := mload(add(s, 160))
            let g := mload(add(s, 192))
            let h := mload(add(s, 224))
            let kp := add(k, 32)
            for { let t := 0 } lt(t, 80) { t := add(t, 1) } {
                let e2 := or(e, shl(64, e))
                let t1 := add(
                    add(add(h, and(xor(xor(shr(14, e2), shr(18, e2)), shr(41, e2)), 0xffffffffffffffff)), xor(and(e, f), and(not(e), g))),
                    add(shr(192, mload(add(kp, mul(t, 8)))), mload(add(w, mul(t, 32))))
                )
                let a2 := or(a, shl(64, a))
                let t2 := add(and(xor(xor(shr(28, a2), shr(34, a2)), shr(39, a2)), 0xffffffffffffffff), xor(xor(and(a, b), and(a, c)), and(b, c)))
                h := g
                g := f
                f := e
                e := and(add(d, t1), 0xffffffffffffffff)
                d := c
                c := b
                b := a
                a := and(add(t1, t2), 0xffffffffffffffff)
            }
            mstore(s, and(add(mload(s), a), 0xffffffffffffffff))
            mstore(add(s, 32), and(add(mload(add(s, 32)), b), 0xffffffffffffffff))
            mstore(add(s, 64), and(add(mload(add(s, 64)), c), 0xffffffffffffffff))
            mstore(add(s, 96), and(add(mload(add(s, 96)), d), 0xffffffffffffffff))
            mstore(add(s, 128), and(add(mload(add(s, 128)), e), 0xffffffffffffffff))
            mstore(add(s, 160), and(add(mload(add(s, 160)), f), 0xffffffffffffffff))
            mstore(add(s, 192), and(add(mload(add(s, 192)), g), 0xffffffffffffffff))
            mstore(add(s, 224), and(add(mload(add(s, 224)), h), 0xffffffffffffffff))
        }
    }
}
```

## Tested on a Local Network

We do not deploy NistBeacon or NistDraw on ETC mainnet. Both run in the end-to-end test, step 8 of [scripts/e2e.ts](https://github.com/classix-dev/etchd/blob/main/scripts/e2e.ts), on a local anvil network whose clock starts at 2026-10-23 13:00 UTC, and in the contract tests on anvil. The pulses are real NIST Beacon 2.0 pulses, fetched into [packages/contract/test/fixtures/nist](https://github.com/classix-dev/etchd/tree/main/packages/contract/test/fixtures/nist).

The test records pulse 1996051 on the devnet calendar, reads it back from the window's CAR with `etchd nist pulse 1996051 --car`, checks it under the pinned NIST key and proves the record with `recordExistedAtBy` on the devnet verifier. On the time-set anvil, `nist.ts deploy` puts NistBeacon and a NistDraw for pulse 1996051 with `closesAt` at 13:24 UTC, ten minutes before the pulse's minute, and three accounts enter.

A copy of the pulse with its last signature byte changed reverts `BadSignature`. The pulse itself draws, and the draw names a winner among the three. A second draw that closed less than 300 s before the pulse reverts `WrongPulse`, and pulse 1996054 given with the signature s + n, which MODEXP alone accepts, reverts `SignatureNotReduced`. The contract tests add the FIPS 180-4 vectors of Sha512.sol, four pulses with their outputs, a pulse that names the other NIST certificate, a flipped bit and the signature of another pulse.

Run them again

```bash
pnpm --filter @etchd/contract test         # NistBeacon and NistDraw on anvil
DEVNET_PORT_OFFSET=2800 pnpm e2e           # the whole end-to-end test, step 8 is NIST
```

## Producing the Inputs

`etchd nist pulse` prints `{fields, signature, outputValue}` for `draw`, checked locally against the pinned key with the signature below the modulus. It reads a calendar's nist-pulse record by default, a daily CAR with `--car`, or NIST itself with `--nist`. It exits 3 when the signature does not check or the pulse is missing and 4 when the source cannot be reached.

Deploy, enter, draw

```bash
ETCHD_DEMO_KEY=0x... node scripts/examples/nist.ts deploy --pulse 2001000 --closes-at 2026-10-26T23:53:00Z
ETCHD_DEMO_KEY=0x... node scripts/examples/nist.ts enter --address 0xDRAW...

# once NIST has published pulse 2001000, from calendar a, a mirrored day or NIST
etchd nist pulse 2001000 -c https://a.etc.calendar.etchd.app --json > pulse.json
etchd nist pulse 2001000 --car a-day.car --json > pulse.json
etchd nist pulse 2001000 --nist --json > pulse.json

ETCHD_DEMO_KEY=0x... node scripts/examples/nist.ts draw --address 0xDRAW... --pulse-file pulse.json
node scripts/examples/nist.ts status --address 0xDRAW...
```

`nist.ts deploy` deploys a NistBeacon too unless `--beacon` names one. Pick a pulse that NIST publishes at least 300 s after `--closes-at`, since the contract refuses any other at draw time. To show that an etchd operator held the pulse by a given time, `etchd records proof https://a.etc.calendar.etchd.app --pulse 2001000 --check --rpc https://rpc.classix.dev --verifier 0xVERIFIER...` prints its record path for `recordExistedAtBy`. The record layout is on [Records](https://etchd.app/docs/records#nist-pulse).

## Tests

[nist.test.ts](https://github.com/classix-dev/etchd/blob/main/packages/contract/test/nist.test.ts) runs against real pulses kept in [test/fixtures/nist](https://github.com/classix-dev/etchd/tree/main/packages/contract/test/fixtures/nist), with the certificate they name.

The fixtures were fetched from beacon.nist.gov on 2026-10-25.

It checks the FIPS 180-4 vectors (the empty message, abc and the 112-byte two-block message), four real pulses with NIST's own outputs and the gas of `verify` under 1,000,000. It refuses a pulse naming the other certificate, one flipped bit, the signature of another pulse, a signature of the wrong length, and `s + n` of a real pulse whose `s` is small enough, after first showing modexp alone accepts it. The same case is refused by `checkNistSignature`. The draw tests pick the winner from pulse 1996051, refuse a pulse one second earlier than `closesAt + 300` and accept one at exactly that time, refuse another pulse than the one chosen, and refuse a draw with no entrants.

## Limits

A NIST key rotation or a reset of chain 2 before the chosen pulse leaves the draw unable to complete, and a new NistBeacon is needed for every new NIST certificate. The margin of 300 s holds only as long as nobody outruns about 23 honest ETC blocks. NIST can publish a value it chose, and the draw trusts it not to, as everyone who uses the beacon does. The precommitment value in each pulse binds the next pulse's local random value, which NistBeacon does not check.
