Skip to content

Commit

Permalink
node-rpc: add safe height option to all namestate requests (for SPV)
Browse files Browse the repository at this point in the history
  • Loading branch information
pinheadmz committed Jul 27, 2022
1 parent 5a235eb commit 8e98a9f
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 68 deletions.
103 changes: 39 additions & 64 deletions lib/node/rpc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -2213,20 +2214,7 @@ class RPC extends RPCBase {

const nameHash = rules.hashName(name);

let ns;
if (this.chain.options.spv) {
// This root is "unsafe" (might have less than 12 confirmations).
const root = this.chain.tip.treeRoot;
// This data may be outdated, if a namestate has been updated
// since the last tree interval.
const data = await this.pool.resolveAtRoot(nameHash, root);
if (data) {
ns = NameState.decode(data);
ns.nameHash = nameHash;
}
} else {
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 @@ -2429,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 @@ -2444,20 +2433,7 @@ class RPC extends RPCBase {
const reserved = rules.isReserved(nameHash, height + 1, network);
const [start, week] = rules.getRollout(nameHash, network);

let ns;
if (this.chain.options.spv) {
// This root is "unsafe" (might have less than 12 confirmations).
const root = this.chain.tip.treeRoot;
// This data may be outdated, if a namestate has been updated
// since the last tree interval.
const data = await this.pool.resolveAtRoot(nameHash, root);
if (data) {
ns = NameState.decode(data);
ns.nameHash = nameHash;
}
} else {
ns = await this.chain.db.getNameState(nameHash);
}
const ns = await this.getNameState(nameHash, safe);

let info = null;

Expand All @@ -2477,31 +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);

if (!name || !rules.verifyName(name))
throw new RPCError(errs.TYPE_ERROR, 'Invalid name.');

const nameHash = rules.hashName(name);

let ns;
if (this.chain.options.spv) {
// This root is "unsafe" (might have less than 12 confirmations).
const root = this.chain.tip.treeRoot;
// This data may be outdated, if a namestate has been updated
// since the last tree interval.
const data = await this.pool.resolveAtRoot(nameHash, root);
if (data) {
ns = NameState.decode(data);
ns.nameHash = nameHash;
}
} else {
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 @@ -2560,29 +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.');

let ns;
if (this.chain.options.spv) {
// This root is "unsafe" (might have less than 12 confirmations).
const root = this.chain.tip.treeRoot;
// This data may be outdated, if a namestate has been updated
// since the last tree interval.
const data = await this.pool.resolveAtRoot(hash, root);
if (data) {
ns = NameState.decode(data);
ns.nameHash = hash;
}
} else {
ns = await this.chain.db.getNameState(hash);
}
const ns = await this.getNameState(hash, safe);

if (!ns)
return null;
Expand Down Expand Up @@ -3151,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/}
);
});
});
});

0 comments on commit 8e98a9f

Please sign in to comment.