itns-sidechain/test/chain-test.js
2022-05-13 22:51:32 +04:00

423 lines
12 KiB
JavaScript

'use strict';
const assert = require('bsert');
const Chain = require('../lib/blockchain/chain');
const BlockStore = require('../lib/blockstore/level');
const WorkerPool = require('../lib/workers/workerpool');
const Miner = require('../lib/mining/miner');
const MemWallet = require('./util/memwallet');
const Network = require('../lib/protocol/network');
const MerkleBlock = require('../lib/primitives/merkleblock');
const network = Network.get('regtest');
const workers = new WorkerPool({
// Must be disabled for `ownership.ignore`.
enabled: false,
size: 2
});
function addBlock(chain, block, wallet) {
if (chain.options.spv) {
const mblock = MerkleBlock.fromBlock(block, wallet.filter);
return chain.add(mblock);
}
return chain.add(block);
}
for (const type of ['full', 'spv']) {
describe(`Chain reorg test (${type})`, function() {
this.timeout(0);
const blocks = new BlockStore({
memory: true,
network
});
const chain = new Chain({
memory: true,
blocks,
network
});
let testBlocks;
if (type === 'full') {
testBlocks = new BlockStore({
memory: true,
network
});
}
const testChain = new Chain({
memory: true,
spv: type === 'spv',
blocks: testBlocks,
network
});
const miner = new Miner({
chain,
workers
});
const cpu = miner.cpu;
const wallet = new MemWallet({
network
});
chain.on('connect', async (entry, block) => {
wallet.addBlock(entry, block.txs);
});
chain.on('disconnect', (entry, block) => {
wallet.removeBlock(entry, block.txs);
});
let fork = null;
let tip1 = null;
let tip2 = null;
before(async () => {
if (type === 'full')
await testBlocks.open();
await blocks.open();
await chain.open();
await testChain.open();
await miner.open();
});
after(async () => {
await miner.close();
await chain.close();
await testChain.close();
await blocks.close();
if (type === 'full')
await testBlocks.close();
});
it('should add addrs to miner', async () => {
miner.addresses.length = 0;
miner.addAddress(wallet.getReceive());
});
it('should mine 10 blocks', async () => {
for (let i = 0; i < 10; i++) {
const block = await cpu.mineBlock();
assert(block);
assert(await chain.add(block));
assert(await addBlock(testChain, block, wallet));
}
fork = chain.tip;
});
it('should mine competing chain', async () => {
for (let i = 0; i < 10; i++) {
const job1 = await cpu.createJob(tip1);
const job2 = await cpu.createJob(tip2);
const mtx = await wallet.create({
outputs: [{
address: wallet.getAddress(),
value: 10 * 1e8
}]
});
assert(job1.addTX(mtx.toTX(), mtx.view));
assert(job2.addTX(mtx.toTX(), mtx.view));
job1.refresh();
job2.refresh();
const blk1 = await job1.mineAsync();
const blk2 = await job2.mineAsync();
const hash1 = blk1.hash();
const hash2 = blk2.hash();
assert(await chain.add(blk1));
assert(await chain.add(blk2));
assert(await addBlock(testChain, blk1, wallet));
assert(await addBlock(testChain, blk2, wallet));
assert.bufferEqual(chain.tip.hash, hash1);
tip1 = await chain.getEntry(hash1);
tip2 = await chain.getEntry(hash2);
assert(tip1);
assert(tip2);
const testTip1 = await testChain.getEntry(hash1);
const testTip2 = await testChain.getEntry(hash2);
assert(testTip1);
assert(testTip2);
assert(!await chain.isMainChain(tip2));
assert(await testChain.isMainChain(tip1));
assert(!await testChain.isMainChain(tip2));
assert.strictEqual(tip1.height, 11 + i);
}
});
if (type === 'spv') {
it('should reorg to alternative chain and back (spv)', async () => {
const reorgs = [];
let resets = 0;
const handleReorg = (tip, competitor) => {
reorgs.push({
competitor,
tip
});
};
const handleReset = () => resets++;
testChain.on('reorganize', handleReorg);
testChain.on('reset', handleReset);
// Replay main chain to the SPV node.
const replayChain = async (fork) => {
let entry = await chain.getEntry(fork.height + 1);
while (entry) {
const blk = await chain.getBlock(entry.hash);
assert(await addBlock(testChain, blk, wallet));
entry = await chain.getEntry(entry.height + 1);
}
};
// This will cause reset for SPV and Reorg.
{
const job = await cpu.createJob(tip2);
job.refresh();
const blk = await job.mineAsync();
const hash = blk.hash();
assert(await chain.add(blk));
assert(await addBlock(testChain, blk, wallet));
// SPV Node will reset instead of reconnecting.
// and wait for the network to add blocks.
tip2 = await chain.getEntry(hash);
assert(tip2);
assert.strictEqual(await testChain.getEntry(hash), null);
assert.bufferEqual(testChain.tip.hash, fork.hash);
assert(await chain.isMainChain(tip2));
}
// sync chain with spv.
await replayChain(fork);
assert.bufferEqual(testChain.tip.hash, tip2.hash);
// Another reorg, BACK TO first chain.
// For SPV, this chain is lost so new blocks
// will get added as orphans. So this should not
// automatically cause reorg/reset until we fulfill
// missing block entries.
for (let i = 0; i < 2; i++) {
const job = await cpu.createJob(tip1);
job.refresh();
const blk = await job.mineAsync();
const hash = blk.hash();
assert(await chain.add(blk));
// i = 1 will give use new best chain tip and tip1 will become best.
tip1 = await chain.getEntry(hash);
assert(tip1);
assert.strictEqual(await testChain.getEntry(hash), null);
}
// Replay current best chain. This should
// cause SPV to reset chain to the fork.
// above reset will catch it and replay again.
await replayChain(fork);
assert.bufferEqual(testChain.tip.hash, fork.hash);
// Now we need to replay blocks once again,
// because above replay caused it to reorg and reset.
await replayChain(fork);
assert.bufferEqual(testChain.tip.hash, tip1.hash);
assert.strictEqual(reorgs.length, 2);
// Add alternative tip2 chain as well.
// This will not cause neither reorg nor reset
// and will simply add alternative chain. (Used in the next test)
let entry = await chain.getEntry(tip2.hash);
for (;;) {
assert.strictEqual(await testChain.getEntry(entry.hash), null);
const block = await chain.getBlock(entry.hash);
if (entry.prevBlock.equals(fork.hash))
assert(await addBlock(testChain, block, wallet), null);
else
assert.strictEqual(await addBlock(testChain, block, wallet), null);
entry = await chain.getEntry(entry.prevBlock);
if (entry.hash.equals(fork.hash))
break;
}
assert.bufferEqual(testChain.tip.hash, tip1.hash);
assert.strictEqual(reorgs.length, 2);
assert.strictEqual(resets, 2);
testChain.removeListener('reorganize', handleReorg);
testChain.removeListener('reset', handleReset);
});
}
if (type === 'full') {
it('should reorg to alternative chain and back (full)', async () => {
const reorgs = [];
const tips = [];
const reset = [];
const handleReorg = (tip, competitor) => {
reorgs.push({
competitor,
tip
});
};
const handleTip = tip => tips.push(tip);
const handleReset = tip => reset.push(tip);
testChain.on('reorganize', handleReorg);
testChain.on('tip', handleTip);
testChain.on('reset', handleReset);
const firstTip = chain.tip;
let secondTip = null;
let firstCompetitor = null;
let secondCompetitor = null;
{
const job = await cpu.createJob(tip2);
job.refresh();
const blk = await job.mineAsync();
const hash = blk.hash();
assert(await chain.add(blk));
assert(await addBlock(testChain, blk, wallet));
tip2 = await chain.getEntry(hash);
firstCompetitor = tip2;
secondTip = tip2;
assert(tip2);
assert(await testChain.getEntry(hash));
assert(await chain.isMainChain(tip2));
assert(await testChain.isMainChain(tip2));
}
assert(!await chain.isMainChain(tip1));
assert(!await testChain.isMainChain(tip1));
assert(await chain.isMainChain(tip2));
assert(await testChain.isMainChain(tip2));
for (let i = 0; i < 2; i++) {
const job = await cpu.createJob(tip1);
job.refresh();
const blk = await job.mineAsync();
const hash = blk.hash();
assert(await chain.add(blk));
assert(await addBlock(testChain, blk, wallet));
tip1 = await chain.getEntry(hash);
// last one.
secondCompetitor = tip1;
assert(tip1);
assert(await testChain.getEntry(hash));
}
assert(await chain.isMainChain(tip1));
assert(await testChain.isMainChain(tip1));
assert(!await chain.isMainChain(tip2));
assert(!await testChain.isMainChain(tip2));
testChain.removeListener('reorganize', handleReorg);
testChain.removeListener('tip', handleTip);
testChain.removeListener('reset', handleReset);
// Check reorg events
assert.strictEqual(reorgs.length, 2, 'it must reorg twice.');
// First competitor is the tip2.
assert.bufferEqual(reorgs[0].competitor.hash, firstCompetitor.hash);
assert.bufferEqual(reorgs[0].tip.hash, firstTip.hash);
assert.bufferEqual(reorgs[1].competitor.hash, secondCompetitor.hash);
assert.bufferEqual(reorgs[1].tip.hash, secondTip.hash);
});
};
it('should remove competing chains on reset', async () => {
// tip1 is the best
// tip2 is alt (prev test)
const prevTip = await testChain.getEntry(tip1.prevBlock);
assert(prevTip);
{
const entry1 = await testChain.getEntry(tip1.hash);
const entry2 = await testChain.getEntry(tip2.hash);
assert(entry1);
assert(entry2);
}
await testChain.reset(tip1.height - 1);
{
const entry1 = await testChain.getEntry(prevTip.hash);
const entry2 = await testChain.getEntry(tip2.hash);
assert(entry1);
assert.strictEqual(entry2, null);
}
// sync with the main chain.
const blk = await chain.getBlock(tip1.hash);
await addBlock(testChain, blk, wallet);
});
it('should fail to connect bad MTP', async () => {
const mtp = await chain.getMedianTime(chain.tip);
const job = await cpu.createJob(tip1);
job.attempt.time = mtp - 1;
const blk = await job.mineAsync();
await assert.rejects(async () => {
await addBlock(testChain, blk, wallet);
}, {
code: 'invalid',
reason: 'time-too-old',
score: 0,
malleated: false
});
});
it('should fail to connect blocks too ahead of time', async () => {
// 2 hours into the future.
const job = await cpu.createJob(tip1);
job.attempt.time = network.now() + 1 + 2 * 60 * 60;
const blk = await job.mineAsync();
await assert.rejects(async () => {
await addBlock(testChain, blk, wallet);
}, {
code: 'invalid',
reason: 'time-too-new',
score: 0,
malleated: true
});
});
});
}