Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cover reorg for double open index #875

Merged
merged 3 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion lib/wallet/txdb.js
Original file line number Diff line number Diff line change
Expand Up @@ -784,7 +784,6 @@ class TXDB {

/**
* Add transaction without a batch.
* @private
* @param {TX} tx
* @param {BlockMeta} [block]
* @returns {Promise<Details?>}
Expand Down Expand Up @@ -880,6 +879,11 @@ class TXDB {
if (!hash)
continue;

const wtx = await this.getTX(hash);

if (wtx.height !== -1)
return;

await this.remove(hash);
}
}
Expand Down Expand Up @@ -1278,6 +1282,8 @@ class TXDB {
// Commit the new state. The balance has updated.
const balance = await this.updateBalance(b, state);

this.unindexOpens(b, tx);

await b.write();

this.unlockTX(tx);
Expand Down Expand Up @@ -1533,6 +1539,14 @@ class TXDB {
if (tx.isCoinbase())
return this.removeRecursive(wtx);

// On unconfirm, if we already have OPEN txs in the pending list we
// remove transaction and it's descendants instead of storing them in
// the pending list. This follows the mempool behaviour where the first
// entries in the mempool will be the ones left, instead of txs coming
// from the block. This ensures consistency with the double open rules.
if (await this.isDoubleOpen(tx))
return this.removeRecursive(wtx);

return this.disconnect(wtx, wtx.getBlock());
}

Expand Down Expand Up @@ -1650,6 +1664,10 @@ class TXDB {
await this.saveCredit(b, credit, path);
}

// Unconfirm will also index OPENs as the transaction is now part of the
// wallet pending transactions.
this.indexOpens(b, tx);

// Undo name state.
await this.undoNameState(b, tx);

Expand Down
3 changes: 2 additions & 1 deletion lib/wallet/walletdb.js
Original file line number Diff line number Diff line change
Expand Up @@ -2215,7 +2215,7 @@ class WalletDB extends EventEmitter {
/**
* Revert TXDB to an older state.
* @param {Number} target
* @returns {Promise}
* @returns {Promise<Number>}
*/

async revert(target) {
Expand All @@ -2241,6 +2241,7 @@ class WalletDB extends EventEmitter {
});

this.logger.info('Rolled back %d WalletDB transactions.', total);
return total;
}

/**
Expand Down
192 changes: 168 additions & 24 deletions test/wallet-auction-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ const Network = require('../lib/protocol/network');
const rules = require('../lib/covenants/rules');
const Address = require('../lib/primitives/address');
const Output = require('../lib/primitives/output');
const Coin = require('../lib/primitives/coin');
const Covenant = require('../lib/primitives/covenant');
const {Resource} = require('../lib/dns/resource');
const {forEvent} = require('./util/common');

const network = Network.get('regtest');
const NAME1 = rules.grindName(10, 2, network);
Expand Down Expand Up @@ -56,7 +58,7 @@ const wdb = new WalletDB({
});

describe('Wallet Auction', function() {
let winner, openAuctionMTX, openAuctionMTX2;
let wallet;

before(async () => {
// Open
Expand All @@ -67,15 +69,19 @@ describe('Wallet Auction', function() {
await wdb.connect();

// Set up wallet
winner = await wdb.create();
wallet = await wdb.create();
chain.on('connect', async (entry, block) => {
await wdb.addBlock(entry, block.txs);
});

chain.on('disconnect', async (entry) => {
await wdb.removeBlock(entry);
});

// Generate blocks to roll out name and fund wallet
let winnerAddr = await winner.createReceive();
let winnerAddr = await wallet.createReceive();
winnerAddr = winnerAddr.getAddress().toString(network);
for (let i = 0; i < 4; i++) {
for (let i = 0; i < 10; i++) {
const block = await cpu.mineBlock(null, winnerAddr);
await chain.add(block);
}
Expand All @@ -90,17 +96,44 @@ describe('Wallet Auction', function() {
});

describe('Duplicate OPENs', function() {
// Prepare several OPEN txs to mine them on the network.
// Because they don't have any height, we can reuse them whenever
// we want.
const OPENS1 = 4;
const openTXs = [];

// block/mempool/confirm indexes
let openIndex = 0;
const insertIndexes = [];

it('should open auction', async () => {
openAuctionMTX = await winner.createOpen(NAME1);
await winner.sign(openAuctionMTX);
const tx = openAuctionMTX.toTX();
await wdb.addTX(tx);
for (let i = 0; i < OPENS1; i++) {
const open = await wallet.createOpen(NAME1);
await wallet.sign(open);

assert.strictEqual(open.inputs.length, 1);
// make sure we don't double spend.
wallet.lockCoin(open.inputs[0].prevout);
openTXs.push(open);
}

// This one will not get confirmed, but will be forever erased.
insertIndexes[0] = openIndex;
const openMTX = openTXs[openIndex++];
const tx = openMTX.toTX();
const addResult = await wdb.addTX(tx);
assert.strictEqual(addResult.size, 1);
assert.ok(addResult.has(wallet.wid));

const pending = await wallet.getPending();
assert.strictEqual(pending.length, 1);
assert.bufferEqual(pending[0].hash, tx.hash());
});

it('should fail to create duplicate open', async () => {
let err;
try {
await winner.createOpen(NAME1);
await wallet.createOpen(NAME1);
} catch (e) {
err = e;
}
Expand All @@ -109,20 +142,48 @@ describe('Wallet Auction', function() {
assert.strictEqual(err.message, `Already sent an open for: ${NAME1}.`);
});

it('should mine 1 block', async () => {
it('should not accept own duplicate open', async () => {
const pendingBefore = await wallet.getPending();
assert.strictEqual(pendingBefore.length, 1);
assert.bufferEqual(pendingBefore[0].hash, openTXs[insertIndexes[0]].hash());

const openMTX = openTXs[openIndex];
const result = await wdb.addTX(openMTX.toTX());
assert.strictEqual(result, null);

const pendingAfter = await wallet.getPending();
assert.strictEqual(pendingAfter.length, 1);
assert.bufferEqual(pendingAfter[0].hash, openTXs[insertIndexes[0]].hash());
});

it('should mine 1 block with different OPEN tx', async () => {
const job = await cpu.createJob();
job.addTX(openAuctionMTX.toTX(), openAuctionMTX.view);

const removeEvents = forEvent(wdb, 'remove tx');

insertIndexes[1] = openIndex;
const openMTX = openTXs[openIndex++];

const [tx, view] = openMTX.commit();
job.addTX(tx, view);
job.refresh();

const block = await job.mineAsync();

assert(await chain.add(block));

const removedTXs = await removeEvents;
assert.strictEqual(removedTXs.length, 1);
const removedTX = removedTXs[0].values[1];
assert.bufferEqual(removedTX.hash(), openTXs[0].hash());

const pending = await wallet.getPending();
assert.strictEqual(pending.length, 0);
});

it('should fail to re-open auction during OPEN phase', async () => {
let err;
try {
await winner.createOpen(NAME1);
await wallet.createOpen(NAME1);
} catch (e) {
err = e;
}
Expand All @@ -140,20 +201,20 @@ describe('Wallet Auction', function() {
});

it('should fail to send bid to null address', async () => {
const mtx = await winner.makeBid(NAME1, 1000, 2000, 0);
const mtx = await wallet.makeBid(NAME1, 1000, 2000, 0);
mtx.outputs[0].address = new Address();
await winner.fill(mtx);
await winner.finalize(mtx);
await wallet.fill(mtx);
await wallet.finalize(mtx);

const fn = async () => await winner.sendMTX(mtx);
const fn = async () => await wallet.sendMTX(mtx);

await assert.rejects(fn, {message: 'Cannot send to null address.'});
});

it('should fail to re-open auction during BIDDING phase', async () => {
let err;
try {
await winner.createOpen(NAME1);
await wallet.createOpen(NAME1);
} catch (e) {
err = e;
}
Expand All @@ -171,16 +232,16 @@ describe('Wallet Auction', function() {
});

it('should open auction (again)', async () => {
openAuctionMTX2 = await winner.createOpen(NAME1);
await winner.sign(openAuctionMTX2);
const tx = openAuctionMTX2.toTX();
await wdb.addTX(tx);
// This one will be inserted and THEN confirmed.
insertIndexes[2] = openIndex;
const mtx = openTXs[openIndex++];
await wdb.addTX(mtx.toTX());
});

it('should fail to create duplicate open (again)', async () => {
let err;
try {
await winner.createOpen(NAME1);
await wallet.createOpen(NAME1);
} catch (e) {
err = e;
}
Expand All @@ -191,7 +252,8 @@ describe('Wallet Auction', function() {

it('should confirm OPEN transaction', async () => {
const job = await cpu.createJob();
job.addTX(openAuctionMTX2.toTX(), openAuctionMTX2.view);
const [tx, view] = openTXs[insertIndexes[2]].commit();
job.addTX(tx, view);
job.refresh();

const block = await job.mineAsync();
Expand All @@ -211,6 +273,88 @@ describe('Wallet Auction', function() {
state = ns.state(chain.height, network);
assert.strictEqual(state, states.BIDDING);
});

it('should create TX spending change of the OPEN', async () => {
// Last OPEN and spending change will be used for the test in
// the pending index test.
const lastOpenMTX = openTXs[insertIndexes[2]];
const change = lastOpenMTX.outputs[1];
assert.notStrictEqual(change.value, 0);

// does not matter where this goes.
const spendMTX = wallet.makeTX([{
value: 1e5,
address: change.address
}]);

const coin = Coin.fromTX(lastOpenMTX.toTX(), 1, wdb.height);
await spendMTX.fund([coin], {
changeAddress: await wallet.changeAddress()
});

// We don't mine this transaction and reuse this to make sure
// double opens are properly removed.
await wallet.sign(spendMTX);
const added = await wdb.addTX(spendMTX.toTX());
assert.strictEqual(added.size, 1);
});

it('should mine enough blocks to expire auction (again)', async () => {
for (let i = 0; i < biddingPeriod + revealPeriod; i++) {
const block = await cpu.mineBlock();
assert(block);
assert(await chain.add(block));
}
});

it('should insert OPEN into the block', async () => {
// This makes sure the confirmed/mined TX does not get removed
const job = await cpu.createJob();

insertIndexes[3] = openIndex;
const openMTX = openTXs[openIndex++];
let countRemoves = 0;

wdb.on('remove tx', () => {
countRemoves++;
});

const [tx, view] = openMTX.commit();
job.addTX(tx, view);
job.refresh();

const block = await job.mineAsync();
assert(await chain.add(block));

assert.strictEqual(countRemoves, 0);
});

it('should revert the two auctions and only leave one open', async () => {
const pendingBefore = await wallet.getPending();
assert.strictEqual(pendingBefore.length, 1);

await wdb.rollback(biddingPeriod + revealPeriod + 2);
const pendingAfter = await wallet.getPending();

// first OPEN and tx spending from first OPEN should get removed.
// This mimics the behaviour of the mempool where OPENs from the block
// will end up getting removed, if there's OPEN sitting there.
assert.strictEqual(pendingAfter.length, 1);

const secondTX = await wallet.getTX(openTXs[insertIndexes[2]].hash());
assert.strictEqual(secondTX, null);
});

it('should resync and recover', async () => {
for (let i = wdb.height; i <= chain.tip.height; i++) {
const entry = await chain.getEntryByHeight(i);
const block = await chain.getBlock(entry.hash);
await wdb.addBlock(entry, block.txs);
}

const secondTX = await wallet.getTX(openTXs[insertIndexes[2]].hash());
assert.notStrictEqual(secondTX, null);
});
});

describe('Batch TXs', function() {
Expand Down
Loading