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
+11 -6
View File
@@ -1,6 +1,6 @@
import { createHash, createPublicKey, verify } from 'node:crypto';
import { z } from 'zod';
import { NETWORK, CONSENSUS, headerMessage, blockClaimMessage, nftTransferMessage, nftTradeMessage, statsSnapshotTuple,
import { NETWORK, CONSENSUS, MAX_TARGET, headerMessage, blockClaimMessage, nftTransferMessage, nftTradeMessage, statsSnapshotTuple,
type BlockHeader, type TestnetBlock, type NftTransfer, type NftState, type NftStateLayer,
isNftTrade, type NftStateView, type NftTrade, type TestnetTransaction, type MiningTemplate } from '../../shared/testnet.ts';
import type { Article } from '../../shared/protocol.ts';
@@ -9,7 +9,7 @@ import type { PowEngine } from './randomx.ts';
import { NETWORK_GENESIS_TIME } from '../../shared/network.ts';
export const sha256 = (data: string | Uint8Array) => createHash('sha256').update(data).digest('hex');
export const MAX_TARGET = `0fff${'f'.repeat(60)}`;
export { MAX_TARGET };
// Consensus manifest: an independently running node derives exactly the same genesis.
export const GENESIS_TIME = NETWORK_GENESIS_TIME;
export const CHAIN_ID = sha256(JSON.stringify([NETWORK, CONSENSUS, GENESIS_TIME, MAX_TARGET]));
@@ -261,19 +261,24 @@ export function makeTemplate(ancestors: TestnetBlock[], state: NftStateView, own
return { header, transactions, blob: miningBlob(header).toString('hex'), nonceOffset: 32 };
}
export async function validateBlock(raw: unknown, ancestors: TestnetBlock[], parentState: NftStateView,
pow: PowEngine, now = Math.floor(Date.now()/1000)): Promise<{block:TestnetBlock;state:NftStateView}> {
pow: PowEngine, now = Math.floor(Date.now()/1000), allowHistoricalFutureTimestamp = false): Promise<{block:TestnetBlock;state:NftStateView}> {
if (Buffer.byteLength(JSON.stringify(raw)) > CONSENSUS.maxBlockBytes) throw new Error('Block size limit exceeded');
const b = blockSchema.parse(raw);
if (b.height !== ancestors.length + 1 || b.previousHash !== (ancestors.at(-1)?.hash ?? GENESIS_HASH)) throw new Error('Invalid parent/height');
if (b.createdAt <= medianTime(ancestors) || b.createdAt > now + CONSENSUS.maxFutureSeconds) throw new Error('Invalid block timestamp');
if (b.createdAt <= medianTime(ancestors) || (!allowHistoricalFutureTimestamp && b.createdAt > now + CONSENSUS.maxFutureSeconds)) {
throw new Error('Invalid block timestamp');
}
if (b.target !== expectedTarget(ancestors) || b.key !== expectedKey(ancestors, b.height)) throw new Error('Invalid difficulty or RandomX epoch key');
if (b.transactionsRoot !== merkleRoot(b.transactions.map(transactionId))) throw new Error('Invalid transaction root');
const next = transition(parentState, b, b.transactions);
if (b.stateRoot !== stateRoot(next) || b.tokenId !== mintId(b)) throw new Error('Invalid NFT state commitment');
if (b.tokenId !== mintId(b)) throw new Error('Invalid NFT state commitment');
const blob = miningBlob(b);
if (b.hash !== sha256(blob)) throw new Error('Invalid block ID');
if (!verifySignature(b.publicKey, b.owner, blockClaimMessage(b.hash), b.signature)) throw new Error('Invalid miner signature');
if (BigInt(`0x${b.powHash}`) > BigInt(`0x${b.target}`)) throw new Error('Insufficient work');
if (await pow.hash(b.key, blob) !== b.powHash) throw new Error('Invalid RandomX proof');
// Verify native work before materializing/sorting the complete NFT state. A malformed
// low-work block must not force O(total NFTs) commitment work on the request path.
const next = transition(parentState, b, b.transactions);
if (b.stateRoot !== stateRoot(next)) throw new Error('Invalid NFT state commitment');
return { block: b, state: next };
}
+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[]):