From bf8b05d4dd73a86763c06e6e9cfaf7d06a551192 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 3 Aug 2022 16:24:04 -0400 Subject: [PATCH 1/3] pool: resolve name at any tree root --- lib/net/pool.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/net/pool.js b/lib/net/pool.js index 910ddb912..abbb69fda 100644 --- a/lib/net/pool.js +++ b/lib/net/pool.js @@ -4269,15 +4269,26 @@ class Pool extends EventEmitter { } /** - * Resolve a name. + * Resolve a name at the "safe" Urkel Tree root. * @param {Buffer} nameHash * @returns {Buffer} */ - async resolve(nameHash) { - assert(Buffer.isBuffer(nameHash)); - + async resolve(nameHash) { const root = await this.chain.getSafeRoot(); + return this.resolveAtRoot(nameHash, root); + } + + /** + * Resolve a name given any Urkel Tree root. + * @param {Buffer} nameHash + * @param {Buffer} root + * @returns {Buffer} + */ + + async resolveAtRoot(nameHash, root) { + assert(Buffer.isBuffer(nameHash)); + assert(Buffer.isBuffer(root)); if (!this.chain.synced) throw new Error('Chain is not synced.'); From 1cb52a00505c4232bb3e5a83e5c7ecd021be0761 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Mon, 18 Oct 2021 13:54:07 -0400 Subject: [PATCH 2/3] node-rpc: add safe height option to all namestate requests (for SPV) --- lib/node/rpc.js | 54 +++++++++++---- test/auction-rpc-test.js | 144 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 182 insertions(+), 16 deletions(-) diff --git a/lib/node/rpc.js b/lib/node/rpc.js index a0a1e41ea..d7f1bd115 100644 --- a/lib/node/rpc.js +++ b/lib/node/rpc.js @@ -2196,15 +2196,16 @@ class RPC extends RPCBase { } async verifyMessageWithName(args, help) { - if (help || args.length !== 3) { + if (help || args.length < 3 || args.length > 4 ) { throw new RPCError(errs.MISC_ERROR, - 'verifymessagewithname "name" "signature" "message"'); + 'verifymessagewithname "name" "signature" "message" (safe)'); } const valid = new Validator(args); const name = valid.str(0, ''); const sig = valid.buf(1, null, 'base64'); const str = valid.str(2); + const safe = valid.bool(3, false); const network = this.network; const height = this.chain.height; @@ -2212,7 +2213,8 @@ class RPC extends RPCBase { throw new RPCError(errs.TYPE_ERROR, 'Invalid name.'); const nameHash = rules.hashName(name); - const ns = await this.chain.db.getNameState(nameHash); + + const ns = await this.getNameState(nameHash, safe); if (!ns || !ns.owner) throw new RPCError(errs.MISC_ERROR, 'Cannot find the name owner.'); @@ -2415,11 +2417,12 @@ class RPC extends RPCBase { } async getNameInfo(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'getnameinfo "name"'); + if (help || args.length < 1 || args.length > 2) + throw new RPCError(errs.MISC_ERROR, 'getnameinfo "name" (safe)'); const valid = new Validator(args); const name = valid.str(0); + const safe = valid.bool(1, false); if (!name || !rules.verifyName(name)) throw new RPCError(errs.TYPE_ERROR, 'Invalid name.'); @@ -2429,7 +2432,8 @@ class RPC extends RPCBase { const nameHash = rules.hashName(name); const reserved = rules.isReserved(nameHash, height + 1, network); const [start, week] = rules.getRollout(nameHash, network); - const ns = await this.chain.db.getNameState(nameHash); + + const ns = await this.getNameState(nameHash, safe); let info = null; @@ -2449,17 +2453,19 @@ class RPC extends RPCBase { } async getNameResource(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'getnameresource "name"'); + if (help || args.length < 1 || args.length > 2) + throw new RPCError(errs.MISC_ERROR, 'getnameresource "name" (safe)'); const valid = new Validator(args); const name = valid.str(0); + const safe = valid.bool(1, false); if (!name || !rules.verifyName(name)) throw new RPCError(errs.TYPE_ERROR, 'Invalid name.'); const nameHash = rules.hashName(name); - const ns = await this.chain.db.getNameState(nameHash); + + const ns = await this.getNameState(nameHash, safe); if (!ns || ns.data.length === 0) return null; @@ -2518,16 +2524,17 @@ class RPC extends RPCBase { } async getNameByHash(args, help) { - if (help || args.length !== 1) - throw new RPCError(errs.MISC_ERROR, 'getnamebyhash "hash"'); + if (help || args.length < 1 || args.length > 2) + throw new RPCError(errs.MISC_ERROR, 'getnamebyhash "hash" (safe)'); const valid = new Validator(args); const hash = valid.bhash(0); + const safe = valid.bool(1, false); if (!hash) throw new RPCError(errs.TYPE_ERROR, 'Invalid name hash.'); - const ns = await this.chain.db.getNameState(hash); + const ns = await this.getNameState(hash, safe); if (!ns) return null; @@ -3096,6 +3103,29 @@ class RPC extends RPCBase { depends: this.mempool.getDepends(entry.tx) }; } + + async getNameState(nameHash, safe) { + if (!safe) { + // Will always return null in SPV mode + return this.chain.db.getNameState(nameHash); + } + + // Safe roots are the last Urkel tree commitment + // with more than 12 confirmations. + const root = await this.chain.getSafeRoot(); + let data; + if (this.chain.options.spv) + data = await this.pool.resolveAtRoot(nameHash, root); + else + data = await this.chain.db.lookup(root, nameHash); + + if (!data) + return null; + + const ns = NameState.decode(data); + ns.nameHash = nameHash; + return ns; + } } /* diff --git a/test/auction-rpc-test.js b/test/auction-rpc-test.js index b111a339b..fe60f6348 100644 --- a/test/auction-rpc-test.js +++ b/test/auction-rpc-test.js @@ -5,8 +5,17 @@ const bio = require('bufio'); const plugin = require('../lib/wallet/plugin'); const rules = require('../lib/covenants/rules'); const common = require('./util/common'); -const {ChainEntry, FullNode, KeyRing, MTX, Network, Path} = require('..'); +const { + ChainEntry, + FullNode, + SPVNode, + KeyRing, + MTX, + Network, + Path +} = require('..'); const {NodeClient, WalletClient} = require('hs-client'); +const {forValue} = require('./util/common'); class TestUtil { constructor(options) { @@ -31,7 +40,9 @@ class TestUtil { this.node = new FullNode({ memory: true, workers: true, - network: this.network.type + network: this.network.type, + listen: true, + bip37: true }); this.node.use(plugin); @@ -153,6 +164,8 @@ describe('Auction RPCs', function() { lockup: 10 }; const COIN = 1e6; + let signAddr, signSig; + const signMsg = 'The Defendants are engaged in a campaign of chaos.'; const mineBlocks = async (num, wallet, account = 'default') => { const address = (await wallet.createAddress(account)).address; @@ -404,9 +417,9 @@ describe('Auction RPCs', function() { it('should create FINALIZE with signing paths', async () => { // Submit TRANSFER. - const address = (await loser.createAddress('default')).address; + signAddr = (await loser.createAddress('default')).address; await util.wrpc('selectwallet', [winner.id]); - assert(await util.wrpc('sendtransfer', [name, address])); + assert(await util.wrpc('sendtransfer', [name, signAddr])); // Mine past TRANSFER lockup period. await mineBlocks(util.network.names.transferLockup, winner); @@ -417,6 +430,30 @@ describe('Auction RPCs', function() { await processJSON(json, submit); }); + it('should verify signed message', async () => { + // Sign and save + await util.wrpc('selectwallet', [loser.id]); + signSig = await util.wrpc('signmessagewithname', [name, signMsg]); + + // Verify at current height + assert(await util.nrpc('verifymessagewithname', [name, signSig, signMsg])); + + // Unable to verify at safe height, historical UTXO is spent + await assert.rejects( + util.nrpc('verifymessagewithname', [name, signSig, signMsg, true]), + {message: /Cannot find the owner's address/} + ); + + // Mine 20 blocks (safe height is still 12 confirmations even on regtest) + await mineBlocks(20, winner); + + // Verify at current height + assert(await util.nrpc('verifymessagewithname', [name, signSig, signMsg])); + + // Verify at safe height + assert(await util.nrpc('verifymessagewithname', [name, signSig, signMsg, true])); + }); + it('should create REVOKE with signing paths', async () => { // Create, assert, submit and mine REVOKE. await util.wrpc('selectwallet', [loser.id]); @@ -424,4 +461,103 @@ describe('Auction RPCs', function() { const json = await util.wrpc('createrevoke', [name]); await processJSON(json, submit, loser, true); }); + + it('should not verify signed message after REVOKE', async () => { + await assert.rejects( + util.nrpc('verifymessagewithname', [name, signSig, signMsg]), + {message: /Invalid name state/} + ); + }); + + it('should not verify signed message at safe height after REVOKE', async () => { + // This safe height is before the REVOKE was confirmed, back when + // the name state was still valid. However, the UTXO that owned the name + // in that state has been spent and no longer exists. + await assert.rejects( + util.nrpc('verifymessagewithname', [name, signSig, signMsg, true]), + {message: /Cannot find the owner's address/} + ); + }); + + describe('SPV', function () { + const spvNode = new SPVNode({ + memory: true, + network: 'regtest', + port: 10000, + brontidePort: 20000, + httpPort: 30000, + only: '127.0.0.1', + noDns: true + }); + + const spvClient = new NodeClient({ + port: 30000 + }); + + before(async () => { + await util.node.connect(); + await spvNode.open(); + await spvNode.connect(); + await spvNode.startSync(); + + await forValue(spvNode.chain, 'height', util.node.chain.height); + + await spvClient.open(); + }); + + after(async () => { + await spvClient.close(); + await spvNode.close(); + }); + + it('should not get current namestate', async () => { + const {info} = await spvClient.execute('getnameinfo', [name]); + assert.strictEqual(info, null); + }); + + it('should get historcial namestate at safe height', async () => { + const {info} = await spvClient.execute('getnameinfo', [name, true]); + assert.strictEqual(info.name, name); + assert.strictEqual(info.state, 'CLOSED'); + assert.strictEqual(info.value, loserBid.bid * COIN); + assert.strictEqual(info.highest, winnerBid.bid * COIN); + }); + + it('should not get current resource', async () => { + const json = await spvClient.execute('getnameresource', [name]); + assert.strictEqual(json, null); + }); + + it('should get historcial resource at safe height', async () => { + const json = await spvClient.execute('getnameresource', [name, true]); + assert.deepStrictEqual( + json, + { + records: [ + { + type: 'NS', + ns: 'example.com.' + } + ] + } + ); + }); + + it('should not verifymessagewithname', async () => { + // No local Urkel tree, namestate is always null + await assert.rejects( + spvClient.execute('verifymessagewithname', [name, signSig, signMsg]), + {message: /Cannot find the name owner/} + ); + }); + + it('should not verifymessagewithname at safe height', async () => { + // This time we do have a valid namestate to work with, but + // SPV nodes still don't have a UTXO set to get addresses from + await assert.rejects( + spvClient.execute('verifymessagewithname', [name, signSig, signMsg, true]), + {message: /Cannot find the owner's address/} + ); + }); + }); }); From 2170fe59ffbc74a803ee1464c29f8e6b28b5cd66 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Thu, 28 Jul 2022 11:43:17 -0400 Subject: [PATCH 3/3] pkg: CHANGELOG --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2935f82b2..4db98e078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ ### Node changes +- RPCs `getnameinfo` `getnameresource` `verifymessagewithname` and `getnamebyhash` +now accept an additional boolean parameter `safe` which will resolve the name from the Urkel +tree at the last "safe height" (committed tree root with > 12 confirmations). SPV +nodes can use this option and retrieve Urkel proofs from the p2p network to respond +to these calls. + - New RPC methods: - `decoderesource` like `decodescript` accepts hex string as input and returns JSON formatted DNS records resource. @@ -13,7 +19,6 @@ - New RPC methods: - `createbatch` and `sendbatch` create batch transactions with any number of outputs with any combination of covenants. - ## v4.0.0 **When upgrading to this version of hsd you must pass