@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.
- package/dist/lightclient.js +112 -130
- package/dist/lightclient.js.map +1 -1
- package/dist/tests/fixtures/webcat.json +53 -23
- package/dist/tests/lightclient.test.js +5 -12
- package/dist/tests/lightclient.test.js.map +1 -1
- package/dist/tests/webcat.test.js +114 -9
- package/dist/tests/webcat.test.js.map +1 -1
- package/package.json +2 -1
- package/src/lightclient.ts +150 -178
- package/src/tests/fixtures/header-merkle-webcat-02.json +101 -0
- package/src/tests/fixtures/webcat.json +53 -23
- package/src/tests/lightclient.test.ts +5 -20
- package/src/tests/webcat.test.ts +157 -16
|
@@ -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("
|
|
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
|
|
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("
|
|
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
|
|
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("
|
|
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
|
|
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("
|
|
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
|
|
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;
|
|
1
|
+
{"version":3,"file":"webcat.test.js","sourceRoot":"","sources":["../../src/tests/webcat.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/lightclient.ts
CHANGED
|
@@ -5,182 +5,18 @@ import {
|
|
|
5
5
|
CanonicalPartSetHeader,
|
|
6
6
|
CanonicalVote,
|
|
7
7
|
} from "./proto/cometbft/types/v1/canonical";
|
|
8
|
-
import {
|
|
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
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
|
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
|
+
}
|