From 2b6e74b42e7b801c056762738fedb7916fb54fb0 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 19 Aug 2022 12:05:00 -0400 Subject: [PATCH 1/6] test: refactor DNS test to run Full / SPV --- test/dns-test.js | 166 +++++++++++++++++------------------------------ 1 file changed, 58 insertions(+), 108 deletions(-) diff --git a/test/dns-test.js b/test/dns-test.js index dfb762d67..5360db5b6 100644 --- a/test/dns-test.js +++ b/test/dns-test.js @@ -13,114 +13,64 @@ const recursiveResolver = new Resolver({timeout: 1000}); rootResolver.setServers([`127.0.0.1:${network.nsPort}`]); recursiveResolver.setServers([`127.0.0.1:${network.rsPort}`]); -describe('Server Configuration', function() { - describe('Full Node', function() { - let node; - - afterEach(async () => { - await node.close(); - }); - - it('should open full node with both DNS servers', async () => { - node = new FullNode({ - memory: true, - network: network.type - }); - - await node.open(); - const res1 = await rootResolver.resolveSoa('.'); - assert(res1); - const res2 = await recursiveResolver.resolveSoa('.'); - assert(res2); - }); - - it('should open full node with neither DNS server', async () => { - node = new FullNode({ - memory: true, - network: network.type, - noDns: true +describe('DNS Servers', function() { + for (const spv of [true, false]) { + describe(spv ? 'SPV Node' : 'Full Node', function() { + const Node = spv ? SPVNode : FullNode; + let node; + + describe('Server Configuration', function () { + afterEach(async () => { + await node.close(); + }); + + it('should open full node with both DNS servers', async () => { + node = new Node({ + memory: true, + network: network.type + }); + + await node.open(); + const res1 = await rootResolver.resolveSoa('.'); + assert(res1); + const res2 = await recursiveResolver.resolveSoa('.'); + assert(res2); + }); + + it('should open full node with neither DNS server', async () => { + node = new Node({ + memory: true, + network: network.type, + noDns: true + }); + + await node.open(); + await assert.rejects( + rootResolver.resolveSoa('.'), + {message: 'querySoa ECONNREFUSED .'} + ); + await assert.rejects( + recursiveResolver.resolveSoa('.'), + {message: 'querySoa ECONNREFUSED .'} + ); + }); + + it('should open full node only with root name server', async () => { + node = new Node({ + memory: true, + network: network.type, + noRs: true + }); + + await node.open(); + const res1 = await rootResolver.resolveSoa('.'); + assert(res1); + await assert.rejects( + recursiveResolver.resolveSoa('.'), + {message: 'querySoa ECONNREFUSED .'} + ); + }); }); - - await node.open(); - await assert.rejects( - rootResolver.resolveSoa('.'), - {message: 'querySoa ECONNREFUSED .'} - ); - await assert.rejects( - recursiveResolver.resolveSoa('.'), - {message: 'querySoa ECONNREFUSED .'} - ); - }); - - it('should open full node only with root name server', async () => { - node = new FullNode({ - memory: true, - network: network.type, - noRs: true - }); - - await node.open(); - const res1 = await rootResolver.resolveSoa('.'); - assert(res1); - await assert.rejects( - recursiveResolver.resolveSoa('.'), - {message: 'querySoa ECONNREFUSED .'} - ); - }); - }); - - describe('SPV Node', function() { - let node; - - afterEach(async () => { - await node.close(); - }); - - it('should open SPV node with both DNS servers', async () => { - node = new SPVNode({ - memory: true, - network: network.type - }); - - await node.open(); - const res1 = await rootResolver.resolveSoa('.'); - assert(res1); - const res2 = await recursiveResolver.resolveSoa('.'); - assert(res2); - }); - - it('should open SPV node with neither DNS server', async () => { - node = new SPVNode({ - memory: true, - network: network.type, - noDns: true - }); - - await node.open(); - await assert.rejects( - rootResolver.resolveSoa('.'), - {message: 'querySoa ECONNREFUSED .'} - ); - await assert.rejects( - recursiveResolver.resolveSoa('.'), - {message: 'querySoa ECONNREFUSED .'} - ); - }); - - it('should open SPV node only with root name server', async () => { - node = new SPVNode({ - memory: true, - network: network.type, - noRs: true - }); - - await node.open(); - const res1 = await rootResolver.resolveSoa('.'); - assert(res1); - await assert.rejects( - recursiveResolver.resolveSoa('.'), - {message: 'querySoa ECONNREFUSED .'} - ); }); - }); + } }); From ee25712fa7619d61de4566185588a3969984b2b8 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 19 Aug 2022 15:24:45 -0400 Subject: [PATCH 2/6] networks, chain: define "safe" height for each network (must always be LESS THAN treeInterval) --- lib/blockchain/chain.js | 2 +- lib/protocol/networks.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/blockchain/chain.js b/lib/blockchain/chain.js index 2b50df898..99b0c335f 100644 --- a/lib/blockchain/chain.js +++ b/lib/blockchain/chain.js @@ -3735,7 +3735,7 @@ class Chain extends AsyncEmitter { // If there's enough proof-of-work // on top of the most recent root, // it should be safe to use it. - if (mod >= 12) + if (mod >= this.network.names.safeRoot) mod = 0; const height = this.height - mod; diff --git a/lib/protocol/networks.js b/lib/protocol/networks.js index d23227d7f..59a3b0d85 100644 --- a/lib/protocol/networks.js +++ b/lib/protocol/networks.js @@ -343,6 +343,13 @@ main.names = { treeInterval: main.pow.blocksPerDay >>> 2, + /** + * Number of confirmations to require before proving from tree root + * @const {Number} + */ + + safeRoot: 12, + /** * Amount of time transfers are locked up for. * @const {Number} @@ -670,6 +677,7 @@ testnet.names = { biddingPeriod: 1 * testnet.pow.blocksPerDay, revealPeriod: 2 * testnet.pow.blocksPerDay, treeInterval: testnet.pow.blocksPerDay >>> 2, + safeRoot: 12, transferLockup: 2 * testnet.pow.blocksPerDay, revocationDelay: 4 * testnet.pow.blocksPerDay, auctionMaturity: (1 + 2 + 4) * testnet.pow.blocksPerDay, @@ -813,6 +821,7 @@ regtest.names = { biddingPeriod: 5, revealPeriod: 10, treeInterval: 5, + safeRoot: 3, transferLockup: 10, revocationDelay: 50, auctionMaturity: 5 + 10 + 50, @@ -960,6 +969,7 @@ simnet.names = { biddingPeriod: 25, revealPeriod: 50, treeInterval: 2, + safeRoot: 1, transferLockup: 5, revocationDelay: 25, auctionMaturity: 25 + 50 + 25, From eb560c2d903b701249c3837f47575387ddfcd97b Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Tue, 23 Aug 2022 12:38:26 -0400 Subject: [PATCH 3/6] chain/dns: always resolve names at safe height, even full node --- lib/blockchain/chain.js | 33 +++++-- lib/dns/server.js | 13 ++- lib/net/pool.js | 6 +- lib/node/fullnode.js | 3 +- lib/node/rpc.js | 9 +- lib/node/spvnode.js | 3 +- test/chain-tree-compaction-test.js | 6 +- test/dns-test.js | 153 ++++++++++++++++++++++++++++- test/ns-test.js | 36 +++++++ 9 files changed, 239 insertions(+), 23 deletions(-) diff --git a/lib/blockchain/chain.js b/lib/blockchain/chain.js index 99b0c335f..68a746ea3 100644 --- a/lib/blockchain/chain.js +++ b/lib/blockchain/chain.js @@ -66,6 +66,10 @@ class Chain extends AsyncEmitter { this.height = -1; this.synced = false; + // Sufficiently confirmed tree root. + // Remains null until chain is synced. + this.safeEntry = null; + this.orphanMap = new BufferMap(); this.orphanPrev = new BufferMap(); } @@ -113,6 +117,7 @@ class Chain extends AsyncEmitter { this.emit('tip', tip); this.maybeSync(); + await this.setSafeEntry(); } /** @@ -2035,6 +2040,7 @@ class Chain extends AsyncEmitter { this.purgeOrphans(); this.maybeSync(); + await this.setSafeEntry(); } /** @@ -2414,6 +2420,7 @@ class Chain extends AsyncEmitter { // Check sync state. this.maybeSync(); + await this.setSafeEntry(); return entry; } @@ -3714,11 +3721,16 @@ class Chain extends AsyncEmitter { } /** - * Get safe tree root. - * @returns {Hash} + * Get safe ChainEntry (for sufficiently confirmed tree root). + * @returns {Promise} */ - async getSafeRoot() { + async setSafeEntry() { + if (!this.synced) { + this.safeEntry = null; + return; + } + // The tree is committed on an interval. // Mainnet is 36 blocks, meaning at height 36, // the name set of the past 36 blocks are @@ -3732,17 +3744,18 @@ class Chain extends AsyncEmitter { let mod = this.height % interval; - // If there's enough proof-of-work + // If there's not enough proof-of-work // on top of the most recent root, - // it should be safe to use it. - if (mod >= this.network.names.safeRoot) - mod = 0; + // go back one tree interval + if (mod < this.network.names.safeRoot) + mod += interval; + + const height = this.height - mod + 1; - const height = this.height - mod; + // Could be null if not enough blocks const entry = await this.getEntryByHeight(height); - assert(entry); - return entry.treeRoot; + this.safeEntry = entry; } } diff --git a/lib/dns/server.js b/lib/dns/server.js index 71ac275cb..a567f7186 100644 --- a/lib/dns/server.js +++ b/lib/dns/server.js @@ -110,6 +110,7 @@ class RootServer extends DNSServer { this.noSig0 = false; this.icann = new RootResolver(RES_OPT); + this.chain = null; this.logger = Logger.global; this.key = secp256k1.privateKeyGenerate(); this.host = '127.0.0.1'; @@ -133,8 +134,7 @@ class RootServer extends DNSServer { this.cache = new RootCache(3000); - if (options) - this.initOptions(options); + this.initOptions(options); // Create SYNTH record to use for root zone NS let ip = IP.toBuffer(this.publicHost); @@ -146,7 +146,11 @@ class RootServer extends DNSServer { } initOptions(options) { - assert(options); + assert(options, 'DNS root server requires options.'); + assert(options.chain && typeof options.chain === 'object', + 'DNS root server requires a blockchain.'); + + this.chain = options.chain; this.parseOptions(options); @@ -234,8 +238,9 @@ class RootServer extends DNSServer { if (!this.lookup) throw new Error('Tree not available.'); + const {treeRoot} = this.chain.safeEntry; const hash = rules.hashName(name); - const data = await this.lookup(hash); + const data = await this.lookup(hash, treeRoot); if (!data) return null; diff --git a/lib/net/pool.js b/lib/net/pool.js index d12d6f42c..8365f7660 100644 --- a/lib/net/pool.js +++ b/lib/net/pool.js @@ -4284,8 +4284,10 @@ class Pool extends EventEmitter { */ async resolve(nameHash) { - const root = await this.chain.getSafeRoot(); - return this.resolveAtRoot(nameHash, root); + const {treeRoot} = this.chain.safeEntry; + if (!treeRoot) + return null; + return this.resolveAtRoot(nameHash, treeRoot); } /** diff --git a/lib/node/fullnode.js b/lib/node/fullnode.js index 1754180d2..9a7d84972 100644 --- a/lib/node/fullnode.js +++ b/lib/node/fullnode.js @@ -163,11 +163,12 @@ class FullNode extends Node { if (!this.config.bool('no-dns')) { this.ns = new RootServer({ + chain: this.chain, logger: this.logger, key: this.identityKey, host: this.config.str('ns-host'), port: this.config.uint('ns-port', this.network.nsPort), - lookup: key => this.chain.db.tree.get(key), + lookup: (key, root) => this.chain.db.lookup(root, key), publicHost: this.config.str('public-host'), noSig0: this.config.bool('no-sig0') }); diff --git a/lib/node/rpc.js b/lib/node/rpc.js index d7f1bd115..30ce16d47 100644 --- a/lib/node/rpc.js +++ b/lib/node/rpc.js @@ -3112,12 +3112,15 @@ class RPC extends RPCBase { // Safe roots are the last Urkel tree commitment // with more than 12 confirmations. - const root = await this.chain.getSafeRoot(); + const {treeRoot} = this.chain.safeEntry; + if (!treeRoot) + return null; + let data; if (this.chain.options.spv) - data = await this.pool.resolveAtRoot(nameHash, root); + data = await this.pool.resolveAtRoot(nameHash, treeRoot); else - data = await this.chain.db.lookup(root, nameHash); + data = await this.chain.db.lookup(treeRoot, nameHash); if (!data) return null; diff --git a/lib/node/spvnode.js b/lib/node/spvnode.js index 87548ab44..6e4f5c972 100644 --- a/lib/node/spvnode.js +++ b/lib/node/spvnode.js @@ -96,11 +96,12 @@ class SPVNode extends Node { if (!this.config.bool('no-dns')) { this.ns = new RootServer({ + chain: this.chain, logger: this.logger, key: this.identityKey, host: this.config.str('ns-host'), port: this.config.uint('ns-port', this.network.nsPort), - lookup: key => this.pool.resolve(key), + lookup: (key, root) => this.pool.resolveAtRoot(key, root), publicHost: this.config.str('public-host'), noSig0: this.config.bool('no-sig0') }); diff --git a/test/chain-tree-compaction-test.js b/test/chain-tree-compaction-test.js index 84ac74bb1..e7610107b 100644 --- a/test/chain-tree-compaction-test.js +++ b/test/chain-tree-compaction-test.js @@ -418,7 +418,11 @@ describe('Tree Compacting', function() { // Update name and attempt to confirm send(update, mempool); // Will "crash" node before completing operation - await mineBlocks(1, mempool); + try { + await mineBlocks(1, mempool); + } catch (e) { + ; + } assert(!chain.opened); // Restore proper batch-write function diff --git a/test/dns-test.js b/test/dns-test.js index 5360db5b6..644d70bee 100644 --- a/test/dns-test.js +++ b/test/dns-test.js @@ -4,7 +4,10 @@ const assert = require('bsert'); const Network = require('../lib/protocol/network'); const FullNode = require('../lib/node/fullnode'); const SPVNode = require('../lib/node/spvnode'); +const MemWallet = require('./util/memwallet'); +const {Resource} = require('../lib/dns/resource'); const network = Network.get('regtest'); +const {forValue} = require('./util/common'); const {Resolver} = require('dns').promises; @@ -13,8 +16,14 @@ const recursiveResolver = new Resolver({timeout: 1000}); rootResolver.setServers([`127.0.0.1:${network.nsPort}`]); recursiveResolver.setServers([`127.0.0.1:${network.rsPort}`]); +const { + treeInterval, + biddingPeriod, + revealPeriod +} = network.names; + describe('DNS Servers', function() { - for (const spv of [true, false]) { + for (const spv of [false, true]) { describe(spv ? 'SPV Node' : 'Full Node', function() { const Node = spv ? SPVNode : FullNode; let node; @@ -71,6 +80,148 @@ describe('DNS Servers', function() { ); }); }); + + describe('HNS name resolution', function () { + // Use a new full node for mining and auction + // so full/spv test nodes do the same jobs. + const miner = new FullNode({ + memory: true, + network: network.type, + listen: true, + bip37: true, + noDns: true + }); + + const wallet = new MemWallet({ + network: network.type + }); + + miner.chain.on('connect', (entry, block) => { + wallet.addBlock(entry, block.txs); + }); + + const node = new Node({ + memory: true, + network: network.type, + only: ['127.0.0.1'], + port: network.port + 100, + brontidePort: network.brontidePort + 100, + httpPort: network.rpcPort + 100 + }); + + async function mineBlocks(n) { + for (; n > 0; n--) { + const block = await miner.miner.mineBlock(); + await miner.chain.add(block); + } + await forValue(node.chain, 'height', miner.chain.height); + } + + let name; + const string = 'Campaign of chaos'; + const resource = Resource.fromJSON({ + records: [{type: 'TXT', txt: [string]}] + }); + + before(async () => { + await miner.open(); + await miner.connect(); + await node.open(); + await node.connect(); + node.startSync(); + + await forValue(miner.pool.peers.list, 'size', 1); + await forValue(node.pool.peers.list, 'size', 1); + }); + + after(async () => { + await node.close(); + await miner.close(); + }); + + it('should fund wallet and win name', async () => { + miner.miner.addresses.length = 0; + miner.miner.addAddress(wallet.getReceive()); + await mineBlocks(20); + + name = await miner.rpc.grindName([4]); + await miner.mempool.addTX((await wallet.sendOpen(name)).toTX()); + await mineBlocks(treeInterval + 1); + await miner.mempool.addTX((await wallet.sendBid(name, 10000, 10000)).toTX()); + await mineBlocks(biddingPeriod); + await miner.mempool.addTX((await wallet.sendReveal(name)).toTX()); + await mineBlocks(revealPeriod); + }); + + it('should not resolve before register', async () => { + await assert.rejects( + rootResolver.resolveTxt(name), + {message: `queryTxt ENOTFOUND ${name}`} + ); + }); + + it('should not resolve immedeately after register', async () => { + await miner.mempool.addTX( + (await wallet.sendRegister(name, resource)) + .toTX() + ); + await mineBlocks(1); + + // Sanity check + const ns = await miner.chain.db.getNameStateByName(name); + assert(ns); + assert.bufferEqual(ns.data, resource.encode()); + + node.ns.resetCache(); + await assert.rejects( + rootResolver.resolveTxt(name), + {message: `queryTxt ENOTFOUND ${name}`} + ); + }); + + it('should not resolve immedeately after tree commit', async () => { + let commitRoot, commitEntry; + miner.chain.on('tree commit', (root, entry) => { + commitRoot = root; + commitEntry = entry; + }); + const n = treeInterval - (miner.chain.height % treeInterval); + await mineBlocks(n); + + assert.deepStrictEqual(commitEntry, miner.chain.tip); + assert.notBufferEqual(commitRoot, miner.chain.tip.treeRoot); + + node.ns.resetCache(); + await assert.rejects( + rootResolver.resolveTxt(name), + {message: `queryTxt ENOTFOUND ${name}`} + ); + + // One more block to commit tree root to block header + await mineBlocks(1); + assert.bufferEqual(commitRoot, miner.chain.tip.treeRoot); + + // Still no. + node.ns.resetCache(); + await assert.rejects( + rootResolver.resolveTxt(name), + {message: `queryTxt ENOTFOUND ${name}`} + ); + }); + + it('should resolve at safe height', async () => { + await mineBlocks(2); + + assert.strictEqual( + miner.chain.height % network.names.treeInterval, + network.names.safeRoot + ); + + node.ns.resetCache(); + const res = await rootResolver.resolveTxt(name); + assert.strictEqual(res[0][0], string); + }); + }); }); } }); diff --git a/test/ns-test.js b/test/ns-test.js index 68842e7fa..20a7e03e0 100644 --- a/test/ns-test.js +++ b/test/ns-test.js @@ -21,6 +21,12 @@ const { describe('RootServer', function() { const ns = new RootServer({ + chain: { + safeEntry: { + treeRoot: Buffer.alloc(32), + time: Date.now() / 1000 + } + }, port: 25349 // regtest }); @@ -155,6 +161,12 @@ describe('RootServer', function() { describe('RootServer Blacklist', function() { const ns = new RootServer({ + chain: { + safeEntry: { + treeRoot: Buffer.alloc(32), + time: Date.now() / 1000 + } + }, port: 25349, // regtest lookup: (hash) => { // Normally an Urkel Tree goes here. @@ -219,6 +231,12 @@ describe('RootServer Blacklist', function() { describe('RootServer Plugins', function() { const ns = new RootServer({ + chain: { + safeEntry: { + treeRoot: Buffer.alloc(32), + time: Date.now() / 1000 + } + }, port: 25349, // regtest lookup: (hash) => { // Normally an Urkel Tree goes here. @@ -311,6 +329,12 @@ describe('RootServer Plugins', function() { describe('RootServer DNSSEC', function () { const ns = new RootServer({ + chain: { + safeEntry: { + treeRoot: Buffer.alloc(32), + time: Date.now() / 1000 + } + }, port: 25349, // regtest lookup: (hash) => { assert(hash instanceof Buffer); @@ -670,6 +694,12 @@ describe('RootServer SIG0', function() { it('should answer with SIG0', async () => { ns = new RootServer({ + chain: { + safeEntry: { + treeRoot: Buffer.alloc(32), + time: Date.now() / 1000 + } + }, port: 25349 }); @@ -689,6 +719,12 @@ describe('RootServer SIG0', function() { it('should not answer with SIG0', async () => { ns = new RootServer({ + chain: { + safeEntry: { + treeRoot: Buffer.alloc(32), + time: Date.now() / 1000 + } + }, port: 25349, noSig0: true }); From d3eccce0eee630101f324d748fb1a9613de327a0 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Tue, 30 Aug 2022 16:30:52 -0400 Subject: [PATCH 4/6] dns: send REFUSED until chain sync --- lib/dns/server.js | 23 +++++++++++++++-------- test/dns-test.js | 40 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/lib/dns/server.js b/lib/dns/server.js index a567f7186..570f8408f 100644 --- a/lib/dns/server.js +++ b/lib/dns/server.js @@ -238,6 +238,9 @@ class RootServer extends DNSServer { if (!this.lookup) throw new Error('Tree not available.'); + if (!this.chain.safeEntry) + throw new Error('Chain is not safe for name resolution.'); + const {treeRoot} = this.chain.safeEntry; const hash = rules.hashName(name); const data = await this.lookup(hash, treeRoot); @@ -385,11 +388,8 @@ class RootServer extends DNSServer { // useless proofs for invalid TLDs // (These requests are most // likely bad anyways) - if (!rules.verifyName(tld)) { - const res = new Message(); - res.code = codes.REFUSED; - return res; - } + if (!rules.verifyName(tld)) + throw new Error('Invalid name.'); // Ask the urkel tree for the name data. const data = !this.blacklist.has(tld) @@ -535,10 +535,17 @@ class RootServer extends DNSServer { if (cache) return cache; - const res = await this.response(req, rinfo); + let res; + try { + res = await this.response(req, rinfo); - if (!util.equal(tld, '_synth.')) - this.cache.set(name, type, res); + if (!util.equal(tld, '_synth.')) + this.cache.set(name, type, res); + } catch (e) { + this.logger.error(e); + res = new Message(); + res.code = codes.REFUSED; + } return res; } diff --git a/test/dns-test.js b/test/dns-test.js index 644d70bee..21811734f 100644 --- a/test/dns-test.js +++ b/test/dns-test.js @@ -122,8 +122,10 @@ describe('DNS Servers', function() { const resource = Resource.fromJSON({ records: [{type: 'TXT', txt: [string]}] }); + const maxTipAge = network.block.maxTipAge; before(async () => { + network.block.maxTipAge = 12 * 60 * 60; await miner.open(); await miner.connect(); await node.open(); @@ -132,19 +134,51 @@ describe('DNS Servers', function() { await forValue(miner.pool.peers.list, 'size', 1); await forValue(node.pool.peers.list, 'size', 1); + + name = await miner.rpc.grindName([4]); + + miner.miner.addresses.length = 0; + miner.miner.addAddress(wallet.getReceive()); }); after(async () => { await node.close(); await miner.close(); + network.block.maxTipAge = maxTipAge; + }); + + it('should refuse to resolve before chain sync', async () => { + await assert.rejects( + rootResolver.resolveTxt(name), + {message: `queryTxt EREFUSED ${name}`} + ); + }); + + it('should not refuse to resolve after chain sync', async () => { + await mineBlocks(2); + await assert.rejects( + rootResolver.resolveTxt(name), + {message: `queryTxt ENOTFOUND ${name}`} + ); + }); + + it('should refuse to resolve invalid name', async () => { + await assert.rejects( + rootResolver.resolveTxt('com\\\\000'), + {message: 'queryTxt EREFUSED com\\\\000'} + ); + }); + + it('should not refuse to resolve valid name', async () => { + await assert.rejects( + rootResolver.resolveTxt('com\\000'), + {message: 'queryTxt ENOTFOUND com\\000'} + ); }); it('should fund wallet and win name', async () => { - miner.miner.addresses.length = 0; - miner.miner.addAddress(wallet.getReceive()); await mineBlocks(20); - name = await miner.rpc.grindName([4]); await miner.mempool.addTX((await wallet.sendOpen(name)).toTX()); await mineBlocks(treeInterval + 1); await miner.mempool.addTX((await wallet.sendBid(name, 10000, 10000)).toTX()); From 1afc6caac20602f00fa8269a5c268e24bbf55a3b Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Sat, 3 Sep 2022 15:18:35 -0500 Subject: [PATCH 5/6] dns: use chain.safeEntry tiemstamp for SOA serial --- lib/dns/server.js | 3 +- test/dns-test.js | 75 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/lib/dns/server.js b/lib/dns/server.js index 570f8408f..3c995039d 100644 --- a/lib/dns/server.js +++ b/lib/dns/server.js @@ -570,7 +570,8 @@ class RootServer extends DNSServer { } serial() { - const date = new Date(); + const time = this.chain.safeEntry ? this.chain.safeEntry.time : 0; + const date = new Date(time * 1000); const y = date.getUTCFullYear() * 1e6; const m = (date.getUTCMonth() + 1) * 1e4; const d = date.getUTCDate() * 1e2; diff --git a/test/dns-test.js b/test/dns-test.js index 21811734f..65ac34630 100644 --- a/test/dns-test.js +++ b/test/dns-test.js @@ -1,6 +1,8 @@ 'use strict'; const assert = require('bsert'); +const {hashName} = require('../lib/covenants/rules'); +const NameState = require('../lib/covenants/namestate'); const Network = require('../lib/protocol/network'); const FullNode = require('../lib/node/fullnode'); const SPVNode = require('../lib/node/spvnode'); @@ -19,7 +21,8 @@ recursiveResolver.setServers([`127.0.0.1:${network.rsPort}`]); const { treeInterval, biddingPeriod, - revealPeriod + revealPeriod, + safeRoot } = network.names; describe('DNS Servers', function() { @@ -111,13 +114,15 @@ describe('DNS Servers', function() { async function mineBlocks(n) { for (; n > 0; n--) { + // force ten minute block intervals + node.network.time.offset += 60 * 10; const block = await miner.miner.mineBlock(); await miner.chain.add(block); } await forValue(node.chain, 'height', miner.chain.height); } - let name; + let name, nameHash, serial; const string = 'Campaign of chaos'; const resource = Resource.fromJSON({ records: [{type: 'TXT', txt: [string]}] @@ -136,6 +141,7 @@ describe('DNS Servers', function() { await forValue(node.pool.peers.list, 'size', 1); name = await miner.rpc.grindName([4]); + nameHash = hashName(name); miner.miner.addresses.length = 0; miner.miner.addAddress(wallet.getReceive()); @@ -148,20 +154,74 @@ describe('DNS Servers', function() { }); it('should refuse to resolve before chain sync', async () => { + assert.strictEqual(node.chain.getProgress(), 0); await assert.rejects( rootResolver.resolveTxt(name), {message: `queryTxt EREFUSED ${name}`} ); }); - it('should not refuse to resolve after chain sync', async () => { + it('should resolve root SOA before chain sync', async () => { + const res = await rootResolver.resolveSoa('.'); + // null UNIX timestamp because there is no safe root yet + assert.strictEqual(res.serial, 1970010100); + }); + + it('should still refuse to resolve after chain sync', async () => { + // On mainnet, a synced chain would have a safe root, + // but on regtest we are "synced" after the first block, + // which is not enough confirmations yet to resolve. await mineBlocks(2); + assert.strictEqual(node.chain.getProgress(), 1); + await assert.rejects( + rootResolver.resolveTxt(name), + {message: `queryTxt EREFUSED ${name}`} + ); + }); + + it('should not refuse to resolve after safe height', async () => { + await mineBlocks(node.chain.height % treeInterval); await assert.rejects( rootResolver.resolveTxt(name), {message: `queryTxt ENOTFOUND ${name}`} ); }); + it('should resolve root SOA after safe height', async () => { + node.ns.resetCache(); + const res = await rootResolver.resolveSoa('.'); + // Block #1 timestamp because the latest tree commitment + // hasn't been confirmed enough times yet, so we drop back + // to the previous tree root commitment, which is genesis (#0), + // but new roots don't appear in headers until the next block. + const {time} = await miner.chain.getEntryByHeight(1); + const date = new Date(time * 1000); + const y = date.getUTCFullYear() * 1e6; + const m = (date.getUTCMonth() + 1) * 1e4; + const d = date.getUTCDate() * 1e2; + const h = date.getUTCHours(); + const expected = y + m + d + h; + assert.strictEqual(res.serial, expected); + serial = res.serial; + }); + + it('should update SOA serial when tree interval is safe', async () => { + const startHeight = node.chain.height; + let newSerial = serial; + while (newSerial === serial) { + await mineBlocks(1); + node.ns.resetCache(); + const res = await rootResolver.resolveSoa('.'); + newSerial = res.serial; + } + const endHeight = node.chain.height; + assert(endHeight > startHeight); + assert.strictEqual( + endHeight % treeInterval, + safeRoot + ); + }); + it('should refuse to resolve invalid name', async () => { await assert.rejects( rootResolver.resolveTxt('com\\\\000'), @@ -177,7 +237,7 @@ describe('DNS Servers', function() { }); it('should fund wallet and win name', async () => { - await mineBlocks(20); + await mineBlocks(19); await miner.mempool.addTX((await wallet.sendOpen(name)).toTX()); await mineBlocks(treeInterval + 1); @@ -201,7 +261,7 @@ describe('DNS Servers', function() { ); await mineBlocks(1); - // Sanity check + // Sanity check: name is registered const ns = await miner.chain.db.getNameStateByName(name); assert(ns); assert.bufferEqual(ns.data, resource.encode()); @@ -241,6 +301,11 @@ describe('DNS Servers', function() { rootResolver.resolveTxt(name), {message: `queryTxt ENOTFOUND ${name}`} ); + + // Sanity check: name is in tree + const raw = await miner.chain.db.lookup(commitRoot, nameHash); + const {data} = NameState.decode(raw); + assert.bufferEqual(data, resource.encode()); }); it('should resolve at safe height', async () => { From 158f1bfb193dec1f05f817e977b80ff72dceb0d4 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 16 Sep 2022 09:52:06 -0400 Subject: [PATCH 6/6] test: cover SOA serial after reorg --- test/dns-test.js | 95 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 5 deletions(-) diff --git a/test/dns-test.js b/test/dns-test.js index 65ac34630..0f361c933 100644 --- a/test/dns-test.js +++ b/test/dns-test.js @@ -103,6 +103,10 @@ describe('DNS Servers', function() { wallet.addBlock(entry, block.txs); }); + miner.chain.on('disconnect', (entry, block) => { + wallet.removeBlock(entry, block.txs); + }); + const node = new Node({ memory: true, network: network.type, @@ -112,12 +116,17 @@ describe('DNS Servers', function() { httpPort: network.rpcPort + 100 }); - async function mineBlocks(n) { + async function mineBlocks(n, tip) { for (; n > 0; n--) { - // force ten minute block intervals - node.network.time.offset += 60 * 10; - const block = await miner.miner.mineBlock(); - await miner.chain.add(block); + // Force twenty minute block intervals + // Why not ten? Because this ensures that even on regtest + // with its short tree interval, the "hours" digits in + // the SOA timestamp will increase every new safe root. + miner.network.time.offset += 60 * 20; + const block = await miner.miner.mineBlock(tip); + const entry = await miner.chain.add(block); + if (tip) + tip = entry; } await forValue(node.chain, 'height', miner.chain.height); } @@ -320,6 +329,82 @@ describe('DNS Servers', function() { const res = await rootResolver.resolveTxt(name); assert.strictEqual(res[0][0], string); }); + + describe('Chain reorg', function () { + it('should update SOA serial at safe height', async () => { + let lastSerial = node.ns.serial(); + let lastRoot = node.chain.safeEntry.treeRoot; + for (let i = 0; i < 100; i++) { + // Update the Urkel tree in every block + await miner.mempool.addTX((await wallet.sendUpdate( + name, + Resource.fromJSON({ + records: [{type: 'TXT', txt: [string]}] + }) + )).toTX()); + await mineBlocks(1); + + const newSerial = node.ns.serial(); + const newRoot = node.chain.safeEntry.treeRoot; + + if (node.chain.height % treeInterval === safeRoot) { + assert.notEqual(newSerial, lastSerial); + assert.notBufferEqual(newRoot, lastRoot); + lastSerial = newSerial; + lastRoot = newRoot; + } else { + assert.strictEqual(newSerial, lastSerial); + assert.bufferEqual(newRoot, lastRoot); + } + } + }); + + it('should survive a shallow reorg', async () => { + const lastSerial = node.ns.serial(); + + // Fork point is within safe root limit + const height = miner.chain.height - (safeRoot - 1); + const fork = await miner.chain.getEntryByHeight(height); + + // Reorg + const waiter = new Promise((resolve) => { + node.chain.once('reorganize', (tip, comp, fork) => { + resolve(tip, comp, fork); + }); + }); + // Guarantee all-new timestamps + miner.network.time.offset += 6000; + + await mineBlocks(safeRoot, fork); + await waiter; + + const newSerial = node.ns.serial(); + assert.strictEqual(lastSerial, newSerial); + }); + + it('should not survive a deep reorg', async () => { + const lastSerial = node.ns.serial(); + + // Fork point exceeds safe root limit + const height = miner.chain.height - safeRoot; + const fork = await miner.chain.getEntryByHeight(height); + + // Reorg + const waiter = new Promise((resolve) => { + node.chain.once('reorganize', (tip, comp, fork) => { + resolve(tip, comp, fork); + }); + }); + // Guarantee all-new timestamps + miner.network.time.offset += 6000; + + await mineBlocks(safeRoot + 1, fork); + await waiter; + + const newSerial = node.ns.serial(); + assert.notStrictEqual(lastSerial, newSerial); + }); + }); }); }); }