@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.
Files changed (64) hide show
  1. package/dist/commit.d.ts +7 -0
  2. package/dist/commit.js +175 -0
  3. package/dist/commit.js.map +1 -0
  4. package/dist/encoding.d.ts +4 -0
  5. package/dist/encoding.js +31 -0
  6. package/dist/encoding.js.map +1 -0
  7. package/dist/lightclient.d.ts +17 -0
  8. package/dist/lightclient.js +275 -0
  9. package/dist/lightclient.js.map +1 -0
  10. package/dist/proto/cometbft/crypto/v1/keys.d.ts +28 -0
  11. package/dist/proto/cometbft/crypto/v1/keys.js +110 -0
  12. package/dist/proto/cometbft/crypto/v1/keys.js.map +1 -0
  13. package/dist/proto/cometbft/crypto/v1/proof.d.ts +60 -0
  14. package/dist/proto/cometbft/crypto/v1/proof.js +416 -0
  15. package/dist/proto/cometbft/crypto/v1/proof.js.map +1 -0
  16. package/dist/proto/cometbft/types/v1/canonical.d.ts +85 -0
  17. package/dist/proto/cometbft/types/v1/canonical.js +586 -0
  18. package/dist/proto/cometbft/types/v1/canonical.js.map +1 -0
  19. package/dist/proto/cometbft/types/v1/types.d.ts +206 -0
  20. package/dist/proto/cometbft/types/v1/types.js +1794 -0
  21. package/dist/proto/cometbft/types/v1/types.js.map +1 -0
  22. package/dist/proto/cometbft/types/v1/validator.d.ts +64 -0
  23. package/dist/proto/cometbft/types/v1/validator.js +382 -0
  24. package/dist/proto/cometbft/types/v1/validator.js.map +1 -0
  25. package/dist/proto/cometbft/version/v1/types.d.ts +41 -0
  26. package/dist/proto/cometbft/version/v1/types.js +154 -0
  27. package/dist/proto/cometbft/version/v1/types.js.map +1 -0
  28. package/dist/proto/gogoproto/gogo.d.ts +1 -0
  29. package/dist/proto/gogoproto/gogo.js +8 -0
  30. package/dist/proto/gogoproto/gogo.js.map +1 -0
  31. package/dist/proto/google/protobuf/descriptor.d.ts +1228 -0
  32. package/dist/proto/google/protobuf/descriptor.js +5056 -0
  33. package/dist/proto/google/protobuf/descriptor.js.map +1 -0
  34. package/dist/proto/google/protobuf/timestamp.d.ts +128 -0
  35. package/dist/proto/google/protobuf/timestamp.js +83 -0
  36. package/dist/proto/google/protobuf/timestamp.js.map +1 -0
  37. package/dist/tests/commit.test.d.ts +1 -0
  38. package/dist/tests/commit.test.js +219 -0
  39. package/dist/tests/commit.test.js.map +1 -0
  40. package/dist/tests/encoding.test.d.ts +1 -0
  41. package/dist/tests/encoding.test.js +31 -0
  42. package/dist/tests/encoding.test.js.map +1 -0
  43. package/dist/tests/fixtures/commit-12.json +64 -0
  44. package/dist/tests/fixtures/validators-12.json +41 -0
  45. package/dist/tests/fixtures/webcat.json +71 -0
  46. package/dist/tests/lightclient.test.d.ts +1 -0
  47. package/dist/tests/lightclient.test.js +234 -0
  48. package/dist/tests/lightclient.test.js.map +1 -0
  49. package/dist/tests/validators.test.d.ts +1 -0
  50. package/dist/tests/validators.test.js +184 -0
  51. package/dist/tests/validators.test.js.map +1 -0
  52. package/dist/tests/webcat.test.d.ts +1 -0
  53. package/dist/tests/webcat.test.js +52 -0
  54. package/dist/tests/webcat.test.js.map +1 -0
  55. package/dist/types.d.ts +62 -0
  56. package/dist/types.js +2 -0
  57. package/dist/types.js.map +1 -0
  58. package/dist/validators.d.ts +6 -0
  59. package/dist/validators.js +55 -0
  60. package/dist/validators.js.map +1 -0
  61. package/package.json +5 -5
  62. package/src/lightclient.ts +176 -0
  63. package/src/tests/lightclient.test.ts +20 -5
  64. 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.0",
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.13",
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.48.0",
34
- "vitest": "^4.0.13"
33
+ "typescript-eslint": "^8.56.1",
34
+ "vitest": "^4.0.18"
35
35
  },
36
36
  "dependencies": {
37
- "@bufbuild/protobuf": "^2.10.1"
37
+ "@bufbuild/protobuf": "^2.11.0"
38
38
  }
39
39
  }
@@ -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("fails quorum and invalidates all signatures when block_id.hash is tampered", async () => {
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(out.quorum).toBe(false);
54
- expect(out.invalidSignatures.length).toBe(out.countedSignatures);
55
- expect(out.ok).toBe(false);
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 () => {
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
3
  import { importCommit } from "../commit";
4
- import { Uint8ArrayToBase64, Uint8ArrayToHex } from "../encoding";
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("verifies a valid commit against the validator set", async () => {
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
- const out = await verifyCommit(sh, vset, cryptoIndex);
23
-
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);
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("flags invalid signatures", async () => {
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
- const out = await verifyCommit(sh, vset, cryptoIndex);
49
-
50
- expect(out.quorum).toBe(false);
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("flags invalid signatures", async () => {
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
- const out = await verifyCommit(sh, vset, cryptoIndex);
71
-
72
- expect(out.quorum).toBe(false);
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("reports unknown validators", async () => {
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
- const out = await verifyCommit(sh, vset, cryptoIndex);
104
-
105
- expect(out.quorum).toBe(false);
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
  });