@inversealtruism/csd-light 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/README.md +11 -0
- package/dist/index.cjs +126 -40
- package/dist/index.d.cts +54 -12
- package/dist/index.d.ts +54 -12
- package/dist/index.js +127 -41
- package/package.json +5 -4
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @inversealtruism/csd-light
|
|
2
|
+
|
|
3
|
+
Light client: headers-first sync with client-side PoW + LWMA + chainwork verification, merkle-inclusion proofs, reorg + checkpoint-start, and a verified-RPC facade with an honest trustLevel.
|
|
4
|
+
|
|
5
|
+
Part of the [Compute Substrate SDK](https://github.com/InverseAltruism/csd-sdk) (L0). Zero `Buffer` — runs in Node, browsers, and MV3 service workers. Deps: `@noble/*` only.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm i @inversealtruism/csd-light
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
See the [repo README](https://github.com/InverseAltruism/csd-sdk#readme) and [examples/quickstart.mjs](https://github.com/InverseAltruism/csd-sdk/blob/master/examples/quickstart.mjs) for usage. MIT.
|
package/dist/index.cjs
CHANGED
|
@@ -23,6 +23,7 @@ __export(index_exports, {
|
|
|
23
23
|
CsdClient: () => import_csd_client2.CsdClient,
|
|
24
24
|
LightClient: () => LightClient,
|
|
25
25
|
expectedBits: () => expectedBits,
|
|
26
|
+
expectedBitsFromWindow: () => expectedBitsFromWindow,
|
|
26
27
|
rpcHeaderToHeader: () => import_csd_client2.rpcHeaderToHeader
|
|
27
28
|
});
|
|
28
29
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -32,29 +33,23 @@ var import_csd_client = require("@inversealtruism/csd-client");
|
|
|
32
33
|
// src/lwma.ts
|
|
33
34
|
var import_csd_codec = require("@inversealtruism/csd-codec");
|
|
34
35
|
var POW_LIMIT_TARGET = (0, import_csd_codec.targetToBigInt)((0, import_csd_codec.bitsToTarget)(import_csd_codec.POW_LIMIT_BITS));
|
|
35
|
-
function
|
|
36
|
+
function expectedBitsFromWindow(window, height) {
|
|
36
37
|
if (height === 0) return import_csd_codec.INITIAL_BITS;
|
|
37
|
-
const parent =
|
|
38
|
-
if (!parent) throw new Error(`expectedBits:
|
|
38
|
+
const parent = window[window.length - 1];
|
|
39
|
+
if (!parent) throw new Error(`expectedBits: empty window for height ${height}`);
|
|
39
40
|
if (height < 2) return parent.bits;
|
|
40
|
-
|
|
41
|
+
const n = Math.min(import_csd_codec.LWMA_WINDOW, height, window.length);
|
|
41
42
|
if (n < 2) return parent.bits;
|
|
42
|
-
|
|
43
|
+
const w = window.slice(window.length - n);
|
|
43
44
|
const times = [];
|
|
44
45
|
const targets = [];
|
|
45
|
-
for (
|
|
46
|
-
const idx = height - 1 - i;
|
|
47
|
-
if (idx < 0) break;
|
|
48
|
-
const h = headers[idx];
|
|
46
|
+
for (const h of w) {
|
|
49
47
|
const tb = (0, import_csd_codec.bitsToTarget)(h.bits);
|
|
50
48
|
if (tb.every((b) => b === 0)) throw new Error("expectedBits: invalid compact bits in window");
|
|
51
49
|
times.push(BigInt(h.time));
|
|
52
50
|
targets.push((0, import_csd_codec.targetToBigInt)(tb));
|
|
53
|
-
if (idx === 0) break;
|
|
54
51
|
}
|
|
55
52
|
if (times.length < 2) return parent.bits;
|
|
56
|
-
times.reverse();
|
|
57
|
-
targets.reverse();
|
|
58
53
|
const m = times.length;
|
|
59
54
|
const t = BigInt(Math.max(import_csd_codec.TARGET_BLOCK_SECS, 1));
|
|
60
55
|
const maxSolve = BigInt(Math.max(import_csd_codec.LWMA_SOLVETIME_MAX_FACTOR, 1) * Math.max(import_csd_codec.TARGET_BLOCK_SECS, 1));
|
|
@@ -63,9 +58,9 @@ function expectedBits(headers, height) {
|
|
|
63
58
|
let dt = times[i] - times[i - 1];
|
|
64
59
|
if (dt < 0n) dt = 0n;
|
|
65
60
|
const st = dt < 1n ? 1n : dt > maxSolve ? maxSolve : dt;
|
|
66
|
-
const
|
|
67
|
-
weightedSum += st *
|
|
68
|
-
denom +=
|
|
61
|
+
const ww = BigInt(i);
|
|
62
|
+
weightedSum += st * ww;
|
|
63
|
+
denom += ww;
|
|
69
64
|
}
|
|
70
65
|
if (denom === 0n) return parent.bits;
|
|
71
66
|
const avgSolvetime = weightedSum / denom;
|
|
@@ -79,6 +74,12 @@ function expectedBits(headers, height) {
|
|
|
79
74
|
if ((0, import_csd_codec.targetToBigInt)((0, import_csd_codec.bitsToTarget)(bits)) > POW_LIMIT_TARGET) return import_csd_codec.POW_LIMIT_BITS;
|
|
80
75
|
return bits;
|
|
81
76
|
}
|
|
77
|
+
function expectedBits(headers, height) {
|
|
78
|
+
if (height === 0) return import_csd_codec.INITIAL_BITS;
|
|
79
|
+
const n = Math.min(import_csd_codec.LWMA_WINDOW, height);
|
|
80
|
+
const window = headers.slice(height - n, height);
|
|
81
|
+
return expectedBitsFromWindow(window, height);
|
|
82
|
+
}
|
|
82
83
|
|
|
83
84
|
// src/index.ts
|
|
84
85
|
var import_csd_client2 = require("@inversealtruism/csd-client");
|
|
@@ -86,8 +87,10 @@ var LightClient = class {
|
|
|
86
87
|
client;
|
|
87
88
|
provider;
|
|
88
89
|
checkpoints;
|
|
89
|
-
/** Verified header chain
|
|
90
|
+
/** Verified header chain. chain[i].height = baseHeight + i. */
|
|
90
91
|
chain = [];
|
|
92
|
+
/** Height of chain[0] — 0 for genesis-start, the seed start for checkpoint-start. */
|
|
93
|
+
baseHeight = 0;
|
|
91
94
|
constructor(opts = {}) {
|
|
92
95
|
this.client = opts.client ?? (opts.baseUrl ? new import_csd_client.CsdClient({ baseUrl: opts.baseUrl }) : void 0);
|
|
93
96
|
this.checkpoints = opts.checkpoints ?? {};
|
|
@@ -103,12 +106,22 @@ var LightClient = class {
|
|
|
103
106
|
get chainwork() {
|
|
104
107
|
return this.tip?.chainwork ?? 0n;
|
|
105
108
|
}
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
/** Whether every header back to genesis was verified (vs trusted from a checkpoint). */
|
|
110
|
+
get fullyVerified() {
|
|
111
|
+
return this.baseHeight === 0;
|
|
112
|
+
}
|
|
113
|
+
at(height) {
|
|
114
|
+
return this.chain[height - this.baseHeight];
|
|
115
|
+
}
|
|
116
|
+
/** The chronological LWMA window (≤ LWMA_WINDOW headers) immediately preceding `height`. */
|
|
117
|
+
windowBefore(height) {
|
|
118
|
+
const startIdx = Math.max(0, height - this.baseHeight - import_csd_codec2.LWMA_WINDOW);
|
|
119
|
+
const endIdx = height - this.baseHeight;
|
|
120
|
+
return this.chain.slice(startIdx, endIdx).map((c) => c.header);
|
|
121
|
+
}
|
|
122
|
+
/** Sync + VERIFY headers [from..to] from genesis (or contiguous to the current tip). */
|
|
123
|
+
async sync(to, from = this.baseHeight + this.chain.length) {
|
|
124
|
+
if (from !== this.baseHeight + this.chain.length) throw new Error(`non-contiguous sync: tip ${this.baseHeight + this.chain.length - 1}, asked from ${from}`);
|
|
112
125
|
for (let h = from; h <= to; h++) {
|
|
113
126
|
const { header, hash } = await this.provider(h);
|
|
114
127
|
this.ingest(h, header, hash);
|
|
@@ -116,27 +129,99 @@ var LightClient = class {
|
|
|
116
129
|
if (!this.tip) throw new Error("sync produced no tip");
|
|
117
130
|
return this.tip;
|
|
118
131
|
}
|
|
119
|
-
/** Verify a single header at
|
|
132
|
+
/** Verify a single header at `height` and append it (full consensus checks). */
|
|
120
133
|
ingest(height, header, claimedHash) {
|
|
121
|
-
if (height !== this.chain.length) throw new Error(`out-of-order ingest at ${height} (
|
|
134
|
+
if (height !== this.baseHeight + this.chain.length) throw new Error(`out-of-order ingest at ${height} (tip ${this.baseHeight + this.chain.length - 1})`);
|
|
135
|
+
const vh = this.verifyOne(height, header, this.windowBefore(height), this.at(height - 1), claimedHash);
|
|
136
|
+
this.chain.push(vh);
|
|
137
|
+
return vh;
|
|
138
|
+
}
|
|
139
|
+
/** Pure verification of one header against a window + parent (no mutation). */
|
|
140
|
+
verifyOne(height, header, window, parent, claimedHash) {
|
|
122
141
|
const hash = (0, import_csd_codec2.headerHash)(header);
|
|
123
142
|
if (claimedHash && claimedHash.toLowerCase() !== hash.toLowerCase()) throw new Error(`header hash mismatch at ${height}`);
|
|
124
143
|
if (height === 0) {
|
|
125
144
|
if (hash.toLowerCase() !== import_csd_codec2.GENESIS_HASH.toLowerCase()) throw new Error(`foreign genesis: ${hash}`);
|
|
126
145
|
if (header.bits !== import_csd_codec2.INITIAL_BITS) throw new Error("genesis bits != INITIAL_BITS");
|
|
127
146
|
} else {
|
|
128
|
-
|
|
147
|
+
if (!parent) throw new Error(`no parent context for height ${height}`);
|
|
129
148
|
if (header.prev.toLowerCase() !== parent.hash.toLowerCase()) throw new Error(`broken prev link at ${height}`);
|
|
130
|
-
const exp =
|
|
149
|
+
const exp = expectedBitsFromWindow(window, height);
|
|
131
150
|
if (header.bits !== exp) throw new Error(`bad bits at ${height}: header ${header.bits.toString(16)} != LWMA ${exp.toString(16)}`);
|
|
132
151
|
}
|
|
133
152
|
if (!(0, import_csd_codec2.powOk)((0, import_csd_codec2.headerHashBytes)(header), header.bits)) throw new Error(`invalid PoW at ${height}`);
|
|
134
153
|
const cp = this.checkpoints[height];
|
|
135
154
|
if (cp && cp.toLowerCase() !== hash.toLowerCase()) throw new Error(`checkpoint mismatch at ${height}`);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
155
|
+
return { height, hash, header, chainwork: (parent?.chainwork ?? 0n) + (0, import_csd_codec2.workForBits)(header.bits) };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Seed a TRUSTED, contiguous header run ending at a pinned checkpoint, so forward sync needs
|
|
159
|
+
* only a small window — not a 27k-block genesis fetch. The seed is the trust anchor (PoW links
|
|
160
|
+
* are still spot-checked, but seed bits aren't LWMA-re-derived; that's the explicit trade for
|
|
161
|
+
* not syncing from genesis). chainwork becomes RELATIVE to the seed. `checkpointHash` MUST match
|
|
162
|
+
* the last seeded header.
|
|
163
|
+
*/
|
|
164
|
+
seedTrusted(seed, checkpointHash) {
|
|
165
|
+
if (this.chain.length) throw new Error("seedTrusted must be called on a fresh client");
|
|
166
|
+
if (!seed.length) throw new Error("empty seed");
|
|
167
|
+
this.baseHeight = seed[0].height;
|
|
168
|
+
let prevHash = null;
|
|
169
|
+
for (let i = 0; i < seed.length; i++) {
|
|
170
|
+
const s = seed[i];
|
|
171
|
+
if (s.height !== this.baseHeight + i) throw new Error("seed not contiguous");
|
|
172
|
+
const hash = (0, import_csd_codec2.headerHash)(s.header);
|
|
173
|
+
if (s.hash && s.hash.toLowerCase() !== hash.toLowerCase()) throw new Error(`seed header hash mismatch at ${s.height}`);
|
|
174
|
+
if (prevHash && s.header.prev.toLowerCase() !== prevHash.toLowerCase()) throw new Error(`seed prev link broken at ${s.height}`);
|
|
175
|
+
if (!(0, import_csd_codec2.powOk)((0, import_csd_codec2.headerHashBytes)(s.header), s.header.bits)) throw new Error(`seed PoW invalid at ${s.height}`);
|
|
176
|
+
this.chain.push({ height: s.height, hash, header: s.header, chainwork: (this.chain[i - 1]?.chainwork ?? 0n) + (0, import_csd_codec2.workForBits)(s.header.bits), trusted: true });
|
|
177
|
+
prevHash = hash;
|
|
178
|
+
}
|
|
179
|
+
if (this.tip.hash.toLowerCase() !== checkpointHash.toLowerCase()) throw new Error(`checkpoint hash mismatch: seeded tip ${this.tip.hash} != ${checkpointHash}`);
|
|
180
|
+
}
|
|
181
|
+
/** Fetch + seed the LWMA window ending at `checkpointHeight`, asserting its hash, then ready to sync forward. */
|
|
182
|
+
async syncFromCheckpoint(checkpointHeight, checkpointHash, context = import_csd_codec2.LWMA_WINDOW) {
|
|
183
|
+
const start = Math.max(0, checkpointHeight - context);
|
|
184
|
+
const seed = [];
|
|
185
|
+
for (let h = start; h <= checkpointHeight; h++) {
|
|
186
|
+
const { header, hash } = await this.provider(h);
|
|
187
|
+
seed.push({ height: h, header, hash });
|
|
188
|
+
}
|
|
189
|
+
this.seedTrusted(seed, checkpointHash);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Offer a competing branch (contiguous headers starting one above a common ancestor we hold).
|
|
193
|
+
* Verifies it from the ancestor; if its cumulative chainwork EXCEEDS our current tip, we roll
|
|
194
|
+
* back to the ancestor and adopt it (max-work rule). Otherwise we keep our chain.
|
|
195
|
+
*/
|
|
196
|
+
tryReorg(alt) {
|
|
197
|
+
if (!alt.length) return { adopted: false, reason: "empty branch" };
|
|
198
|
+
const ancestorHeight = alt[0].height - 1;
|
|
199
|
+
const ancestor = this.at(ancestorHeight);
|
|
200
|
+
if (!ancestor) return { adopted: false, reason: `no common ancestor at ${ancestorHeight}` };
|
|
201
|
+
const verified = [];
|
|
202
|
+
let prev = ancestor;
|
|
203
|
+
const baseWindow = this.windowBefore(ancestorHeight + 1);
|
|
204
|
+
const window = [...baseWindow];
|
|
205
|
+
for (let i = 0; i < alt.length; i++) {
|
|
206
|
+
const a = alt[i];
|
|
207
|
+
if (a.height !== ancestorHeight + 1 + i) return { adopted: false, reason: "alt not contiguous" };
|
|
208
|
+
let vh;
|
|
209
|
+
try {
|
|
210
|
+
vh = this.verifyOne(a.height, a.header, window, prev, a.hash);
|
|
211
|
+
} catch (e) {
|
|
212
|
+
return { adopted: false, reason: `alt invalid at ${a.height}: ${e?.message}` };
|
|
213
|
+
}
|
|
214
|
+
verified.push(vh);
|
|
215
|
+
prev = vh;
|
|
216
|
+
window.push(a.header);
|
|
217
|
+
if (window.length > import_csd_codec2.LWMA_WINDOW) window.shift();
|
|
218
|
+
}
|
|
219
|
+
const altTip = verified[verified.length - 1];
|
|
220
|
+
if (altTip.chainwork <= this.chainwork) return { adopted: false, reason: `alt work ${altTip.chainwork} \u2264 current ${this.chainwork}` };
|
|
221
|
+
const rolledBack = this.baseHeight + this.chain.length - 1 - ancestorHeight;
|
|
222
|
+
this.chain.length = ancestorHeight - this.baseHeight + 1;
|
|
223
|
+
for (const v of verified) this.chain.push(v);
|
|
224
|
+
return { adopted: true, rolledBack, newTip: altTip.height };
|
|
140
225
|
}
|
|
141
226
|
/** Verify a tx's inclusion against a verified header (merkle proof built from the block). */
|
|
142
227
|
async verifyTxInclusion(txidHex) {
|
|
@@ -144,26 +229,26 @@ var LightClient = class {
|
|
|
144
229
|
const t = await this.client.tx(txidHex);
|
|
145
230
|
if (!t.ok || t.height == null) return { trustLevel: "rpc-trusted", included: false, reason: "tx not in a block (mempool/unknown)" };
|
|
146
231
|
const height = t.height;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
232
|
+
const tipHeight = this.baseHeight + this.chain.length - 1;
|
|
233
|
+
if (height < this.baseHeight) return { trustLevel: "rpc-trusted", included: false, reason: `tx below the synced base (${this.baseHeight})` };
|
|
234
|
+
if (height > tipHeight) {
|
|
235
|
+
const gap = height - tipHeight;
|
|
236
|
+
if (gap > 256) return { trustLevel: "rpc-trusted", included: false, reason: `tx at ${height} is ${gap} blocks beyond tip \u2014 sync(${height}) first` };
|
|
150
237
|
await this.sync(height);
|
|
151
238
|
}
|
|
152
|
-
const verified = this.
|
|
239
|
+
const verified = this.at(height);
|
|
153
240
|
if (!verified) return { trustLevel: "rpc-trusted", included: false, reason: "could not verify the containing header" };
|
|
154
241
|
const b = await this.client.blockByHeight(height);
|
|
155
242
|
const txids = b.txs.map((x) => x.txid);
|
|
156
243
|
const pos = txids.findIndex((x) => x.toLowerCase() === txidHex.toLowerCase());
|
|
157
244
|
if (pos < 0) return { trustLevel: "rpc-trusted", included: false, reason: "tx not listed in block" };
|
|
158
|
-
const
|
|
159
|
-
const ok = (0, import_csd_codec2.verifyMerkleProof)(txidHex, pos, branch, verified.header.merkle);
|
|
245
|
+
const ok = (0, import_csd_codec2.verifyMerkleProof)(txidHex, pos, (0, import_csd_codec2.merkleBranch)(txids, pos), verified.header.merkle);
|
|
160
246
|
if (!ok) return { trustLevel: "rpc-trusted", included: false, reason: "merkle proof failed" };
|
|
161
|
-
return { trustLevel: "verified-inclusion", included: true, blockHeight: height, confirmations:
|
|
247
|
+
return { trustLevel: "verified-inclusion", included: true, blockHeight: height, confirmations: tipHeight - height + 1 };
|
|
162
248
|
}
|
|
163
249
|
/**
|
|
164
|
-
* Balance for an address. HONEST:
|
|
165
|
-
*
|
|
166
|
-
* style block scan (`trustLevel: 'scanned'`). Surfaced, never hidden.
|
|
250
|
+
* Balance for an address. HONEST: `rpc-trusted` — a header chain cannot prove an output is still
|
|
251
|
+
* unspent (no UTXO commitment). A future Neutrino-style scan would yield `trustLevel:'scanned'`.
|
|
167
252
|
*/
|
|
168
253
|
async balance(addr) {
|
|
169
254
|
if (!this.client) throw new Error("no client");
|
|
@@ -176,5 +261,6 @@ var LightClient = class {
|
|
|
176
261
|
CsdClient,
|
|
177
262
|
LightClient,
|
|
178
263
|
expectedBits,
|
|
264
|
+
expectedBitsFromWindow,
|
|
179
265
|
rpcHeaderToHeader
|
|
180
266
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -2,9 +2,16 @@ import { BlockHeader } from '@inversealtruism/csd-codec';
|
|
|
2
2
|
import { CsdClient } from '@inversealtruism/csd-client';
|
|
3
3
|
export { CsdClient, RpcTxJson, rpcHeaderToHeader } from '@inversealtruism/csd-client';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Expected `bits` for the block at `height`, given the CHRONOLOGICAL window of the (up to
|
|
7
|
+
* LWMA_WINDOW) headers immediately preceding it — `window[last]` is the parent (height-1).
|
|
8
|
+
* This is the core; works for any height with only a 45-header window (cheap spot-checks /
|
|
9
|
+
* checkpoint-start, no full chain needed). Mirrors expected_bits_strict exactly.
|
|
10
|
+
*/
|
|
11
|
+
declare function expectedBitsFromWindow(window: BlockHeader[], height: number): number;
|
|
5
12
|
/**
|
|
6
13
|
* Expected `bits` for the block at `height`, given the canonical chain `headers[0..=parent]`
|
|
7
|
-
* (index = height
|
|
14
|
+
* (index = height). Convenience wrapper over expectedBitsFromWindow.
|
|
8
15
|
*/
|
|
9
16
|
declare function expectedBits(headers: BlockHeader[], height: number): number;
|
|
10
17
|
|
|
@@ -14,6 +21,7 @@ interface VerifiedHeader {
|
|
|
14
21
|
hash: string;
|
|
15
22
|
header: BlockHeader;
|
|
16
23
|
chainwork: bigint;
|
|
24
|
+
trusted?: boolean;
|
|
17
25
|
}
|
|
18
26
|
interface InclusionResult {
|
|
19
27
|
trustLevel: TrustLevel;
|
|
@@ -22,7 +30,12 @@ interface InclusionResult {
|
|
|
22
30
|
confirmations?: number;
|
|
23
31
|
reason?: string;
|
|
24
32
|
}
|
|
25
|
-
|
|
33
|
+
interface ReorgResult {
|
|
34
|
+
adopted: boolean;
|
|
35
|
+
rolledBack?: number;
|
|
36
|
+
newTip?: number;
|
|
37
|
+
reason?: string;
|
|
38
|
+
}
|
|
26
39
|
type HeaderProvider = (height: number) => Promise<{
|
|
27
40
|
header: BlockHeader;
|
|
28
41
|
hash: string;
|
|
@@ -39,24 +52,53 @@ declare class LightClient {
|
|
|
39
52
|
private readonly client?;
|
|
40
53
|
private readonly provider;
|
|
41
54
|
private readonly checkpoints;
|
|
42
|
-
/** Verified header chain
|
|
55
|
+
/** Verified header chain. chain[i].height = baseHeight + i. */
|
|
43
56
|
readonly chain: VerifiedHeader[];
|
|
57
|
+
/** Height of chain[0] — 0 for genesis-start, the seed start for checkpoint-start. */
|
|
58
|
+
baseHeight: number;
|
|
44
59
|
constructor(opts?: LightClientOptions);
|
|
45
60
|
get tip(): VerifiedHeader | undefined;
|
|
46
61
|
get chainwork(): bigint;
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
/** Whether every header back to genesis was verified (vs trusted from a checkpoint). */
|
|
63
|
+
get fullyVerified(): boolean;
|
|
64
|
+
private at;
|
|
65
|
+
/** The chronological LWMA window (≤ LWMA_WINDOW headers) immediately preceding `height`. */
|
|
66
|
+
private windowBefore;
|
|
67
|
+
/** Sync + VERIFY headers [from..to] from genesis (or contiguous to the current tip). */
|
|
51
68
|
sync(to: number, from?: number): Promise<VerifiedHeader>;
|
|
52
|
-
/** Verify a single header at
|
|
69
|
+
/** Verify a single header at `height` and append it (full consensus checks). */
|
|
53
70
|
ingest(height: number, header: BlockHeader, claimedHash?: string): VerifiedHeader;
|
|
71
|
+
/** Pure verification of one header against a window + parent (no mutation). */
|
|
72
|
+
private verifyOne;
|
|
73
|
+
/**
|
|
74
|
+
* Seed a TRUSTED, contiguous header run ending at a pinned checkpoint, so forward sync needs
|
|
75
|
+
* only a small window — not a 27k-block genesis fetch. The seed is the trust anchor (PoW links
|
|
76
|
+
* are still spot-checked, but seed bits aren't LWMA-re-derived; that's the explicit trade for
|
|
77
|
+
* not syncing from genesis). chainwork becomes RELATIVE to the seed. `checkpointHash` MUST match
|
|
78
|
+
* the last seeded header.
|
|
79
|
+
*/
|
|
80
|
+
seedTrusted(seed: {
|
|
81
|
+
height: number;
|
|
82
|
+
header: BlockHeader;
|
|
83
|
+
hash?: string;
|
|
84
|
+
}[], checkpointHash: string): void;
|
|
85
|
+
/** Fetch + seed the LWMA window ending at `checkpointHeight`, asserting its hash, then ready to sync forward. */
|
|
86
|
+
syncFromCheckpoint(checkpointHeight: number, checkpointHash: string, context?: number): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Offer a competing branch (contiguous headers starting one above a common ancestor we hold).
|
|
89
|
+
* Verifies it from the ancestor; if its cumulative chainwork EXCEEDS our current tip, we roll
|
|
90
|
+
* back to the ancestor and adopt it (max-work rule). Otherwise we keep our chain.
|
|
91
|
+
*/
|
|
92
|
+
tryReorg(alt: {
|
|
93
|
+
height: number;
|
|
94
|
+
header: BlockHeader;
|
|
95
|
+
hash?: string;
|
|
96
|
+
}[]): ReorgResult;
|
|
54
97
|
/** Verify a tx's inclusion against a verified header (merkle proof built from the block). */
|
|
55
98
|
verifyTxInclusion(txidHex: string): Promise<InclusionResult>;
|
|
56
99
|
/**
|
|
57
|
-
* Balance for an address. HONEST:
|
|
58
|
-
*
|
|
59
|
-
* style block scan (`trustLevel: 'scanned'`). Surfaced, never hidden.
|
|
100
|
+
* Balance for an address. HONEST: `rpc-trusted` — a header chain cannot prove an output is still
|
|
101
|
+
* unspent (no UTXO commitment). A future Neutrino-style scan would yield `trustLevel:'scanned'`.
|
|
60
102
|
*/
|
|
61
103
|
balance(addr: string): Promise<{
|
|
62
104
|
confirmed: number;
|
|
@@ -65,4 +107,4 @@ declare class LightClient {
|
|
|
65
107
|
}>;
|
|
66
108
|
}
|
|
67
109
|
|
|
68
|
-
export { type HeaderProvider, type InclusionResult, LightClient, type LightClientOptions, type TrustLevel, type VerifiedHeader, expectedBits };
|
|
110
|
+
export { type HeaderProvider, type InclusionResult, LightClient, type LightClientOptions, type ReorgResult, type TrustLevel, type VerifiedHeader, expectedBits, expectedBitsFromWindow };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,9 +2,16 @@ import { BlockHeader } from '@inversealtruism/csd-codec';
|
|
|
2
2
|
import { CsdClient } from '@inversealtruism/csd-client';
|
|
3
3
|
export { CsdClient, RpcTxJson, rpcHeaderToHeader } from '@inversealtruism/csd-client';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Expected `bits` for the block at `height`, given the CHRONOLOGICAL window of the (up to
|
|
7
|
+
* LWMA_WINDOW) headers immediately preceding it — `window[last]` is the parent (height-1).
|
|
8
|
+
* This is the core; works for any height with only a 45-header window (cheap spot-checks /
|
|
9
|
+
* checkpoint-start, no full chain needed). Mirrors expected_bits_strict exactly.
|
|
10
|
+
*/
|
|
11
|
+
declare function expectedBitsFromWindow(window: BlockHeader[], height: number): number;
|
|
5
12
|
/**
|
|
6
13
|
* Expected `bits` for the block at `height`, given the canonical chain `headers[0..=parent]`
|
|
7
|
-
* (index = height
|
|
14
|
+
* (index = height). Convenience wrapper over expectedBitsFromWindow.
|
|
8
15
|
*/
|
|
9
16
|
declare function expectedBits(headers: BlockHeader[], height: number): number;
|
|
10
17
|
|
|
@@ -14,6 +21,7 @@ interface VerifiedHeader {
|
|
|
14
21
|
hash: string;
|
|
15
22
|
header: BlockHeader;
|
|
16
23
|
chainwork: bigint;
|
|
24
|
+
trusted?: boolean;
|
|
17
25
|
}
|
|
18
26
|
interface InclusionResult {
|
|
19
27
|
trustLevel: TrustLevel;
|
|
@@ -22,7 +30,12 @@ interface InclusionResult {
|
|
|
22
30
|
confirmations?: number;
|
|
23
31
|
reason?: string;
|
|
24
32
|
}
|
|
25
|
-
|
|
33
|
+
interface ReorgResult {
|
|
34
|
+
adopted: boolean;
|
|
35
|
+
rolledBack?: number;
|
|
36
|
+
newTip?: number;
|
|
37
|
+
reason?: string;
|
|
38
|
+
}
|
|
26
39
|
type HeaderProvider = (height: number) => Promise<{
|
|
27
40
|
header: BlockHeader;
|
|
28
41
|
hash: string;
|
|
@@ -39,24 +52,53 @@ declare class LightClient {
|
|
|
39
52
|
private readonly client?;
|
|
40
53
|
private readonly provider;
|
|
41
54
|
private readonly checkpoints;
|
|
42
|
-
/** Verified header chain
|
|
55
|
+
/** Verified header chain. chain[i].height = baseHeight + i. */
|
|
43
56
|
readonly chain: VerifiedHeader[];
|
|
57
|
+
/** Height of chain[0] — 0 for genesis-start, the seed start for checkpoint-start. */
|
|
58
|
+
baseHeight: number;
|
|
44
59
|
constructor(opts?: LightClientOptions);
|
|
45
60
|
get tip(): VerifiedHeader | undefined;
|
|
46
61
|
get chainwork(): bigint;
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
/** Whether every header back to genesis was verified (vs trusted from a checkpoint). */
|
|
63
|
+
get fullyVerified(): boolean;
|
|
64
|
+
private at;
|
|
65
|
+
/** The chronological LWMA window (≤ LWMA_WINDOW headers) immediately preceding `height`. */
|
|
66
|
+
private windowBefore;
|
|
67
|
+
/** Sync + VERIFY headers [from..to] from genesis (or contiguous to the current tip). */
|
|
51
68
|
sync(to: number, from?: number): Promise<VerifiedHeader>;
|
|
52
|
-
/** Verify a single header at
|
|
69
|
+
/** Verify a single header at `height` and append it (full consensus checks). */
|
|
53
70
|
ingest(height: number, header: BlockHeader, claimedHash?: string): VerifiedHeader;
|
|
71
|
+
/** Pure verification of one header against a window + parent (no mutation). */
|
|
72
|
+
private verifyOne;
|
|
73
|
+
/**
|
|
74
|
+
* Seed a TRUSTED, contiguous header run ending at a pinned checkpoint, so forward sync needs
|
|
75
|
+
* only a small window — not a 27k-block genesis fetch. The seed is the trust anchor (PoW links
|
|
76
|
+
* are still spot-checked, but seed bits aren't LWMA-re-derived; that's the explicit trade for
|
|
77
|
+
* not syncing from genesis). chainwork becomes RELATIVE to the seed. `checkpointHash` MUST match
|
|
78
|
+
* the last seeded header.
|
|
79
|
+
*/
|
|
80
|
+
seedTrusted(seed: {
|
|
81
|
+
height: number;
|
|
82
|
+
header: BlockHeader;
|
|
83
|
+
hash?: string;
|
|
84
|
+
}[], checkpointHash: string): void;
|
|
85
|
+
/** Fetch + seed the LWMA window ending at `checkpointHeight`, asserting its hash, then ready to sync forward. */
|
|
86
|
+
syncFromCheckpoint(checkpointHeight: number, checkpointHash: string, context?: number): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Offer a competing branch (contiguous headers starting one above a common ancestor we hold).
|
|
89
|
+
* Verifies it from the ancestor; if its cumulative chainwork EXCEEDS our current tip, we roll
|
|
90
|
+
* back to the ancestor and adopt it (max-work rule). Otherwise we keep our chain.
|
|
91
|
+
*/
|
|
92
|
+
tryReorg(alt: {
|
|
93
|
+
height: number;
|
|
94
|
+
header: BlockHeader;
|
|
95
|
+
hash?: string;
|
|
96
|
+
}[]): ReorgResult;
|
|
54
97
|
/** Verify a tx's inclusion against a verified header (merkle proof built from the block). */
|
|
55
98
|
verifyTxInclusion(txidHex: string): Promise<InclusionResult>;
|
|
56
99
|
/**
|
|
57
|
-
* Balance for an address. HONEST:
|
|
58
|
-
*
|
|
59
|
-
* style block scan (`trustLevel: 'scanned'`). Surfaced, never hidden.
|
|
100
|
+
* Balance for an address. HONEST: `rpc-trusted` — a header chain cannot prove an output is still
|
|
101
|
+
* unspent (no UTXO commitment). A future Neutrino-style scan would yield `trustLevel:'scanned'`.
|
|
60
102
|
*/
|
|
61
103
|
balance(addr: string): Promise<{
|
|
62
104
|
confirmed: number;
|
|
@@ -65,4 +107,4 @@ declare class LightClient {
|
|
|
65
107
|
}>;
|
|
66
108
|
}
|
|
67
109
|
|
|
68
|
-
export { type HeaderProvider, type InclusionResult, LightClient, type LightClientOptions, type TrustLevel, type VerifiedHeader, expectedBits };
|
|
110
|
+
export { type HeaderProvider, type InclusionResult, LightClient, type LightClientOptions, type ReorgResult, type TrustLevel, type VerifiedHeader, expectedBits, expectedBitsFromWindow };
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
verifyMerkleProof,
|
|
8
8
|
merkleBranch,
|
|
9
9
|
GENESIS_HASH,
|
|
10
|
-
INITIAL_BITS as INITIAL_BITS2
|
|
10
|
+
INITIAL_BITS as INITIAL_BITS2,
|
|
11
|
+
LWMA_WINDOW as LWMA_WINDOW2
|
|
11
12
|
} from "@inversealtruism/csd-codec";
|
|
12
13
|
import { CsdClient, rpcHeaderToHeader } from "@inversealtruism/csd-client";
|
|
13
14
|
|
|
@@ -24,29 +25,23 @@ import {
|
|
|
24
25
|
TARGET_BLOCK_SECS
|
|
25
26
|
} from "@inversealtruism/csd-codec";
|
|
26
27
|
var POW_LIMIT_TARGET = targetToBigInt(bitsToTarget(POW_LIMIT_BITS));
|
|
27
|
-
function
|
|
28
|
+
function expectedBitsFromWindow(window, height) {
|
|
28
29
|
if (height === 0) return INITIAL_BITS;
|
|
29
|
-
const parent =
|
|
30
|
-
if (!parent) throw new Error(`expectedBits:
|
|
30
|
+
const parent = window[window.length - 1];
|
|
31
|
+
if (!parent) throw new Error(`expectedBits: empty window for height ${height}`);
|
|
31
32
|
if (height < 2) return parent.bits;
|
|
32
|
-
|
|
33
|
+
const n = Math.min(LWMA_WINDOW, height, window.length);
|
|
33
34
|
if (n < 2) return parent.bits;
|
|
34
|
-
|
|
35
|
+
const w = window.slice(window.length - n);
|
|
35
36
|
const times = [];
|
|
36
37
|
const targets = [];
|
|
37
|
-
for (
|
|
38
|
-
const idx = height - 1 - i;
|
|
39
|
-
if (idx < 0) break;
|
|
40
|
-
const h = headers[idx];
|
|
38
|
+
for (const h of w) {
|
|
41
39
|
const tb = bitsToTarget(h.bits);
|
|
42
40
|
if (tb.every((b) => b === 0)) throw new Error("expectedBits: invalid compact bits in window");
|
|
43
41
|
times.push(BigInt(h.time));
|
|
44
42
|
targets.push(targetToBigInt(tb));
|
|
45
|
-
if (idx === 0) break;
|
|
46
43
|
}
|
|
47
44
|
if (times.length < 2) return parent.bits;
|
|
48
|
-
times.reverse();
|
|
49
|
-
targets.reverse();
|
|
50
45
|
const m = times.length;
|
|
51
46
|
const t = BigInt(Math.max(TARGET_BLOCK_SECS, 1));
|
|
52
47
|
const maxSolve = BigInt(Math.max(LWMA_SOLVETIME_MAX_FACTOR, 1) * Math.max(TARGET_BLOCK_SECS, 1));
|
|
@@ -55,9 +50,9 @@ function expectedBits(headers, height) {
|
|
|
55
50
|
let dt = times[i] - times[i - 1];
|
|
56
51
|
if (dt < 0n) dt = 0n;
|
|
57
52
|
const st = dt < 1n ? 1n : dt > maxSolve ? maxSolve : dt;
|
|
58
|
-
const
|
|
59
|
-
weightedSum += st *
|
|
60
|
-
denom +=
|
|
53
|
+
const ww = BigInt(i);
|
|
54
|
+
weightedSum += st * ww;
|
|
55
|
+
denom += ww;
|
|
61
56
|
}
|
|
62
57
|
if (denom === 0n) return parent.bits;
|
|
63
58
|
const avgSolvetime = weightedSum / denom;
|
|
@@ -71,6 +66,12 @@ function expectedBits(headers, height) {
|
|
|
71
66
|
if (targetToBigInt(bitsToTarget(bits)) > POW_LIMIT_TARGET) return POW_LIMIT_BITS;
|
|
72
67
|
return bits;
|
|
73
68
|
}
|
|
69
|
+
function expectedBits(headers, height) {
|
|
70
|
+
if (height === 0) return INITIAL_BITS;
|
|
71
|
+
const n = Math.min(LWMA_WINDOW, height);
|
|
72
|
+
const window = headers.slice(height - n, height);
|
|
73
|
+
return expectedBitsFromWindow(window, height);
|
|
74
|
+
}
|
|
74
75
|
|
|
75
76
|
// src/index.ts
|
|
76
77
|
import { CsdClient as CsdClient2, rpcHeaderToHeader as rpcHeaderToHeader2 } from "@inversealtruism/csd-client";
|
|
@@ -78,8 +79,10 @@ var LightClient = class {
|
|
|
78
79
|
client;
|
|
79
80
|
provider;
|
|
80
81
|
checkpoints;
|
|
81
|
-
/** Verified header chain
|
|
82
|
+
/** Verified header chain. chain[i].height = baseHeight + i. */
|
|
82
83
|
chain = [];
|
|
84
|
+
/** Height of chain[0] — 0 for genesis-start, the seed start for checkpoint-start. */
|
|
85
|
+
baseHeight = 0;
|
|
83
86
|
constructor(opts = {}) {
|
|
84
87
|
this.client = opts.client ?? (opts.baseUrl ? new CsdClient({ baseUrl: opts.baseUrl }) : void 0);
|
|
85
88
|
this.checkpoints = opts.checkpoints ?? {};
|
|
@@ -95,12 +98,22 @@ var LightClient = class {
|
|
|
95
98
|
get chainwork() {
|
|
96
99
|
return this.tip?.chainwork ?? 0n;
|
|
97
100
|
}
|
|
98
|
-
/**
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
/** Whether every header back to genesis was verified (vs trusted from a checkpoint). */
|
|
102
|
+
get fullyVerified() {
|
|
103
|
+
return this.baseHeight === 0;
|
|
104
|
+
}
|
|
105
|
+
at(height) {
|
|
106
|
+
return this.chain[height - this.baseHeight];
|
|
107
|
+
}
|
|
108
|
+
/** The chronological LWMA window (≤ LWMA_WINDOW headers) immediately preceding `height`. */
|
|
109
|
+
windowBefore(height) {
|
|
110
|
+
const startIdx = Math.max(0, height - this.baseHeight - LWMA_WINDOW2);
|
|
111
|
+
const endIdx = height - this.baseHeight;
|
|
112
|
+
return this.chain.slice(startIdx, endIdx).map((c) => c.header);
|
|
113
|
+
}
|
|
114
|
+
/** Sync + VERIFY headers [from..to] from genesis (or contiguous to the current tip). */
|
|
115
|
+
async sync(to, from = this.baseHeight + this.chain.length) {
|
|
116
|
+
if (from !== this.baseHeight + this.chain.length) throw new Error(`non-contiguous sync: tip ${this.baseHeight + this.chain.length - 1}, asked from ${from}`);
|
|
104
117
|
for (let h = from; h <= to; h++) {
|
|
105
118
|
const { header, hash } = await this.provider(h);
|
|
106
119
|
this.ingest(h, header, hash);
|
|
@@ -108,27 +121,99 @@ var LightClient = class {
|
|
|
108
121
|
if (!this.tip) throw new Error("sync produced no tip");
|
|
109
122
|
return this.tip;
|
|
110
123
|
}
|
|
111
|
-
/** Verify a single header at
|
|
124
|
+
/** Verify a single header at `height` and append it (full consensus checks). */
|
|
112
125
|
ingest(height, header, claimedHash) {
|
|
113
|
-
if (height !== this.chain.length) throw new Error(`out-of-order ingest at ${height} (
|
|
126
|
+
if (height !== this.baseHeight + this.chain.length) throw new Error(`out-of-order ingest at ${height} (tip ${this.baseHeight + this.chain.length - 1})`);
|
|
127
|
+
const vh = this.verifyOne(height, header, this.windowBefore(height), this.at(height - 1), claimedHash);
|
|
128
|
+
this.chain.push(vh);
|
|
129
|
+
return vh;
|
|
130
|
+
}
|
|
131
|
+
/** Pure verification of one header against a window + parent (no mutation). */
|
|
132
|
+
verifyOne(height, header, window, parent, claimedHash) {
|
|
114
133
|
const hash = headerHash(header);
|
|
115
134
|
if (claimedHash && claimedHash.toLowerCase() !== hash.toLowerCase()) throw new Error(`header hash mismatch at ${height}`);
|
|
116
135
|
if (height === 0) {
|
|
117
136
|
if (hash.toLowerCase() !== GENESIS_HASH.toLowerCase()) throw new Error(`foreign genesis: ${hash}`);
|
|
118
137
|
if (header.bits !== INITIAL_BITS2) throw new Error("genesis bits != INITIAL_BITS");
|
|
119
138
|
} else {
|
|
120
|
-
|
|
139
|
+
if (!parent) throw new Error(`no parent context for height ${height}`);
|
|
121
140
|
if (header.prev.toLowerCase() !== parent.hash.toLowerCase()) throw new Error(`broken prev link at ${height}`);
|
|
122
|
-
const exp =
|
|
141
|
+
const exp = expectedBitsFromWindow(window, height);
|
|
123
142
|
if (header.bits !== exp) throw new Error(`bad bits at ${height}: header ${header.bits.toString(16)} != LWMA ${exp.toString(16)}`);
|
|
124
143
|
}
|
|
125
144
|
if (!powOk(headerHashBytes(header), header.bits)) throw new Error(`invalid PoW at ${height}`);
|
|
126
145
|
const cp = this.checkpoints[height];
|
|
127
146
|
if (cp && cp.toLowerCase() !== hash.toLowerCase()) throw new Error(`checkpoint mismatch at ${height}`);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
147
|
+
return { height, hash, header, chainwork: (parent?.chainwork ?? 0n) + workForBits(header.bits) };
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Seed a TRUSTED, contiguous header run ending at a pinned checkpoint, so forward sync needs
|
|
151
|
+
* only a small window — not a 27k-block genesis fetch. The seed is the trust anchor (PoW links
|
|
152
|
+
* are still spot-checked, but seed bits aren't LWMA-re-derived; that's the explicit trade for
|
|
153
|
+
* not syncing from genesis). chainwork becomes RELATIVE to the seed. `checkpointHash` MUST match
|
|
154
|
+
* the last seeded header.
|
|
155
|
+
*/
|
|
156
|
+
seedTrusted(seed, checkpointHash) {
|
|
157
|
+
if (this.chain.length) throw new Error("seedTrusted must be called on a fresh client");
|
|
158
|
+
if (!seed.length) throw new Error("empty seed");
|
|
159
|
+
this.baseHeight = seed[0].height;
|
|
160
|
+
let prevHash = null;
|
|
161
|
+
for (let i = 0; i < seed.length; i++) {
|
|
162
|
+
const s = seed[i];
|
|
163
|
+
if (s.height !== this.baseHeight + i) throw new Error("seed not contiguous");
|
|
164
|
+
const hash = headerHash(s.header);
|
|
165
|
+
if (s.hash && s.hash.toLowerCase() !== hash.toLowerCase()) throw new Error(`seed header hash mismatch at ${s.height}`);
|
|
166
|
+
if (prevHash && s.header.prev.toLowerCase() !== prevHash.toLowerCase()) throw new Error(`seed prev link broken at ${s.height}`);
|
|
167
|
+
if (!powOk(headerHashBytes(s.header), s.header.bits)) throw new Error(`seed PoW invalid at ${s.height}`);
|
|
168
|
+
this.chain.push({ height: s.height, hash, header: s.header, chainwork: (this.chain[i - 1]?.chainwork ?? 0n) + workForBits(s.header.bits), trusted: true });
|
|
169
|
+
prevHash = hash;
|
|
170
|
+
}
|
|
171
|
+
if (this.tip.hash.toLowerCase() !== checkpointHash.toLowerCase()) throw new Error(`checkpoint hash mismatch: seeded tip ${this.tip.hash} != ${checkpointHash}`);
|
|
172
|
+
}
|
|
173
|
+
/** Fetch + seed the LWMA window ending at `checkpointHeight`, asserting its hash, then ready to sync forward. */
|
|
174
|
+
async syncFromCheckpoint(checkpointHeight, checkpointHash, context = LWMA_WINDOW2) {
|
|
175
|
+
const start = Math.max(0, checkpointHeight - context);
|
|
176
|
+
const seed = [];
|
|
177
|
+
for (let h = start; h <= checkpointHeight; h++) {
|
|
178
|
+
const { header, hash } = await this.provider(h);
|
|
179
|
+
seed.push({ height: h, header, hash });
|
|
180
|
+
}
|
|
181
|
+
this.seedTrusted(seed, checkpointHash);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Offer a competing branch (contiguous headers starting one above a common ancestor we hold).
|
|
185
|
+
* Verifies it from the ancestor; if its cumulative chainwork EXCEEDS our current tip, we roll
|
|
186
|
+
* back to the ancestor and adopt it (max-work rule). Otherwise we keep our chain.
|
|
187
|
+
*/
|
|
188
|
+
tryReorg(alt) {
|
|
189
|
+
if (!alt.length) return { adopted: false, reason: "empty branch" };
|
|
190
|
+
const ancestorHeight = alt[0].height - 1;
|
|
191
|
+
const ancestor = this.at(ancestorHeight);
|
|
192
|
+
if (!ancestor) return { adopted: false, reason: `no common ancestor at ${ancestorHeight}` };
|
|
193
|
+
const verified = [];
|
|
194
|
+
let prev = ancestor;
|
|
195
|
+
const baseWindow = this.windowBefore(ancestorHeight + 1);
|
|
196
|
+
const window = [...baseWindow];
|
|
197
|
+
for (let i = 0; i < alt.length; i++) {
|
|
198
|
+
const a = alt[i];
|
|
199
|
+
if (a.height !== ancestorHeight + 1 + i) return { adopted: false, reason: "alt not contiguous" };
|
|
200
|
+
let vh;
|
|
201
|
+
try {
|
|
202
|
+
vh = this.verifyOne(a.height, a.header, window, prev, a.hash);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
return { adopted: false, reason: `alt invalid at ${a.height}: ${e?.message}` };
|
|
205
|
+
}
|
|
206
|
+
verified.push(vh);
|
|
207
|
+
prev = vh;
|
|
208
|
+
window.push(a.header);
|
|
209
|
+
if (window.length > LWMA_WINDOW2) window.shift();
|
|
210
|
+
}
|
|
211
|
+
const altTip = verified[verified.length - 1];
|
|
212
|
+
if (altTip.chainwork <= this.chainwork) return { adopted: false, reason: `alt work ${altTip.chainwork} \u2264 current ${this.chainwork}` };
|
|
213
|
+
const rolledBack = this.baseHeight + this.chain.length - 1 - ancestorHeight;
|
|
214
|
+
this.chain.length = ancestorHeight - this.baseHeight + 1;
|
|
215
|
+
for (const v of verified) this.chain.push(v);
|
|
216
|
+
return { adopted: true, rolledBack, newTip: altTip.height };
|
|
132
217
|
}
|
|
133
218
|
/** Verify a tx's inclusion against a verified header (merkle proof built from the block). */
|
|
134
219
|
async verifyTxInclusion(txidHex) {
|
|
@@ -136,26 +221,26 @@ var LightClient = class {
|
|
|
136
221
|
const t = await this.client.tx(txidHex);
|
|
137
222
|
if (!t.ok || t.height == null) return { trustLevel: "rpc-trusted", included: false, reason: "tx not in a block (mempool/unknown)" };
|
|
138
223
|
const height = t.height;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
224
|
+
const tipHeight = this.baseHeight + this.chain.length - 1;
|
|
225
|
+
if (height < this.baseHeight) return { trustLevel: "rpc-trusted", included: false, reason: `tx below the synced base (${this.baseHeight})` };
|
|
226
|
+
if (height > tipHeight) {
|
|
227
|
+
const gap = height - tipHeight;
|
|
228
|
+
if (gap > 256) return { trustLevel: "rpc-trusted", included: false, reason: `tx at ${height} is ${gap} blocks beyond tip \u2014 sync(${height}) first` };
|
|
142
229
|
await this.sync(height);
|
|
143
230
|
}
|
|
144
|
-
const verified = this.
|
|
231
|
+
const verified = this.at(height);
|
|
145
232
|
if (!verified) return { trustLevel: "rpc-trusted", included: false, reason: "could not verify the containing header" };
|
|
146
233
|
const b = await this.client.blockByHeight(height);
|
|
147
234
|
const txids = b.txs.map((x) => x.txid);
|
|
148
235
|
const pos = txids.findIndex((x) => x.toLowerCase() === txidHex.toLowerCase());
|
|
149
236
|
if (pos < 0) return { trustLevel: "rpc-trusted", included: false, reason: "tx not listed in block" };
|
|
150
|
-
const
|
|
151
|
-
const ok = verifyMerkleProof(txidHex, pos, branch, verified.header.merkle);
|
|
237
|
+
const ok = verifyMerkleProof(txidHex, pos, merkleBranch(txids, pos), verified.header.merkle);
|
|
152
238
|
if (!ok) return { trustLevel: "rpc-trusted", included: false, reason: "merkle proof failed" };
|
|
153
|
-
return { trustLevel: "verified-inclusion", included: true, blockHeight: height, confirmations:
|
|
239
|
+
return { trustLevel: "verified-inclusion", included: true, blockHeight: height, confirmations: tipHeight - height + 1 };
|
|
154
240
|
}
|
|
155
241
|
/**
|
|
156
|
-
* Balance for an address. HONEST:
|
|
157
|
-
*
|
|
158
|
-
* style block scan (`trustLevel: 'scanned'`). Surfaced, never hidden.
|
|
242
|
+
* Balance for an address. HONEST: `rpc-trusted` — a header chain cannot prove an output is still
|
|
243
|
+
* unspent (no UTXO commitment). A future Neutrino-style scan would yield `trustLevel:'scanned'`.
|
|
159
244
|
*/
|
|
160
245
|
async balance(addr) {
|
|
161
246
|
if (!this.client) throw new Error("no client");
|
|
@@ -167,5 +252,6 @@ export {
|
|
|
167
252
|
CsdClient2 as CsdClient,
|
|
168
253
|
LightClient,
|
|
169
254
|
expectedBits,
|
|
255
|
+
expectedBitsFromWindow,
|
|
170
256
|
rpcHeaderToHeader2 as rpcHeaderToHeader
|
|
171
257
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inversealtruism/csd-light",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Compute Substrate light client — headers-first sync with client-side PoW/LWMA/chainwork verification + merkle-inclusion proofs, behind a Helios-style verified-RPC facade with an honest trustLevel on every read.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -15,11 +15,12 @@
|
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
|
-
"LICENSE"
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"README.md"
|
|
19
20
|
],
|
|
20
21
|
"dependencies": {
|
|
21
|
-
"@inversealtruism/csd-
|
|
22
|
-
"@inversealtruism/csd-
|
|
22
|
+
"@inversealtruism/csd-client": "0.1.2",
|
|
23
|
+
"@inversealtruism/csd-codec": "0.1.2"
|
|
23
24
|
},
|
|
24
25
|
"license": "MIT",
|
|
25
26
|
"sideEffects": false,
|