@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.
@@ -5,42 +5,54 @@
5
5
  "block": "11",
6
6
  "app": "0"
7
7
  },
8
- "chain_id": "test-chain-hGjZNv",
9
- "height": "493",
10
- "time": "2025-11-26T21:34:06.159025Z",
8
+ "chain_id": "webcat-test-02",
9
+ "height": "18720",
10
+ "time": "2026-03-08T03:00:52.980342151Z",
11
11
  "last_block_id": {
12
- "hash": "6ABE9483553796549941230EFFC982A4B389FE475D37C43815E6B735E5AEDE7B",
12
+ "hash": "8271A9B3A0689FF0CB095E39175403A74BC374ABBE23184B20D902199FDC0BA7",
13
13
  "parts": {
14
14
  "total": 1,
15
- "hash": "FA7002E34B341CC2625B2A2EB546ED9A1D2B77D1146A5650D9D2E3682940EE29"
15
+ "hash": "AEB84D5BAB13692BCBD9603B9A94AA310203648DF16A755EA2BA9A2A1D667BCB"
16
16
  }
17
17
  },
18
- "last_commit_hash": "C7B78BBAF834E298EB6570836DAA9116A56BED2F652D8BAF6C28B1F33ABC8AE8",
18
+ "last_commit_hash": "9E3394940C1C94504E0465CCED1CE7047639BDF5506E34EA07FE729C5E4EA5FC",
19
19
  "data_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
20
- "validators_hash": "04C72627103FD466C7B4C30C0E654D2B58EA140493C009B8BAB66749FFEBE60C",
21
- "next_validators_hash": "04C72627103FD466C7B4C30C0E654D2B58EA140493C009B8BAB66749FFEBE60C",
20
+ "validators_hash": "166D75D14EE1555615C57D2F013454152214BDB143D458C00A02B2AC0F2AC128",
21
+ "next_validators_hash": "166D75D14EE1555615C57D2F013454152214BDB143D458C00A02B2AC0F2AC128",
22
22
  "consensus_hash": "048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F",
23
- "app_hash": "180B4624DAB5D631EAD41DC7BEC0F63D2A25DF826B068604B7D858206ED92DD6",
23
+ "app_hash": "6068FFC66BC857FE76B4E8A779176ABBA6CBFC3F119F961915AF01C293EDC9D3",
24
24
  "last_results_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
25
25
  "evidence_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
26
- "proposer_address": "F588CF8C0CA8D88CDD2F909C8262018B361CF5D5"
26
+ "proposer_address": "B734AC6CC1F14AAC8315FE8DBB07402ED4E45D72"
27
27
  },
28
28
  "commit": {
29
- "height": "493",
29
+ "height": "18720",
30
30
  "round": 0,
31
31
  "block_id": {
32
- "hash": "2A1D00CC2A092465E85EA2C24986BEE0105285039DC1873BB6B0CA7F610EC89D",
32
+ "hash": "1E1B52E922F1E20D8113D38A70675BFB0CF83609C00CD6CE637FC79B0C73A890",
33
33
  "parts": {
34
34
  "total": 1,
35
- "hash": "379E62CC2B2741AD07E1F325307BBDECDAA10F2405A22C9B9350615A5DAF2725"
35
+ "hash": "CC7EAC61FA5FB28ADC4F26BEB76CD54CDB66C2A952E81CBEE33B55D0ED1B0A78"
36
36
  }
37
37
  },
38
38
  "signatures": [
39
39
  {
40
40
  "block_id_flag": 2,
41
- "validator_address": "F588CF8C0CA8D88CDD2F909C8262018B361CF5D5",
42
- "timestamp": "2025-11-26T21:34:07.1855Z",
43
- "signature": "ttukjhivwXmf9YKEFSceZe0b6D98esb+9nMCeEz6e1z5MDfxMKun7O0nPis0PoDWcqBifWJy2NdbfO0zySILDQ=="
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=="
44
56
  }
45
57
  ]
46
58
  }
@@ -48,24 +60,42 @@
48
60
  "validator_set": {
49
61
  "validators": [
50
62
  {
51
- "address": "F588CF8C0CA8D88CDD2F909C8262018B361CF5D5",
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",
52
82
  "pub_key": {
53
83
  "type": "tendermint/PubKeyEd25519",
54
- "value": "Pn7hPwDPYkoBBTKxdxL0QZLdPCupGXyds/qSyOIBSZg="
84
+ "value": "WBK6Af3uKtC+VTv8x48f2TPoNnsebSXSBTIsJku/nQw="
55
85
  },
56
- "power": "10",
86
+ "power": "1",
57
87
  "name": null
58
88
  }
59
89
  ],
60
90
  "proposer": {
61
- "address": "F588CF8C0CA8D88CDD2F909C8262018B361CF5D5",
91
+ "address": "B734AC6CC1F14AAC8315FE8DBB07402ED4E45D72",
62
92
  "pub_key": {
63
93
  "type": "tendermint/PubKeyEd25519",
64
- "value": "Pn7hPwDPYkoBBTKxdxL0QZLdPCupGXyds/qSyOIBSZg="
94
+ "value": "WBK6Af3uKtC+VTv8x48f2TPoNnsebSXSBTIsJku/nQw="
65
95
  },
66
- "power": "10",
96
+ "power": "1",
67
97
  "name": null
68
98
  },
69
- "total_voting_power": "10"
99
+ "total_voting_power": "3"
70
100
  }
71
101
  }
@@ -38,7 +38,7 @@ describe("lightclient.verifyCommit", () => {
38
38
  expect(out.countedSignatures).toBeGreaterThan(0);
39
39
  });
40
40
 
41
- it("throws when commit block_id.hash does not match the header hash", async () => {
41
+ it("fails quorum and invalidates all signatures when block_id.hash is tampered", async () => {
42
42
  const vResp = validatorsFixture as unknown as ValidatorJson;
43
43
  const { proto: vset, cryptoIndex } = await importValidators(vResp);
44
44
 
@@ -48,26 +48,11 @@ describe("lightclient.verifyCommit", () => {
48
48
  h.slice(0, -2) + (h.slice(-2) === "00" ? "01" : "00");
49
49
 
50
50
  const sh = importCommit(badCommit as CommitJson);
51
+ const out = await verifyCommit(sh, vset, cryptoIndex);
51
52
 
52
- await expect(verifyCommit(sh, vset, cryptoIndex)).rejects.toThrow(
53
- /header hash does not match commit blockid hash/i,
54
- );
55
- });
56
-
57
- it("throws when app_hash is tampered", async () => {
58
- const vResp = validatorsFixture as unknown as ValidatorJson;
59
- const { proto: vset, cryptoIndex } = await importValidators(vResp);
60
-
61
- const badHeader = clone(commitFixture) as any;
62
- const appHash: string = badHeader.signed_header.header.app_hash;
63
- badHeader.signed_header.header.app_hash =
64
- appHash.slice(0, -2) + (appHash.slice(-2) === "00" ? "01" : "00");
65
-
66
- const sh = importCommit(badHeader as CommitJson);
67
-
68
- await expect(verifyCommit(sh, vset, cryptoIndex)).rejects.toThrow(
69
- /header hash does not match commit blockid hash/i,
70
- );
53
+ expect(out.quorum).toBe(false);
54
+ expect(out.invalidSignatures.length).toBe(out.countedSignatures);
55
+ expect(out.ok).toBe(false);
71
56
  });
72
57
 
73
58
  it("drops below 2/3 quorum when two votes are ABSENT", async () => {
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
3
  import { importCommit } from "../commit";
4
- import { Uint8ArrayToBase64 } from "../encoding";
4
+ import { Uint8ArrayToBase64, Uint8ArrayToHex } from "../encoding";
5
5
  import { verifyCommit } from "../lightclient";
6
6
  import type { CommitJson, ValidatorJson } from "../types";
7
7
  import { importValidators } from "../validators";
@@ -11,35 +11,61 @@ function clone<T>(x: T): T {
11
11
  return JSON.parse(JSON.stringify(x));
12
12
  }
13
13
 
14
+ function flipLastHexNibble(hex: string): string {
15
+ const last = hex.at(-1);
16
+ if (!last) throw new Error("Cannot mutate an empty hex string");
17
+
18
+ const replacement = last.toLowerCase() === "0" ? "1" : "0";
19
+ return `${hex.slice(0, -1)}${replacement}`;
20
+ }
21
+
14
22
  describe("lightclient.verifyCommit", () => {
15
- it("rejects this fixture when header hash does not match commit block_id.hash", async () => {
23
+ it("verifies a valid commit against the validator set", async () => {
16
24
  const validators = blockFixture.validator_set as unknown as ValidatorJson;
17
25
  const commit = blockFixture as unknown as CommitJson;
18
26
 
19
27
  const { proto: vset, cryptoIndex } = await importValidators(validators);
20
28
  const sh = importCommit(commit);
21
29
 
22
- await expect(verifyCommit(sh, vset, cryptoIndex)).rejects.toThrow(
23
- /header hash does not match commit blockid hash/i,
24
- );
30
+ const out = await verifyCommit(sh, vset, cryptoIndex);
31
+
32
+ expect(out.quorum).toBe(true);
33
+ expect(out.ok).toBe(true);
34
+ expect(out.signedPower > 0n).toBe(true);
35
+ expect(out.signedPower <= out.totalPower).toBe(true);
36
+ expect(out.headerTime).toBeDefined();
37
+ expect(out.appHash instanceof Uint8Array).toBe(true);
38
+ expect(out.blockIdHash instanceof Uint8Array).toBe(true);
39
+ expect(out.unknownValidators.length).toBe(0);
40
+ expect(out.invalidSignatures.length).toBe(0);
41
+ expect(out.countedSignatures).toBeGreaterThan(0);
25
42
  });
26
43
 
27
- it("throws when commit block_id.hash does not match header hash", async () => {
44
+ it("flags invalid signatures", async () => {
28
45
  const validators = blockFixture.validator_set as unknown as ValidatorJson;
29
46
  const commit = clone(blockFixture) as unknown as CommitJson;
30
47
 
48
+ // Flip one byte of the BlockID hash to keep the signature well-formed but
49
+ // cryptographically invalid for the mutated sign-bytes.
31
50
  commit.signed_header.commit.block_id.hash =
32
51
  "3A1D00CC2A092465E85EA2C24986BEE0105285039DC1873BB6B0CA7F610EC89D";
33
52
 
34
53
  const { proto: vset, cryptoIndex } = await importValidators(validators);
35
54
  const sh = importCommit(commit);
36
55
 
37
- await expect(verifyCommit(sh, vset, cryptoIndex)).rejects.toThrow(
38
- /header hash does not match commit blockid hash/i,
56
+ const out = await verifyCommit(sh, vset, cryptoIndex);
57
+
58
+ expect(out.quorum).toBe(false);
59
+ expect(out.ok).toBe(false);
60
+ expect(out.signedPower).toBe(0n);
61
+ expect(out.invalidSignatures).toHaveLength(vset.validators.length);
62
+ expect(out.invalidSignatures).toContain(
63
+ Uint8ArrayToHex(vset.validators[0].address).toUpperCase(),
39
64
  );
65
+ expect(out.countedSignatures).toBe(vset.validators.length);
40
66
  });
41
67
 
42
- it("still rejects on header/commit hash mismatch even when a signature is corrupted", async () => {
68
+ it("flags invalid signatures", async () => {
43
69
  const validators = blockFixture.validator_set as unknown as ValidatorJson;
44
70
  const commit = clone(blockFixture) as unknown as CommitJson;
45
71
 
@@ -50,9 +76,15 @@ describe("lightclient.verifyCommit", () => {
50
76
  const { proto: vset, cryptoIndex } = await importValidators(validators);
51
77
  const sh = importCommit(commit);
52
78
 
53
- await expect(verifyCommit(sh, vset, cryptoIndex)).rejects.toThrow(
54
- /header hash does not match commit blockid hash/i,
55
- );
79
+ const out = await verifyCommit(sh, vset, cryptoIndex);
80
+
81
+ expect(out.quorum).toBe(false);
82
+ expect(out.ok).toBe(false);
83
+ expect(out.signedPower).toBe(2n);
84
+ expect(out.invalidSignatures).toEqual([
85
+ Uint8ArrayToHex(vset.validators[0].address).toUpperCase(),
86
+ ]);
87
+ expect(out.countedSignatures).toBe(vset.validators.length);
56
88
  });
57
89
 
58
90
  it("rejects malformed signature bytes", async () => {
@@ -67,7 +99,7 @@ describe("lightclient.verifyCommit", () => {
67
99
  );
68
100
  });
69
101
 
70
- it("still rejects on header/commit hash mismatch even when validator is unknown", async () => {
102
+ it("reports unknown validators", async () => {
71
103
  const validators = blockFixture.validator_set as unknown as ValidatorJson;
72
104
  const commit = clone(blockFixture) as unknown as CommitJson;
73
105
 
@@ -77,8 +109,117 @@ describe("lightclient.verifyCommit", () => {
77
109
  const { proto: vset, cryptoIndex } = await importValidators(validators);
78
110
  const sh = importCommit(commit);
79
111
 
80
- await expect(verifyCommit(sh, vset, cryptoIndex)).rejects.toThrow(
81
- /header hash does not match commit blockid hash/i,
82
- );
112
+ const out = await verifyCommit(sh, vset, cryptoIndex);
113
+
114
+ expect(out.quorum).toBe(false);
115
+ expect(out.ok).toBe(false);
116
+ expect(out.signedPower).toBe(2n);
117
+ expect(out.invalidSignatures).toEqual([]);
118
+ expect(out.unknownValidators).toEqual([
119
+ "0000000000000000000000000000000000000000",
120
+ ]);
121
+ expect(out.countedSignatures).toBe(vset.validators.length - 1);
122
+ });
123
+
124
+ it("detects tampering of every header field by checking the header merkle root against commit.block_id.hash", async () => {
125
+ const validators = blockFixture.validator_set as unknown as ValidatorJson;
126
+ const { proto: vset, cryptoIndex } = await importValidators(validators);
127
+
128
+ const mutators: Record<string, (commit: any) => void> = {
129
+ "header.version.block": (commit) => {
130
+ commit.signed_header.header.version.block = String(
131
+ BigInt(commit.signed_header.header.version.block) + 1n,
132
+ );
133
+ },
134
+ "header.version.app": (commit) => {
135
+ commit.signed_header.header.version.app = String(
136
+ BigInt(commit.signed_header.header.version.app) + 1n,
137
+ );
138
+ },
139
+ "header.chain_id": (commit) => {
140
+ commit.signed_header.header.chain_id = `${commit.signed_header.header.chain_id}-tampered`;
141
+ },
142
+ "header.height": (commit) => {
143
+ commit.signed_header.header.height = String(
144
+ BigInt(commit.signed_header.header.height) + 1n,
145
+ );
146
+ commit.signed_header.commit.height = commit.signed_header.header.height;
147
+ },
148
+ "header.time": (commit) => {
149
+ commit.signed_header.header.time = "2026-03-08T03:00:52.980342152Z";
150
+ },
151
+ "header.last_block_id.hash": (commit) => {
152
+ commit.signed_header.header.last_block_id.hash = flipLastHexNibble(
153
+ commit.signed_header.header.last_block_id.hash,
154
+ );
155
+ },
156
+ "header.last_block_id.parts.total": (commit) => {
157
+ commit.signed_header.header.last_block_id.parts.total += 1;
158
+ },
159
+ "header.last_block_id.parts.hash": (commit) => {
160
+ commit.signed_header.header.last_block_id.parts.hash =
161
+ flipLastHexNibble(
162
+ commit.signed_header.header.last_block_id.parts.hash,
163
+ );
164
+ },
165
+ "header.last_commit_hash": (commit) => {
166
+ commit.signed_header.header.last_commit_hash = flipLastHexNibble(
167
+ commit.signed_header.header.last_commit_hash,
168
+ );
169
+ },
170
+ "header.data_hash": (commit) => {
171
+ commit.signed_header.header.data_hash = flipLastHexNibble(
172
+ commit.signed_header.header.data_hash,
173
+ );
174
+ },
175
+ "header.validators_hash": (commit) => {
176
+ commit.signed_header.header.validators_hash = flipLastHexNibble(
177
+ commit.signed_header.header.validators_hash,
178
+ );
179
+ },
180
+ "header.next_validators_hash": (commit) => {
181
+ commit.signed_header.header.next_validators_hash = flipLastHexNibble(
182
+ commit.signed_header.header.next_validators_hash,
183
+ );
184
+ },
185
+ "header.consensus_hash": (commit) => {
186
+ commit.signed_header.header.consensus_hash = flipLastHexNibble(
187
+ commit.signed_header.header.consensus_hash,
188
+ );
189
+ },
190
+ "header.app_hash": (commit) => {
191
+ commit.signed_header.header.app_hash = flipLastHexNibble(
192
+ commit.signed_header.header.app_hash,
193
+ );
194
+ },
195
+ "header.last_results_hash": (commit) => {
196
+ commit.signed_header.header.last_results_hash = flipLastHexNibble(
197
+ commit.signed_header.header.last_results_hash,
198
+ );
199
+ },
200
+ "header.evidence_hash": (commit) => {
201
+ commit.signed_header.header.evidence_hash = flipLastHexNibble(
202
+ commit.signed_header.header.evidence_hash,
203
+ );
204
+ },
205
+ "header.proposer_address": (commit) => {
206
+ commit.signed_header.header.proposer_address = flipLastHexNibble(
207
+ commit.signed_header.header.proposer_address,
208
+ );
209
+ },
210
+ };
211
+
212
+ for (const [field, mutate] of Object.entries(mutators)) {
213
+ const tampered = clone(blockFixture);
214
+ mutate(tampered);
215
+
216
+ const out = await verifyCommit(
217
+ importCommit(tampered as unknown as CommitJson),
218
+ vset,
219
+ cryptoIndex,
220
+ );
221
+
222
+ expect(out.ok, `${field} tampering should be detected`).toBe(false);
223
+ }
83
224
  });
84
225
  });