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

[expirimental] escher proof injection #24

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
141 changes: 141 additions & 0 deletions lib/proof.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ const Bits = require('./bits');
const common = require('./common');
const errors = require('./errors');
const inspect = Symbol.for('nodejs.util.inspect.custom');
const {
Null,
Internal,
Leaf
} = require('./nodes');

const {
EMPTY,
Expand Down Expand Up @@ -322,6 +327,142 @@ class Proof {
return [PROOF_OK, this.value];
}

// Compute the expected tree root hash from a proof
computeRoot(key, hash, bits) {
assert(Buffer.isBuffer(key));
assert(hash && typeof hash.digest === 'function');
assert((bits >>> 0) === bits);
assert(bits > 0 && (bits & 7) === 0);
assert(key.length === (bits >>> 3));

if (!this.isSane(hash, bits))
throw new Error('Proof not sane');

const node = this.recreateNode(key, hash);
assert(node);
const leaf = node.hash(hash);

let next = leaf;
let depth = this.depth;

// Traverse bits right to left.
for (let i = this.nodes.length - 1; i >= 0; i--) {
const {prefix, node} = this.nodes[i];

if (depth < prefix.size + 1)
throw new Error('Proof has negative depth');

depth -= 1;

if (hasBit(key, depth))
next = hashInternal(hash, prefix, node, next);
else
next = hashInternal(hash, prefix, next, node);

depth -= prefix.size;

if (!prefix.has(key, depth))
throw new Error('Proof path mismath');
}

if (depth !== 0)
throw new Error('Proof too deep');

return next;
}

// Re-create the collision node at the end of the proof
recreateNode(key, hash) {
switch (this.type) {
case TYPE_DEADEND:
return new Null();
case TYPE_SHORT:
if (this.prefix.has(key, this.depth))
throw new Error('Short proof has same path');
return new Internal(this.prefix, this.left, this.right);
case TYPE_COLLISION:
if (this.key.equals(key))
throw new Error('Collision proof has same key');
return new Leaf(
hashLeaf(hash, this.key, this.hash),
this.key,
this.value // unknown
);
case TYPE_EXISTS:
return new Leaf(
hashValue(hash, key, this.value),
key,
this.value
);
default:
return null;
}
}

// Insert or update the proven node.
// This will change the root hash of the proof!
insert(key, value, hash) {
switch (this.type) {
case TYPE_DEADEND: {
// Replace empty node with new leaf
break;
}
case TYPE_SHORT: {
// Replace existing internal node with new internal node.
// Push new leaf and old internal node down a level in the tree.
// The old internal node's prefix value must be adjusted
// since we now have a shorter collision.
const node = this.recreateNode(key, hash);

// Count how many bits the new key collides with the internal node
const bits = node.prefix.count(key, this.depth);

// Increase depth by that amount of bits
this.depth += bits;

// The new key creates a branch in the middle of the existing
// internal node's prefix. We split that prefix at the new collision
// point and put a new internal node there. Our new leaf will be one
// of its children. The other child is the original internal node
// but now with a shorter prefix. The new leaf and modified original
// internal node are pushed down on level in the tree.
const [front, back] = node.prefix.split(bits);
const siblingHash = hashInternal(
hash,
back,
node.left,
node.right
);
this.push(front, siblingHash);
this.depth += 1;
break;
}
case TYPE_COLLISION: {
// Replace existing leaf with internal node,
// push existing and new leaf nodes down a level in the tree.
const node = this.recreateNode(key, hash);
const prefix = node.bits.collide(key, this.depth);
this.push(prefix, node.hash(hash));
this.depth += prefix.size + 1;
break;
}
case TYPE_EXISTS: {
// Update value only
break;
}
default:
throw new Error('Unknown proof');
}

this.prefix = null;
this.left = null;
this.right = null;
this.key = null;
this.hash = null;
this.value = value;
this.type = TYPE_EXISTS;
}

getSize(hash, bits) {
assert(hash && typeof hash.digest === 'function');
assert((bits >>> 0) === bits);
Expand Down
144 changes: 144 additions & 0 deletions test/escher-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
'use strict';

const assert = require('bsert');
const crypto = require('crypto');
const blake2b160 = require('bcrypto/lib/blake2b160');
const {Tree, Proof} = require('../lib/urkel');
const {types, typesByVal} = Proof;

describe('Proof Insertion', function() {
this.timeout(30000);

/**
* Escher protocol: Data must fit inside 512 byte UPDATE covenant item
*
* 1 version (0x01)
* 20 current tree root
* 1 method (REGISTER: 0x00, UPDATE: 0x01)
* 20 compound namehash (H(sld.tld) = key)
* ... params
*
* REGISTER: 0x00
* 32 NEW ed25519 public key
* 4-438 Urkel proof-of-nonexistence of namehash
*
* UPDATE: 0x01
* 32 NEW ed25519 public key
* 64 signature
* 4-374 urkel proof of OLD public key at namehash
*/

const hash = blake2b160;
const bits = 160;
const tree = new Tree({hash, bits});
const entries = 2000;
const proofSizeLimit = 374;

const data = [];
for (let i = 0; i < entries; i++) {
data.push({
key: crypto.randomBytes(20),
value: crypto.randomBytes(32)
});
}

before(async () => {
await tree.open();
});

after(async () => {
await tree.close();
});

const count = {
'TYPE_DEADEND': 0,
'TYPE_SHORT': 0,
'TYPE_COLLISION': 0
};

let maxProofSize = 0;
let maxDepth = 0;

it(`should insert ${entries} entries`, async () => {
for (const datum of data) {
// Prove nonexistence
const proof = await tree.prove(datum.key);

const size = proof.getSize(hash, bits);
if (size > maxProofSize)
maxProofSize = size;
if (proof.depth > maxDepth)
maxDepth = proof.depth;

count[typesByVal[proof.type]]++;

// Insert into proof and compute new root hash
proof.insert(datum.key, datum.value, hash);
const expectedRoot = proof.computeRoot(datum.key, hash, bits);

// Insert into actual tree and compute new root hash
const b = tree.batch();
await b.insert(datum.key, datum.value);
await b.commit();
const actualRoot = tree.rootHash();

// Compare
assert.bufferEqual(expectedRoot, actualRoot);
}
});

it(`should update ${entries} entries`, async () => {
for (const datum of data) {
// Get current existence proof
const proof = await tree.prove(datum.key);

const size = proof.getSize(hash, bits);
if (size > maxProofSize)
maxProofSize = size;
if (proof.depth > maxDepth)
maxDepth = proof.depth;

assert.strictEqual(proof.type, types.TYPE_EXISTS);

// Modify value
datum.value = crypto.randomBytes(33);

// Insert into proof and compute new root hash
proof.insert(datum.key, datum.value, hash);
const expectedRoot = proof.computeRoot(datum.key, hash, bits);

// Insert into actual tree and compute new root hash
const b = tree.batch();
await b.insert(datum.key, datum.value);
await b.commit();
const actualRoot = tree.rootHash();

// Compare
assert.bufferEqual(expectedRoot, actualRoot);
}
});

it('should have at least one TYPE_DEADEND', () => {
console.log(count['TYPE_DEADEND']);
assert(count['TYPE_DEADEND']);
});

it('should have at least one TYPE_SHORT', () => {
console.log(count['TYPE_SHORT']);
assert(count['TYPE_SHORT']);
});

it('should have at least one TYPE_COLLISION', () => {
console.log(count['TYPE_COLLISION']);
assert(count['TYPE_COLLISION']);
});

it(`should have max proof size < ${proofSizeLimit} bytes`, () => {
console.log(maxProofSize);
assert(maxProofSize < proofSizeLimit);
});

it('should have max depth', () => {
console.log(maxDepth);
});
});
37 changes: 37 additions & 0 deletions test/tree-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@ describe('Urkel radix', function() {

await tree.open();

// Create an empty non-existence proof and verify.
{
const ss = tree.snapshot(tree.rootHash());
const proof = await ss.prove(FOO1);
assert.deepStrictEqual(reencode(tree, proof), proof);
assert.deepStrictEqual(rejson(tree, proof), proof);
const [code, data] = verify(tree.rootHash(), FOO1, proof);
assert.strictEqual(code, 0);
assert.strictEqual(data, null);

assert.bufferEqual(
tree.rootHash(),
proof.computeRoot(FOO1, tree.hash, tree.bits)
);
}

const batch = tree.batch();

// Insert some values.
Expand Down Expand Up @@ -103,6 +119,8 @@ describe('Urkel radix', function() {
const [code, data] = verify(first, FOO2, proof);
assert.strictEqual(code, 0);
assert.bufferEqual(data, BAR2);

assert.bufferEqual(first, proof.computeRoot(FOO2, tree.hash, tree.bits));
}

// Create a non-existent proof and verify.
Expand All @@ -114,6 +132,8 @@ describe('Urkel radix', function() {
const [code, data] = verify(first, FOO5, proof);
assert.strictEqual(code, 0);
assert.strictEqual(data, null);

assert.bufferEqual(first, proof.computeRoot(FOO5, tree.hash, tree.bits));
}

// Create a non-existent proof and verify.
Expand All @@ -125,6 +145,8 @@ describe('Urkel radix', function() {
const [code, data] = verify(first, FOO4, proof);
assert.strictEqual(code, 0);
assert.strictEqual(data, null);

assert.bufferEqual(first, proof.computeRoot(FOO4, tree.hash, tree.bits));
}

// Create a proof and verify.
Expand All @@ -136,6 +158,11 @@ describe('Urkel radix', function() {
const [code, data] = verify(tree.rootHash(), FOO2, proof);
assert.strictEqual(code, 0);
assert.bufferEqual(data, BAR2);

assert.bufferEqual(
tree.rootHash(),
proof.computeRoot(FOO2, tree.hash, tree.bits)
);
}

// Create a non-existent proof and verify.
Expand All @@ -147,6 +174,11 @@ describe('Urkel radix', function() {
const [code, data] = verify(tree.rootHash(), FOO5, proof);
assert.strictEqual(code, 0);
assert.strictEqual(data, null);

assert.bufferEqual(
tree.rootHash(),
proof.computeRoot(FOO5, tree.hash, tree.bits)
);
}

// Create a proof and verify.
Expand All @@ -158,6 +190,11 @@ describe('Urkel radix', function() {
const [code, data] = verify(tree.rootHash(), FOO4, proof);
assert.strictEqual(code, 0);
assert.strictEqual(data, null);

assert.bufferEqual(
tree.rootHash(),
proof.computeRoot(FOO4, tree.hash, tree.bits)
);
}

// Iterate over values.
Expand Down