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

rpc/pool: use urkel proofs for namestate in spv mode #647

Merged
merged 3 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
19 changes: 15 additions & 4 deletions lib/net/pool.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
Expand Down
54 changes: 42 additions & 12 deletions lib/node/rpc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2196,23 +2196,25 @@ 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;

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.owner)
throw new RPCError(errs.MISC_ERROR, 'Cannot find the name owner.');
Expand Down Expand Up @@ -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.');
Expand All @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}

/*
Expand Down
144 changes: 140 additions & 4 deletions test/auction-rpc-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -417,11 +430,134 @@ 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]);
const submit = true;
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/}
);
});
});
});