@inversealtruism/csd-light 0.1.0
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/LICENSE +21 -0
- package/dist/index.cjs +180 -0
- package/dist/index.d.cts +68 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +171 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 InverseAltruism
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
CsdClient: () => import_csd_client2.CsdClient,
|
|
24
|
+
LightClient: () => LightClient,
|
|
25
|
+
expectedBits: () => expectedBits,
|
|
26
|
+
rpcHeaderToHeader: () => import_csd_client2.rpcHeaderToHeader
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
var import_csd_codec2 = require("@inversealtruism/csd-codec");
|
|
30
|
+
var import_csd_client = require("@inversealtruism/csd-client");
|
|
31
|
+
|
|
32
|
+
// src/lwma.ts
|
|
33
|
+
var import_csd_codec = require("@inversealtruism/csd-codec");
|
|
34
|
+
var POW_LIMIT_TARGET = (0, import_csd_codec.targetToBigInt)((0, import_csd_codec.bitsToTarget)(import_csd_codec.POW_LIMIT_BITS));
|
|
35
|
+
function expectedBits(headers, height) {
|
|
36
|
+
if (height === 0) return import_csd_codec.INITIAL_BITS;
|
|
37
|
+
const parent = headers[height - 1];
|
|
38
|
+
if (!parent) throw new Error(`expectedBits: missing parent for height ${height}`);
|
|
39
|
+
if (height < 2) return parent.bits;
|
|
40
|
+
let n = Math.min(import_csd_codec.LWMA_WINDOW, height);
|
|
41
|
+
if (n < 2) return parent.bits;
|
|
42
|
+
if (n > 1e3) n = 1e3;
|
|
43
|
+
const times = [];
|
|
44
|
+
const targets = [];
|
|
45
|
+
for (let i = 0; i < n; i++) {
|
|
46
|
+
const idx = height - 1 - i;
|
|
47
|
+
if (idx < 0) break;
|
|
48
|
+
const h = headers[idx];
|
|
49
|
+
const tb = (0, import_csd_codec.bitsToTarget)(h.bits);
|
|
50
|
+
if (tb.every((b) => b === 0)) throw new Error("expectedBits: invalid compact bits in window");
|
|
51
|
+
times.push(BigInt(h.time));
|
|
52
|
+
targets.push((0, import_csd_codec.targetToBigInt)(tb));
|
|
53
|
+
if (idx === 0) break;
|
|
54
|
+
}
|
|
55
|
+
if (times.length < 2) return parent.bits;
|
|
56
|
+
times.reverse();
|
|
57
|
+
targets.reverse();
|
|
58
|
+
const m = times.length;
|
|
59
|
+
const t = BigInt(Math.max(import_csd_codec.TARGET_BLOCK_SECS, 1));
|
|
60
|
+
const maxSolve = BigInt(Math.max(import_csd_codec.LWMA_SOLVETIME_MAX_FACTOR, 1) * Math.max(import_csd_codec.TARGET_BLOCK_SECS, 1));
|
|
61
|
+
let weightedSum = 0n, denom = 0n;
|
|
62
|
+
for (let i = 1; i < m; i++) {
|
|
63
|
+
let dt = times[i] - times[i - 1];
|
|
64
|
+
if (dt < 0n) dt = 0n;
|
|
65
|
+
const st = dt < 1n ? 1n : dt > maxSolve ? maxSolve : dt;
|
|
66
|
+
const w = BigInt(i);
|
|
67
|
+
weightedSum += st * w;
|
|
68
|
+
denom += w;
|
|
69
|
+
}
|
|
70
|
+
if (denom === 0n) return parent.bits;
|
|
71
|
+
const avgSolvetime = weightedSum / denom;
|
|
72
|
+
let sumTarget = 0n;
|
|
73
|
+
for (const tg of targets) sumTarget += tg;
|
|
74
|
+
const avgTarget = sumTarget / BigInt(m);
|
|
75
|
+
let nextTarget = avgTarget * avgSolvetime / t;
|
|
76
|
+
if (nextTarget > POW_LIMIT_TARGET) nextTarget = POW_LIMIT_TARGET;
|
|
77
|
+
if (nextTarget === 0n || nextTarget >= 1n << 256n) return import_csd_codec.POW_LIMIT_BITS;
|
|
78
|
+
const bits = (0, import_csd_codec.targetToBits)((0, import_csd_codec.bigIntToTarget)(nextTarget));
|
|
79
|
+
if ((0, import_csd_codec.targetToBigInt)((0, import_csd_codec.bitsToTarget)(bits)) > POW_LIMIT_TARGET) return import_csd_codec.POW_LIMIT_BITS;
|
|
80
|
+
return bits;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/index.ts
|
|
84
|
+
var import_csd_client2 = require("@inversealtruism/csd-client");
|
|
85
|
+
var LightClient = class {
|
|
86
|
+
client;
|
|
87
|
+
provider;
|
|
88
|
+
checkpoints;
|
|
89
|
+
/** Verified header chain, index = height. */
|
|
90
|
+
chain = [];
|
|
91
|
+
constructor(opts = {}) {
|
|
92
|
+
this.client = opts.client ?? (opts.baseUrl ? new import_csd_client.CsdClient({ baseUrl: opts.baseUrl }) : void 0);
|
|
93
|
+
this.checkpoints = opts.checkpoints ?? {};
|
|
94
|
+
this.provider = opts.headerProvider ?? (async (h) => {
|
|
95
|
+
if (!this.client) throw new Error("LightClient needs a client/baseUrl or a headerProvider");
|
|
96
|
+
const b = await this.client.blockByHeight(h);
|
|
97
|
+
return { header: (0, import_csd_client.rpcHeaderToHeader)(b.header), hash: b.hash, txids: b.txs.map((t) => t.txid) };
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
get tip() {
|
|
101
|
+
return this.chain[this.chain.length - 1];
|
|
102
|
+
}
|
|
103
|
+
get chainwork() {
|
|
104
|
+
return this.tip?.chainwork ?? 0n;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Sync + VERIFY headers [from..to] inclusive onto the chain. `from` must be 0 (genesis) or
|
|
108
|
+
* exactly chain.length (contiguous). Throws on any consensus violation. Returns the new tip.
|
|
109
|
+
*/
|
|
110
|
+
async sync(to, from = this.chain.length) {
|
|
111
|
+
if (from !== this.chain.length) throw new Error(`non-contiguous sync: have ${this.chain.length}, asked from ${from}`);
|
|
112
|
+
for (let h = from; h <= to; h++) {
|
|
113
|
+
const { header, hash } = await this.provider(h);
|
|
114
|
+
this.ingest(h, header, hash);
|
|
115
|
+
}
|
|
116
|
+
if (!this.tip) throw new Error("sync produced no tip");
|
|
117
|
+
return this.tip;
|
|
118
|
+
}
|
|
119
|
+
/** Verify a single header at the given height and append it (consensus checks). */
|
|
120
|
+
ingest(height, header, claimedHash) {
|
|
121
|
+
if (height !== this.chain.length) throw new Error(`out-of-order ingest at ${height} (have ${this.chain.length})`);
|
|
122
|
+
const hash = (0, import_csd_codec2.headerHash)(header);
|
|
123
|
+
if (claimedHash && claimedHash.toLowerCase() !== hash.toLowerCase()) throw new Error(`header hash mismatch at ${height}`);
|
|
124
|
+
if (height === 0) {
|
|
125
|
+
if (hash.toLowerCase() !== import_csd_codec2.GENESIS_HASH.toLowerCase()) throw new Error(`foreign genesis: ${hash}`);
|
|
126
|
+
if (header.bits !== import_csd_codec2.INITIAL_BITS) throw new Error("genesis bits != INITIAL_BITS");
|
|
127
|
+
} else {
|
|
128
|
+
const parent = this.chain[height - 1];
|
|
129
|
+
if (header.prev.toLowerCase() !== parent.hash.toLowerCase()) throw new Error(`broken prev link at ${height}`);
|
|
130
|
+
const exp = expectedBits(this.chain.map((c) => c.header), height);
|
|
131
|
+
if (header.bits !== exp) throw new Error(`bad bits at ${height}: header ${header.bits.toString(16)} != LWMA ${exp.toString(16)}`);
|
|
132
|
+
}
|
|
133
|
+
if (!(0, import_csd_codec2.powOk)((0, import_csd_codec2.headerHashBytes)(header), header.bits)) throw new Error(`invalid PoW at ${height}`);
|
|
134
|
+
const cp = this.checkpoints[height];
|
|
135
|
+
if (cp && cp.toLowerCase() !== hash.toLowerCase()) throw new Error(`checkpoint mismatch at ${height}`);
|
|
136
|
+
const chainwork = (this.chain[height - 1]?.chainwork ?? 0n) + (0, import_csd_codec2.workForBits)(header.bits);
|
|
137
|
+
const vh = { height, hash, header, chainwork };
|
|
138
|
+
this.chain.push(vh);
|
|
139
|
+
return vh;
|
|
140
|
+
}
|
|
141
|
+
/** Verify a tx's inclusion against a verified header (merkle proof built from the block). */
|
|
142
|
+
async verifyTxInclusion(txidHex) {
|
|
143
|
+
if (!this.client) return { trustLevel: "rpc-trusted", included: false, reason: "no client for proof fetch" };
|
|
144
|
+
const t = await this.client.tx(txidHex);
|
|
145
|
+
if (!t.ok || t.height == null) return { trustLevel: "rpc-trusted", included: false, reason: "tx not in a block (mempool/unknown)" };
|
|
146
|
+
const height = t.height;
|
|
147
|
+
if (height >= this.chain.length) {
|
|
148
|
+
const gap = height - this.chain.length + 1;
|
|
149
|
+
if (gap > 256) return { trustLevel: "rpc-trusted", included: false, reason: `tx at height ${height} is ${gap} blocks beyond the synced tip \u2014 call sync(${height}) first` };
|
|
150
|
+
await this.sync(height);
|
|
151
|
+
}
|
|
152
|
+
const verified = this.chain[height];
|
|
153
|
+
if (!verified) return { trustLevel: "rpc-trusted", included: false, reason: "could not verify the containing header" };
|
|
154
|
+
const b = await this.client.blockByHeight(height);
|
|
155
|
+
const txids = b.txs.map((x) => x.txid);
|
|
156
|
+
const pos = txids.findIndex((x) => x.toLowerCase() === txidHex.toLowerCase());
|
|
157
|
+
if (pos < 0) return { trustLevel: "rpc-trusted", included: false, reason: "tx not listed in block" };
|
|
158
|
+
const branch = (0, import_csd_codec2.merkleBranch)(txids, pos);
|
|
159
|
+
const ok = (0, import_csd_codec2.verifyMerkleProof)(txidHex, pos, branch, verified.header.merkle);
|
|
160
|
+
if (!ok) return { trustLevel: "rpc-trusted", included: false, reason: "merkle proof failed" };
|
|
161
|
+
return { trustLevel: "verified-inclusion", included: true, blockHeight: height, confirmations: this.chain.length - height };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Balance for an address. HONEST: this is `rpc-trusted` — a header chain cannot prove an output
|
|
165
|
+
* is still unspent (no UTXO commitment). A future `scanBalance` will derive it from a Neutrino-
|
|
166
|
+
* style block scan (`trustLevel: 'scanned'`). Surfaced, never hidden.
|
|
167
|
+
*/
|
|
168
|
+
async balance(addr) {
|
|
169
|
+
if (!this.client) throw new Error("no client");
|
|
170
|
+
const u = await this.client.utxos(addr);
|
|
171
|
+
return { confirmed: u.confirmed_balance, trustLevel: "rpc-trusted", note: "balance is RPC-trusted; a header chain cannot prove non-spend (no UTXO commitment)" };
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
175
|
+
0 && (module.exports = {
|
|
176
|
+
CsdClient,
|
|
177
|
+
LightClient,
|
|
178
|
+
expectedBits,
|
|
179
|
+
rpcHeaderToHeader
|
|
180
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { BlockHeader } from '@inversealtruism/csd-codec';
|
|
2
|
+
import { CsdClient } from '@inversealtruism/csd-client';
|
|
3
|
+
export { CsdClient, RpcTxJson, rpcHeaderToHeader } from '@inversealtruism/csd-client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Expected `bits` for the block at `height`, given the canonical chain `headers[0..=parent]`
|
|
7
|
+
* (index = height; headers[height-1] is the parent). Mirrors expected_bits_strict exactly.
|
|
8
|
+
*/
|
|
9
|
+
declare function expectedBits(headers: BlockHeader[], height: number): number;
|
|
10
|
+
|
|
11
|
+
type TrustLevel = "verified-inclusion" | "scanned" | "rpc-trusted";
|
|
12
|
+
interface VerifiedHeader {
|
|
13
|
+
height: number;
|
|
14
|
+
hash: string;
|
|
15
|
+
header: BlockHeader;
|
|
16
|
+
chainwork: bigint;
|
|
17
|
+
}
|
|
18
|
+
interface InclusionResult {
|
|
19
|
+
trustLevel: TrustLevel;
|
|
20
|
+
included: boolean;
|
|
21
|
+
blockHeight?: number;
|
|
22
|
+
confirmations?: number;
|
|
23
|
+
reason?: string;
|
|
24
|
+
}
|
|
25
|
+
/** A header provider — defaults to a CsdClient, injectable for tests. */
|
|
26
|
+
type HeaderProvider = (height: number) => Promise<{
|
|
27
|
+
header: BlockHeader;
|
|
28
|
+
hash: string;
|
|
29
|
+
txids: string[];
|
|
30
|
+
}>;
|
|
31
|
+
interface LightClientOptions {
|
|
32
|
+
client?: CsdClient;
|
|
33
|
+
baseUrl?: string;
|
|
34
|
+
headerProvider?: HeaderProvider;
|
|
35
|
+
/** Pin checkpoints {height: expectedHash} to bound/accelerate sync (optional). */
|
|
36
|
+
checkpoints?: Record<number, string>;
|
|
37
|
+
}
|
|
38
|
+
declare class LightClient {
|
|
39
|
+
private readonly client?;
|
|
40
|
+
private readonly provider;
|
|
41
|
+
private readonly checkpoints;
|
|
42
|
+
/** Verified header chain, index = height. */
|
|
43
|
+
readonly chain: VerifiedHeader[];
|
|
44
|
+
constructor(opts?: LightClientOptions);
|
|
45
|
+
get tip(): VerifiedHeader | undefined;
|
|
46
|
+
get chainwork(): bigint;
|
|
47
|
+
/**
|
|
48
|
+
* Sync + VERIFY headers [from..to] inclusive onto the chain. `from` must be 0 (genesis) or
|
|
49
|
+
* exactly chain.length (contiguous). Throws on any consensus violation. Returns the new tip.
|
|
50
|
+
*/
|
|
51
|
+
sync(to: number, from?: number): Promise<VerifiedHeader>;
|
|
52
|
+
/** Verify a single header at the given height and append it (consensus checks). */
|
|
53
|
+
ingest(height: number, header: BlockHeader, claimedHash?: string): VerifiedHeader;
|
|
54
|
+
/** Verify a tx's inclusion against a verified header (merkle proof built from the block). */
|
|
55
|
+
verifyTxInclusion(txidHex: string): Promise<InclusionResult>;
|
|
56
|
+
/**
|
|
57
|
+
* Balance for an address. HONEST: this is `rpc-trusted` — a header chain cannot prove an output
|
|
58
|
+
* is still unspent (no UTXO commitment). A future `scanBalance` will derive it from a Neutrino-
|
|
59
|
+
* style block scan (`trustLevel: 'scanned'`). Surfaced, never hidden.
|
|
60
|
+
*/
|
|
61
|
+
balance(addr: string): Promise<{
|
|
62
|
+
confirmed: number;
|
|
63
|
+
trustLevel: TrustLevel;
|
|
64
|
+
note: string;
|
|
65
|
+
}>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { type HeaderProvider, type InclusionResult, LightClient, type LightClientOptions, type TrustLevel, type VerifiedHeader, expectedBits };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { BlockHeader } from '@inversealtruism/csd-codec';
|
|
2
|
+
import { CsdClient } from '@inversealtruism/csd-client';
|
|
3
|
+
export { CsdClient, RpcTxJson, rpcHeaderToHeader } from '@inversealtruism/csd-client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Expected `bits` for the block at `height`, given the canonical chain `headers[0..=parent]`
|
|
7
|
+
* (index = height; headers[height-1] is the parent). Mirrors expected_bits_strict exactly.
|
|
8
|
+
*/
|
|
9
|
+
declare function expectedBits(headers: BlockHeader[], height: number): number;
|
|
10
|
+
|
|
11
|
+
type TrustLevel = "verified-inclusion" | "scanned" | "rpc-trusted";
|
|
12
|
+
interface VerifiedHeader {
|
|
13
|
+
height: number;
|
|
14
|
+
hash: string;
|
|
15
|
+
header: BlockHeader;
|
|
16
|
+
chainwork: bigint;
|
|
17
|
+
}
|
|
18
|
+
interface InclusionResult {
|
|
19
|
+
trustLevel: TrustLevel;
|
|
20
|
+
included: boolean;
|
|
21
|
+
blockHeight?: number;
|
|
22
|
+
confirmations?: number;
|
|
23
|
+
reason?: string;
|
|
24
|
+
}
|
|
25
|
+
/** A header provider — defaults to a CsdClient, injectable for tests. */
|
|
26
|
+
type HeaderProvider = (height: number) => Promise<{
|
|
27
|
+
header: BlockHeader;
|
|
28
|
+
hash: string;
|
|
29
|
+
txids: string[];
|
|
30
|
+
}>;
|
|
31
|
+
interface LightClientOptions {
|
|
32
|
+
client?: CsdClient;
|
|
33
|
+
baseUrl?: string;
|
|
34
|
+
headerProvider?: HeaderProvider;
|
|
35
|
+
/** Pin checkpoints {height: expectedHash} to bound/accelerate sync (optional). */
|
|
36
|
+
checkpoints?: Record<number, string>;
|
|
37
|
+
}
|
|
38
|
+
declare class LightClient {
|
|
39
|
+
private readonly client?;
|
|
40
|
+
private readonly provider;
|
|
41
|
+
private readonly checkpoints;
|
|
42
|
+
/** Verified header chain, index = height. */
|
|
43
|
+
readonly chain: VerifiedHeader[];
|
|
44
|
+
constructor(opts?: LightClientOptions);
|
|
45
|
+
get tip(): VerifiedHeader | undefined;
|
|
46
|
+
get chainwork(): bigint;
|
|
47
|
+
/**
|
|
48
|
+
* Sync + VERIFY headers [from..to] inclusive onto the chain. `from` must be 0 (genesis) or
|
|
49
|
+
* exactly chain.length (contiguous). Throws on any consensus violation. Returns the new tip.
|
|
50
|
+
*/
|
|
51
|
+
sync(to: number, from?: number): Promise<VerifiedHeader>;
|
|
52
|
+
/** Verify a single header at the given height and append it (consensus checks). */
|
|
53
|
+
ingest(height: number, header: BlockHeader, claimedHash?: string): VerifiedHeader;
|
|
54
|
+
/** Verify a tx's inclusion against a verified header (merkle proof built from the block). */
|
|
55
|
+
verifyTxInclusion(txidHex: string): Promise<InclusionResult>;
|
|
56
|
+
/**
|
|
57
|
+
* Balance for an address. HONEST: this is `rpc-trusted` — a header chain cannot prove an output
|
|
58
|
+
* is still unspent (no UTXO commitment). A future `scanBalance` will derive it from a Neutrino-
|
|
59
|
+
* style block scan (`trustLevel: 'scanned'`). Surfaced, never hidden.
|
|
60
|
+
*/
|
|
61
|
+
balance(addr: string): Promise<{
|
|
62
|
+
confirmed: number;
|
|
63
|
+
trustLevel: TrustLevel;
|
|
64
|
+
note: string;
|
|
65
|
+
}>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { type HeaderProvider, type InclusionResult, LightClient, type LightClientOptions, type TrustLevel, type VerifiedHeader, expectedBits };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
headerHash,
|
|
4
|
+
headerHashBytes,
|
|
5
|
+
powOk,
|
|
6
|
+
workForBits,
|
|
7
|
+
verifyMerkleProof,
|
|
8
|
+
merkleBranch,
|
|
9
|
+
GENESIS_HASH,
|
|
10
|
+
INITIAL_BITS as INITIAL_BITS2
|
|
11
|
+
} from "@inversealtruism/csd-codec";
|
|
12
|
+
import { CsdClient, rpcHeaderToHeader } from "@inversealtruism/csd-client";
|
|
13
|
+
|
|
14
|
+
// src/lwma.ts
|
|
15
|
+
import {
|
|
16
|
+
bitsToTarget,
|
|
17
|
+
targetToBigInt,
|
|
18
|
+
bigIntToTarget,
|
|
19
|
+
targetToBits,
|
|
20
|
+
INITIAL_BITS,
|
|
21
|
+
POW_LIMIT_BITS,
|
|
22
|
+
LWMA_WINDOW,
|
|
23
|
+
LWMA_SOLVETIME_MAX_FACTOR,
|
|
24
|
+
TARGET_BLOCK_SECS
|
|
25
|
+
} from "@inversealtruism/csd-codec";
|
|
26
|
+
var POW_LIMIT_TARGET = targetToBigInt(bitsToTarget(POW_LIMIT_BITS));
|
|
27
|
+
function expectedBits(headers, height) {
|
|
28
|
+
if (height === 0) return INITIAL_BITS;
|
|
29
|
+
const parent = headers[height - 1];
|
|
30
|
+
if (!parent) throw new Error(`expectedBits: missing parent for height ${height}`);
|
|
31
|
+
if (height < 2) return parent.bits;
|
|
32
|
+
let n = Math.min(LWMA_WINDOW, height);
|
|
33
|
+
if (n < 2) return parent.bits;
|
|
34
|
+
if (n > 1e3) n = 1e3;
|
|
35
|
+
const times = [];
|
|
36
|
+
const targets = [];
|
|
37
|
+
for (let i = 0; i < n; i++) {
|
|
38
|
+
const idx = height - 1 - i;
|
|
39
|
+
if (idx < 0) break;
|
|
40
|
+
const h = headers[idx];
|
|
41
|
+
const tb = bitsToTarget(h.bits);
|
|
42
|
+
if (tb.every((b) => b === 0)) throw new Error("expectedBits: invalid compact bits in window");
|
|
43
|
+
times.push(BigInt(h.time));
|
|
44
|
+
targets.push(targetToBigInt(tb));
|
|
45
|
+
if (idx === 0) break;
|
|
46
|
+
}
|
|
47
|
+
if (times.length < 2) return parent.bits;
|
|
48
|
+
times.reverse();
|
|
49
|
+
targets.reverse();
|
|
50
|
+
const m = times.length;
|
|
51
|
+
const t = BigInt(Math.max(TARGET_BLOCK_SECS, 1));
|
|
52
|
+
const maxSolve = BigInt(Math.max(LWMA_SOLVETIME_MAX_FACTOR, 1) * Math.max(TARGET_BLOCK_SECS, 1));
|
|
53
|
+
let weightedSum = 0n, denom = 0n;
|
|
54
|
+
for (let i = 1; i < m; i++) {
|
|
55
|
+
let dt = times[i] - times[i - 1];
|
|
56
|
+
if (dt < 0n) dt = 0n;
|
|
57
|
+
const st = dt < 1n ? 1n : dt > maxSolve ? maxSolve : dt;
|
|
58
|
+
const w = BigInt(i);
|
|
59
|
+
weightedSum += st * w;
|
|
60
|
+
denom += w;
|
|
61
|
+
}
|
|
62
|
+
if (denom === 0n) return parent.bits;
|
|
63
|
+
const avgSolvetime = weightedSum / denom;
|
|
64
|
+
let sumTarget = 0n;
|
|
65
|
+
for (const tg of targets) sumTarget += tg;
|
|
66
|
+
const avgTarget = sumTarget / BigInt(m);
|
|
67
|
+
let nextTarget = avgTarget * avgSolvetime / t;
|
|
68
|
+
if (nextTarget > POW_LIMIT_TARGET) nextTarget = POW_LIMIT_TARGET;
|
|
69
|
+
if (nextTarget === 0n || nextTarget >= 1n << 256n) return POW_LIMIT_BITS;
|
|
70
|
+
const bits = targetToBits(bigIntToTarget(nextTarget));
|
|
71
|
+
if (targetToBigInt(bitsToTarget(bits)) > POW_LIMIT_TARGET) return POW_LIMIT_BITS;
|
|
72
|
+
return bits;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/index.ts
|
|
76
|
+
import { CsdClient as CsdClient2, rpcHeaderToHeader as rpcHeaderToHeader2 } from "@inversealtruism/csd-client";
|
|
77
|
+
var LightClient = class {
|
|
78
|
+
client;
|
|
79
|
+
provider;
|
|
80
|
+
checkpoints;
|
|
81
|
+
/** Verified header chain, index = height. */
|
|
82
|
+
chain = [];
|
|
83
|
+
constructor(opts = {}) {
|
|
84
|
+
this.client = opts.client ?? (opts.baseUrl ? new CsdClient({ baseUrl: opts.baseUrl }) : void 0);
|
|
85
|
+
this.checkpoints = opts.checkpoints ?? {};
|
|
86
|
+
this.provider = opts.headerProvider ?? (async (h) => {
|
|
87
|
+
if (!this.client) throw new Error("LightClient needs a client/baseUrl or a headerProvider");
|
|
88
|
+
const b = await this.client.blockByHeight(h);
|
|
89
|
+
return { header: rpcHeaderToHeader(b.header), hash: b.hash, txids: b.txs.map((t) => t.txid) };
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
get tip() {
|
|
93
|
+
return this.chain[this.chain.length - 1];
|
|
94
|
+
}
|
|
95
|
+
get chainwork() {
|
|
96
|
+
return this.tip?.chainwork ?? 0n;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Sync + VERIFY headers [from..to] inclusive onto the chain. `from` must be 0 (genesis) or
|
|
100
|
+
* exactly chain.length (contiguous). Throws on any consensus violation. Returns the new tip.
|
|
101
|
+
*/
|
|
102
|
+
async sync(to, from = this.chain.length) {
|
|
103
|
+
if (from !== this.chain.length) throw new Error(`non-contiguous sync: have ${this.chain.length}, asked from ${from}`);
|
|
104
|
+
for (let h = from; h <= to; h++) {
|
|
105
|
+
const { header, hash } = await this.provider(h);
|
|
106
|
+
this.ingest(h, header, hash);
|
|
107
|
+
}
|
|
108
|
+
if (!this.tip) throw new Error("sync produced no tip");
|
|
109
|
+
return this.tip;
|
|
110
|
+
}
|
|
111
|
+
/** Verify a single header at the given height and append it (consensus checks). */
|
|
112
|
+
ingest(height, header, claimedHash) {
|
|
113
|
+
if (height !== this.chain.length) throw new Error(`out-of-order ingest at ${height} (have ${this.chain.length})`);
|
|
114
|
+
const hash = headerHash(header);
|
|
115
|
+
if (claimedHash && claimedHash.toLowerCase() !== hash.toLowerCase()) throw new Error(`header hash mismatch at ${height}`);
|
|
116
|
+
if (height === 0) {
|
|
117
|
+
if (hash.toLowerCase() !== GENESIS_HASH.toLowerCase()) throw new Error(`foreign genesis: ${hash}`);
|
|
118
|
+
if (header.bits !== INITIAL_BITS2) throw new Error("genesis bits != INITIAL_BITS");
|
|
119
|
+
} else {
|
|
120
|
+
const parent = this.chain[height - 1];
|
|
121
|
+
if (header.prev.toLowerCase() !== parent.hash.toLowerCase()) throw new Error(`broken prev link at ${height}`);
|
|
122
|
+
const exp = expectedBits(this.chain.map((c) => c.header), height);
|
|
123
|
+
if (header.bits !== exp) throw new Error(`bad bits at ${height}: header ${header.bits.toString(16)} != LWMA ${exp.toString(16)}`);
|
|
124
|
+
}
|
|
125
|
+
if (!powOk(headerHashBytes(header), header.bits)) throw new Error(`invalid PoW at ${height}`);
|
|
126
|
+
const cp = this.checkpoints[height];
|
|
127
|
+
if (cp && cp.toLowerCase() !== hash.toLowerCase()) throw new Error(`checkpoint mismatch at ${height}`);
|
|
128
|
+
const chainwork = (this.chain[height - 1]?.chainwork ?? 0n) + workForBits(header.bits);
|
|
129
|
+
const vh = { height, hash, header, chainwork };
|
|
130
|
+
this.chain.push(vh);
|
|
131
|
+
return vh;
|
|
132
|
+
}
|
|
133
|
+
/** Verify a tx's inclusion against a verified header (merkle proof built from the block). */
|
|
134
|
+
async verifyTxInclusion(txidHex) {
|
|
135
|
+
if (!this.client) return { trustLevel: "rpc-trusted", included: false, reason: "no client for proof fetch" };
|
|
136
|
+
const t = await this.client.tx(txidHex);
|
|
137
|
+
if (!t.ok || t.height == null) return { trustLevel: "rpc-trusted", included: false, reason: "tx not in a block (mempool/unknown)" };
|
|
138
|
+
const height = t.height;
|
|
139
|
+
if (height >= this.chain.length) {
|
|
140
|
+
const gap = height - this.chain.length + 1;
|
|
141
|
+
if (gap > 256) return { trustLevel: "rpc-trusted", included: false, reason: `tx at height ${height} is ${gap} blocks beyond the synced tip \u2014 call sync(${height}) first` };
|
|
142
|
+
await this.sync(height);
|
|
143
|
+
}
|
|
144
|
+
const verified = this.chain[height];
|
|
145
|
+
if (!verified) return { trustLevel: "rpc-trusted", included: false, reason: "could not verify the containing header" };
|
|
146
|
+
const b = await this.client.blockByHeight(height);
|
|
147
|
+
const txids = b.txs.map((x) => x.txid);
|
|
148
|
+
const pos = txids.findIndex((x) => x.toLowerCase() === txidHex.toLowerCase());
|
|
149
|
+
if (pos < 0) return { trustLevel: "rpc-trusted", included: false, reason: "tx not listed in block" };
|
|
150
|
+
const branch = merkleBranch(txids, pos);
|
|
151
|
+
const ok = verifyMerkleProof(txidHex, pos, branch, verified.header.merkle);
|
|
152
|
+
if (!ok) return { trustLevel: "rpc-trusted", included: false, reason: "merkle proof failed" };
|
|
153
|
+
return { trustLevel: "verified-inclusion", included: true, blockHeight: height, confirmations: this.chain.length - height };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Balance for an address. HONEST: this is `rpc-trusted` — a header chain cannot prove an output
|
|
157
|
+
* is still unspent (no UTXO commitment). A future `scanBalance` will derive it from a Neutrino-
|
|
158
|
+
* style block scan (`trustLevel: 'scanned'`). Surfaced, never hidden.
|
|
159
|
+
*/
|
|
160
|
+
async balance(addr) {
|
|
161
|
+
if (!this.client) throw new Error("no client");
|
|
162
|
+
const u = await this.client.utxos(addr);
|
|
163
|
+
return { confirmed: u.confirmed_balance, trustLevel: "rpc-trusted", note: "balance is RPC-trusted; a header chain cannot prove non-spend (no UTXO commitment)" };
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
export {
|
|
167
|
+
CsdClient2 as CsdClient,
|
|
168
|
+
LightClient,
|
|
169
|
+
expectedBits,
|
|
170
|
+
rpcHeaderToHeader2 as rpcHeaderToHeader
|
|
171
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@inversealtruism/csd-light",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@inversealtruism/csd-codec": "0.1.0",
|
|
22
|
+
"@inversealtruism/csd-client": "0.1.0"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/InverseAltruism/csd-sdk.git",
|
|
32
|
+
"directory": "packages/light"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://cairn-substrate.com",
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
37
|
+
"test": "tsx test/light.test.ts"
|
|
38
|
+
}
|
|
39
|
+
}
|