diff --git a/CHANGELOG.md b/CHANGELOG.md index e85f09b6..c0c4b2a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ **When upgrading to this version of hsd, you must pass `--chain-migrate=4` and `--wallet-migrate=7` when you run it for the first time.** +### Network + +**End Airdrop soft fork has been included. ([#927](https://github.com/handshake-org/hsd/pull/927)) +Miners who want to support the soft-fork need to start signalling with `airstop` bit.** + ### Wallet Changes #### Wallet HTTP API @@ -213,6 +218,11 @@ The following methods have been deprecated: ## v6.0.0 +### Network + +**ICANN Lockup soft fork has been included. ([#819](https://github.com/handshake-org/hsd/pull/819), [#828](https://github.com/handshake-org/hsd/pull/828), [#834](https://github.com/handshake-org/hsd/pull/834)) +Miners who want to support the soft-fork need to start signalling with `icannlockup` bit.** + ### Node and Wallet HTTP API Validation errors, request paremeter errors or bad HTTP requests will no longer return (and log) `500` status code, instead will return `400`. diff --git a/lib/blockchain/chain.js b/lib/blockchain/chain.js index 07d634be..bb96ac36 100644 --- a/lib/blockchain/chain.js +++ b/lib/blockchain/chain.js @@ -644,6 +644,14 @@ class Chain extends AsyncEmitter { // Airdrop proof. if (!output.covenant.isClaim()) { + // Disable airdrop claims if airstop is activated + if (state.hasAirstop) { + throw new VerifyError(block, + 'invalid', + 'bad-airdrop-disabled', + 100); + } + let proof; try { proof = AirdropProof.decode(witness.items[0]); @@ -739,6 +747,10 @@ class Chain extends AsyncEmitter { if (await this.isActive(prev, deployments.icannlockup)) state.nameFlags |= rules.nameFlags.VERIFY_COVENANTS_LOCKUP; + // Disable airdrop claims. + if (await this.isActive(prev, deployments.airstop)) + state.hasAirstop = true; + return state; } @@ -762,6 +774,9 @@ class Chain extends AsyncEmitter { if (!this.state.hasICANNLockup() && state.hasICANNLockup()) this.logger.warning('ICANN lockup has been activated.'); + if (!this.state.hasAirstop && state.hasAirstop) + this.logger.warning('Airdrop claims has been disabled.'); + this.state = state; } @@ -4115,6 +4130,7 @@ class DeploymentState { this.flags = Script.flags.MANDATORY_VERIFY_FLAGS; this.lockFlags = common.MANDATORY_LOCKTIME_FLAGS; this.nameFlags = rules.MANDATORY_VERIFY_COVENANT_FLAGS; + this.hasAirstop = false; } hasHardening() { diff --git a/lib/mempool/mempool.js b/lib/mempool/mempool.js index 8768cb47..b698c4f9 100644 --- a/lib/mempool/mempool.js +++ b/lib/mempool/mempool.js @@ -41,6 +41,9 @@ const Claim = require('../primitives/claim'); const AirdropProof = require('../primitives/airdropproof'); const {types} = rules; +/** @typedef {import('../types').Hash} Hash */ +/** @typedef {import('../blockchain/chainentry')} ChainEntry */ + /** * Mempool * Represents a mempool. @@ -274,6 +277,7 @@ class Mempool extends EventEmitter { // for a now expired name. Another // example is a stale BID for a name // which has now reached the REVEAL state. + const prevState = this.nextState; const state = await this.getNextState(); const hardened = state.hasHardening(); const invalid = this.contracts.invalidate(block.height, hardened); @@ -316,13 +320,18 @@ class Mempool extends EventEmitter { } } + // If the next block activates airstop we drop any leftover proofs, + // they can no longer be mined. + if (!prevState.hasAirstop && state.hasAirstop) + this.dropAirdrops(); + this.cache.sync(block.hash); await this.cache.flush(); this.tip = block.hash; - if (invalid.length > 0) { + if (invalid.size > 0) { this.logger.info( 'Invalidated %d txs for block %d.', invalid.size, block.height); @@ -375,7 +384,7 @@ class Mempool extends EventEmitter { const proof = AirdropProof.decode(witness.items[0]); const entry = AirdropEntry.fromAirdrop(proof, this.chain.height); - this.trackAirdrop(entry, -1); + this.trackAirdrop(entry); continue; } @@ -387,7 +396,7 @@ class Mempool extends EventEmitter { const entry = ClaimEntry.fromClaim(claim, data, this.chain.height); - this.trackClaim(entry, -1); + this.trackClaim(entry); } let total = 0; @@ -1048,11 +1057,10 @@ class Mempool extends EventEmitter { * fully processed. * @method * @param {Claim} claim - * @param {Number?} id * @returns {Promise} */ - async addClaim(claim, id) { + async addClaim(claim) { if (this.chain.height + 1 < this.network.txStart) { throw new VerifyError(claim, 'invalid', @@ -1063,7 +1071,7 @@ class Mempool extends EventEmitter { const hash = claim.hash(); const unlock = await this.locker.lock(hash); try { - return await this._addClaim(claim, id); + return await this._addClaim(claim); } finally { unlock(); } @@ -1074,15 +1082,11 @@ class Mempool extends EventEmitter { * @method * @private * @param {Claim} claim - * @param {Number?} id * @returns {Promise} */ - async _addClaim(claim, id) { - if (id == null) - id = -1; - - await this.insertClaim(claim, id); + async _addClaim(claim) { + await this.insertClaim(claim); if (util.now() - this.lastFlush > 10) { await this.cache.flush(); @@ -1095,11 +1099,10 @@ class Mempool extends EventEmitter { * @method * @private * @param {Claim} claim - * @param {Number?} id * @returns {Promise} */ - async insertClaim(claim, id) { + async insertClaim(claim) { const height = this.chain.height + 1; const tip = this.chain.tip; const hash = claim.hash(); @@ -1175,7 +1178,7 @@ class Mempool extends EventEmitter { const entry = ClaimEntry.fromClaim(claim, data, this.chain.height); - this.trackClaim(entry, id); + this.trackClaim(entry); // Trim size if we're too big. if (this.limitSize(hash)) { @@ -1192,10 +1195,9 @@ class Mempool extends EventEmitter { /** * Track claim entry. * @param {ClaimEntry} entry - * @param {Number} id */ - trackClaim(entry, id) { + trackClaim(entry) { assert(!this.claims.has(entry.hash)); assert(!this.claimNames.has(entry.nameHash)); @@ -1213,7 +1215,6 @@ class Mempool extends EventEmitter { /** * Untrack claim entry. * @param {ClaimEntry} entry - * @param {Number} id */ untrackClaim(entry) { @@ -1261,11 +1262,10 @@ class Mempool extends EventEmitter { * fully processed. * @method * @param {AirdropProof} proof - * @param {Number?} id * @returns {Promise} */ - async addAirdrop(proof, id) { + async addAirdrop(proof) { if (this.chain.height + 1 < this.network.txStart) { throw new VerifyError(proof, 'invalid', @@ -1276,7 +1276,7 @@ class Mempool extends EventEmitter { const hash = proof.hash(); const unlock = await this.locker.lock(hash); try { - return await this._addAirdrop(proof, id); + return await this._addAirdrop(proof); } finally { unlock(); } @@ -1287,15 +1287,11 @@ class Mempool extends EventEmitter { * @method * @private * @param {AirdropProof} proof - * @param {Number?} id * @returns {Promise} */ - async _addAirdrop(proof, id) { - if (id == null) - id = -1; - - await this.insertAirdrop(proof, id); + async _addAirdrop(proof) { + await this.insertAirdrop(proof); if (util.now() - this.lastFlush > 10) { await this.cache.flush(); @@ -1308,11 +1304,10 @@ class Mempool extends EventEmitter { * @method * @private * @param {AirdropProof} proof - * @param {Number?} id * @returns {Promise} */ - async insertAirdrop(proof, id) { + async insertAirdrop(proof) { const hash = proof.hash(); // We can maybe ignore this. @@ -1326,6 +1321,9 @@ class Mempool extends EventEmitter { if (!proof.isSane()) throw new VerifyError(proof, 'invalid', 'bad-airdrop-proof', 100); + if (this.nextState.hasAirstop) + throw new VerifyError(proof, 'invalid', 'bad-airdrop-disabled', 0); + if (this.chain.height + 1 >= this.network.goosigStop) { const key = proof.getKey(); @@ -1365,7 +1363,7 @@ class Mempool extends EventEmitter { const entry = AirdropEntry.fromAirdrop(proof, this.chain.height); - this.trackAirdrop(entry, id); + this.trackAirdrop(entry); // Trim size if we're too big. if (this.limitSize(hash)) { @@ -1382,10 +1380,9 @@ class Mempool extends EventEmitter { /** * Track airdrop proof entry. * @param {AirdropEntry} entry - * @param {Number} id */ - trackAirdrop(entry, id) { + trackAirdrop(entry) { assert(!this.airdrops.has(entry.hash)); assert(!this.airdropIndex.has(entry.position)); @@ -1402,7 +1399,6 @@ class Mempool extends EventEmitter { /** * Untrack airdrop proof entry. * @param {AirdropEntry} entry - * @param {Number} id */ untrackAirdrop(entry) { @@ -2557,7 +2553,7 @@ class Mempool extends EventEmitter { * Map a transaction to the mempool. * @private * @param {MempoolEntry} entry - * @param {CoinView} view + * @param {CoinView} [view] */ trackEntry(entry, view) { diff --git a/lib/protocol/networks.js b/lib/protocol/networks.js index 6f376545..e0ec8432 100644 --- a/lib/protocol/networks.js +++ b/lib/protocol/networks.js @@ -480,6 +480,16 @@ main.deployments = { required: false, force: false }, + airstop: { + name: 'airstop', + bit: 2, + startTime: 1751328000, // July 1st, 2025 + timeout: 1759881600, // October 8th, 2025 + threshold: -1, + window: -1, + required: false, + force: false + }, testdummy: { name: 'testdummy', bit: 28, @@ -501,6 +511,7 @@ main.deployments = { main.deploys = [ main.deployments.hardening, main.deployments.icannlockup, + main.deployments.airstop, main.deployments.testdummy ]; @@ -731,6 +742,16 @@ testnet.deployments = { required: false, force: false }, + airstop: { + name: 'airstop', + bit: 2, + startTime: 1751328000, // July 1st, 2025 + timeout: 1759881600, // October 8th, 2025 + threshold: -1, + window: -1, + required: false, + force: false + }, testdummy: { name: 'testdummy', bit: 28, @@ -746,6 +767,7 @@ testnet.deployments = { testnet.deploys = [ testnet.deployments.hardening, testnet.deployments.icannlockup, + testnet.deployments.airstop, testnet.deployments.testdummy ]; @@ -885,6 +907,16 @@ regtest.deployments = { required: false, force: false }, + airstop: { + name: 'airstop', + bit: 2, + startTime: 1751328000, // July 1st, 2025 + timeout: 1759881600, // October 8th, 2025 + threshold: -1, + window: -1, + required: false, + force: false + }, testdummy: { name: 'testdummy', bit: 28, @@ -900,6 +932,7 @@ regtest.deployments = { regtest.deploys = [ regtest.deployments.hardening, regtest.deployments.icannlockup, + regtest.deployments.airstop, regtest.deployments.testdummy ]; @@ -1043,6 +1076,16 @@ simnet.deployments = { required: false, force: false }, + airstop: { + name: 'airstop', + bit: 2, + startTime: 1751328000, // July 1st, 2025 + timeout: 1759881600, // October 8th, 2025 + threshold: -1, + window: -1, + required: false, + force: false + }, testdummy: { name: 'testdummy', bit: 28, @@ -1058,6 +1101,7 @@ simnet.deployments = { simnet.deploys = [ simnet.deployments.hardening, simnet.deployments.icannlockup, + simnet.deployments.airstop, simnet.deployments.testdummy ]; diff --git a/test/chain-airstop-test.js b/test/chain-airstop-test.js new file mode 100644 index 00000000..618b5611 --- /dev/null +++ b/test/chain-airstop-test.js @@ -0,0 +1,477 @@ +'use strict'; + +const fs = require('fs'); +const { resolve } = require('path'); +const assert = require('bsert'); +const chainCommon = require('../lib/blockchain/common'); +const Network = require('../lib/protocol/network'); +const AirdropProof = require('../lib/primitives/airdropproof'); +const NodeContext = require('./util/node-context'); +const { thresholdStates } = chainCommon; + +const network = Network.get('regtest'); + +const AIRDROP_PROOF_FILE = resolve(__dirname, 'data', 'airdrop-proof.base64'); +const FAUCET_PROOF_FILE = resolve(__dirname, 'data', 'faucet-proof.base64'); +const read = file => Buffer.from(fs.readFileSync(file, 'binary'), 'base64'); + +// Sent to: +// { +// pub: '02a8959cc6491aed3fb96b3b684400311f2779fb092b026a4b170b35c175d48cec', +// hash: '95cb6129c6b98179866094b2717bfbe27d9c1921', +// addr: 'hs1qjh9kz2wxhxqhnpnqjje8z7lmuf7ecxfp6kxlly' +// } + +// Same as airdrop-test.js +const rawProof = read(AIRDROP_PROOF_FILE); +const rawFaucetProof = read(FAUCET_PROOF_FILE); // hs1qmjpjjgpz7dmg37paq9uksx4yjp675690dafg3q + +const airdropProof = AirdropProof.decode(rawProof); +const faucetProof = AirdropProof.decode(rawFaucetProof); + +const SOFT_FORK_NAME = 'airstop'; + +const networkDeployments = network.deployments; +const ACTUAL_START = networkDeployments[SOFT_FORK_NAME].startTime; +const ACTUAL_TIMEOUT = networkDeployments[SOFT_FORK_NAME].timeout; + +describe('BIP-9 - Airstop (integration)', function () { + const checkBIP9Info = (info, expected) => { + expected = expected || {}; + expected.startTime = expected.startTime || network.deployments[SOFT_FORK_NAME].startTime; + expected.timeout = expected.timeout || network.deployments[SOFT_FORK_NAME].timeout; + + assert(info, 'BIP9 info should be returned'); + assert.strictEqual(info.status, expected.status); + assert.strictEqual(info.bit, network.deployments[SOFT_FORK_NAME].bit); + assert.strictEqual(info.startTime, expected.startTime); + assert.strictEqual(info.timeout, expected.timeout); + }; + + const checkBIP9Statistics = (stats, expected) => { + expected = expected || {}; + + assert.strictEqual(stats.period, expected.period || network.minerWindow); + assert.strictEqual(stats.threshold, expected.threshold || network.activationThreshold); + assert.strictEqual(stats.elapsed, expected.elapsed); + assert.strictEqual(stats.count, expected.count); + assert.strictEqual(stats.possible, expected.possible); + }; + + describe('Success (integration)', function () { + const nodeCtx = new NodeContext(); + + before(async () => { + network.deployments[SOFT_FORK_NAME].startTime = 0; + network.deployments[SOFT_FORK_NAME].timeout = 0xffffffff; + + await nodeCtx.open(); + }); + + after(async () => { + network.deployments[SOFT_FORK_NAME].startTime = ACTUAL_START; + network.deployments[SOFT_FORK_NAME].timeout = ACTUAL_TIMEOUT; + + await nodeCtx.close(); + }); + + afterEach(() => { + nodeCtx.mempool.dropAirdrops(); + }); + + it('should be able to add airdrop & faucet proofs to the mempool', async () => { + await nodeCtx.mempool.addAirdrop(airdropProof); + await nodeCtx.mempool.addAirdrop(faucetProof); + assert.strictEqual(nodeCtx.mempool.airdrops.size, 2); + }); + + it('should be able to mine airdrop & faucet proofs', async () => { + await tryClaimingAirdropProofs(nodeCtx, [airdropProof, faucetProof]); + }); + + it('should be in DEFINED state', async () => { + const state = await getForkDeploymentState(nodeCtx.chain); + const bip9info = await getBIP9Info(nodeCtx); + + assert.strictEqual(state, thresholdStates.DEFINED); + checkBIP9Info(bip9info, { status: 'defined' }); + }); + + it('should start the soft-fork', async () => { + await mineNBlocks(network.minerWindow - 2, nodeCtx); + + // We are now at the threshold of the window. + { + const state = await getForkDeploymentState(nodeCtx.chain); + const bip9info = await getBIP9Info(nodeCtx); + assert.strictEqual(state, thresholdStates.DEFINED); + + checkBIP9Info(bip9info, { status: 'defined' }); + } + + // go into new window and change the state to started. + await mineBlock(nodeCtx); + + { + const state = await getForkDeploymentState(nodeCtx.chain); + const bip9info = await getBIP9Info(nodeCtx); + assert.strictEqual(state, thresholdStates.STARTED); + checkBIP9Info(bip9info, { status: 'started' }); + + checkBIP9Statistics(bip9info.statistics, { + elapsed: 0, + count: 0, + possible: true + }); + } + }); + + it('should still be able to add airdrop & faucet proofs to the mempool', async () => { + await nodeCtx.mempool.addAirdrop(airdropProof); + await nodeCtx.mempool.addAirdrop(faucetProof); + assert.strictEqual(nodeCtx.mempool.airdrops.size, 2); + }); + + it('should still be able to mine airdrop & faucet proofs', async () => { + await tryClaimingAirdropProofs(nodeCtx, [airdropProof, faucetProof]); + }); + + it('should lock in the soft-fork', async () => { + // Reach the height just before the start of the next window + await mineNBlocks(network.minerWindow - 1, nodeCtx, { signalFork: true }); + + { + const state = await getForkDeploymentState(nodeCtx.chain); + const bip9info = await getBIP9Info(nodeCtx); + assert.strictEqual(state, thresholdStates.STARTED); + checkBIP9Info(bip9info, { status: 'started' }); + + checkBIP9Statistics(bip9info.statistics, { + elapsed: network.minerWindow - 1, + count: network.minerWindow - 1, + possible: true + }); + } + + // After this the deployment goes to LOCKED_IN state. + await mineBlock(nodeCtx, { signalFork: true }); + + { + const state = await getForkDeploymentState(nodeCtx.chain); + const bip9info = await getBIP9Info(nodeCtx); + + assert.strictEqual(state, thresholdStates.LOCKED_IN); + checkBIP9Info(bip9info, { status: 'locked_in' }); + + assert(!bip9info.statistics); + } + }); + + it('should still be able to add airdrop & faucet proofs to the mempool', async () => { + await nodeCtx.mempool.addAirdrop(airdropProof); + await nodeCtx.mempool.addAirdrop(faucetProof); + assert.strictEqual(nodeCtx.mempool.airdrops.size, 2); + }); + + it('should still be able to mine airdrop & faucet proofs', async () => { + await tryClaimingAirdropProofs(nodeCtx, [airdropProof, faucetProof]); + }); + + it('should activate the soft-fork', async () => { + // Advance to ACTIVE state. + await mineNBlocks(network.minerWindow - 1, nodeCtx); + + const blockToAdd = await nodeCtx.miner.mineBlock(nodeCtx.chain.tip); + + await nodeCtx.mempool.addAirdrop(airdropProof); + await nodeCtx.mempool.addAirdrop(faucetProof); + assert.strictEqual(nodeCtx.mempool.airdrops.size, 2); + + await nodeCtx.chain.add(blockToAdd); + // mempool must drop airdrops if next block no longer + // allows them. + assert.strictEqual(nodeCtx.mempool.airdrops.size, 0); + + { + const state = await getForkDeploymentState(nodeCtx.chain); + const bip9info = await getBIP9Info(nodeCtx); + + assert.strictEqual(state, thresholdStates.ACTIVE); + checkBIP9Info(bip9info, { status: 'active' }); + + assert(!bip9info.statistics); + } + }); + + it('should not be able to add airdrops to the mempool', async () => { + let err; + + try { + await nodeCtx.mempool.addAirdrop(airdropProof); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.code, 'invalid'); + assert.strictEqual(err.reason, 'bad-airdrop-disabled'); + assert.strictEqual(err.score, 0); + + err = null; + + try { + await nodeCtx.mempool.addAirdrop(faucetProof); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.code, 'invalid'); + assert.strictEqual(err.reason, 'bad-airdrop-disabled'); + assert.strictEqual(err.score, 0); + }); + + it('should not be able to mine airdrop & faucet proofs anymore', async () => { + let err; + + try { + await tryClaimingAirdropProofs(nodeCtx, [airdropProof]); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.code, 'invalid'); + assert.strictEqual(err.reason, 'bad-airdrop-disabled'); + assert.strictEqual(err.score, 100); + + nodeCtx.mempool.dropAirdrops(); + + err = null; + + try { + await tryClaimingAirdropProofs(nodeCtx, [faucetProof]); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.code, 'invalid'); + assert.strictEqual(err.reason, 'bad-airdrop-disabled'); + assert.strictEqual(err.score, 100); + }); + }); + + describe('Failure (integration)', function () { + const nodeCtx = new NodeContext(); + + before(async () => { + network.deployments[SOFT_FORK_NAME].startTime = 0; + network.deployments[SOFT_FORK_NAME].timeout = 0xffffffff; + + await nodeCtx.open(); + }); + + after(async () => { + network.deployments[SOFT_FORK_NAME].startTime = ACTUAL_START; + network.deployments[SOFT_FORK_NAME].timeout = ACTUAL_TIMEOUT; + + await nodeCtx.close(); + }); + + afterEach(() => { + nodeCtx.mempool.dropAirdrops(); + }); + + it('should be able to add airdrop & faucet proofs to the mempool', async () => { + await nodeCtx.mempool.addAirdrop(airdropProof); + await nodeCtx.mempool.addAirdrop(faucetProof); + assert.strictEqual(nodeCtx.mempool.airdrops.size, 2); + }); + + it('should be able to mine airdrop & faucet proofs', async () => { + await tryClaimingAirdropProofs(nodeCtx, [airdropProof, faucetProof]); + }); + + it('should be in DEFINED state', async () => { + const state = await getForkDeploymentState(nodeCtx.chain); + const bip9info = await getBIP9Info(nodeCtx); + + assert.strictEqual(state, chainCommon.thresholdStates.DEFINED); + checkBIP9Info(bip9info, { status: 'defined' }); + }); + + it('should start the soft-fork', async () => { + await mineNBlocks(network.minerWindow - 2, nodeCtx); + + // We are now at the threshold of the window. + { + const state = await getForkDeploymentState(nodeCtx.chain); + const bip9info = await getBIP9Info(nodeCtx); + assert.strictEqual(state, thresholdStates.DEFINED); + + checkBIP9Info(bip9info, { status: 'defined' }); + } + + // go into new window and change the state to started. + await mineBlock(nodeCtx); + + { + const state = await getForkDeploymentState(nodeCtx.chain); + const bip9info = await getBIP9Info(nodeCtx); + assert.strictEqual(state, thresholdStates.STARTED); + checkBIP9Info(bip9info, { status: 'started' }); + + checkBIP9Statistics(bip9info.statistics, { + elapsed: 0, + count: 0, + possible: true + }); + } + }); + + it('should still be able to mine airdrop & faucet proofs', async () => { + await tryClaimingAirdropProofs(nodeCtx, [airdropProof, faucetProof]); + }); + + it('should fail to lock in the soft-fork', async () => { + // Reach the height just before the start of the next window + await mineNBlocks(network.minerWindow - 1, nodeCtx, { signalFork: false }); + + { + const state = await getForkDeploymentState(nodeCtx.chain); + const bip9info = await getBIP9Info(nodeCtx); + assert.strictEqual(state, thresholdStates.STARTED); + checkBIP9Info(bip9info, { status: 'started' }); + + checkBIP9Statistics(bip9info.statistics, { + elapsed: network.minerWindow - 1, + count: 0, + possible: false + }); + } + + // After this the deployment stays in STARTED state. + await mineBlock(nodeCtx, { signalFork: false }); + + { + const state = await getForkDeploymentState(nodeCtx.chain); + const bip9info = await getBIP9Info(nodeCtx); + + assert.strictEqual(state, thresholdStates.STARTED); + checkBIP9Info(bip9info, { status: 'started' }); + + checkBIP9Statistics(bip9info.statistics, { + elapsed: 0, + count: 0, + possible: true + }); + } + }); + + it('should still be able to add airdrop & faucet proofs to the mempool', async () => { + await nodeCtx.mempool.addAirdrop(airdropProof); + await nodeCtx.mempool.addAirdrop(faucetProof); + assert.strictEqual(nodeCtx.mempool.airdrops.size, 2); + }); + + it('should still be able to mine airdrop & faucet proofs', async () => { + await tryClaimingAirdropProofs(nodeCtx, [airdropProof, faucetProof]); + }); + }); +}); + +/** + * Attempts to mine and add a block with all provided proofs + * and then revert the chain to the previous state. + * + * Throws errors if chain fails to add the block. + * + * @param {NodeContext} nodeCtx + * @param {AirdropProof[]} proofs + * @returns {Promise} + */ +async function tryClaimingAirdropProofs(nodeCtx, proofs) { + assert.ok(Array.isArray(proofs) && proofs.length > 0); + + // We don't want mempool to safeguard miner. + const bakAirstop = nodeCtx.mempool.nextState.hasAirstop; + nodeCtx.mempool.nextState.hasAirstop = false; + + for (const proof of proofs) + await nodeCtx.mempool.addAirdrop(proof); + + nodeCtx.mempool.nextState.hasAirstop = bakAirstop; + + assert.strictEqual(nodeCtx.mempool.airdrops.size, proofs.length); + + const [block] = await nodeCtx.mineBlocks(1); + assert(block.txs[0].inputs.length === 3); + assert(block.txs[0].outputs.length === 3); + assert.strictEqual(nodeCtx.mempool.airdrops.size, 0); + + // NOTE: reset WONT re-add proofs to the mempool. + await nodeCtx.chain.reset(nodeCtx.height - 1); +} + +/** + * Mine N new blocks + * @param {number} n number of blocks to mine + * @param {NodeContext} node + * @param {Chain} node.chain + * @param {Miner} node.miner + * @param {object} opts + * @param {boolean} opts.signalFork whether to signal the fork + */ +async function mineNBlocks(n, node, opts = {}) { + for (let i = 0; i < n; i++) + await mineBlock(node, opts); +} + +/** + * Mine a new block + * @param {NodeContext} node + * @param {object} opts + * @param {boolean} opts.signalFork whether to signal the fork + */ +async function mineBlock(node, opts = {}) { + assert(node); + const chain = node.chain; + const miner = node.miner; + + const signalFork = opts.signalFork || false; + + const job = await miner.cpu.createJob(chain.tip); + + // opt out of all + job.attempt.version = 0; + + if (signalFork) + job.attempt.version |= (1 << network.deployments[SOFT_FORK_NAME].bit); + + job.refresh(); + + const block = await job.mineAsync(); + await chain.add(block); + + return block; +} + +/** + * Get deployment state (number) + * @param {Chain} chain + * @returns {Promise} + */ +async function getForkDeploymentState(chain) { + const prev = chain.tip; + const state = await chain.getState(prev, network.deployments[SOFT_FORK_NAME]); + return state; +} + +/** + * @param {NodeContext} nodeCtx + */ +async function getBIP9Info(nodeCtx) { + const info = await nodeCtx.nrpc('getblockchaininfo'); + return info.softforks[SOFT_FORK_NAME]; +} diff --git a/test/chain-icann-lockup-test.js b/test/chain-icann-lockup-test.js index 56c68d8c..45ca8e6a 100644 --- a/test/chain-icann-lockup-test.js +++ b/test/chain-icann-lockup-test.js @@ -849,7 +849,7 @@ describe('BIP9 - ICANN lockup (integration)', function() { }); } - // After this it should go to the ACTIVE state. + // After this it should go to the LOCKED_IN state. await mineBlock(node); {