test: request name proofs from SPV to full node

This commit is contained in:
Matthew Zipkin 2022-05-04 13:33:12 -04:00 committed by Nodari Chkuaselidze
parent 9ce67a497d
commit 0557686540
No known key found for this signature in database
GPG key ID: B018A7BB437D1F05
3 changed files with 252 additions and 2 deletions

View file

@ -3515,7 +3515,7 @@ class Pool extends EventEmitter {
const item = this.nameMap.get(hash);
assert(item);
item.reject(new Error('Timed out.'));
item.reject(new Error('Peer removed.'));
this.nameMap.delete(hash);
}

250
test/net-spv-test.js Normal file
View file

@ -0,0 +1,250 @@
'use strict';
const assert = require('bsert');
const random = require('bcrypto/lib/random');
const ChainEntry = require('../lib/blockchain/chainentry');
const Network = require('../lib/protocol/network');
const FullNode = require('../lib/node/fullnode');
const SPVNode = require('../lib/node/spvnode');
const rules = require('../lib/covenants/rules');
const NameState = require('../lib/covenants/namestate');
const {Resource} = require('../lib/dns/resource');
const {types: packetTypes} = require('../lib/net/packets');
const {types: urkelTypes} = require('urkel').Proof;
const network = Network.get('regtest');
const {
treeInterval,
biddingPeriod,
revealPeriod
} = network.names;
const SAFE_ROOT = 12;
describe('SPV', function() {
describe('Name Resource Lookup', function() {
const full = new FullNode({
network: 'regtest',
listen: true,
bip37: true,
noDns: true,
plugins: [require('../lib/wallet/plugin')]
});
const spv = new SPVNode({
network: 'regtest',
only: '127.0.0.1',
port: 10000,
brontidePort: 20000,
httpPort: 30000
});
const {wdb} = full.require('walletdb');
let wallet, addr;
const name = 'sad';
const nameHash = rules.hashName(name);
before(async () => {
await full.open();
await spv.open();
wallet = await wdb.get('primary');
});
after(async () => {
await spv.close();
await full.close();
});
async function mineBlocks(n) {
for (; n > 0; n--) {
const block = await full.miner.mineBlock(null, addr);
await full.chain.add(block);
}
}
// Create a chain of block headers and add directly to SPV node.
// Change the tree root in the headers to something random
// at every expected treeInterval + 1. This simulates the SPV node
// experiencing a chain split away from the full node, and it will try to
// request name proofs with tree roots the full node will not recognize.
// The full node state remains unaffected.
async function mineSPVFork(n, tip) {
let treeRoot = tip.treeRoot;
for (; n > 0; n--) {
const job = await full.miner.createJob(tip, addr);
if (job.attempt.height % treeInterval === 1) {
treeRoot = random.randomBytes(32);
}
job.attempt.treeRoot = treeRoot;;
const block = await full.miner.cpu.mineAsync(job);
await spv.chain.add(block);
tip = ChainEntry.fromBlock(block);
}
}
it('should connect nodes', async () => {
const waiter = new Promise((res, rej) => {
full.pool.on('connection', () => res());
});
await full.connect();
await spv.connect();
await spv.startSync();
await waiter;
assert.strictEqual(spv.pool.peers.outbound, 1);
assert.strictEqual(full.pool.peers.inbound, 1);
});
it('should generate blocks', async () => {
addr = await wallet.receiveAddress(0);
const waiter = new Promise((res, rej) => {
spv.on('connect', (entry) => {
if (entry.height === 10)
res();
});
});
await mineBlocks(10);
await waiter;
assert.strictEqual(full.chain.height, spv.chain.height);
});
it('should get proof of nonexistence', async () => {
const waiter = new Promise((res, rej) => {
spv.pool.once('packet', (packet) => {
if (packet.type === packetTypes.PROOF)
res(packet.proof.type);
});
});
const ns = await spv.pool.resolve(nameHash);
assert.strictEqual(ns, null);
const proofType = await waiter;
assert.strictEqual(proofType, urkelTypes.TYPE_DEADEND);
});
it('should run auction and register name', async () => {
await wallet.sendOpen(name, false);
await mineBlocks(treeInterval + 1);
await wallet.sendBid(name, 10000, 10000);
await mineBlocks(biddingPeriod);
await wallet.sendReveal(name);
await mineBlocks(revealPeriod);
await wallet.sendUpdate(
name,
Resource.fromJSON(
{
records: [
{type: 'NS', ns: 'one.'}
]
}
)
);
await mineBlocks(treeInterval + SAFE_ROOT);
});
it('should get proof of existence with data', async () => {
const waiter = new Promise((res, rej) => {
spv.pool.once('packet', (packet) => {
if (packet.type === packetTypes.PROOF)
res(packet.proof.type);
});
});
const raw = await spv.pool.resolve(nameHash);
const ns = NameState.decode(raw);
const res = Resource.decode(ns.data);
assert.strictEqual(res.records[0].ns, 'one.');
const proofType = await waiter;
assert.strictEqual(proofType, urkelTypes.TYPE_EXISTS);
});
it('should update name data', async () => {
await wallet.sendUpdate(
name,
Resource.fromJSON(
{
records: [
{type: 'NS', ns: 'two.'}
]
}
)
);
await mineBlocks(treeInterval + SAFE_ROOT);
});
it('should get updated data', async () => {
const waiter = new Promise((res, rej) => {
spv.pool.once('packet', (packet) => {
if (packet.type === packetTypes.PROOF)
res(packet.proof.type);
});
});
const raw = await spv.pool.resolve(nameHash);
const ns = NameState.decode(raw);
const res = Resource.decode(ns.data);
assert.strictEqual(res.records[0].ns, 'two.');
const proofType = await waiter;
assert.strictEqual(proofType, urkelTypes.TYPE_EXISTS);
});
it('should get historical data', async () => {
// Send the SPV node back in time
const height = full.chain.height - treeInterval - SAFE_ROOT;
const entry = await full.chain.getEntry(height);
await spv.chain.invalidate(entry.hash);
assert(full.chain.height > spv.chain.height);
// Get old data
const waiter1 = new Promise((res, rej) => {
spv.pool.once('packet', (packet) => {
if (packet.type === packetTypes.PROOF)
res(packet.proof.type);
});
});
const raw = await spv.pool.resolve(nameHash);
const ns = NameState.decode(raw);
const res = Resource.decode(ns.data);
assert.strictEqual(res.records[0].ns, 'one.');
const proofType = await waiter1;
assert.strictEqual(proofType, urkelTypes.TYPE_EXISTS);
// Restore
const waiter2 = new Promise((res, rej) => {
spv.on('connect', (entry) => {
if (entry.height === full.chain.height)
res();
});
});
await spv.chain.removeInvalid(entry.hash);
await waiter2;
assert.strictEqual(full.chain.height, spv.chain.height);
});
it('should request name data with unknown tree root', async () => {
// SPV node teleports to a parallel dimension
await mineSPVFork(100, full.chain.tip);
// Get the SPV node peer from the full node's perspective
const peer = full.pool.peers.head();
let err;
peer.on('error', e => err = e);
// SPV node tries to make request and gets disconnected instantly
await assert.rejects(
spv.pool.resolve(nameHash),
{
message: 'Peer removed.'
}
);
// This is the error thrown by the full node trying to serve the proof.
assert(err);
assert.strictEqual(err.code, 'ERR_MISSING_NODE');
// :-(
assert.strictEqual(spv.pool.peers.outbound, 0);
assert.strictEqual(full.pool.peers.inbound, 0);
});
});
});

View file

@ -124,7 +124,7 @@ describe('SlidingWindow (Functional)', function() {
}
}
assert.equal(err.message, 'Timed out.');
assert.equal(err.message, 'Peer removed.');
assert.strictEqual(packets, maxProofRPS);
assert.strictEqual(count, maxProofRPS);