This commit is contained in:
2026-10-29 11:45:51 -04:00
parent b3a1179703
commit b427fa4217
35 changed files with 469 additions and 158 deletions
+29 -8
View File
@@ -44,12 +44,13 @@ export class Ledger {
private closed = false;
private revision = 0;
private reorgs = 0;
private readonly nowSeconds: () => number;
private canonicalCache: { tip: string; blocks: TestnetBlock[] } | undefined;
private constructor(file: string, db: DatabaseSync, pow: PowEngine) {
this.file = file; this.db = db; this.pow = pow; this.index = new CanonicalIndex(db);
private constructor(file: string, db: DatabaseSync, pow: PowEngine, nowSeconds: () => number) {
this.file = file; this.db = db; this.pow = pow; this.nowSeconds = nowSeconds; this.index = new CanonicalIndex(db);
}
static async open(dir: string, pow: PowEngine): Promise<Ledger> {
static async open(dir: string, pow: PowEngine, nowSeconds: () => number = () => Math.floor(Date.now() / 1000)): Promise<Ledger> {
const directory = join(dir, PROFILE);
await mkdir(directory, { recursive: true, mode: 0o700 });
const file = join(directory, 'ledger.sqlite');
@@ -120,17 +121,36 @@ export class Ledger {
for (const block of oldDisk.blocks) insertBlock.run(block.hash, block.previousHash, JSON.stringify(block));
const insertPending = db.prepare('INSERT INTO pending_transfers(position, id, transaction_json) VALUES(?, ?, ?)');
oldDisk.pendingTransfers.forEach((tx, index) => insertPending.run(index, transactionId(tx), JSON.stringify(tx)));
insertMeta.run('legacy_json_imported_at', String(nowSeconds()));
insertMeta.run('legacy_json_block_count', String(oldDisk.blocks.length));
}
insertMeta.run('legacy_json_migrated', '1');
db.exec('COMMIT');
} catch (error) { db.exec('ROLLBACK'); throw error; }
}
const ledger = new Ledger(file, db, pow);
for (const row of db.prepare('SELECT block_json FROM blocks ORDER BY sequence').iterate() as Iterable<{ block_json: string }>) {
const ledger = new Ledger(file, db, pow, nowSeconds);
const importedAtText = (db.prepare('SELECT value FROM metadata WHERE key=?').get('legacy_json_imported_at') as { value: string } | undefined)?.value;
const importedCountText = (db.prepare('SELECT value FROM metadata WHERE key=?').get('legacy_json_block_count') as { value: string } | undefined)?.value;
const legacyImportedAt = importedAtText !== undefined && /^(0|[1-9]\d{0,10})$/.test(importedAtText)
? Number(importedAtText) : undefined;
const legacyImportedCount = importedCountText !== undefined && /^(0|[1-9]\d{0,9})$/.test(importedCountText)
? Number(importedCountText) : undefined;
if ((importedAtText !== undefined && (legacyImportedAt === undefined || !Number.isSafeInteger(legacyImportedAt))) ||
(importedCountText !== undefined && (legacyImportedCount === undefined || !Number.isSafeInteger(legacyImportedCount))) ||
(legacyImportedAt === undefined) !== (legacyImportedCount === undefined)) {
throw new Error('Legacy ledger import timestamp metadata is invalid');
}
for (const row of db.prepare('SELECT sequence, block_json FROM blocks ORDER BY sequence').iterate() as Iterable<{ sequence: number; block_json: string }>) {
const raw = blockSchema.parse(JSON.parse(row.block_json));
if (ledger.nodes.has(raw.hash)) throw new Error('Duplicate persisted block');
const node = await ledger.validate(raw);
const importedLegacyBlock = legacyImportedCount !== undefined && row.sequence <= legacyImportedCount;
// Historical SQLite blocks already passed admission; wall-clock rollback must not
// strand them. Imported JSON blocks retain their original admission clock so an
// untrusted legacy file cannot inject arbitrarily future-dated history.
const node = importedLegacyBlock
? await ledger.validate(raw, false, legacyImportedAt!)
: await ledger.validate(raw, true);
ledger.nodes.set(node.block.hash, node);
if (node.work > ledger.tipWork()) ledger.tip = node.block.hash;
}
@@ -297,10 +317,11 @@ export class Ledger {
if (this.canonicalCache?.tip !== this.tip) this.canonicalCache = { tip: this.tip, blocks: this.branch(this.tip) };
return this.canonicalCache.blocks; // Internal immutable block references only; public APIs clone their slice.
}
private async validate(raw: TestnetBlock): Promise<Node> {
private async validate(raw: TestnetBlock, historicalReplay = false, now = this.nowSeconds()): Promise<Node> {
const ancestors = this.branch(raw.previousHash);
const parent = this.nodes.get(raw.previousHash);
const { block, state } = await validateBlock(raw, ancestors, parent?.state ?? {}, this.pow);
const { block, state } = await validateBlock(raw, ancestors, parent?.state ?? {}, this.pow,
now, historicalReplay);
return { block, state, work: (parent?.work ?? 0n) + workFor(block.target) };
}
private filterPending(parent: NftStateView, chain: TestnetBlock[], candidates: TestnetTransaction[]):