This commit is contained in:
2026-10-19 21:47:38 -04:00
parent f8eeae826f
commit b1d2b5c10e
17 changed files with 549 additions and 92 deletions
+9
View File
@@ -208,6 +208,8 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
await (await request(first.base, '/api/mining/template', 400, {})).text();
await (await request(first.base, '/api/blocks', 400, {})).text();
await (await request(first.base, '/api/transfer', 400, {})).text();
await (await request(first.base, '/api/trades', 400, {})).text();
await (await request(first.base, `/api/decks/${'x'.repeat(64)}`, 400)).text();
await (await request(first.base, '/api/mining/template', 403, {}, { Origin: 'https://evil.example' })).text();
await (await request(first.base, '/api/mining/template', 415, {}, { 'Content-Type': 'text/plain' })).text();
const malformed = await fetch(`${first.base}/api/blocks`, { method: 'POST',
@@ -240,6 +242,13 @@ test('native RandomX HTTP: three blocks, mined transfer, peer gossip/sync and du
const block2 = await mine(first.base, alice);
assert.equal(block2.height, 2);
await submit(first.base, block2);
const aliceDeck = await json<{ address: string; tip: string; total: number; nfts: { block: TestnetBlock }[] }>(
first.base, `/api/decks/${alice.owner}`);
assert.equal(aliceDeck.address, alice.owner);
assert.equal(aliceDeck.tip, block2.hash);
assert.equal(aliceDeck.total, 2);
assert.deepEqual(aliceDeck.nfts.map(item => item.block.tokenId), [block1.tokenId, block2.tokenId]);
await (await request(first.base, `/api/decks/${'z'.repeat(64)}`, 400)).text();
// A truly independent process reconstructs the canonical chain over HTTP and checks real work.
const second = await start(secondDir, first.base);
+56 -2
View File
@@ -13,8 +13,8 @@ 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';
import { NETWORK, CONSENSUS, blockClaimMessage, nftTransferMessage, testnetOwnerOf,
type MiningTemplate, type NftState, type NftStateLayer, type NftStateView,
type NftTransfer, type TestnetBlock } from '../shared/testnet.ts';
nftTradeMessage, type MiningTemplate, type NftState, type NftStateLayer, type NftStateView,
type NftTrade, type NftTransfer, type TestnetBlock } from '../shared/testnet.ts';
import { deriveStats } from '../shared/enrichment.ts';
import type { PowEngine } from '../server/testnet/randomx.ts';
@@ -77,6 +77,15 @@ function transfer(cardHash: string, from: Identity = alice, to: Identity = bob,
sequence, publicKey: from.publicKey };
return { ...tx, signature: from.sign(nftTransferMessage(tx)) };
}
function trade(offered: TestnetBlock[], requested: TestnetBlock[], proposer: Identity = alice,
counterparty: Identity = bob): NftTrade {
const terms = { network: NETWORK, chainId: CHAIN_ID, proposer: proposer.owner, counterparty: counterparty.owner,
offered: offered.map(block => ({ cardHash: block.tokenId, sequence: 0 })),
requested: requested.map(block => ({ cardHash: block.tokenId, sequence: 0 })) };
const message = nftTradeMessage(terms);
return { type: 'trade', ...terms, proposerPublicKey: proposer.publicKey, proposerSignature: proposer.sign(message),
counterpartyPublicKey: counterparty.publicKey, counterpartySignature: counterparty.sign(message) };
}
async function fixture(t: { after: (fn: () => Promise<void>) => void }) {
const dir = await mkdtemp(join(tmpdir(), 'wikideck-testnet-'));
const engine = new FakePow();
@@ -192,6 +201,51 @@ test('NFT maturity, ordered mempool, block commitments and replay protection', a
await assert.rejects(restarted.submitTransfer(transfer(minted.tokenId, carol, alice, 0)), /owner\/sequence/);
});
test('signed multi-card trade is validated, mempooled, mined, and applied atomically', async t => {
const { ledger } = await fixture(t);
const chain = branch();
const aliceFirst = await extend(chain, alice); await ledger.submit(aliceFirst);
const bobFirst = await extend(chain, bob); await ledger.submit(bobFirst);
const aliceSecond = await extend(chain, alice); await ledger.submit(aliceSecond);
const bobSecond = await extend(chain, bob); await ledger.submit(bobSecond);
await ledger.submit(await extend(chain, alice)); // All four NFTs are mature at the next inclusion height.
const offered = [aliceFirst, aliceSecond], requested = [bobFirst, bobSecond];
const invalid = trade(offered, requested);
const badTerms = { ...invalid, requested: invalid.requested.map((leg, index) =>
index === 1 ? { ...leg, sequence: 1 } : leg) };
const badMessage = nftTradeMessage(badTerms);
const badSequence = { ...badTerms, proposerSignature: alice.sign(badMessage), counterpartySignature: bob.sign(badMessage) };
const before = ledger.snapshot();
await assert.rejects(ledger.submitTransaction(badSequence), /missing, stale, or immature/);
const badSignature = { ...invalid, proposerSignature: bob.sign(nftTradeMessage(invalid)) };
await assert.rejects(ledger.submitTransaction(badSignature), /signature/i);
assert.deepEqual(ledger.snapshot().pendingTransfers, []);
for (const block of offered) assert.equal(testnetOwnerOf(before, block.tokenId), alice.owner);
for (const block of requested) assert.equal(testnetOwnerOf(before, block.tokenId), bob.owner);
const bundle = trade(offered, requested);
await ledger.submitTransaction(bundle);
assert.deepEqual(ledger.snapshot().pendingTransfers, [bundle]);
const template = ledger.template(alice.owner, alice.publicKey, articleFor(ledger.snapshot().blocks), statsSnapshot());
assert.deepEqual(template.transactions, [bundle]);
await ledger.submit(await mine(template));
const after = ledger.snapshot();
assert.equal(after.pendingTransfers.length, 0);
for (const block of offered) assert.equal(testnetOwnerOf(after, block.tokenId), bob.owner);
for (const block of requested) assert.equal(testnetOwnerOf(after, block.tokenId), alice.owner);
assert.deepEqual(after.transfers.at(-1), bundle);
const deck = ledger.deck(alice.owner, 0, 2);
assert.equal(deck.total, after.blocks.filter(block => testnetOwnerOf(after, block.tokenId) === alice.owner).length);
assert.equal(deck.nfts.length, 2);
assert.equal(deck.next, 2);
assert.deepEqual(ledger.deck(alice.owner, deck.next!, 2).nfts.map(item => item.block.tokenId).sort(),
ledger.deck(alice.owner).nfts.slice(2).map(item => item.block.tokenId).sort());
assert.deepEqual(ledger.deck(alice.owner).nfts.map(item => item.block.tokenId).sort(),
after.blocks.filter(block => testnetOwnerOf(after, block.tokenId) === alice.owner)
.map(block => block.tokenId).sort());
});
test('reorg rolls back NFT ownership and requeues only still-valid disconnected transfers in order', async t => {
const { ledger, dir } = await fixture(t);
const original = branch();
+1 -1
View File
@@ -54,7 +54,7 @@ test('Node WebCrypto backup roundtrip preserves SPKI SHA256 identity and signing
key: createPublicKey({ key: spki, format: 'der', type: 'spki' }), dsaEncoding: 'ieee-p1363',
}, Buffer.from(signature, 'base64')), true);
assert.equal(backup.iterations, 600000);
assert.equal(backup.network, 'wikideck-randomx-testnet-5');
assert.equal(backup.network, 'wikideck-randomx-testnet-6');
assert.equal(Buffer.from(backup.salt, 'base64').length, 16);
assert.equal(Buffer.from(backup.iv, 'base64').length, 12);
assert.ok(Buffer.byteLength(JSON.stringify(backup)) < 32 * 1024);