@secondlayer/shared 6.22.0 → 6.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/src/crypto/ed25519.d.ts +22 -0
  2. package/dist/src/crypto/ed25519.js +80 -0
  3. package/dist/src/crypto/ed25519.js.map +10 -0
  4. package/dist/src/crypto/secondlayer-webhook.d.ts +40 -0
  5. package/dist/src/crypto/secondlayer-webhook.js +130 -0
  6. package/dist/src/crypto/secondlayer-webhook.js.map +11 -0
  7. package/dist/src/db/index.d.ts +2 -0
  8. package/dist/src/db/queries/chain-reorgs.d.ts +2 -0
  9. package/dist/src/db/queries/contracts.d.ts +2 -0
  10. package/dist/src/db/queries/integrity.d.ts +2 -0
  11. package/dist/src/db/queries/subgraph-gaps.d.ts +2 -0
  12. package/dist/src/db/queries/subgraph-operations.d.ts +2 -0
  13. package/dist/src/db/queries/subgraphs.d.ts +2 -0
  14. package/dist/src/db/queries/subscriptions.d.ts +2 -0
  15. package/dist/src/db/schema.d.ts +2 -0
  16. package/dist/src/index.d.ts +26 -24
  17. package/dist/src/index.js +54 -53
  18. package/dist/src/index.js.map +5 -5
  19. package/dist/src/leader.d.ts +53 -0
  20. package/dist/src/leader.js +257 -0
  21. package/dist/src/leader.js.map +12 -0
  22. package/dist/src/node/client.d.ts +52 -0
  23. package/dist/src/node/client.js +188 -1
  24. package/dist/src/node/client.js.map +5 -4
  25. package/dist/src/node/consensus.d.ts +38 -0
  26. package/dist/src/node/consensus.js +67 -0
  27. package/dist/src/node/consensus.js.map +10 -0
  28. package/dist/src/node/local-client.d.ts +2 -0
  29. package/dist/src/node/nakamoto.d.ts +90 -0
  30. package/dist/src/node/nakamoto.js +177 -0
  31. package/dist/src/node/nakamoto.js.map +10 -0
  32. package/dist/src/streams-bulk-manifest.d.ts +33 -0
  33. package/dist/src/streams-bulk-manifest.js +104 -0
  34. package/dist/src/streams-bulk-manifest.js.map +11 -0
  35. package/dist/src/types.d.ts +2 -0
  36. package/migrations/0089_blocks_index_block_hash.ts +25 -0
  37. package/package.json +25 -1
@@ -0,0 +1,38 @@
1
+ /** Mainnet PoX schedule (from /v2/pox) — overridable for other networks. */
2
+ declare const MAINNET_FIRST_BURN_HEIGHT = 666050;
3
+ declare const MAINNET_REWARD_CYCLE_LENGTH = 2100;
4
+ interface RewardSetSigner {
5
+ /** 33-byte compressed secp256k1 public key (hex, no 0x). */
6
+ signing_key: string;
7
+ weight: number;
8
+ }
9
+ interface RewardSet {
10
+ signers: RewardSetSigner[];
11
+ total_weight: number;
12
+ }
13
+ interface SignerVerification {
14
+ /** Summed weight of distinct reward-set signers that signed the block. */
15
+ signedWeight: number;
16
+ totalWeight: number;
17
+ /** floor(total_weight * 7 / 10). */
18
+ threshold: number;
19
+ thresholdMet: boolean;
20
+ /** Count of distinct reward-set keys that signed. */
21
+ matchedSigners: number;
22
+ }
23
+ /** Reward cycle for a burn block height. Defaults to the mainnet PoX schedule. */
24
+ declare function rewardCycle(burnBlockHeight: number, opts?: {
25
+ firstBurnHeight?: number
26
+ cycleLength?: number
27
+ }): number;
28
+ /** Recover the signer pubkey (compressed hex) from a 65-byte VRS recoverable
29
+ * ECDSA signature over the block_hash. */
30
+ declare function recoverSignerKey(blockHashHex: string, vrsSignatureHex: string): string;
31
+ /**
32
+ * Recover each header signer signature, match it to the reward set, and sum the
33
+ * distinct matched signers' weights against the 70% threshold. A signature that
34
+ * fails to recover or whose key isn't in the set simply doesn't count — never
35
+ * a false positive.
36
+ */
37
+ declare function verifySignerSignatures(blockHashHex: string, signerSignaturesHex: string[], rewardSet: RewardSet): SignerVerification;
38
+ export { verifySignerSignatures, rewardCycle, recoverSignerKey, SignerVerification, RewardSetSigner, RewardSet, MAINNET_REWARD_CYCLE_LENGTH, MAINNET_FIRST_BURN_HEIGHT };
@@ -0,0 +1,67 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
17
+ // src/node/consensus.ts
18
+ import { recoverPublicKey } from "@secondlayer/stacks/utils";
19
+ var strip = (h) => h.startsWith("0x") ? h.slice(2) : h;
20
+ var MAINNET_FIRST_BURN_HEIGHT = 666050;
21
+ var MAINNET_REWARD_CYCLE_LENGTH = 2100;
22
+ function rewardCycle(burnBlockHeight, opts = {}) {
23
+ const first = opts.firstBurnHeight ?? MAINNET_FIRST_BURN_HEIGHT;
24
+ const length = opts.cycleLength ?? MAINNET_REWARD_CYCLE_LENGTH;
25
+ return Math.floor((burnBlockHeight - first) / length);
26
+ }
27
+ function recoverSignerKey(blockHashHex, vrsSignatureHex) {
28
+ return strip(recoverPublicKey(strip(blockHashHex), strip(vrsSignatureHex), true));
29
+ }
30
+ function verifySignerSignatures(blockHashHex, signerSignaturesHex, rewardSet) {
31
+ const byKey = new Map(rewardSet.signers.map((s) => [strip(s.signing_key), s.weight]));
32
+ const seen = new Set;
33
+ let signedWeight = 0;
34
+ let matchedSigners = 0;
35
+ for (const sig of signerSignaturesHex) {
36
+ let pubkey;
37
+ try {
38
+ pubkey = recoverSignerKey(blockHashHex, sig);
39
+ } catch {
40
+ continue;
41
+ }
42
+ const weight = byKey.get(pubkey);
43
+ if (weight !== undefined && !seen.has(pubkey)) {
44
+ seen.add(pubkey);
45
+ signedWeight += weight;
46
+ matchedSigners += 1;
47
+ }
48
+ }
49
+ const threshold = Math.floor(rewardSet.total_weight * 7 / 10);
50
+ return {
51
+ signedWeight,
52
+ totalWeight: rewardSet.total_weight,
53
+ threshold,
54
+ thresholdMet: signedWeight >= threshold,
55
+ matchedSigners
56
+ };
57
+ }
58
+ export {
59
+ verifySignerSignatures,
60
+ rewardCycle,
61
+ recoverSignerKey,
62
+ MAINNET_REWARD_CYCLE_LENGTH,
63
+ MAINNET_FIRST_BURN_HEIGHT
64
+ };
65
+
66
+ //# debugId=1937481E7D2EF9AA64756E2164756E21
67
+ //# sourceMappingURL=consensus.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/node/consensus.ts"],
4
+ "sourcesContent": [
5
+ "import { recoverPublicKey } from \"@secondlayer/stacks/utils\";\n\n/**\n * Consensus verification: do ≥70% of a reward cycle's signer weight attest to a\n * Nakamoto block? Each header `signer_signature` is a recoverable ECDSA over the\n * block_hash; recover the pubkey, match it to the cycle's reward set, and sum the\n * matched signers' weights. Verified bit-exact against mainnet 3.4.0.0.3 (see\n * docs/design/trustless-verification-proofs.md, signer-signature appendix).\n */\nconst strip = (h: string): string => (h.startsWith(\"0x\") ? h.slice(2) : h);\n\n/** Mainnet PoX schedule (from /v2/pox) — overridable for other networks. */\nexport const MAINNET_FIRST_BURN_HEIGHT = 666_050;\nexport const MAINNET_REWARD_CYCLE_LENGTH = 2100;\n\nexport interface RewardSetSigner {\n\t/** 33-byte compressed secp256k1 public key (hex, no 0x). */\n\tsigning_key: string;\n\tweight: number;\n}\n\nexport interface RewardSet {\n\tsigners: RewardSetSigner[];\n\ttotal_weight: number;\n}\n\nexport interface SignerVerification {\n\t/** Summed weight of distinct reward-set signers that signed the block. */\n\tsignedWeight: number;\n\ttotalWeight: number;\n\t/** floor(total_weight * 7 / 10). */\n\tthreshold: number;\n\tthresholdMet: boolean;\n\t/** Count of distinct reward-set keys that signed. */\n\tmatchedSigners: number;\n}\n\n/** Reward cycle for a burn block height. Defaults to the mainnet PoX schedule. */\nexport function rewardCycle(\n\tburnBlockHeight: number,\n\topts: { firstBurnHeight?: number; cycleLength?: number } = {},\n): number {\n\tconst first = opts.firstBurnHeight ?? MAINNET_FIRST_BURN_HEIGHT;\n\tconst length = opts.cycleLength ?? MAINNET_REWARD_CYCLE_LENGTH;\n\treturn Math.floor((burnBlockHeight - first) / length);\n}\n\n/** Recover the signer pubkey (compressed hex) from a 65-byte VRS recoverable\n * ECDSA signature over the block_hash. */\nexport function recoverSignerKey(\n\tblockHashHex: string,\n\tvrsSignatureHex: string,\n): string {\n\treturn strip(\n\t\trecoverPublicKey(strip(blockHashHex), strip(vrsSignatureHex), true),\n\t);\n}\n\n/**\n * Recover each header signer signature, match it to the reward set, and sum the\n * distinct matched signers' weights against the 70% threshold. A signature that\n * fails to recover or whose key isn't in the set simply doesn't count — never\n * a false positive.\n */\nexport function verifySignerSignatures(\n\tblockHashHex: string,\n\tsignerSignaturesHex: string[],\n\trewardSet: RewardSet,\n): SignerVerification {\n\tconst byKey = new Map<string, number>(\n\t\trewardSet.signers.map((s) => [strip(s.signing_key), s.weight]),\n\t);\n\tconst seen = new Set<string>();\n\tlet signedWeight = 0;\n\tlet matchedSigners = 0;\n\tfor (const sig of signerSignaturesHex) {\n\t\tlet pubkey: string;\n\t\ttry {\n\t\t\tpubkey = recoverSignerKey(blockHashHex, sig);\n\t\t} catch {\n\t\t\tcontinue; // malformed signature → no credit\n\t\t}\n\t\tconst weight = byKey.get(pubkey);\n\t\tif (weight !== undefined && !seen.has(pubkey)) {\n\t\t\tseen.add(pubkey);\n\t\t\tsignedWeight += weight;\n\t\t\tmatchedSigners += 1;\n\t\t}\n\t}\n\tconst threshold = Math.floor((rewardSet.total_weight * 7) / 10);\n\treturn {\n\t\tsignedWeight,\n\t\ttotalWeight: rewardSet.total_weight,\n\t\tthreshold,\n\t\tthresholdMet: signedWeight >= threshold,\n\t\tmatchedSigners,\n\t};\n}\n"
6
+ ],
7
+ "mappings": ";;;;;;;;;;;;;;;;;AAAA;AASA,IAAM,QAAQ,CAAC,MAAuB,EAAE,WAAW,IAAI,IAAI,EAAE,MAAM,CAAC,IAAI;AAGjE,IAAM,4BAA4B;AAClC,IAAM,8BAA8B;AAyBpC,SAAS,WAAW,CAC1B,iBACA,OAA2D,CAAC,GACnD;AAAA,EACT,MAAM,QAAQ,KAAK,mBAAmB;AAAA,EACtC,MAAM,SAAS,KAAK,eAAe;AAAA,EACnC,OAAO,KAAK,OAAO,kBAAkB,SAAS,MAAM;AAAA;AAK9C,SAAS,gBAAgB,CAC/B,cACA,iBACS;AAAA,EACT,OAAO,MACN,iBAAiB,MAAM,YAAY,GAAG,MAAM,eAAe,GAAG,IAAI,CACnE;AAAA;AASM,SAAS,sBAAsB,CACrC,cACA,qBACA,WACqB;AAAA,EACrB,MAAM,QAAQ,IAAI,IACjB,UAAU,QAAQ,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,EAAE,MAAM,CAAC,CAC9D;AAAA,EACA,MAAM,OAAO,IAAI;AAAA,EACjB,IAAI,eAAe;AAAA,EACnB,IAAI,iBAAiB;AAAA,EACrB,WAAW,OAAO,qBAAqB;AAAA,IACtC,IAAI;AAAA,IACJ,IAAI;AAAA,MACH,SAAS,iBAAiB,cAAc,GAAG;AAAA,MAC1C,MAAM;AAAA,MACP;AAAA;AAAA,IAED,MAAM,SAAS,MAAM,IAAI,MAAM;AAAA,IAC/B,IAAI,WAAW,aAAa,CAAC,KAAK,IAAI,MAAM,GAAG;AAAA,MAC9C,KAAK,IAAI,MAAM;AAAA,MACf,gBAAgB;AAAA,MAChB,kBAAkB;AAAA,IACnB;AAAA,EACD;AAAA,EACA,MAAM,YAAY,KAAK,MAAO,UAAU,eAAe,IAAK,EAAE;AAAA,EAC9D,OAAO;AAAA,IACN;AAAA,IACA,aAAa,UAAU;AAAA,IACvB;AAAA,IACA,cAAc,gBAAgB;AAAA,IAC9B;AAAA,EACD;AAAA;",
8
+ "debugId": "1937481E7D2EF9AA64756E2164756E21",
9
+ "names": []
10
+ }
@@ -6,6 +6,8 @@ interface BlocksTable {
6
6
  parent_hash: string;
7
7
  burn_block_height: number;
8
8
  burn_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
9
+ /** Nakamoto StacksBlockId. Null on rows ingested before it was persisted. */
10
+ index_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
9
11
  timestamp: number;
10
12
  canonical: Generated<boolean>;
11
13
  created_at: Generated<Date>;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Nakamoto block-header parsing + consensus hashing for trustless verification.
3
+ *
4
+ * Every constant here is verified bit-exact against mainnet `stacks-node
5
+ * 3.4.0.0.3` (see docs/design/trustless-verification-proofs.md, Appendix). This
6
+ * Building block for transaction-inclusion / block-canonicity proofs: fetch a
7
+ * raw `/v3/blocks/{id}` body, parse the header, and recompute the block_hash,
8
+ * index_block_hash, and tx_merkle_root the chain itself commits to.
9
+ */
10
+ /** SHA-512/256 — the hash Stacks uses everywhere (NOT truncated SHA-512). */
11
+ declare function sha512_256(bytes: Uint8Array): Uint8Array;
12
+ interface NakamotoBlockHeader {
13
+ version: number;
14
+ chainLength: bigint;
15
+ burnSpent: bigint;
16
+ /** 20-byte consensus hash (hex). */
17
+ consensusHash: string;
18
+ /** 32-byte parent StacksBlockId (hex). */
19
+ parentBlockId: string;
20
+ /** 32-byte SHA512/256 merkle root over the block's txids (hex). */
21
+ txMerkleRoot: string;
22
+ /** 32-byte MARF root after applying this block (hex). */
23
+ stateIndexRoot: string;
24
+ timestamp: bigint;
25
+ /** 65-byte recoverable ECDSA miner signature (hex). */
26
+ minerSignature: string;
27
+ /** Per-signer recoverable ECDSA signatures, reward-set order (hex, 65B each). */
28
+ signerSignatures: string[];
29
+ /** Full serialized pox_treatment BitVec bytes (u16 bits ‖ u32 len ‖ data). */
30
+ poxTreatment: Uint8Array;
31
+ /**
32
+ * Exact bytes whose SHA512/256 IS the block_hash / signer_signature_hash:
33
+ * the header with the signer_signature vector omitted (header[0:206] ‖ pox).
34
+ */
35
+ signerSignatureHashPreimage: Uint8Array;
36
+ /** Offset at which the tx `Vec` begins (= total header byte length). */
37
+ headerByteLength: number;
38
+ }
39
+ /** Parse the Nakamoto block header from the raw `/v3/blocks` body. */
40
+ declare function parseNakamotoBlockHeader(raw: Uint8Array): NakamotoBlockHeader;
41
+ /**
42
+ * block_hash (== signer_signature_hash): SHA512/256 over the header with the
43
+ * signer_signature vector omitted. This is what each signer signs.
44
+ */
45
+ declare function nakamotoBlockHash(header: NakamotoBlockHeader): string;
46
+ /** index_block_hash (StacksBlockId) = SHA512/256(block_hash ‖ consensus_hash). */
47
+ declare function nakamotoBlockId(blockHashHex: string, consensusHashHex: string): string;
48
+ /** A Stacks txid = SHA512/256 of the transaction's consensus serialization. */
49
+ declare function stacksTxid(rawTx: Uint8Array): string;
50
+ /**
51
+ * tx_merkle_root over the block's txids (hex), reproducing the consensus rule:
52
+ * leaf = H(0x00 ‖ txid), node = H(0x01 ‖ left ‖ right), odd level duplicates the
53
+ * last node. Returns the root hex; throws on an empty tx list.
54
+ */
55
+ declare function txMerkleRoot(txidsHex: string[]): string;
56
+ /** One authentication-path step: the sibling hash and which side it's on. */
57
+ interface MerkleProofStep {
58
+ /** Side the SIBLING is on relative to the accumulator. */
59
+ position: "left" | "right";
60
+ /** Sibling node hash (hex). */
61
+ hash: string;
62
+ }
63
+ /**
64
+ * Build the tx-inclusion authentication path for the tx at `index` in a block,
65
+ * reproducing the consensus merkle tree (incl. duplicate-last-on-odd). The path
66
+ * lets a verifier recompute `tx_merkle_root` from just the target txid.
67
+ */
68
+ declare function txMerkleProof(txidsHex: string[], index: number): MerkleProofStep[];
69
+ /**
70
+ * Verify a tx-inclusion proof: fold the target `txid` (hex) up through `path`
71
+ * and check it equals `txMerkleRoot` (hex). The verifier recomputes the txid
72
+ * itself from the raw tx bytes, so nothing here is trusted.
73
+ */
74
+ declare function verifyTxMerkleProof(txidHex: string, path: MerkleProofStep[], txMerkleRootHex: string): boolean;
75
+ /**
76
+ * Fetch and parse a Nakamoto block from a stacks-node. `blockId` is the
77
+ * index_block_hash (with or without 0x). Returns the raw bytes + parsed header +
78
+ * the recomputed block_hash / index_block_hash so a caller can cross-check.
79
+ */
80
+ declare function fetchNakamotoBlock(opts: {
81
+ nodeUrl: string
82
+ blockId: string
83
+ fetchImpl?: typeof fetch
84
+ }): Promise<{
85
+ raw: Uint8Array
86
+ header: NakamotoBlockHeader
87
+ blockHash: string
88
+ indexBlockHash: string
89
+ }>;
90
+ export { verifyTxMerkleProof, txMerkleRoot, txMerkleProof, stacksTxid, sha512_256, parseNakamotoBlockHeader, nakamotoBlockId, nakamotoBlockHash, fetchNakamotoBlock, NakamotoBlockHeader, MerkleProofStep };
@@ -0,0 +1,177 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
17
+ // src/node/nakamoto.ts
18
+ import { createHash } from "node:crypto";
19
+ function sha512_256(bytes) {
20
+ return createHash("sha512-256").update(bytes).digest();
21
+ }
22
+ var toHex = (b) => Buffer.from(b).toString("hex");
23
+ var fromHex = (h) => Uint8Array.from(Buffer.from(h.startsWith("0x") ? h.slice(2) : h, "hex"));
24
+ var PREFIX_LEN = 206;
25
+ var CONSENSUS_HASH_OFF = 17;
26
+ var TX_MERKLE_ROOT_OFF = 69;
27
+ var STATE_INDEX_ROOT_OFF = 101;
28
+ var TIMESTAMP_OFF = 133;
29
+ var MINER_SIG_OFF = 141;
30
+ var SIGNER_VEC_OFF = 206;
31
+ var SIG_LEN = 65;
32
+ function u32(b, off) {
33
+ return new DataView(b.buffer, b.byteOffset, b.byteLength).getUint32(off);
34
+ }
35
+ function u64(b, off) {
36
+ return new DataView(b.buffer, b.byteOffset, b.byteLength).getBigUint64(off);
37
+ }
38
+ function parseNakamotoBlockHeader(raw) {
39
+ if (raw.length < PREFIX_LEN + 4) {
40
+ throw new Error("raw block too short for a Nakamoto header");
41
+ }
42
+ const signerCount = u32(raw, SIGNER_VEC_OFF);
43
+ const sigsStart = SIGNER_VEC_OFF + 4;
44
+ const signerSignatures = [];
45
+ for (let i = 0;i < signerCount; i++) {
46
+ const off = sigsStart + i * SIG_LEN;
47
+ signerSignatures.push(toHex(raw.subarray(off, off + SIG_LEN)));
48
+ }
49
+ const poxOff = sigsStart + signerCount * SIG_LEN;
50
+ const poxDataLen = u32(raw, poxOff + 2);
51
+ const poxEnd = poxOff + 6 + poxDataLen;
52
+ const poxTreatment = raw.subarray(poxOff, poxEnd);
53
+ const preimage = new Uint8Array(PREFIX_LEN + poxTreatment.length);
54
+ preimage.set(raw.subarray(0, PREFIX_LEN), 0);
55
+ preimage.set(poxTreatment, PREFIX_LEN);
56
+ return {
57
+ version: raw[0],
58
+ chainLength: u64(raw, 1),
59
+ burnSpent: u64(raw, 9),
60
+ consensusHash: toHex(raw.subarray(CONSENSUS_HASH_OFF, CONSENSUS_HASH_OFF + 20)),
61
+ parentBlockId: toHex(raw.subarray(37, 69)),
62
+ txMerkleRoot: toHex(raw.subarray(TX_MERKLE_ROOT_OFF, TX_MERKLE_ROOT_OFF + 32)),
63
+ stateIndexRoot: toHex(raw.subarray(STATE_INDEX_ROOT_OFF, STATE_INDEX_ROOT_OFF + 32)),
64
+ timestamp: u64(raw, TIMESTAMP_OFF),
65
+ minerSignature: toHex(raw.subarray(MINER_SIG_OFF, MINER_SIG_OFF + SIG_LEN)),
66
+ signerSignatures,
67
+ poxTreatment,
68
+ signerSignatureHashPreimage: preimage,
69
+ headerByteLength: poxEnd
70
+ };
71
+ }
72
+ function nakamotoBlockHash(header) {
73
+ return toHex(sha512_256(header.signerSignatureHashPreimage));
74
+ }
75
+ function nakamotoBlockId(blockHashHex, consensusHashHex) {
76
+ const a = fromHex(blockHashHex);
77
+ const b = fromHex(consensusHashHex);
78
+ const buf = new Uint8Array(a.length + b.length);
79
+ buf.set(a, 0);
80
+ buf.set(b, a.length);
81
+ return toHex(sha512_256(buf));
82
+ }
83
+ function stacksTxid(rawTx) {
84
+ return toHex(sha512_256(rawTx));
85
+ }
86
+ var LEAF_TAG = 0;
87
+ var NODE_TAG = 1;
88
+ function tagged(tag, ...parts) {
89
+ const len = parts.reduce((n, p) => n + p.length, 1);
90
+ const buf = new Uint8Array(len);
91
+ buf[0] = tag;
92
+ let o = 1;
93
+ for (const p of parts) {
94
+ buf.set(p, o);
95
+ o += p.length;
96
+ }
97
+ return sha512_256(buf);
98
+ }
99
+ function txMerkleRoot(txidsHex) {
100
+ if (txidsHex.length === 0)
101
+ throw new Error("no transactions");
102
+ let level = txidsHex.map((t) => tagged(LEAF_TAG, fromHex(t)));
103
+ while (level.length > 1) {
104
+ if (level.length % 2 === 1)
105
+ level.push(level[level.length - 1]);
106
+ const next = [];
107
+ for (let i = 0;i < level.length; i += 2) {
108
+ next.push(tagged(NODE_TAG, level[i], level[i + 1]));
109
+ }
110
+ level = next;
111
+ }
112
+ return toHex(level[0]);
113
+ }
114
+ function txMerkleProof(txidsHex, index) {
115
+ if (index < 0 || index >= txidsHex.length) {
116
+ throw new Error("index out of range");
117
+ }
118
+ let level = txidsHex.map((t) => tagged(LEAF_TAG, fromHex(t)));
119
+ let idx = index;
120
+ const path = [];
121
+ while (level.length > 1) {
122
+ if (level.length % 2 === 1)
123
+ level.push(level[level.length - 1]);
124
+ const siblingIdx = idx % 2 === 0 ? idx + 1 : idx - 1;
125
+ path.push({
126
+ position: idx % 2 === 0 ? "right" : "left",
127
+ hash: toHex(level[siblingIdx])
128
+ });
129
+ const next = [];
130
+ for (let i = 0;i < level.length; i += 2) {
131
+ next.push(tagged(NODE_TAG, level[i], level[i + 1]));
132
+ }
133
+ level = next;
134
+ idx = Math.floor(idx / 2);
135
+ }
136
+ return path;
137
+ }
138
+ function verifyTxMerkleProof(txidHex, path, txMerkleRootHex) {
139
+ let acc = tagged(LEAF_TAG, fromHex(txidHex));
140
+ for (const step of path) {
141
+ const sib = fromHex(step.hash);
142
+ acc = step.position === "right" ? tagged(NODE_TAG, acc, sib) : tagged(NODE_TAG, sib, acc);
143
+ }
144
+ const root = txMerkleRootHex.startsWith("0x") ? txMerkleRootHex.slice(2) : txMerkleRootHex;
145
+ return toHex(acc) === root;
146
+ }
147
+ async function fetchNakamotoBlock(opts) {
148
+ const id = opts.blockId.startsWith("0x") ? opts.blockId.slice(2) : opts.blockId;
149
+ const f = opts.fetchImpl ?? fetch;
150
+ const res = await f(`${opts.nodeUrl.replace(/\/+$/, "")}/v3/blocks/${id}`);
151
+ if (!res.ok) {
152
+ throw new Error(`/v3/blocks/${id} returned ${res.status}`);
153
+ }
154
+ const raw = new Uint8Array(await res.arrayBuffer());
155
+ const header = parseNakamotoBlockHeader(raw);
156
+ const blockHash = nakamotoBlockHash(header);
157
+ return {
158
+ raw,
159
+ header,
160
+ blockHash,
161
+ indexBlockHash: nakamotoBlockId(blockHash, header.consensusHash)
162
+ };
163
+ }
164
+ export {
165
+ verifyTxMerkleProof,
166
+ txMerkleRoot,
167
+ txMerkleProof,
168
+ stacksTxid,
169
+ sha512_256,
170
+ parseNakamotoBlockHeader,
171
+ nakamotoBlockId,
172
+ nakamotoBlockHash,
173
+ fetchNakamotoBlock
174
+ };
175
+
176
+ //# debugId=7DE7C366A0D5645764756E2164756E21
177
+ //# sourceMappingURL=nakamoto.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/node/nakamoto.ts"],
4
+ "sourcesContent": [
5
+ "import { createHash } from \"node:crypto\";\n\n/**\n * Nakamoto block-header parsing + consensus hashing for trustless verification.\n *\n * Every constant here is verified bit-exact against mainnet `stacks-node\n * 3.4.0.0.3` (see docs/design/trustless-verification-proofs.md, Appendix). This\n * Building block for transaction-inclusion / block-canonicity proofs: fetch a\n * raw `/v3/blocks/{id}` body, parse the header, and recompute the block_hash,\n * index_block_hash, and tx_merkle_root the chain itself commits to.\n */\n\n/** SHA-512/256 — the hash Stacks uses everywhere (NOT truncated SHA-512). */\nexport function sha512_256(bytes: Uint8Array): Uint8Array {\n\treturn createHash(\"sha512-256\").update(bytes).digest();\n}\n\nconst toHex = (b: Uint8Array): string => Buffer.from(b).toString(\"hex\");\nconst fromHex = (h: string): Uint8Array =>\n\tUint8Array.from(Buffer.from(h.startsWith(\"0x\") ? h.slice(2) : h, \"hex\"));\n\n// Fixed-size header prefix: version..miner_signature, before signer_signature.\nconst PREFIX_LEN = 206;\nconst CONSENSUS_HASH_OFF = 17;\nconst TX_MERKLE_ROOT_OFF = 69;\nconst STATE_INDEX_ROOT_OFF = 101;\nconst TIMESTAMP_OFF = 133;\nconst MINER_SIG_OFF = 141;\nconst SIGNER_VEC_OFF = 206; // u32 count, then 65 bytes per signer\nconst SIG_LEN = 65;\n\nexport interface NakamotoBlockHeader {\n\tversion: number;\n\tchainLength: bigint;\n\tburnSpent: bigint;\n\t/** 20-byte consensus hash (hex). */\n\tconsensusHash: string;\n\t/** 32-byte parent StacksBlockId (hex). */\n\tparentBlockId: string;\n\t/** 32-byte SHA512/256 merkle root over the block's txids (hex). */\n\ttxMerkleRoot: string;\n\t/** 32-byte MARF root after applying this block (hex). */\n\tstateIndexRoot: string;\n\ttimestamp: bigint;\n\t/** 65-byte recoverable ECDSA miner signature (hex). */\n\tminerSignature: string;\n\t/** Per-signer recoverable ECDSA signatures, reward-set order (hex, 65B each). */\n\tsignerSignatures: string[];\n\t/** Full serialized pox_treatment BitVec bytes (u16 bits ‖ u32 len ‖ data). */\n\tpoxTreatment: Uint8Array;\n\t/**\n\t * Exact bytes whose SHA512/256 IS the block_hash / signer_signature_hash:\n\t * the header with the signer_signature vector omitted (header[0:206] ‖ pox).\n\t */\n\tsignerSignatureHashPreimage: Uint8Array;\n\t/** Offset at which the tx `Vec` begins (= total header byte length). */\n\theaderByteLength: number;\n}\n\nfunction u32(b: Uint8Array, off: number): number {\n\treturn new DataView(b.buffer, b.byteOffset, b.byteLength).getUint32(off);\n}\nfunction u64(b: Uint8Array, off: number): bigint {\n\treturn new DataView(b.buffer, b.byteOffset, b.byteLength).getBigUint64(off);\n}\n\n/** Parse the Nakamoto block header from the raw `/v3/blocks` body. */\nexport function parseNakamotoBlockHeader(raw: Uint8Array): NakamotoBlockHeader {\n\tif (raw.length < PREFIX_LEN + 4) {\n\t\tthrow new Error(\"raw block too short for a Nakamoto header\");\n\t}\n\tconst signerCount = u32(raw, SIGNER_VEC_OFF);\n\tconst sigsStart = SIGNER_VEC_OFF + 4;\n\tconst signerSignatures: string[] = [];\n\tfor (let i = 0; i < signerCount; i++) {\n\t\tconst off = sigsStart + i * SIG_LEN;\n\t\tsignerSignatures.push(toHex(raw.subarray(off, off + SIG_LEN)));\n\t}\n\t// pox_treatment: u16 num_bits ‖ u32 data_len ‖ data[data_len].\n\tconst poxOff = sigsStart + signerCount * SIG_LEN;\n\tconst poxDataLen = u32(raw, poxOff + 2);\n\tconst poxEnd = poxOff + 6 + poxDataLen;\n\tconst poxTreatment = raw.subarray(poxOff, poxEnd);\n\n\t// block_hash preimage = header minus signer_signature = prefix[0:206] ‖ pox.\n\tconst preimage = new Uint8Array(PREFIX_LEN + poxTreatment.length);\n\tpreimage.set(raw.subarray(0, PREFIX_LEN), 0);\n\tpreimage.set(poxTreatment, PREFIX_LEN);\n\n\treturn {\n\t\tversion: raw[0],\n\t\tchainLength: u64(raw, 1),\n\t\tburnSpent: u64(raw, 9),\n\t\tconsensusHash: toHex(\n\t\t\traw.subarray(CONSENSUS_HASH_OFF, CONSENSUS_HASH_OFF + 20),\n\t\t),\n\t\tparentBlockId: toHex(raw.subarray(37, 69)),\n\t\ttxMerkleRoot: toHex(\n\t\t\traw.subarray(TX_MERKLE_ROOT_OFF, TX_MERKLE_ROOT_OFF + 32),\n\t\t),\n\t\tstateIndexRoot: toHex(\n\t\t\traw.subarray(STATE_INDEX_ROOT_OFF, STATE_INDEX_ROOT_OFF + 32),\n\t\t),\n\t\ttimestamp: u64(raw, TIMESTAMP_OFF),\n\t\tminerSignature: toHex(raw.subarray(MINER_SIG_OFF, MINER_SIG_OFF + SIG_LEN)),\n\t\tsignerSignatures,\n\t\tpoxTreatment,\n\t\tsignerSignatureHashPreimage: preimage,\n\t\theaderByteLength: poxEnd,\n\t};\n}\n\n/**\n * block_hash (== signer_signature_hash): SHA512/256 over the header with the\n * signer_signature vector omitted. This is what each signer signs.\n */\nexport function nakamotoBlockHash(header: NakamotoBlockHeader): string {\n\treturn toHex(sha512_256(header.signerSignatureHashPreimage));\n}\n\n/** index_block_hash (StacksBlockId) = SHA512/256(block_hash ‖ consensus_hash). */\nexport function nakamotoBlockId(\n\tblockHashHex: string,\n\tconsensusHashHex: string,\n): string {\n\tconst a = fromHex(blockHashHex);\n\tconst b = fromHex(consensusHashHex);\n\tconst buf = new Uint8Array(a.length + b.length);\n\tbuf.set(a, 0);\n\tbuf.set(b, a.length);\n\treturn toHex(sha512_256(buf));\n}\n\n/** A Stacks txid = SHA512/256 of the transaction's consensus serialization. */\nexport function stacksTxid(rawTx: Uint8Array): string {\n\treturn toHex(sha512_256(rawTx));\n}\n\nconst LEAF_TAG = 0x00;\nconst NODE_TAG = 0x01;\n\nfunction tagged(tag: number, ...parts: Uint8Array[]): Uint8Array {\n\tconst len = parts.reduce((n, p) => n + p.length, 1);\n\tconst buf = new Uint8Array(len);\n\tbuf[0] = tag;\n\tlet o = 1;\n\tfor (const p of parts) {\n\t\tbuf.set(p, o);\n\t\to += p.length;\n\t}\n\treturn sha512_256(buf);\n}\n\n/**\n * tx_merkle_root over the block's txids (hex), reproducing the consensus rule:\n * leaf = H(0x00 ‖ txid), node = H(0x01 ‖ left ‖ right), odd level duplicates the\n * last node. Returns the root hex; throws on an empty tx list.\n */\nexport function txMerkleRoot(txidsHex: string[]): string {\n\tif (txidsHex.length === 0) throw new Error(\"no transactions\");\n\tlet level = txidsHex.map((t) => tagged(LEAF_TAG, fromHex(t)));\n\twhile (level.length > 1) {\n\t\tif (level.length % 2 === 1) level.push(level[level.length - 1]);\n\t\tconst next: Uint8Array[] = [];\n\t\tfor (let i = 0; i < level.length; i += 2) {\n\t\t\tnext.push(tagged(NODE_TAG, level[i], level[i + 1]));\n\t\t}\n\t\tlevel = next;\n\t}\n\treturn toHex(level[0]);\n}\n\n/** One authentication-path step: the sibling hash and which side it's on. */\nexport interface MerkleProofStep {\n\t/** Side the SIBLING is on relative to the accumulator. */\n\tposition: \"left\" | \"right\";\n\t/** Sibling node hash (hex). */\n\thash: string;\n}\n\n/**\n * Build the tx-inclusion authentication path for the tx at `index` in a block,\n * reproducing the consensus merkle tree (incl. duplicate-last-on-odd). The path\n * lets a verifier recompute `tx_merkle_root` from just the target txid.\n */\nexport function txMerkleProof(\n\ttxidsHex: string[],\n\tindex: number,\n): MerkleProofStep[] {\n\tif (index < 0 || index >= txidsHex.length) {\n\t\tthrow new Error(\"index out of range\");\n\t}\n\tlet level = txidsHex.map((t) => tagged(LEAF_TAG, fromHex(t)));\n\tlet idx = index;\n\tconst path: MerkleProofStep[] = [];\n\twhile (level.length > 1) {\n\t\tif (level.length % 2 === 1) level.push(level[level.length - 1]);\n\t\tconst siblingIdx = idx % 2 === 0 ? idx + 1 : idx - 1;\n\t\tpath.push({\n\t\t\tposition: idx % 2 === 0 ? \"right\" : \"left\",\n\t\t\thash: toHex(level[siblingIdx]),\n\t\t});\n\t\tconst next: Uint8Array[] = [];\n\t\tfor (let i = 0; i < level.length; i += 2) {\n\t\t\tnext.push(tagged(NODE_TAG, level[i], level[i + 1]));\n\t\t}\n\t\tlevel = next;\n\t\tidx = Math.floor(idx / 2);\n\t}\n\treturn path;\n}\n\n/**\n * Verify a tx-inclusion proof: fold the target `txid` (hex) up through `path`\n * and check it equals `txMerkleRoot` (hex). The verifier recomputes the txid\n * itself from the raw tx bytes, so nothing here is trusted.\n */\nexport function verifyTxMerkleProof(\n\ttxidHex: string,\n\tpath: MerkleProofStep[],\n\ttxMerkleRootHex: string,\n): boolean {\n\tlet acc = tagged(LEAF_TAG, fromHex(txidHex));\n\tfor (const step of path) {\n\t\tconst sib = fromHex(step.hash);\n\t\tacc =\n\t\t\tstep.position === \"right\"\n\t\t\t\t? tagged(NODE_TAG, acc, sib)\n\t\t\t\t: tagged(NODE_TAG, sib, acc);\n\t}\n\tconst root = txMerkleRootHex.startsWith(\"0x\")\n\t\t? txMerkleRootHex.slice(2)\n\t\t: txMerkleRootHex;\n\treturn toHex(acc) === root;\n}\n\n/**\n * Fetch and parse a Nakamoto block from a stacks-node. `blockId` is the\n * index_block_hash (with or without 0x). Returns the raw bytes + parsed header +\n * the recomputed block_hash / index_block_hash so a caller can cross-check.\n */\nexport async function fetchNakamotoBlock(opts: {\n\tnodeUrl: string;\n\tblockId: string;\n\tfetchImpl?: typeof fetch;\n}): Promise<{\n\traw: Uint8Array;\n\theader: NakamotoBlockHeader;\n\tblockHash: string;\n\tindexBlockHash: string;\n}> {\n\tconst id = opts.blockId.startsWith(\"0x\")\n\t\t? opts.blockId.slice(2)\n\t\t: opts.blockId;\n\tconst f = opts.fetchImpl ?? fetch;\n\tconst res = await f(`${opts.nodeUrl.replace(/\\/+$/, \"\")}/v3/blocks/${id}`);\n\tif (!res.ok) {\n\t\tthrow new Error(`/v3/blocks/${id} returned ${res.status}`);\n\t}\n\tconst raw = new Uint8Array(await res.arrayBuffer());\n\tconst header = parseNakamotoBlockHeader(raw);\n\tconst blockHash = nakamotoBlockHash(header);\n\treturn {\n\t\traw,\n\t\theader,\n\t\tblockHash,\n\t\tindexBlockHash: nakamotoBlockId(blockHash, header.consensusHash),\n\t};\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "7DE7C366A0D5645764756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * ed25519 signing for the Streams cold-bulk parquet manifest.
3
+ *
4
+ * The live Streams lane is ed25519-signed; the bulk manifest was plain JSON with
5
+ * only per-file sha256, so a tampered manifest+file pair verified cleanly. This
6
+ * signs the manifest itself with the same platform key, so a consumer can trust
7
+ * the file hashes only after the manifest signature checks out — making the two
8
+ * availability lanes symmetric.
9
+ *
10
+ * The signed bytes are the manifest's canonical JSON with the signature envelope
11
+ * fields removed, so signer and verifier agree without a separate canonical
12
+ * form: `signature`/`key_id` are appended last, so stripping them and
13
+ * re-serializing reproduces the exact pre-sign bytes.
14
+ */
15
+ type SignatureEnvelope = {
16
+ signature?: string
17
+ key_id?: string
18
+ };
19
+ /** The exact bytes a manifest signature covers: the manifest JSON minus the
20
+ * signature envelope fields. */
21
+ declare function canonicalStreamsBulkManifestPayload(manifest: Record<string, unknown> & SignatureEnvelope): string;
22
+ /**
23
+ * Attach an ed25519 `signature` + `key_id` over the manifest's canonical bytes.
24
+ * Returns a new manifest; re-signing one that already carries a signature signs
25
+ * over its un-enveloped form (idempotent shape).
26
+ */
27
+ declare function signStreamsBulkManifest<T extends Record<string, unknown> & SignatureEnvelope>(manifest: T, privateKeyPem: string): T & {
28
+ signature: string
29
+ key_id: string
30
+ };
31
+ /** Verify a manifest's ed25519 signature against the published public key. */
32
+ declare function verifyStreamsBulkManifestSignature(manifest: Record<string, unknown> & SignatureEnvelope, publicKeyPem: string): boolean;
33
+ export { verifyStreamsBulkManifestSignature, signStreamsBulkManifest, canonicalStreamsBulkManifestPayload };
@@ -0,0 +1,104 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
17
+ // src/crypto/ed25519.ts
18
+ var exports_ed25519 = {};
19
+ __export(exports_ed25519, {
20
+ verifyEd25519: () => verifyEd25519,
21
+ signEd25519: () => signEd25519,
22
+ publicKeyPemFromPrivate: () => publicKeyPemFromPrivate,
23
+ loadEd25519PublicKey: () => loadEd25519PublicKey,
24
+ loadEd25519PrivateKey: () => loadEd25519PrivateKey,
25
+ generateEd25519KeyPair: () => generateEd25519KeyPair,
26
+ ed25519KeyId: () => ed25519KeyId
27
+ });
28
+ import {
29
+ createHash,
30
+ createPrivateKey,
31
+ createPublicKey,
32
+ generateKeyPairSync,
33
+ sign as nodeSign,
34
+ verify as nodeVerify
35
+ } from "node:crypto";
36
+ function generateEd25519KeyPair() {
37
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
38
+ return {
39
+ privateKeyPem: privateKey.export({ format: "pem", type: "pkcs8" }).toString(),
40
+ publicKeyPem: publicKey.export({ format: "pem", type: "spki" }).toString()
41
+ };
42
+ }
43
+ function loadEd25519PrivateKey(pem) {
44
+ return createPrivateKey(pem);
45
+ }
46
+ function loadEd25519PublicKey(pem) {
47
+ return createPublicKey(pem);
48
+ }
49
+ function publicKeyPemFromPrivate(privateKeyPem) {
50
+ return createPublicKey(createPrivateKey(privateKeyPem)).export({ format: "pem", type: "spki" }).toString();
51
+ }
52
+ function ed25519KeyId(publicKeyPem) {
53
+ const der = createPublicKey(publicKeyPem).export({
54
+ format: "der",
55
+ type: "spki"
56
+ });
57
+ return createHash("sha256").update(der).digest("base64url").slice(0, 16);
58
+ }
59
+ function signEd25519(payload, privateKey) {
60
+ return nodeSign(null, Buffer.from(payload, "utf8"), privateKey).toString("base64");
61
+ }
62
+ function verifyEd25519(payload, signatureBase64, publicKey) {
63
+ try {
64
+ return nodeVerify(null, Buffer.from(payload, "utf8"), publicKey, Buffer.from(signatureBase64, "base64"));
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ // src/streams-bulk-manifest.ts
71
+ function canonicalStreamsBulkManifestPayload(manifest) {
72
+ const { signature: _signature, key_id: _keyId, ...rest } = manifest;
73
+ return JSON.stringify(rest);
74
+ }
75
+ function normalizePem(pem) {
76
+ return pem.includes("\\n") ? pem.replace(/\\n/g, `
77
+ `) : pem;
78
+ }
79
+ function signStreamsBulkManifest(manifest, privateKeyPem) {
80
+ const pem = normalizePem(privateKeyPem);
81
+ const privateKey = loadEd25519PrivateKey(pem);
82
+ const keyId = ed25519KeyId(publicKeyPemFromPrivate(pem));
83
+ const { signature: _signature, key_id: _keyId, ...base } = manifest;
84
+ const payload = JSON.stringify(base);
85
+ return {
86
+ ...base,
87
+ signature: signEd25519(payload, privateKey),
88
+ key_id: keyId
89
+ };
90
+ }
91
+ function verifyStreamsBulkManifestSignature(manifest, publicKeyPem) {
92
+ if (!manifest.signature)
93
+ return false;
94
+ const payload = canonicalStreamsBulkManifestPayload(manifest);
95
+ return verifyEd25519(payload, manifest.signature, loadEd25519PublicKey(publicKeyPem));
96
+ }
97
+ export {
98
+ verifyStreamsBulkManifestSignature,
99
+ signStreamsBulkManifest,
100
+ canonicalStreamsBulkManifestPayload
101
+ };
102
+
103
+ //# debugId=CAF4339AB8D4DD3E64756E2164756E21
104
+ //# sourceMappingURL=streams-bulk-manifest.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/crypto/ed25519.ts", "../src/streams-bulk-manifest.ts"],
4
+ "sourcesContent": [
5
+ "import {\n\ttype KeyObject,\n\tcreateHash,\n\tcreatePrivateKey,\n\tcreatePublicKey,\n\tgenerateKeyPairSync,\n\tsign as nodeSign,\n\tverify as nodeVerify,\n} from \"node:crypto\";\n\n/**\n * Asymmetric ed25519 signing for Streams response proofs.\n *\n * Asymmetric (not HMAC) so the proof is real: only the server holds the private\n * key, and any consumer verifies with the published public key — no shared\n * secret to leak. ed25519 uses node's `sign`/`verify` with a `null` algorithm.\n * Keys are PEM (PKCS8 private / SPKI public) for env transport; load once and\n * reuse the KeyObject on hot paths.\n */\n\nexport function generateEd25519KeyPair(): {\n\tprivateKeyPem: string;\n\tpublicKeyPem: string;\n} {\n\tconst { privateKey, publicKey } = generateKeyPairSync(\"ed25519\");\n\treturn {\n\t\tprivateKeyPem: privateKey\n\t\t\t.export({ format: \"pem\", type: \"pkcs8\" })\n\t\t\t.toString(),\n\t\tpublicKeyPem: publicKey.export({ format: \"pem\", type: \"spki\" }).toString(),\n\t};\n}\n\nexport function loadEd25519PrivateKey(pem: string): KeyObject {\n\treturn createPrivateKey(pem);\n}\n\nexport function loadEd25519PublicKey(pem: string): KeyObject {\n\treturn createPublicKey(pem);\n}\n\nexport function publicKeyPemFromPrivate(privateKeyPem: string): string {\n\treturn createPublicKey(createPrivateKey(privateKeyPem))\n\t\t.export({ format: \"pem\", type: \"spki\" })\n\t\t.toString();\n}\n\n/** Stable short id for a public key (rotation hint via X-Signature-KeyId). */\nexport function ed25519KeyId(publicKeyPem: string): string {\n\tconst der = createPublicKey(publicKeyPem).export({\n\t\tformat: \"der\",\n\t\ttype: \"spki\",\n\t});\n\treturn createHash(\"sha256\").update(der).digest(\"base64url\").slice(0, 16);\n}\n\nexport function signEd25519(payload: string, privateKey: KeyObject): string {\n\treturn nodeSign(null, Buffer.from(payload, \"utf8\"), privateKey).toString(\n\t\t\"base64\",\n\t);\n}\n\nexport function verifyEd25519(\n\tpayload: string,\n\tsignatureBase64: string,\n\tpublicKey: KeyObject,\n): boolean {\n\ttry {\n\t\treturn nodeVerify(\n\t\t\tnull,\n\t\t\tBuffer.from(payload, \"utf8\"),\n\t\t\tpublicKey,\n\t\t\tBuffer.from(signatureBase64, \"base64\"),\n\t\t);\n\t} catch {\n\t\treturn false;\n\t}\n}\n",
6
+ "import {\n\ted25519KeyId,\n\tloadEd25519PrivateKey,\n\tloadEd25519PublicKey,\n\tpublicKeyPemFromPrivate,\n\tsignEd25519,\n\tverifyEd25519,\n} from \"./crypto/ed25519.ts\";\n\n/**\n * ed25519 signing for the Streams cold-bulk parquet manifest.\n *\n * The live Streams lane is ed25519-signed; the bulk manifest was plain JSON with\n * only per-file sha256, so a tampered manifest+file pair verified cleanly. This\n * signs the manifest itself with the same platform key, so a consumer can trust\n * the file hashes only after the manifest signature checks out — making the two\n * availability lanes symmetric.\n *\n * The signed bytes are the manifest's canonical JSON with the signature envelope\n * fields removed, so signer and verifier agree without a separate canonical\n * form: `signature`/`key_id` are appended last, so stripping them and\n * re-serializing reproduces the exact pre-sign bytes.\n */\ntype SignatureEnvelope = { signature?: string; key_id?: string };\n\n/** The exact bytes a manifest signature covers: the manifest JSON minus the\n * signature envelope fields. */\nexport function canonicalStreamsBulkManifestPayload(\n\tmanifest: Record<string, unknown> & SignatureEnvelope,\n): string {\n\tconst { signature: _signature, key_id: _keyId, ...rest } = manifest;\n\treturn JSON.stringify(rest);\n}\n\nfunction normalizePem(pem: string): string {\n\t// Env transport often escapes newlines; restore real PEM line breaks.\n\treturn pem.includes(\"\\\\n\") ? pem.replace(/\\\\n/g, \"\\n\") : pem;\n}\n\n/**\n * Attach an ed25519 `signature` + `key_id` over the manifest's canonical bytes.\n * Returns a new manifest; re-signing one that already carries a signature signs\n * over its un-enveloped form (idempotent shape).\n */\nexport function signStreamsBulkManifest<\n\tT extends Record<string, unknown> & SignatureEnvelope,\n>(\n\tmanifest: T,\n\tprivateKeyPem: string,\n): T & { signature: string; key_id: string } {\n\tconst pem = normalizePem(privateKeyPem);\n\tconst privateKey = loadEd25519PrivateKey(pem);\n\tconst keyId = ed25519KeyId(publicKeyPemFromPrivate(pem));\n\tconst { signature: _signature, key_id: _keyId, ...base } = manifest;\n\tconst payload = JSON.stringify(base);\n\treturn {\n\t\t...(base as T),\n\t\tsignature: signEd25519(payload, privateKey),\n\t\tkey_id: keyId,\n\t};\n}\n\n/** Verify a manifest's ed25519 signature against the published public key. */\nexport function verifyStreamsBulkManifestSignature(\n\tmanifest: Record<string, unknown> & SignatureEnvelope,\n\tpublicKeyPem: string,\n): boolean {\n\tif (!manifest.signature) return false;\n\tconst payload = canonicalStreamsBulkManifestPayload(manifest);\n\treturn verifyEd25519(\n\t\tpayload,\n\t\tmanifest.signature,\n\t\tloadEd25519PublicKey(publicKeyPem),\n\t);\n}\n"
7
+ ],
8
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAMC;AAAA,YACA;AAAA;AAaM,SAAS,sBAAsB,GAGpC;AAAA,EACD,QAAQ,YAAY,cAAc,oBAAoB,SAAS;AAAA,EAC/D,OAAO;AAAA,IACN,eAAe,WACb,OAAO,EAAE,QAAQ,OAAO,MAAM,QAAQ,CAAC,EACvC,SAAS;AAAA,IACX,cAAc,UAAU,OAAO,EAAE,QAAQ,OAAO,MAAM,OAAO,CAAC,EAAE,SAAS;AAAA,EAC1E;AAAA;AAGM,SAAS,qBAAqB,CAAC,KAAwB;AAAA,EAC7D,OAAO,iBAAiB,GAAG;AAAA;AAGrB,SAAS,oBAAoB,CAAC,KAAwB;AAAA,EAC5D,OAAO,gBAAgB,GAAG;AAAA;AAGpB,SAAS,uBAAuB,CAAC,eAA+B;AAAA,EACtE,OAAO,gBAAgB,iBAAiB,aAAa,CAAC,EACpD,OAAO,EAAE,QAAQ,OAAO,MAAM,OAAO,CAAC,EACtC,SAAS;AAAA;AAIL,SAAS,YAAY,CAAC,cAA8B;AAAA,EAC1D,MAAM,MAAM,gBAAgB,YAAY,EAAE,OAAO;AAAA,IAChD,QAAQ;AAAA,IACR,MAAM;AAAA,EACP,CAAC;AAAA,EACD,OAAO,WAAW,QAAQ,EAAE,OAAO,GAAG,EAAE,OAAO,WAAW,EAAE,MAAM,GAAG,EAAE;AAAA;AAGjE,SAAS,WAAW,CAAC,SAAiB,YAA+B;AAAA,EAC3E,OAAO,SAAS,MAAM,OAAO,KAAK,SAAS,MAAM,GAAG,UAAU,EAAE,SAC/D,QACD;AAAA;AAGM,SAAS,aAAa,CAC5B,SACA,iBACA,WACU;AAAA,EACV,IAAI;AAAA,IACH,OAAO,WACN,MACA,OAAO,KAAK,SAAS,MAAM,GAC3B,WACA,OAAO,KAAK,iBAAiB,QAAQ,CACtC;AAAA,IACC,MAAM;AAAA,IACP,OAAO;AAAA;AAAA;;;AChDF,SAAS,mCAAmC,CAClD,UACS;AAAA,EACT,QAAQ,WAAW,YAAY,QAAQ,WAAW,SAAS;AAAA,EAC3D,OAAO,KAAK,UAAU,IAAI;AAAA;AAG3B,SAAS,YAAY,CAAC,KAAqB;AAAA,EAE1C,OAAO,IAAI,SAAS,KAAK,IAAI,IAAI,QAAQ,QAAQ;AAAA,CAAI,IAAI;AAAA;AAQnD,SAAS,uBAEf,CACA,UACA,eAC4C;AAAA,EAC5C,MAAM,MAAM,aAAa,aAAa;AAAA,EACtC,MAAM,aAAa,sBAAsB,GAAG;AAAA,EAC5C,MAAM,QAAQ,aAAa,wBAAwB,GAAG,CAAC;AAAA,EACvD,QAAQ,WAAW,YAAY,QAAQ,WAAW,SAAS;AAAA,EAC3D,MAAM,UAAU,KAAK,UAAU,IAAI;AAAA,EACnC,OAAO;AAAA,OACF;AAAA,IACJ,WAAW,YAAY,SAAS,UAAU;AAAA,IAC1C,QAAQ;AAAA,EACT;AAAA;AAIM,SAAS,kCAAkC,CACjD,UACA,cACU;AAAA,EACV,IAAI,CAAC,SAAS;AAAA,IAAW,OAAO;AAAA,EAChC,MAAM,UAAU,oCAAoC,QAAQ;AAAA,EAC5D,OAAO,cACN,SACA,SAAS,WACT,qBAAqB,YAAY,CAClC;AAAA;",
9
+ "debugId": "CAF4339AB8D4DD3E64756E2164756E21",
10
+ "names": []
11
+ }
@@ -5,6 +5,8 @@ interface BlocksTable {
5
5
  parent_hash: string;
6
6
  burn_block_height: number;
7
7
  burn_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
8
+ /** Nakamoto StacksBlockId. Null on rows ingested before it was persisted. */
9
+ index_block_hash: ColumnType<string | null, string | null | undefined, string | null>;
8
10
  timestamp: number;
9
11
  canonical: Generated<boolean>;
10
12
  created_at: Generated<Date>;
@@ -0,0 +1,25 @@
1
+ import { type Kysely, sql } from "kysely";
2
+ import { onChainPlane } from "../src/db/migration-role.ts";
3
+
4
+ // Persist the Nakamoto `index_block_hash` (StacksBlockId) on `blocks`. It
5
+ // already arrives in the node's /new_block payload but was dropped at insert;
6
+ // keeping it lets the tx-inclusion proof endpoint resolve a block's signed
7
+ // header from /v3/blocks without an extra node round-trip per request. Nullable:
8
+ // historical rows backfill lazily / on demand. `blocks` is a chain-plane table,
9
+ // so the DDL no-ops on the control DB under the source/target split.
10
+ export async function up(db: Kysely<unknown>): Promise<void> {
11
+ await onChainPlane(async () => {
12
+ await sql`SET lock_timeout = '30s'`.execute(db);
13
+ await sql`ALTER TABLE blocks ADD COLUMN IF NOT EXISTS index_block_hash TEXT`.execute(
14
+ db,
15
+ );
16
+ });
17
+ }
18
+
19
+ export async function down(db: Kysely<unknown>): Promise<void> {
20
+ await onChainPlane(async () => {
21
+ await sql`ALTER TABLE blocks DROP COLUMN IF EXISTS index_block_hash`.execute(
22
+ db,
23
+ );
24
+ });
25
+ }