forked from WikiDeck/wikideck
More cards
This commit is contained in:
@@ -13,6 +13,7 @@ import { CHAIN_ID, GENESIS_HASH, GENESIS_TIME, MAX_TARGET, address, makeTemplate
|
||||
mintId, sha256, stateRoot, transferId, transition, workFor } from '../server/testnet/consensus.ts';
|
||||
import { NETWORK, CONSENSUS, blockClaimMessage, nftTransferMessage, testnetOwnerOf,
|
||||
type MiningTemplate, type NftState, type NftTransfer, type TestnetBlock } from '../shared/testnet.ts';
|
||||
import { deriveStats } from '../shared/enrichment.ts';
|
||||
import type { PowEngine } from '../server/testnet/randomx.ts';
|
||||
|
||||
// Deliberately test-only: production has no fake PoW selector or fallback.
|
||||
@@ -48,10 +49,23 @@ async function mine(template: MiningTemplate, miner: Identity = alice): Promise<
|
||||
}
|
||||
type Branch = { blocks: TestnetBlock[]; state: NftState };
|
||||
function branch(): Branch { return { blocks: [], state: {} }; }
|
||||
function articleFor(_ancestors: TestnetBlock[]) {
|
||||
const pageId = 50_000;
|
||||
return { pageId, title: `Test article ${pageId}`, extract: `A deterministic test article snapshot for Wikipedia page ${pageId}.`,
|
||||
image: 'https://upload.wikimedia.org/wikipedia/commons/test-image.jpg',
|
||||
url: `https://en.wikipedia.org/?curid=${pageId}`, revisionId: 1, source: 'live' as const, category: 'Culture' };
|
||||
}
|
||||
function statsSnapshot() {
|
||||
const pageBytes = 2048, languageCount = 3;
|
||||
return { capturedAt: '2026-10-19T00:00:00.000Z', revisionId: 1, pageBytes, languageCount,
|
||||
languagesTruncated: false, pageviews: { status: 'unavailable' as const, months: [], latestMonth: '2026-09',
|
||||
total: null, dailyAverage: null, trendPercent: null }, stats: deriveStats(pageBytes, languageCount, null) };
|
||||
}
|
||||
async function extend(chain: Branch, miner: Identity = alice, transactions: NftTransfer[] = [],
|
||||
spacing = 30): Promise<TestnetBlock> {
|
||||
const now = GENESIS_TIME + (chain.blocks.length + 1) * spacing;
|
||||
const block = await mine(makeTemplate(chain.blocks, chain.state, miner.owner, miner.publicKey, transactions, now), miner);
|
||||
const block = await mine(makeTemplate(chain.blocks, chain.state, miner.owner, miner.publicKey,
|
||||
transactions, articleFor(chain.blocks), statsSnapshot(), now), miner);
|
||||
chain.state = transition(chain.state, block, transactions);
|
||||
chain.blocks.push(block);
|
||||
return block;
|
||||
@@ -87,7 +101,7 @@ test('testnet genesis is empty, manifest-bound, isolated and defensively copied'
|
||||
await assert.rejects(readFile(join(dir, 'chain.json')), /ENOENT/);
|
||||
assert.equal(JSON.parse(await readFile(file, 'utf8')).version, 1);
|
||||
if (process.platform !== 'win32') assert.equal((await stat(file)).mode & 0o777, 0o600);
|
||||
const candidate = await mine(ledger.template(alice.owner, alice.publicKey));
|
||||
const candidate = await mine(ledger.template(alice.owner, alice.publicKey, articleFor([]), statsSnapshot()));
|
||||
assert.deepEqual(await ledger.submit(candidate), { accepted: true, reorg: false });
|
||||
candidate.article.title = 'caller mutation';
|
||||
const copy = ledger.block(candidate.hash)!;
|
||||
@@ -156,7 +170,8 @@ test('NFT maturity, ordered mempool, block commitments and replay protection', a
|
||||
assert.equal(testnetOwnerOf(ledger.snapshot(), minted.tokenId), alice.owner); // Unconfirmed is not ownership.
|
||||
const restarted = await Ledger.open(dir, new FakePow());
|
||||
assert.deepEqual(restarted.snapshot().pendingTransfers, [tx, tx2]);
|
||||
const block = await mine(restarted.template(alice.owner, alice.publicKey));
|
||||
const block = await mine(restarted.template(alice.owner, alice.publicKey,
|
||||
articleFor(restarted.snapshot().blocks), statsSnapshot()));
|
||||
assert.deepEqual(block.transactions, [tx, tx2]);
|
||||
await restarted.submit(block);
|
||||
assert.equal(testnetOwnerOf(restarted.snapshot(), minted.tokenId), carol.owner);
|
||||
@@ -189,7 +204,8 @@ test('reorg rolls back NFT ownership and requeues only still-valid disconnected
|
||||
assert.deepEqual(snapshot.pendingTransfers, [tx, tx2]); // Orphan issuance is discarded.
|
||||
assert.ok(ledger.block(committed.hash));
|
||||
assert.deepEqual((await Ledger.open(dir, new FakePow())).snapshot(), snapshot);
|
||||
const next = await mine(ledger.template(bob.owner, bob.publicKey), bob);
|
||||
const next = await mine(ledger.template(bob.owner, bob.publicKey,
|
||||
articleFor(ledger.snapshot().blocks), statsSnapshot()), bob);
|
||||
await ledger.submit(next);
|
||||
assert.equal(testnetOwnerOf(ledger.snapshot(), minted.tokenId), carol.owner);
|
||||
});
|
||||
@@ -210,11 +226,17 @@ test('reorg removes mempool conflicts and does not requeue already committed tra
|
||||
assert.equal(testnetOwnerOf(ledger.snapshot(), minted.tokenId), carol.owner);
|
||||
});
|
||||
|
||||
test('invalid parent, manifest, proof, catalog, transaction/state commitments leave ledger unchanged', async t => {
|
||||
test('invalid parent, manifest, proof, article/stat snapshots, and commitments leave ledger unchanged', async t => {
|
||||
const { ledger } = await fixture(t);
|
||||
const valid = await extend(branch());
|
||||
assert.notEqual(mintId(valid), mintId({ ...valid,
|
||||
statsSnapshot: { ...valid.statsSnapshot, capturedAt: '2026-10-19T00:00:01.000Z' } }));
|
||||
for (const changed of [ { previousHash: 'aa'.repeat(32) }, { chainId: 'bb'.repeat(32) },
|
||||
{ network: 'another-network' }, { article: { ...valid.article, extract: 'tampered catalog' } },
|
||||
{ network: 'another-network' }, { article: { ...valid.article, extract: 'tampered snapshot' } },
|
||||
{ article: { ...valid.article, image: 'https://evil.example/image.jpg' } },
|
||||
{ article: { ...valid.article, image: undefined } },
|
||||
{ statsSnapshot: { ...valid.statsSnapshot, capturedAt: '2026-10-19T00:00:01.000Z' } },
|
||||
{ statsSnapshot: { ...valid.statsSnapshot, stats: { ...valid.statsSnapshot.stats, depth: 0 } } },
|
||||
{ stateRoot: '00'.repeat(32) }, { transactionsRoot: '00'.repeat(32) },
|
||||
{ tokenId: '00'.repeat(32) }, { powHash: '00'.repeat(32) }, { nonce: valid.nonce + 1 },
|
||||
{ signature: bob.sign(blockClaimMessage(valid.hash)) } ]) {
|
||||
@@ -223,7 +245,11 @@ test('invalid parent, manifest, proof, catalog, transaction/state commitments le
|
||||
}
|
||||
await assert.rejects(ledger.submitTransfer({ ...transfer('aa'.repeat(32)), chainId: 'bb'.repeat(32) }));
|
||||
await ledger.submit(valid); // Queue recovers from failed validations.
|
||||
assert.equal(ledger.size, 1);
|
||||
const repeated = await mine(ledger.template(alice.owner, alice.publicKey, valid.article, valid.statsSnapshot));
|
||||
assert.equal(repeated.article.pageId, valid.article.pageId);
|
||||
assert.notEqual(repeated.tokenId, valid.tokenId);
|
||||
await ledger.submit(repeated);
|
||||
assert.equal(ledger.size, 2);
|
||||
});
|
||||
|
||||
test('startup rechecks every proof, rejects tampering and mismatched manifests without resetting disk', async t => {
|
||||
@@ -286,7 +312,8 @@ test('mempool is ordered, capped at 256, and templates include at most 64 withou
|
||||
await Promise.all(transactions.map(tx => ledger.submitTransfer(tx)));
|
||||
assert.deepEqual(ledger.snapshot().pendingTransfers, transactions);
|
||||
await assert.rejects(ledger.submitTransfer(transfer(minted.tokenId, alice, bob, 256)), /256/);
|
||||
const template = ledger.template(alice.owner, alice.publicKey);
|
||||
const template = ledger.template(alice.owner, alice.publicKey,
|
||||
articleFor(ledger.snapshot().blocks), statsSnapshot());
|
||||
assert.deepEqual(template.transactions, transactions.slice(0, 64));
|
||||
await assert.rejects(ledger.submitTransfer(transfer(mintId(template.header))), /256/);
|
||||
const block = await mine(template);
|
||||
|
||||
Reference in New Issue
Block a user