@freedomofpress/cometbft 0.1.2 → 0.1.3

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.
@@ -1,36 +1,68 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { importCommit } from "../commit";
3
- import { Uint8ArrayToBase64 } from "../encoding";
3
+ import { Uint8ArrayToBase64, Uint8ArrayToHex } from "../encoding";
4
4
  import { verifyCommit } from "../lightclient";
5
5
  import { importValidators } from "../validators";
6
6
  import blockFixture from "./fixtures/webcat.json";
7
7
  function clone(x) {
8
8
  return JSON.parse(JSON.stringify(x));
9
9
  }
10
+ function flipLastHexNibble(hex) {
11
+ const last = hex.at(-1);
12
+ if (!last)
13
+ throw new Error("Cannot mutate an empty hex string");
14
+ const replacement = last.toLowerCase() === "0" ? "1" : "0";
15
+ return `${hex.slice(0, -1)}${replacement}`;
16
+ }
10
17
  describe("lightclient.verifyCommit", () => {
11
- it("rejects this fixture when header hash does not match commit block_id.hash", async () => {
18
+ it("verifies a valid commit against the validator set", async () => {
12
19
  const validators = blockFixture.validator_set;
13
20
  const commit = blockFixture;
14
21
  const { proto: vset, cryptoIndex } = await importValidators(validators);
15
22
  const sh = importCommit(commit);
16
- await expect(verifyCommit(sh, vset, cryptoIndex)).rejects.toThrow(/header hash does not match commit blockid hash/i);
23
+ const out = await verifyCommit(sh, vset, cryptoIndex);
24
+ expect(out.quorum).toBe(true);
25
+ expect(out.ok).toBe(true);
26
+ expect(out.signedPower > 0n).toBe(true);
27
+ expect(out.signedPower <= out.totalPower).toBe(true);
28
+ expect(out.headerTime).toBeDefined();
29
+ expect(out.appHash instanceof Uint8Array).toBe(true);
30
+ expect(out.blockIdHash instanceof Uint8Array).toBe(true);
31
+ expect(out.unknownValidators.length).toBe(0);
32
+ expect(out.invalidSignatures.length).toBe(0);
33
+ expect(out.countedSignatures).toBeGreaterThan(0);
17
34
  });
18
- it("throws when commit block_id.hash does not match header hash", async () => {
35
+ it("flags invalid signatures", async () => {
19
36
  const validators = blockFixture.validator_set;
20
37
  const commit = clone(blockFixture);
38
+ // Flip one byte of the BlockID hash to keep the signature well-formed but
39
+ // cryptographically invalid for the mutated sign-bytes.
21
40
  commit.signed_header.commit.block_id.hash =
22
41
  "3A1D00CC2A092465E85EA2C24986BEE0105285039DC1873BB6B0CA7F610EC89D";
23
42
  const { proto: vset, cryptoIndex } = await importValidators(validators);
24
43
  const sh = importCommit(commit);
25
- await expect(verifyCommit(sh, vset, cryptoIndex)).rejects.toThrow(/header hash does not match commit blockid hash/i);
44
+ const out = await verifyCommit(sh, vset, cryptoIndex);
45
+ expect(out.quorum).toBe(false);
46
+ expect(out.ok).toBe(false);
47
+ expect(out.signedPower).toBe(0n);
48
+ expect(out.invalidSignatures).toHaveLength(vset.validators.length);
49
+ expect(out.invalidSignatures).toContain(Uint8ArrayToHex(vset.validators[0].address).toUpperCase());
50
+ expect(out.countedSignatures).toBe(vset.validators.length);
26
51
  });
27
- it("still rejects on header/commit hash mismatch even when a signature is corrupted", async () => {
52
+ it("flags invalid signatures", async () => {
28
53
  const validators = blockFixture.validator_set;
29
54
  const commit = clone(blockFixture);
30
55
  commit.signed_header.commit.signatures[0].signature = Uint8ArrayToBase64(new Uint8Array(64));
31
56
  const { proto: vset, cryptoIndex } = await importValidators(validators);
32
57
  const sh = importCommit(commit);
33
- await expect(verifyCommit(sh, vset, cryptoIndex)).rejects.toThrow(/header hash does not match commit blockid hash/i);
58
+ const out = await verifyCommit(sh, vset, cryptoIndex);
59
+ expect(out.quorum).toBe(false);
60
+ expect(out.ok).toBe(false);
61
+ expect(out.signedPower).toBe(2n);
62
+ expect(out.invalidSignatures).toEqual([
63
+ Uint8ArrayToHex(vset.validators[0].address).toUpperCase(),
64
+ ]);
65
+ expect(out.countedSignatures).toBe(vset.validators.length);
34
66
  });
35
67
  it("rejects malformed signature bytes", async () => {
36
68
  const validators = blockFixture.validator_set;
@@ -39,14 +71,87 @@ describe("lightclient.verifyCommit", () => {
39
71
  commit.signed_header.commit.signatures[0].signature = "AA==";
40
72
  await expect(async () => importCommit(commit)).rejects.toThrow(/signature must be 64 bytes/);
41
73
  });
42
- it("still rejects on header/commit hash mismatch even when validator is unknown", async () => {
74
+ it("reports unknown validators", async () => {
43
75
  const validators = blockFixture.validator_set;
44
76
  const commit = clone(blockFixture);
45
77
  commit.signed_header.commit.signatures[0].validator_address =
46
78
  "0000000000000000000000000000000000000000";
47
79
  const { proto: vset, cryptoIndex } = await importValidators(validators);
48
80
  const sh = importCommit(commit);
49
- await expect(verifyCommit(sh, vset, cryptoIndex)).rejects.toThrow(/header hash does not match commit blockid hash/i);
81
+ const out = await verifyCommit(sh, vset, cryptoIndex);
82
+ expect(out.quorum).toBe(false);
83
+ expect(out.ok).toBe(false);
84
+ expect(out.signedPower).toBe(2n);
85
+ expect(out.invalidSignatures).toEqual([]);
86
+ expect(out.unknownValidators).toEqual([
87
+ "0000000000000000000000000000000000000000",
88
+ ]);
89
+ expect(out.countedSignatures).toBe(vset.validators.length - 1);
90
+ });
91
+ it("detects tampering of every header field by checking the header merkle root against commit.block_id.hash", async () => {
92
+ const validators = blockFixture.validator_set;
93
+ const { proto: vset, cryptoIndex } = await importValidators(validators);
94
+ const mutators = {
95
+ "header.version.block": (commit) => {
96
+ commit.signed_header.header.version.block = String(BigInt(commit.signed_header.header.version.block) + 1n);
97
+ },
98
+ "header.version.app": (commit) => {
99
+ commit.signed_header.header.version.app = String(BigInt(commit.signed_header.header.version.app) + 1n);
100
+ },
101
+ "header.chain_id": (commit) => {
102
+ commit.signed_header.header.chain_id = `${commit.signed_header.header.chain_id}-tampered`;
103
+ },
104
+ "header.height": (commit) => {
105
+ commit.signed_header.header.height = String(BigInt(commit.signed_header.header.height) + 1n);
106
+ commit.signed_header.commit.height = commit.signed_header.header.height;
107
+ },
108
+ "header.time": (commit) => {
109
+ commit.signed_header.header.time = "2026-03-08T03:00:52.980342152Z";
110
+ },
111
+ "header.last_block_id.hash": (commit) => {
112
+ commit.signed_header.header.last_block_id.hash = flipLastHexNibble(commit.signed_header.header.last_block_id.hash);
113
+ },
114
+ "header.last_block_id.parts.total": (commit) => {
115
+ commit.signed_header.header.last_block_id.parts.total += 1;
116
+ },
117
+ "header.last_block_id.parts.hash": (commit) => {
118
+ commit.signed_header.header.last_block_id.parts.hash =
119
+ flipLastHexNibble(commit.signed_header.header.last_block_id.parts.hash);
120
+ },
121
+ "header.last_commit_hash": (commit) => {
122
+ commit.signed_header.header.last_commit_hash = flipLastHexNibble(commit.signed_header.header.last_commit_hash);
123
+ },
124
+ "header.data_hash": (commit) => {
125
+ commit.signed_header.header.data_hash = flipLastHexNibble(commit.signed_header.header.data_hash);
126
+ },
127
+ "header.validators_hash": (commit) => {
128
+ commit.signed_header.header.validators_hash = flipLastHexNibble(commit.signed_header.header.validators_hash);
129
+ },
130
+ "header.next_validators_hash": (commit) => {
131
+ commit.signed_header.header.next_validators_hash = flipLastHexNibble(commit.signed_header.header.next_validators_hash);
132
+ },
133
+ "header.consensus_hash": (commit) => {
134
+ commit.signed_header.header.consensus_hash = flipLastHexNibble(commit.signed_header.header.consensus_hash);
135
+ },
136
+ "header.app_hash": (commit) => {
137
+ commit.signed_header.header.app_hash = flipLastHexNibble(commit.signed_header.header.app_hash);
138
+ },
139
+ "header.last_results_hash": (commit) => {
140
+ commit.signed_header.header.last_results_hash = flipLastHexNibble(commit.signed_header.header.last_results_hash);
141
+ },
142
+ "header.evidence_hash": (commit) => {
143
+ commit.signed_header.header.evidence_hash = flipLastHexNibble(commit.signed_header.header.evidence_hash);
144
+ },
145
+ "header.proposer_address": (commit) => {
146
+ commit.signed_header.header.proposer_address = flipLastHexNibble(commit.signed_header.header.proposer_address);
147
+ },
148
+ };
149
+ for (const [field, mutate] of Object.entries(mutators)) {
150
+ const tampered = clone(blockFixture);
151
+ mutate(tampered);
152
+ const out = await verifyCommit(importCommit(tampered), vset, cryptoIndex);
153
+ expect(out.ok, `${field} tampering should be detected`).toBe(false);
154
+ }
50
155
  });
51
156
  });
52
157
  //# sourceMappingURL=webcat.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"webcat.test.js","sourceRoot":"","sources":["../../src/tests/webcat.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,YAAY,MAAM,wBAAwB,CAAC;AAElD,SAAS,KAAK,CAAI,CAAI;IACpB,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AACvC,CAAC;AAED,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,2EAA2E,EAAE,KAAK,IAAI,EAAE;QACzF,MAAM,UAAU,GAAG,YAAY,CAAC,aAAyC,CAAC;QAC1E,MAAM,MAAM,GAAG,YAAqC,CAAC;QAErD,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,MAAM,gBAAgB,CAAC,UAAU,CAAC,CAAC;QACxE,MAAM,EAAE,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;QAEhC,MAAM,MAAM,CAAC,YAAY,CAAC,EAAE,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAC/D,iDAAiD,CAClD,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC3E,MAAM,UAAU,GAAG,YAAY,CAAC,aAAyC,CAAC;QAC1E,MAAM,MAAM,GAAG,KAAK,CAAC,YAAY,CAA0B,CAAC;QAE5D,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI;YACvC,kEAAkE,CAAC;QAErE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,MAAM,gBAAgB,CAAC,UAAU,CAAC,CAAC;QACxE,MAAM,EAAE,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;QAEhC,MAAM,MAAM,CAAC,YAAY,CAAC,EAAE,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAC/D,iDAAiD,CAClD,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iFAAiF,EAAE,KAAK,IAAI,EAAE;QAC/F,MAAM,UAAU,GAAG,YAAY,CAAC,aAAyC,CAAC;QAC1E,MAAM,MAAM,GAAG,KAAK,CAAC,YAAY,CAA0B,CAAC;QAE5D,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,kBAAkB,CACtE,IAAI,UAAU,CAAC,EAAE,CAAC,CACnB,CAAC;QAEF,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,MAAM,gBAAgB,CAAC,UAAU,CAAC,CAAC;QACxE,MAAM,EAAE,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;QAEhC,MAAM,MAAM,CAAC,YAAY,CAAC,EAAE,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAC/D,iDAAiD,CAClD,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,KAAK,IAAI,EAAE;QACjD,MAAM,UAAU,GAAG,YAAY,CAAC,aAAyC,CAAC;QAC1E,MAAM,MAAM,GAAG,KAAK,CAAC,YAAY,CAA0B,CAAC;QAE5D,wDAAwD;QACxD,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,MAAM,CAAC;QAE7D,MAAM,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAC5D,4BAA4B,CAC7B,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6EAA6E,EAAE,KAAK,IAAI,EAAE;QAC3F,MAAM,UAAU,GAAG,YAAY,CAAC,aAAyC,CAAC;QAC1E,MAAM,MAAM,GAAG,KAAK,CAAC,YAAY,CAA0B,CAAC;QAE5D,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,iBAAiB;YACzD,0CAA0C,CAAC;QAE7C,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,MAAM,gBAAgB,CAAC,UAAU,CAAC,CAAC;QACxE,MAAM,EAAE,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;QAEhC,MAAM,MAAM,CAAC,YAAY,CAAC,EAAE,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAC/D,iDAAiD,CAClD,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"webcat.test.js","sourceRoot":"","sources":["../../src/tests/webcat.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@freedomofpress/cometbft",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "A CometBFT light client for the browser.",
5
+ "type": "module",
5
6
  "repository": {
6
7
  "type": "git",
7
8
  "url": "https://github.com/freedomofpress/cometbft-ts"
@@ -5,182 +5,18 @@ import {
5
5
  CanonicalPartSetHeader,
6
6
  CanonicalVote,
7
7
  } from "./proto/cometbft/types/v1/canonical";
8
- import { SignedHeader, SignedMsgType } from "./proto/cometbft/types/v1/types";
8
+ import {
9
+ BlockID,
10
+ SignedHeader,
11
+ SignedMsgType,
12
+ } from "./proto/cometbft/types/v1/types";
9
13
  import {
10
14
  Validator as ProtoValidator,
11
15
  ValidatorSet as ProtoValidatorSet,
12
16
  } from "./proto/cometbft/types/v1/validator";
17
+ import { Consensus } from "./proto/cometbft/version/v1/types";
13
18
  import { Timestamp as PbTimestamp } from "./proto/google/protobuf/timestamp";
14
19
 
15
- const LEAF_PREFIX = new Uint8Array([0]);
16
- const INNER_PREFIX = new Uint8Array([1]);
17
-
18
- function concatBytes(...parts: Uint8Array[]): Uint8Array {
19
- const totalLen = parts.reduce((acc, p) => acc + p.length, 0);
20
- const out = new Uint8Array(totalLen);
21
- let offset = 0;
22
- for (const p of parts) {
23
- out.set(p, offset);
24
- offset += p.length;
25
- }
26
- return out;
27
- }
28
-
29
- async function sha256(input: Uint8Array): Promise<Uint8Array> {
30
- const digest = await crypto.subtle.digest("SHA-256", new Uint8Array(input));
31
- return new Uint8Array(digest);
32
- }
33
-
34
- function encodeVarint(value: bigint): Uint8Array {
35
- if (value < 0n) throw new Error("encodeVarint expects a non-negative bigint");
36
- const bytes: number[] = [];
37
- let v = value;
38
- while (v >= 0x80n) {
39
- bytes.push(Number((v & 0x7fn) | 0x80n));
40
- v >>= 7n;
41
- }
42
- bytes.push(Number(v));
43
- return new Uint8Array(bytes);
44
- }
45
-
46
- function encodeFieldTag(fieldNumber: number, wireType: number): Uint8Array {
47
- return encodeVarint(BigInt((fieldNumber << 3) | wireType));
48
- }
49
-
50
- function encodeProtoBytes(fieldNumber: number, value: Uint8Array): Uint8Array {
51
- return concatBytes(
52
- encodeFieldTag(fieldNumber, 2),
53
- encodeVarint(BigInt(value.length)),
54
- value,
55
- );
56
- }
57
-
58
- function encodeProtoUint64(fieldNumber: number, value: bigint): Uint8Array {
59
- return concatBytes(encodeFieldTag(fieldNumber, 0), encodeVarint(value));
60
- }
61
-
62
- function encodeProtoInt64(fieldNumber: number, value: bigint): Uint8Array {
63
- const v = value < 0n ? (1n << 64n) + value : value;
64
- return concatBytes(encodeFieldTag(fieldNumber, 0), encodeVarint(v));
65
- }
66
-
67
- function cdcEncodeString(value: string): Uint8Array | undefined {
68
- if (value.length === 0) return undefined;
69
- return encodeProtoBytes(1, new TextEncoder().encode(value));
70
- }
71
-
72
- function cdcEncodeInt64(value: bigint): Uint8Array {
73
- return encodeProtoInt64(1, value);
74
- }
75
-
76
- function cdcEncodeBytes(value: Uint8Array): Uint8Array | undefined {
77
- if (value.length === 0) return undefined;
78
- return encodeProtoBytes(1, value);
79
- }
80
-
81
- function encodeTimestamp(ts: PbTimestamp): Uint8Array {
82
- const seconds = encodeProtoInt64(1, ts.seconds ?? 0n);
83
- const nanos = ts.nanos
84
- ? concatBytes(encodeFieldTag(2, 0), encodeVarint(BigInt(ts.nanos)))
85
- : new Uint8Array(0);
86
- return concatBytes(seconds, nanos);
87
- }
88
-
89
- function encodePartSetHeader(total: number, hash: Uint8Array): Uint8Array {
90
- return concatBytes(
91
- encodeProtoUint64(1, BigInt(total)),
92
- encodeProtoBytes(2, hash),
93
- );
94
- }
95
-
96
- function encodeBlockId(
97
- hash: Uint8Array,
98
- partSetTotal: number,
99
- partSetHash: Uint8Array,
100
- ): Uint8Array {
101
- const psh = encodePartSetHeader(partSetTotal, partSetHash);
102
- return concatBytes(encodeProtoBytes(1, hash), encodeProtoBytes(2, psh));
103
- }
104
-
105
- async function merkleLeafHash(leaf: Uint8Array): Promise<Uint8Array> {
106
- return sha256(concatBytes(LEAF_PREFIX, leaf));
107
- }
108
-
109
- async function merkleInnerHash(
110
- left: Uint8Array,
111
- right: Uint8Array,
112
- ): Promise<Uint8Array> {
113
- return sha256(concatBytes(INNER_PREFIX, left, right));
114
- }
115
-
116
- function merkleSplitPoint(length: number): number {
117
- if (length < 1) throw new Error("Trying to split a tree with size < 1");
118
- const bitLen = Math.floor(Math.log2(length)) + 1;
119
- let k = 1 << (bitLen - 1);
120
- if (k === length) k >>= 1;
121
- return k;
122
- }
123
-
124
- async function merkleHashFromByteSlices(
125
- items: Uint8Array[],
126
- ): Promise<Uint8Array> {
127
- if (items.length === 0) return sha256(new Uint8Array(0));
128
- if (items.length === 1) return merkleLeafHash(items[0]);
129
-
130
- const k = merkleSplitPoint(items.length);
131
- const left = await merkleHashFromByteSlices(items.slice(0, k));
132
- const right = await merkleHashFromByteSlices(items.slice(k));
133
- return merkleInnerHash(left, right);
134
- }
135
-
136
- async function computeHeaderHash(
137
- header: SignedHeader["header"],
138
- ): Promise<Uint8Array> {
139
- if (!header) throw new Error("SignedHeader missing header");
140
- if (!header.lastBlockId || !header.lastBlockId.partSetHeader) {
141
- throw new Error("Header lastBlockId is missing");
142
- }
143
- if (!header.time) throw new Error("Header time is missing");
144
-
145
- const version = concatBytes(
146
- encodeProtoUint64(1, header.version?.block ?? 0n),
147
- encodeProtoUint64(2, header.version?.app ?? 0n),
148
- );
149
-
150
- const lastBlockId = encodeBlockId(
151
- header.lastBlockId.hash,
152
- header.lastBlockId.partSetHeader.total,
153
- header.lastBlockId.partSetHeader.hash,
154
- );
155
-
156
- const fields: Uint8Array[] = [
157
- version,
158
- cdcEncodeString(header.chainId),
159
- cdcEncodeInt64(header.height),
160
- encodeTimestamp(header.time),
161
- lastBlockId,
162
- cdcEncodeBytes(header.lastCommitHash),
163
- cdcEncodeBytes(header.dataHash),
164
- cdcEncodeBytes(header.validatorsHash),
165
- cdcEncodeBytes(header.nextValidatorsHash),
166
- cdcEncodeBytes(header.consensusHash),
167
- cdcEncodeBytes(header.appHash),
168
- cdcEncodeBytes(header.lastResultsHash),
169
- cdcEncodeBytes(header.evidenceHash),
170
- cdcEncodeBytes(header.proposerAddress),
171
- ].filter((x): x is Uint8Array => Boolean(x));
172
-
173
- return merkleHashFromByteSlices(fields);
174
- }
175
-
176
- function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
177
- if (a.length !== b.length) return false;
178
- for (let i = 0; i < a.length; i++) {
179
- if (a[i] !== b[i]) return false;
180
- }
181
- return true;
182
- }
183
-
184
20
  export type CryptoIndex = Map<string, CryptoKey>;
185
21
 
186
22
  export interface VerifyOutcome {
@@ -246,6 +82,143 @@ function hasTwoThirds(signed: bigint, total: bigint): boolean {
246
82
  return signed * 3n > total * 2n;
247
83
  }
248
84
 
85
+ function concatBytes(...parts: Uint8Array[]): Uint8Array {
86
+ const length = parts.reduce((acc, p) => acc + p.length, 0);
87
+ const out = new Uint8Array(length);
88
+ let offset = 0;
89
+ for (const p of parts) {
90
+ out.set(p, offset);
91
+ offset += p.length;
92
+ }
93
+ return out;
94
+ }
95
+
96
+ function encodeUvarintBigint(value: bigint): Uint8Array {
97
+ if (value < 0n) throw new Error("uvarint cannot encode negative values");
98
+
99
+ const bytes: number[] = [];
100
+ let v = value;
101
+ while (v >= 0x80n) {
102
+ bytes.push(Number((v & 0x7fn) | 0x80n));
103
+ v >>= 7n;
104
+ }
105
+ bytes.push(Number(v));
106
+ return new Uint8Array(bytes);
107
+ }
108
+
109
+ function encodeLengthPrefixed(value: Uint8Array): Uint8Array {
110
+ return concatBytes(encodeUvarintBigint(BigInt(value.length)), value);
111
+ }
112
+
113
+ function encodeString(value: string): Uint8Array {
114
+ if (value.length === 0) return new Uint8Array();
115
+ return concatBytes(
116
+ new Uint8Array([0x0a]),
117
+ encodeLengthPrefixed(new TextEncoder().encode(value)),
118
+ );
119
+ }
120
+
121
+ function encodeBytes(value: Uint8Array): Uint8Array {
122
+ if (value.length === 0) return new Uint8Array();
123
+ return concatBytes(new Uint8Array([0x0a]), encodeLengthPrefixed(value));
124
+ }
125
+
126
+ function encodeInt64Value(value: bigint): Uint8Array {
127
+ if (value === 0n) return new Uint8Array();
128
+ return concatBytes(new Uint8Array([0x08]), encodeUvarintBigint(value));
129
+ }
130
+
131
+ function encodeTimestamp(timestamp?: PbTimestamp): Uint8Array {
132
+ if (!timestamp) return new Uint8Array();
133
+ return PbTimestamp.encode(timestamp).finish();
134
+ }
135
+
136
+ function encodeVersionForHeaderHash(
137
+ version: NonNullable<SignedHeader["header"]>["version"],
138
+ ): Uint8Array {
139
+ if (!version) return new Uint8Array();
140
+ return Consensus.encode(version).finish();
141
+ }
142
+
143
+ function encodeBlockIdForHeaderHash(
144
+ blockId: NonNullable<SignedHeader["header"]>["lastBlockId"],
145
+ ): Uint8Array {
146
+ if (!blockId) return new Uint8Array();
147
+ return BlockID.encode(blockId).finish();
148
+ }
149
+
150
+ async function sha256(data: Uint8Array): Promise<Uint8Array> {
151
+ return new Uint8Array(
152
+ await crypto.subtle.digest("SHA-256", new Uint8Array(data)),
153
+ );
154
+ }
155
+
156
+ async function hashLeaf(leaf: Uint8Array): Promise<Uint8Array> {
157
+ return sha256(concatBytes(new Uint8Array([0]), leaf));
158
+ }
159
+
160
+ async function hashInner(
161
+ left: Uint8Array,
162
+ right: Uint8Array,
163
+ ): Promise<Uint8Array> {
164
+ return sha256(concatBytes(new Uint8Array([1]), left, right));
165
+ }
166
+
167
+ function getSplitPoint(size: number): number {
168
+ if (size < 1) throw new Error("Cannot split an empty merkle tree");
169
+ const p = 2 ** Math.floor(Math.log2(size));
170
+ return p < size ? p : p / 2;
171
+ }
172
+
173
+ async function simpleMerkleHashFromByteSlices(
174
+ chunks: Uint8Array[],
175
+ ): Promise<Uint8Array> {
176
+ if (chunks.length === 0) throw new Error("Cannot hash an empty merkle tree");
177
+ if (chunks.length === 1) return hashLeaf(chunks[0]);
178
+
179
+ const split = getSplitPoint(chunks.length);
180
+ const left = await simpleMerkleHashFromByteSlices(chunks.slice(0, split));
181
+ const right = await simpleMerkleHashFromByteSlices(chunks.slice(split));
182
+ return hashInner(left, right);
183
+ }
184
+
185
+ function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
186
+ if (a.length !== b.length) return false;
187
+ for (let i = 0; i < a.length; i++) {
188
+ if (a[i] !== b[i]) return false;
189
+ }
190
+ return true;
191
+ }
192
+
193
+ async function hashHeaderForBlockId(
194
+ header: NonNullable<SignedHeader["header"]>,
195
+ ): Promise<Uint8Array> {
196
+ if (!header.lastBlockId) {
197
+ throw new Error(
198
+ "Header missing lastBlockId required for hash verification",
199
+ );
200
+ }
201
+
202
+ const fields: Uint8Array[] = [
203
+ encodeVersionForHeaderHash(header.version),
204
+ encodeString(header.chainId),
205
+ encodeInt64Value(header.height),
206
+ encodeTimestamp(header.time),
207
+ encodeBlockIdForHeaderHash(header.lastBlockId),
208
+ encodeBytes(header.lastCommitHash),
209
+ encodeBytes(header.dataHash),
210
+ encodeBytes(header.validatorsHash),
211
+ encodeBytes(header.nextValidatorsHash),
212
+ encodeBytes(header.consensusHash),
213
+ encodeBytes(header.appHash),
214
+ encodeBytes(header.lastResultsHash),
215
+ encodeBytes(header.evidenceHash),
216
+ encodeBytes(header.proposerAddress),
217
+ ];
218
+
219
+ return simpleMerkleHashFromByteSlices(fields);
220
+ }
221
+
249
222
  export async function verifyCommit(
250
223
  sh: SignedHeader,
251
224
  vset: ProtoValidatorSet,
@@ -301,13 +274,11 @@ export async function verifyCommit(
301
274
  const blockIdHash: Uint8Array = bid.hash;
302
275
  const partsHash: Uint8Array = bid.partSetHeader.hash;
303
276
  const partsTotal: number = bid.partSetHeader.total;
304
-
305
- const expectedBlockHash = await computeHeaderHash(header);
306
- if (!bytesEqual(expectedBlockHash, blockIdHash)) {
307
- throw new Error(
308
- "Header hash does not match commit BlockID hash (header fields were tampered or inconsistent)",
309
- );
310
- }
277
+ const expectedBlockIdHash = await hashHeaderForBlockId(header);
278
+ const headerMatchesCommitBlockId = bytesEqual(
279
+ expectedBlockIdHash,
280
+ blockIdHash,
281
+ );
311
282
 
312
283
  let signedPower = 0n;
313
284
  const unknown: string[] = [];
@@ -374,9 +345,10 @@ export async function verifyCommit(
374
345
  }
375
346
 
376
347
  const quorum = hasTwoThirds(signedPower, totalPower);
348
+ const ok = quorum && headerMatchesCommitBlockId;
377
349
 
378
350
  return {
379
- ok: quorum,
351
+ ok,
380
352
  quorum,
381
353
  signedPower,
382
354
  totalPower,
@@ -0,0 +1,101 @@
1
+ {
2
+ "signed_header": {
3
+ "header": {
4
+ "version": {
5
+ "block": "11",
6
+ "app": "0"
7
+ },
8
+ "chain_id": "webcat-test-02",
9
+ "height": "18720",
10
+ "time": "2026-03-08T03:00:52.980342151Z",
11
+ "last_block_id": {
12
+ "hash": "8271A9B3A0689FF0CB095E39175403A74BC374ABBE23184B20D902199FDC0BA7",
13
+ "parts": {
14
+ "total": 1,
15
+ "hash": "AEB84D5BAB13692BCBD9603B9A94AA310203648DF16A755EA2BA9A2A1D667BCB"
16
+ }
17
+ },
18
+ "last_commit_hash": "9E3394940C1C94504E0465CCED1CE7047639BDF5506E34EA07FE729C5E4EA5FC",
19
+ "data_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
20
+ "validators_hash": "166D75D14EE1555615C57D2F013454152214BDB143D458C00A02B2AC0F2AC128",
21
+ "next_validators_hash": "166D75D14EE1555615C57D2F013454152214BDB143D458C00A02B2AC0F2AC128",
22
+ "consensus_hash": "048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F",
23
+ "app_hash": "6068FFC66BC857FE76B4E8A779176ABBA6CBFC3F119F961915AF01C293EDC9D3",
24
+ "last_results_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
25
+ "evidence_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
26
+ "proposer_address": "B734AC6CC1F14AAC8315FE8DBB07402ED4E45D72"
27
+ },
28
+ "commit": {
29
+ "height": "18720",
30
+ "round": 0,
31
+ "block_id": {
32
+ "hash": "1E1B52E922F1E20D8113D38A70675BFB0CF83609C00CD6CE637FC79B0C73A890",
33
+ "parts": {
34
+ "total": 1,
35
+ "hash": "CC7EAC61FA5FB28ADC4F26BEB76CD54CDB66C2A952E81CBEE33B55D0ED1B0A78"
36
+ }
37
+ },
38
+ "signatures": [
39
+ {
40
+ "block_id_flag": 2,
41
+ "validator_address": "48926DF1CC10DBBFAA78953F9BE38E59020DE01D",
42
+ "timestamp": "2026-03-08T03:01:53.984641899Z",
43
+ "signature": "V1swep5n+4AX155lTrNXSyS7k0L5Wnx2E8zrBGqWEDHf8pW5tFO1ron1xHqfMmJh9BSK3FaOWnIVToqcrvCiBg=="
44
+ },
45
+ {
46
+ "block_id_flag": 2,
47
+ "validator_address": "A39CFEBE6E8CA10B6101C46884A6EB8E62D04417",
48
+ "timestamp": "2026-03-08T03:01:54.033082245Z",
49
+ "signature": "VLIvwTw5aC15Oq8AkmiXudyIFKIo3VBOgU1eIQN+JxgsNtJwr8tuZsaGf6ds7BY6kQ8E93pxDm1CUk2pYrrZDQ=="
50
+ },
51
+ {
52
+ "block_id_flag": 2,
53
+ "validator_address": "B734AC6CC1F14AAC8315FE8DBB07402ED4E45D72",
54
+ "timestamp": "2026-03-08T03:01:54.025260639Z",
55
+ "signature": "ODjl80UkQB7uBQmf0TUCbgg+p8Mvv9ihvmbyYou3YWB7nqcwp5tzz8MiFkoPo8zfiLL+83AqKeYm7S3y3kPxDQ=="
56
+ }
57
+ ]
58
+ }
59
+ },
60
+ "validator_set": {
61
+ "validators": [
62
+ {
63
+ "address": "48926DF1CC10DBBFAA78953F9BE38E59020DE01D",
64
+ "pub_key": {
65
+ "type": "tendermint/PubKeyEd25519",
66
+ "value": "zEK+pyAEhhCaoHYhhT+rrorJoc9kZbfY+JkbMtF8u5Y="
67
+ },
68
+ "power": "1",
69
+ "name": null
70
+ },
71
+ {
72
+ "address": "A39CFEBE6E8CA10B6101C46884A6EB8E62D04417",
73
+ "pub_key": {
74
+ "type": "tendermint/PubKeyEd25519",
75
+ "value": "8dZUej72UriKCPIDvk01c6yLaAB2ssYIli3fT3jKwSM="
76
+ },
77
+ "power": "1",
78
+ "name": null
79
+ },
80
+ {
81
+ "address": "B734AC6CC1F14AAC8315FE8DBB07402ED4E45D72",
82
+ "pub_key": {
83
+ "type": "tendermint/PubKeyEd25519",
84
+ "value": "WBK6Af3uKtC+VTv8x48f2TPoNnsebSXSBTIsJku/nQw="
85
+ },
86
+ "power": "1",
87
+ "name": null
88
+ }
89
+ ],
90
+ "proposer": {
91
+ "address": "B734AC6CC1F14AAC8315FE8DBB07402ED4E45D72",
92
+ "pub_key": {
93
+ "type": "tendermint/PubKeyEd25519",
94
+ "value": "WBK6Af3uKtC+VTv8x48f2TPoNnsebSXSBTIsJku/nQw="
95
+ },
96
+ "power": "1",
97
+ "name": null
98
+ },
99
+ "total_voting_power": "3"
100
+ }
101
+ }