Improvements

This commit is contained in:
2026-10-20 04:07:37 -04:00
parent 0cc5032857
commit 3cee89d023
20 changed files with 511 additions and 23 deletions
+31 -2
View File
@@ -4,7 +4,7 @@ import { DatabaseSync } from 'node:sqlite';
import { z } from 'zod';
import { NETWORK, CONSENSUS, type TestnetBlock,
type NftState, type NftStateView, type TestnetTransaction, type MiningTemplate,
type TestnetDeck, type TestnetSnapshot } from '../../shared/testnet.ts';
type TestnetDeck, type TestnetSnapshot, type TestnetSummary, type CanonicalPage } from '../../shared/testnet.ts';
import { CHAIN_ID, GENESIS_HASH, applyTransactionOverlay, blockSchema, makeTemplate,
applyTransferOverlay, materializeState, transactionId, transactionSchema, transferSchema,
validateBlock, workFor } from './consensus.ts';
@@ -12,6 +12,7 @@ import type { PowEngine } from './randomx.ts';
import type { Article } from '../../shared/protocol.ts';
import type { MintStatsSnapshot } from '../../shared/enrichment.ts';
import { PROFILE } from '../../shared/network.ts';
import { RpcError } from '../resources.ts';
const MAX_PENDING = 256;
const MAX_DECK_PAGE = 24;
@@ -39,6 +40,8 @@ export class Ledger {
private readonly db: DatabaseSync;
private readonly pow: PowEngine;
private closed = false;
private revision = 0;
private canonicalCache: { tip: string; blocks: TestnetBlock[] } | undefined;
private constructor(file: string, db: DatabaseSync, pow: PowEngine) { this.file = file; this.db = db; this.pow = pow; }
static async open(dir: string, pow: PowEngine): Promise<Ledger> {
@@ -190,6 +193,7 @@ export class Ledger {
if (wins) this.tip = node.block.hash;
this.pending = pending;
if (filtered) this.pendingState = filtered.state;
this.revision++;
return { accepted: true, reorg: disconnected.length > 0 };
});
}
@@ -211,6 +215,7 @@ export class Ledger {
await this.persist(undefined, pending);
this.pending = pending;
this.pendingState = nextPendingState;
this.revision++;
return id;
});
}
@@ -228,6 +233,27 @@ export class Ledger {
peers: this.peers, algorithm: 'RandomX',
maturity: CONSENSUS.coinbaseMaturity });
}
/** Small metadata/mempool response. No full-chain traversal or clone. */
summary(): TestnetSummary {
return { network: NETWORK, chainId: CHAIN_ID, genesis: GENESIS_HASH, tip: this.tip,
height: this.height(), revision: this.revision, cumulativeWork: this.tipWork().toString(),
pendingTransfers: structuredClone(this.pending), peers: this.peers, algorithm: 'RandomX', maturity: CONSENSUS.coinbaseMaturity };
}
peerStatus() { return { network: NETWORK, chainId: CHAIN_ID, size: this.size, tip: this.tip }; }
pendingTransactions(): TestnetTransaction[] { return structuredClone(this.pending); }
legacySnapshot(maxBlocks: number): TestnetSnapshot {
// Reject before any full-chain copy/serialization. Never truncate ownership history silently.
if (this.height() > maxBlocks) throw new RpcError(413, 'Full snapshot disabled at this height; use /api/summary and /api/canonical pages.');
return this.snapshot();
}
canonicalPage(tip: string, offset: number, limit = 16): CanonicalPage {
if (tip !== this.tip) throw new RpcError(409, 'Canonical tip changed; restart paged loading.');
if (!Number.isSafeInteger(offset) || offset < 0 || offset > this.height() ||
!Number.isSafeInteger(limit) || limit < 1 || limit > 16) throw new RpcError(400, 'Invalid canonical page');
const blocks = structuredClone(this.canonical().slice(offset, offset + limit));
return { network: NETWORK, chainId: CHAIN_ID, tip: this.tip, height: this.height(), offset, blocks,
next: offset + blocks.length < this.height() ? offset + blocks.length : null };
}
deck(address: string, offset = 0, limit = MAX_DECK_PAGE): TestnetDeck {
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > MAX_DECK_PAGE) {
throw new Error('Invalid deck page');
@@ -259,7 +285,10 @@ export class Ledger {
}
return result.reverse();
}
private canonical(): TestnetBlock[] { return this.branch(this.tip); }
private canonical(): TestnetBlock[] {
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> {
const ancestors = this.branch(raw.previousHash);
const parent = this.nodes.get(raw.previousHash);