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
+66 -2
View File
@@ -11,7 +11,7 @@ import { setTimeout as delay } from 'node:timers/promises';
import { Ledger } from '../server/testnet/ledger.ts';
import { PeerNetwork } from '../server/testnet/peers.ts';
import { CHAIN_ID, GENESIS_HASH, GENESIS_TIME, MAX_TARGET, address, makeTemplate, miningBlob,
mintId, sha256, stateRoot, transferId, transition, workFor } from '../server/testnet/consensus.ts';
mintId, sha256, stateRoot, transferId, transition, validateBlock, workFor } from '../server/testnet/consensus.ts';
import { NETWORK, CONSENSUS, blockClaimMessage, nftTransferMessage, testnetOwnerOf, transactionCardIds,
nftTradeMessage, type MiningTemplate, type NftState, type NftStateLayer, type NftStateView,
type NftTrade, type NftTransfer, type TestnetBlock } from '../shared/testnet.ts';
@@ -152,6 +152,51 @@ test('timestamp attack boundaries reject at median and beyond future limit befor
assert.equal(engine.calls,calls);
});
test('historical replay survives a local clock rollback after accepting the future-skew boundary',async t=>{
const dir=await mkdtemp(join(tmpdir(),'wikideck-clock-replay-'));
const admissionTime=GENESIS_TIME+CONSENSUS.targetSeconds*20;
const powEngine=new FakePow();
const ledger=await Ledger.open(dir,powEngine,()=>admissionTime);
const template=makeTemplate([],{},alice.owner,alice.publicKey,[],articleFor([]),statsSnapshot(),
admissionTime+CONSENSUS.maxFutureSeconds);
const block=await mine(template);
assert.equal(block.createdAt,admissionTime+CONSENSUS.maxFutureSeconds);
assert.deepEqual(await ledger.submit(block),{accepted:true,reorg:false});
const accepted=ledger.snapshot();ledger.close();
const restarted=await Ledger.open(dir,new FakePow(),()=>admissionTime-CONSENSUS.maxFutureSeconds-1);
t.after(async()=>{restarted.close();await rm(dir,{recursive:true,force:true});});
assert.deepEqual(restarted.snapshot(),accepted);
});
test('legacy JSON import cannot seed future-dated blocks, even after restart',async t=>{
const dir=await mkdtemp(join(tmpdir(),'wikideck-future-legacy-'));
const admissionTime=GENESIS_TIME+CONSENSUS.targetSeconds*20;
const networkDir=join(dir,'testnet');await mkdir(networkDir,{recursive:true});
const template=makeTemplate([],{},alice.owner,alice.publicKey,[],articleFor([]),statsSnapshot(),
admissionTime+CONSENSUS.maxFutureSeconds+1);
const futureBlock=await mine(template);
await writeFile(join(networkDir,'ledger.json'),JSON.stringify({version:1,network:NETWORK,chainId:CHAIN_ID,
blocks:[futureBlock],pendingTransfers:[]}));
for(let attempt=0;attempt<2;attempt++) {
await assert.rejects(Ledger.open(dir,new FakePow(),()=>admissionTime),/timestamp/);
}
t.after(async()=>{await rm(dir,{recursive:true,force:true});});
});
test('invalid claimed RandomX proof is rejected before full NFT-state enumeration',async()=>{
const template=makeTemplate([],{},alice.owner,alice.publicKey,[],articleFor([]),statsSnapshot());
const badHeader={...template.header,stateRoot:'f'.repeat(64)};
const blob=miningBlob(badHeader),hash=sha256(blob);
const candidate:TestnetBlock={...badHeader,hash,tokenId:mintId(badHeader),powHash:'0'.repeat(64),
signature:alice.sign(blockClaimMessage(hash)),transactions:[]};
let enumerations=0,hashCalls=0;
const parentState=new Proxy(Object.create(null) as NftState,{ownKeys(target){enumerations++;return Reflect.ownKeys(target);}});
const verifier:PowEngine={async hash(){hashCalls++;return 'f'.repeat(64);},close(){}};
await assert.rejects(validateBlock(candidate,[],parentState,verifier),/Invalid RandomX proof/);
assert.equal(hashCalls,1);
assert.equal(enumerations,0,'invalid work must be rejected before materializing the full NFT state');
});
test('DAG retains equal-work branches, serializes duplicates and selects strictly greater cumulative work', async t => {
const { ledger, dir } = await fixture(t);
t.mock.method(Date, 'now', () => (GENESIS_TIME + CONSENSUS.targetSeconds * 20) * 1000);
@@ -182,6 +227,25 @@ test('DAG retains equal-work branches, serializes duplicates and selects strictl
assert.deepEqual(restart.allBlocks, stored);
});
test('a deeper competing fork disconnects the old chain only after cumulative work wins',async t=>{
const {ledger}=await fixture(t);const original=branch(),competitor=branch();
t.mock.method(Date,'now',()=>(GENESIS_TIME+CONSENSUS.targetSeconds*100)*1000);
for(let i=0;i<30;i++)await ledger.submit(await extend(original,alice,[],CONSENSUS.targetSeconds));
assert.equal(ledger.snapshot().tip,original.blocks.at(-1)!.hash);
for(let i=0;i<15;i++){
const result=await ledger.submit(await extend(competitor,bob,[],1));
assert.equal(result.reorg,false);
assert.equal(ledger.snapshot().tip,original.blocks.at(-1)!.hash);
}
assert.equal(competitor.blocks.at(-1)!.height,15);
assert.notEqual(competitor.blocks.at(-1)!.target,MAX_TARGET);
const winning=await ledger.submit(await extend(competitor,bob,[],1));
assert.deepEqual(winning,{accepted:true,reorg:true});
assert.ok(ledger.snapshot().cumulativeWork===competitor.blocks.reduce((sum,block)=>sum+workFor(block.target),0n).toString());
assert.deepEqual(ledger.snapshot().blocks,competitor.blocks);
assert.equal(ledger.size,46,'both branches remain available in the DAG');
});
test('equal-work ties preserve the current tip across restart and only extension triggers reorg', async t => {
const { dir, ledger } = await fixture(t);
const a = branch(), b = branch();
@@ -473,7 +537,7 @@ test('SQLite startup rechecks every proof and fails closed on database corruptio
tampered.prepare('UPDATE blocks SET block_json = ? WHERE hash = ?')
.run(JSON.stringify({ ...originalBlock, stateRoot: '00'.repeat(32) }), row.hash);
tampered.close();
await assert.rejects(Ledger.open(dir, new FakePow()), /state commitment/);
await assert.rejects(Ledger.open(dir, new FakePow()), /block ID/);
const restoreTampered = new DatabaseSync(file);
restoreTampered.prepare('UPDATE blocks SET block_json = ? WHERE hash = ?').run(row.block_json, row.hash);
restoreTampered.prepare("UPDATE metadata SET value = 'wrong' WHERE key = 'network'").run();