@freedomofpress/cometbft 0.1.0 → 0.1.2
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/commit.d.ts +7 -0
- package/dist/commit.js +175 -0
- package/dist/commit.js.map +1 -0
- package/dist/encoding.d.ts +4 -0
- package/dist/encoding.js +31 -0
- package/dist/encoding.js.map +1 -0
- package/dist/lightclient.d.ts +17 -0
- package/dist/lightclient.js +275 -0
- package/dist/lightclient.js.map +1 -0
- package/dist/proto/cometbft/crypto/v1/keys.d.ts +28 -0
- package/dist/proto/cometbft/crypto/v1/keys.js +110 -0
- package/dist/proto/cometbft/crypto/v1/keys.js.map +1 -0
- package/dist/proto/cometbft/crypto/v1/proof.d.ts +60 -0
- package/dist/proto/cometbft/crypto/v1/proof.js +416 -0
- package/dist/proto/cometbft/crypto/v1/proof.js.map +1 -0
- package/dist/proto/cometbft/types/v1/canonical.d.ts +85 -0
- package/dist/proto/cometbft/types/v1/canonical.js +586 -0
- package/dist/proto/cometbft/types/v1/canonical.js.map +1 -0
- package/dist/proto/cometbft/types/v1/types.d.ts +206 -0
- package/dist/proto/cometbft/types/v1/types.js +1794 -0
- package/dist/proto/cometbft/types/v1/types.js.map +1 -0
- package/dist/proto/cometbft/types/v1/validator.d.ts +64 -0
- package/dist/proto/cometbft/types/v1/validator.js +382 -0
- package/dist/proto/cometbft/types/v1/validator.js.map +1 -0
- package/dist/proto/cometbft/version/v1/types.d.ts +41 -0
- package/dist/proto/cometbft/version/v1/types.js +154 -0
- package/dist/proto/cometbft/version/v1/types.js.map +1 -0
- package/dist/proto/gogoproto/gogo.d.ts +1 -0
- package/dist/proto/gogoproto/gogo.js +8 -0
- package/dist/proto/gogoproto/gogo.js.map +1 -0
- package/dist/proto/google/protobuf/descriptor.d.ts +1228 -0
- package/dist/proto/google/protobuf/descriptor.js +5056 -0
- package/dist/proto/google/protobuf/descriptor.js.map +1 -0
- package/dist/proto/google/protobuf/timestamp.d.ts +128 -0
- package/dist/proto/google/protobuf/timestamp.js +83 -0
- package/dist/proto/google/protobuf/timestamp.js.map +1 -0
- package/dist/tests/commit.test.d.ts +1 -0
- package/dist/tests/commit.test.js +219 -0
- package/dist/tests/commit.test.js.map +1 -0
- package/dist/tests/encoding.test.d.ts +1 -0
- package/dist/tests/encoding.test.js +31 -0
- package/dist/tests/encoding.test.js.map +1 -0
- package/dist/tests/fixtures/commit-12.json +64 -0
- package/dist/tests/fixtures/validators-12.json +41 -0
- package/dist/tests/fixtures/webcat.json +71 -0
- package/dist/tests/lightclient.test.d.ts +1 -0
- package/dist/tests/lightclient.test.js +234 -0
- package/dist/tests/lightclient.test.js.map +1 -0
- package/dist/tests/validators.test.d.ts +1 -0
- package/dist/tests/validators.test.js +184 -0
- package/dist/tests/validators.test.js.map +1 -0
- package/dist/tests/webcat.test.d.ts +1 -0
- package/dist/tests/webcat.test.js +52 -0
- package/dist/tests/webcat.test.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validators.d.ts +6 -0
- package/dist/validators.js +55 -0
- package/dist/validators.js.map +1 -0
- package/package.json +5 -5
- package/src/lightclient.ts +176 -0
- package/src/tests/lightclient.test.ts +20 -5
- package/src/tests/webcat.test.ts +17 -47
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { base64ToUint8Array, Uint8ArrayToHex } from "./encoding";
|
|
2
|
+
export async function importValidators(resp) {
|
|
3
|
+
const seen = new Set();
|
|
4
|
+
const cryptoIndex = new Map();
|
|
5
|
+
const protoValidators = [];
|
|
6
|
+
if (!resp.validators || resp.validators.length < 1) {
|
|
7
|
+
throw new Error("Missing validators");
|
|
8
|
+
}
|
|
9
|
+
let countedPower = 0n;
|
|
10
|
+
for (const v of resp.validators) {
|
|
11
|
+
if (!v.address || v.address.length !== 40) {
|
|
12
|
+
throw new Error(`Validator address must be 40 HEX digits, provided: ${v.address}`);
|
|
13
|
+
}
|
|
14
|
+
if (!v.pub_key?.type || !v.pub_key?.value) {
|
|
15
|
+
throw new Error("Validator key object is invalid");
|
|
16
|
+
}
|
|
17
|
+
if (v.pub_key.type !== "tendermint/PubKeyEd25519") {
|
|
18
|
+
throw new Error(`Key of type ${v.pub_key.type} is currently unsupported.`);
|
|
19
|
+
}
|
|
20
|
+
const rawKey = base64ToUint8Array(v.pub_key.value);
|
|
21
|
+
const key = await crypto.subtle.importKey("raw", new Uint8Array(rawKey), { name: "Ed25519" }, false, ["verify"]);
|
|
22
|
+
const sha = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array(rawKey)));
|
|
23
|
+
const addrHex = Uint8ArrayToHex(sha.slice(0, 20)).toUpperCase();
|
|
24
|
+
if (addrHex !== v.address.toUpperCase()) {
|
|
25
|
+
throw new Error(`Address ${v.address} does not match its public key`);
|
|
26
|
+
}
|
|
27
|
+
if (seen.has(addrHex)) {
|
|
28
|
+
throw new Error("Duplicate entry in validators set");
|
|
29
|
+
}
|
|
30
|
+
seen.add(addrHex);
|
|
31
|
+
cryptoIndex.set(addrHex, key);
|
|
32
|
+
const powerNum = Number(v.voting_power) || Number(v.power);
|
|
33
|
+
if (!Number.isFinite(powerNum) ||
|
|
34
|
+
!Number.isInteger(powerNum) ||
|
|
35
|
+
powerNum < 1) {
|
|
36
|
+
throw new Error(`Invalid voting power for ${addrHex}`);
|
|
37
|
+
}
|
|
38
|
+
countedPower += BigInt(powerNum);
|
|
39
|
+
const protoV = {
|
|
40
|
+
address: new Uint8Array(sha.slice(0, 20)),
|
|
41
|
+
pubKeyBytes: rawKey,
|
|
42
|
+
pubKeyType: "ed25519",
|
|
43
|
+
votingPower: BigInt(powerNum),
|
|
44
|
+
proposerPriority: 0n, // JSON gives string "0"; use 0n by default
|
|
45
|
+
};
|
|
46
|
+
protoValidators.push(protoV);
|
|
47
|
+
}
|
|
48
|
+
const protoSet = {
|
|
49
|
+
validators: protoValidators,
|
|
50
|
+
proposer: undefined,
|
|
51
|
+
totalVotingPower: countedPower,
|
|
52
|
+
};
|
|
53
|
+
return { proto: protoSet, cryptoIndex };
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=validators.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validators.js","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@freedomofpress/cometbft",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "A CometBFT light client for the browser.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
"homepage": "https://github.com/freedomofpress/cometbft-ts#readme",
|
|
28
28
|
"bugs": "https://github.com/freedomofpress/cometbft-ts/issues",
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@vitest/coverage-v8": "^4.0.
|
|
30
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
31
31
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
32
32
|
"typescript": "^5.9.3",
|
|
33
|
-
"typescript-eslint": "^8.
|
|
34
|
-
"vitest": "^4.0.
|
|
33
|
+
"typescript-eslint": "^8.56.1",
|
|
34
|
+
"vitest": "^4.0.18"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@bufbuild/protobuf": "^2.
|
|
37
|
+
"@bufbuild/protobuf": "^2.11.0"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/src/lightclient.ts
CHANGED
|
@@ -12,6 +12,175 @@ import {
|
|
|
12
12
|
} from "./proto/cometbft/types/v1/validator";
|
|
13
13
|
import { Timestamp as PbTimestamp } from "./proto/google/protobuf/timestamp";
|
|
14
14
|
|
|
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
|
+
|
|
15
184
|
export type CryptoIndex = Map<string, CryptoKey>;
|
|
16
185
|
|
|
17
186
|
export interface VerifyOutcome {
|
|
@@ -133,6 +302,13 @@ export async function verifyCommit(
|
|
|
133
302
|
const partsHash: Uint8Array = bid.partSetHeader.hash;
|
|
134
303
|
const partsTotal: number = bid.partSetHeader.total;
|
|
135
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
|
+
}
|
|
311
|
+
|
|
136
312
|
let signedPower = 0n;
|
|
137
313
|
const unknown: string[] = [];
|
|
138
314
|
const invalid: string[] = [];
|
|
@@ -38,7 +38,7 @@ describe("lightclient.verifyCommit", () => {
|
|
|
38
38
|
expect(out.countedSignatures).toBeGreaterThan(0);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
it("
|
|
41
|
+
it("throws when commit block_id.hash does not match the header hash", async () => {
|
|
42
42
|
const vResp = validatorsFixture as unknown as ValidatorJson;
|
|
43
43
|
const { proto: vset, cryptoIndex } = await importValidators(vResp);
|
|
44
44
|
|
|
@@ -48,11 +48,26 @@ 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);
|
|
52
51
|
|
|
53
|
-
expect(
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
);
|
|
56
71
|
});
|
|
57
72
|
|
|
58
73
|
it("drops below 2/3 quorum when two votes are ABSENT", async () => {
|
package/src/tests/webcat.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { importCommit } from "../commit";
|
|
4
|
-
import { Uint8ArrayToBase64
|
|
4
|
+
import { Uint8ArrayToBase64 } from "../encoding";
|
|
5
5
|
import { verifyCommit } from "../lightclient";
|
|
6
6
|
import type { CommitJson, ValidatorJson } from "../types";
|
|
7
7
|
import { importValidators } from "../validators";
|
|
@@ -12,51 +12,34 @@ function clone<T>(x: T): T {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
describe("lightclient.verifyCommit", () => {
|
|
15
|
-
it("
|
|
15
|
+
it("rejects this fixture when header hash does not match commit block_id.hash", async () => {
|
|
16
16
|
const validators = blockFixture.validator_set as unknown as ValidatorJson;
|
|
17
17
|
const commit = blockFixture as unknown as CommitJson;
|
|
18
18
|
|
|
19
19
|
const { proto: vset, cryptoIndex } = await importValidators(validators);
|
|
20
20
|
const sh = importCommit(commit);
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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);
|
|
22
|
+
await expect(verifyCommit(sh, vset, cryptoIndex)).rejects.toThrow(
|
|
23
|
+
/header hash does not match commit blockid hash/i,
|
|
24
|
+
);
|
|
34
25
|
});
|
|
35
26
|
|
|
36
|
-
it("
|
|
27
|
+
it("throws when commit block_id.hash does not match header hash", async () => {
|
|
37
28
|
const validators = blockFixture.validator_set as unknown as ValidatorJson;
|
|
38
29
|
const commit = clone(blockFixture) as unknown as CommitJson;
|
|
39
30
|
|
|
40
|
-
// Flip one byte of the BlockID hash to keep the signature well-formed but
|
|
41
|
-
// cryptographically invalid for the mutated sign-bytes.
|
|
42
31
|
commit.signed_header.commit.block_id.hash =
|
|
43
32
|
"3A1D00CC2A092465E85EA2C24986BEE0105285039DC1873BB6B0CA7F610EC89D";
|
|
44
33
|
|
|
45
34
|
const { proto: vset, cryptoIndex } = await importValidators(validators);
|
|
46
35
|
const sh = importCommit(commit);
|
|
47
36
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
expect(out.ok).toBe(false);
|
|
52
|
-
expect(out.signedPower).toBe(0n);
|
|
53
|
-
expect(out.invalidSignatures).toEqual([
|
|
54
|
-
Uint8ArrayToHex(vset.validators[0].address).toUpperCase(),
|
|
55
|
-
]);
|
|
56
|
-
expect(out.countedSignatures).toBe(1);
|
|
37
|
+
await expect(verifyCommit(sh, vset, cryptoIndex)).rejects.toThrow(
|
|
38
|
+
/header hash does not match commit blockid hash/i,
|
|
39
|
+
);
|
|
57
40
|
});
|
|
58
41
|
|
|
59
|
-
it("
|
|
42
|
+
it("still rejects on header/commit hash mismatch even when a signature is corrupted", async () => {
|
|
60
43
|
const validators = blockFixture.validator_set as unknown as ValidatorJson;
|
|
61
44
|
const commit = clone(blockFixture) as unknown as CommitJson;
|
|
62
45
|
|
|
@@ -67,15 +50,9 @@ describe("lightclient.verifyCommit", () => {
|
|
|
67
50
|
const { proto: vset, cryptoIndex } = await importValidators(validators);
|
|
68
51
|
const sh = importCommit(commit);
|
|
69
52
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
expect(out.ok).toBe(false);
|
|
74
|
-
expect(out.signedPower).toBe(0n);
|
|
75
|
-
expect(out.invalidSignatures).toEqual([
|
|
76
|
-
Uint8ArrayToHex(vset.validators[0].address).toUpperCase(),
|
|
77
|
-
]);
|
|
78
|
-
expect(out.countedSignatures).toBe(1);
|
|
53
|
+
await expect(verifyCommit(sh, vset, cryptoIndex)).rejects.toThrow(
|
|
54
|
+
/header hash does not match commit blockid hash/i,
|
|
55
|
+
);
|
|
79
56
|
});
|
|
80
57
|
|
|
81
58
|
it("rejects malformed signature bytes", async () => {
|
|
@@ -90,7 +67,7 @@ describe("lightclient.verifyCommit", () => {
|
|
|
90
67
|
);
|
|
91
68
|
});
|
|
92
69
|
|
|
93
|
-
it("
|
|
70
|
+
it("still rejects on header/commit hash mismatch even when validator is unknown", async () => {
|
|
94
71
|
const validators = blockFixture.validator_set as unknown as ValidatorJson;
|
|
95
72
|
const commit = clone(blockFixture) as unknown as CommitJson;
|
|
96
73
|
|
|
@@ -100,15 +77,8 @@ describe("lightclient.verifyCommit", () => {
|
|
|
100
77
|
const { proto: vset, cryptoIndex } = await importValidators(validators);
|
|
101
78
|
const sh = importCommit(commit);
|
|
102
79
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
expect(out.ok).toBe(false);
|
|
107
|
-
expect(out.signedPower).toBe(0n);
|
|
108
|
-
expect(out.invalidSignatures).toEqual([]);
|
|
109
|
-
expect(out.unknownValidators).toEqual([
|
|
110
|
-
"0000000000000000000000000000000000000000",
|
|
111
|
-
]);
|
|
112
|
-
expect(out.countedSignatures).toBe(0);
|
|
80
|
+
await expect(verifyCommit(sh, vset, cryptoIndex)).rejects.toThrow(
|
|
81
|
+
/header hash does not match commit blockid hash/i,
|
|
82
|
+
);
|
|
113
83
|
});
|
|
114
84
|
});
|