diff --git a/contracts/implementation.sol b/contracts/implementation.sol index e31abdc..99c3e64 100644 --- a/contracts/implementation.sol +++ b/contracts/implementation.sol @@ -1,5 +1,6 @@ pragma solidity ^0.4.24; +import {D} from "./data.sol"; import {PatriciaTree} from "./tree.sol"; contract PatriciaTreeImplementation { @@ -13,6 +14,10 @@ contract PatriciaTreeImplementation { return tree.get(key); } + function getValue(bytes32 hash) public view returns (bytes) { + return tree.values[hash]; + } + function getRootHash() public view returns (bytes32) { return tree.getRootHash(); } @@ -37,3 +42,140 @@ contract PatriciaTreeImplementation { tree.insert(key, value); } } + +contract PatriciaTreeMerkleProof { + using PatriciaTree for PatriciaTree.Tree; + PatriciaTree.Tree tree; + + enum Status {OPENED, ONGOING, SUCCESS, FAILURE} + + event OnChangeStatus(Status s); + + modifier onlyFor(Status _status) { + require(status == _status); + _; + } + + mapping(bytes32 => bool) committedValues; + + Status public status; + D.Edge originalRootEdge; + bytes32 originalRoot; + D.Edge targetRootEdge; + bytes32 targetRoot; + + constructor() public { + // Init status + status = Status.OPENED; + } + + function commitOriginalEdge( + uint _originalLabelLength, + bytes32 _originalLabel, + bytes32 _originalValue + ) public onlyFor(Status.OPENED) { + // Init original root edge + originalRootEdge.label = D.Label(_originalLabel, _originalLabelLength); + originalRootEdge.node = _originalValue; + originalRoot = PatriciaTree.edgeHash(originalRootEdge); + } + + function commitTargetEdge( + uint _targetLabelLength, + bytes32 _targetLabel, + bytes32 _targetValue + ) public onlyFor(Status.OPENED) { + // Init target root edge + targetRootEdge.label = D.Label(_targetLabel, _targetLabelLength); + targetRootEdge.node = _targetValue; + targetRoot = PatriciaTree.edgeHash(targetRootEdge); + } + + function insert(bytes key, bytes value) public { + bytes32 k = keccak256(value); + committedValues[k] = true; + tree.insert(key, value); + } + + function commitNode( + bytes32 nodeHash, + uint firstEdgeLabelLength, + bytes32 firstEdgeLabel, + bytes32 firstEdgeValue, + uint secondEdgeLabelLength, + bytes32 secondEdgeLabel, + bytes32 secondEdgeValue + ) public onlyFor(Status.OPENED) { + D.Label memory k0 = D.Label(firstEdgeLabel, firstEdgeLabelLength); + D.Edge memory e0 = D.Edge(firstEdgeValue, k0); + D.Label memory k1 = D.Label(secondEdgeLabel, secondEdgeLabelLength); + D.Edge memory e1 = D.Edge(secondEdgeValue, k1); + require(tree.nodes[nodeHash].children[0].node == 0); + require(tree.nodes[nodeHash].children[1].node == 0); + require(nodeHash == keccak256( + abi.encodePacked(PatriciaTree.edgeHash(e0), PatriciaTree.edgeHash(e1))) + ); + tree.nodes[nodeHash].children[0] = e0; + tree.nodes[nodeHash].children[1] = e1; + } + + function commitValue(bytes value) public onlyFor(Status.OPENED) { + bytes32 k = keccak256(value); + committedValues[k] = true; + tree.values[k] = value; + } + + function seal() public onlyFor(Status.OPENED) { +// require(_verifyEdge(originalRootEdge)); + tree.rootEdge = originalRootEdge; + tree.root = PatriciaTree.edgeHash(tree.rootEdge); + _changeStatus(Status.ONGOING); + } + + function proof() public onlyFor(Status.ONGOING) { + require(targetRootEdge.node == tree.rootEdge.node); + require(targetRootEdge.label.length == tree.rootEdge.label.length); + require(targetRootEdge.label.data == tree.rootEdge.label.data); + require(_verifyEdge(tree.rootEdge)); + _changeStatus(Status.SUCCESS); + } + + function getRootHash() public view returns (bytes32) { + return tree.getRootHash(); + } + + function _verifyEdge(D.Edge memory _edge) internal view returns (bool) { + if (_edge.node == 0) { + // Empty. Return true because there is nothing to verify + return true; + } else if (_isLeaf(_edge)) { + // check stored value of the leaf node + require(_hasValue(_edge.node)); + } else { + D.Edge[2] memory children = tree.nodes[_edge.node].children; + // its node value should be the hashed value of its child nodes + require(_edge.node == keccak256( + abi.encodePacked(PatriciaTree.edgeHash(children[0]), PatriciaTree.edgeHash(children[1])) + )); + // check children recursively + require(_verifyEdge(children[0])); + require(_verifyEdge(children[1])); + } + return true; + } + + function _isLeaf(D.Edge _edge) internal view returns (bool) { + return (tree.nodes[_edge.node].children[0].node == 0 && tree.nodes[_edge.node].children[1].node == 0); + } + + function _hasValue(bytes32 valHash) internal view returns (bool) { + return committedValues[valHash]; + } + + function _changeStatus(Status _status) internal { + require(status < _status); + // unidirectional + status = _status; + emit OnChangeStatus(status); + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2a90fa9..ea9cba6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "solidity-patricia-tree", - "version": "1.0.3", + "version": "1.0.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 8fbc673..b975d64 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "solidity-patricia-tree", - "version": "1.0.3", + "version": "1.0.4", "description": "Patricia Tree solidity implemenation", "directories": { "test": "test" diff --git a/test/PatriciaTreeMerkleProof.test.js b/test/PatriciaTreeMerkleProof.test.js new file mode 100644 index 0000000..4c087c5 --- /dev/null +++ b/test/PatriciaTreeMerkleProof.test.js @@ -0,0 +1,273 @@ +const chai = require('chai') +const assert = chai.assert +const BigNumber = web3.BigNumber + +const PatriciaTreeImplementation = artifacts.require('PatriciaTreeImplementation') +const PatriciaTreeMerkleProof = artifacts.require('PatriciaTreeMerkleProof') + +const Status = { + OPENED: 0, + ONGOING: 1, + SUCCESS: 2, + FAILURE: 3 +} + +contract('PatriciaTreeMerkleProof', async ([_, primary, nonPrimary]) => { + let originalRootHash + let originalRootEdge + let targetRootEdge + let targetRootHash + let snapshotTree + let testNode1 + let testNode2 + before('Plasma manages its state with PatriciaTreeImplementation', async () => { + // Data for merkle proof. We can make some transitions on the child chain + + // Get the original root edge + let plasmaTree = await PatriciaTreeImplementation.new({ from: primary }) + await plasmaTree.insert('key1', 'val1', { from: primary }) + await plasmaTree.insert('key2', 'val2', { from: primary }) + originalRootEdge = await plasmaTree.getRootEdge() + originalRootHash = await plasmaTree.getRootHash() + + // Get the target root edge + await plasmaTree.insert('key3', 'val3', { from: primary }) + await plasmaTree.insert('key4', 'val4', { from: primary }) + targetRootEdge = await plasmaTree.getRootEdge() + targetRootHash = await plasmaTree.getRootHash() + + // Get the target root edge + await plasmaTree.insert('key5', 'val5', { from: primary }) + let testRootHash = (await plasmaTree.getRootEdge())[2] + testNode1 = [testRootHash, ...(await plasmaTree.getNode(testRootHash))] + await plasmaTree.insert('key6', 'val6', { from: primary }) + testRootHash = (await plasmaTree.getRootEdge())[2] + testNode2 = [testRootHash, ...(await plasmaTree.getNode(testRootHash))] + + // Init a test tree + snapshotTree = await PatriciaTreeImplementation.new({ from: primary }) + await snapshotTree.insert('key1', 'val1', { from: primary }) + await snapshotTree.insert('key2', 'val2', { from: primary }) + }) + + describe('constructor()', async () => { + let merkleProofCase + it('should assign the original root edge and the target root edge', async () => { + merkleProofCase = await PatriciaTreeMerkleProof.new({ from: primary }) + assert.ok('deployed successfully') + }) + it('should set its initial status as OPENED', async () => { + merkleProofCase = await PatriciaTreeMerkleProof.new({ from: primary }) + assert.equal((await merkleProofCase.status()).toNumber(), Status.OPENED) + }) + }) + + context('Once deployed successfully', async () => { + let merkleProofCase + let dataToCommit + const commitNodes = async (nodes) => { + for (const node of nodes) { + await merkleProofCase.commitNode(...node, { from: primary }) + } + } + + const commitValues = async (values) => { + for (const value of values) { + await merkleProofCase.commitValue(value, { from: primary }) + } + } + + before('prepare', async () => { + let rootValueOfOriginalState = originalRootEdge[2] + dataToCommit = await getDataToCommit(snapshotTree, rootValueOfOriginalState) + }) + + beforeEach('Use a newly deployed PatriciaTreeMerkleProof for every test', async () => { + merkleProofCase = await PatriciaTreeMerkleProof.new({ from: primary }) + await merkleProofCase.commitOriginalEdge(...originalRootEdge, { from: primary }) + await merkleProofCase.commitTargetEdge(...targetRootEdge, { from: primary }) + }) + + describe('commitNode()', async () => { + it('should be called only when the case is in OPENED status', async () => { + await commitNodes(dataToCommit.nodes) + assert.ok('successfully committed nodes') + }) + + it('should revert when an invalid node is added', async () => { + await merkleProofCase.commitNode(...testNode1, { from: primary }) + assert.ok('Successfully passed because the passed node is valid') + try { + await merkleProofCase.commitNode(0, ...originalRootEdge, ...originalRootEdge, { from: primary }) + assert.fail('should revert') + } catch (e) { + assert.ok('reverted successfully') + } + }) + + it('should revert if same hash already exists', async () => { + await merkleProofCase.commitNode(...testNode1, { from: primary }) + assert.ok('Successfully passed because same hash does not exist') + try { + await merkleProofCase.commitNode(...testNode1, { from: primary }) + assert.fail('should revert because the same hash already exists') + } catch (e) { + assert.ok('reverted successfully') + } + }) + + it('should revert when it is not in OPENED status', async () => { + await commitNodes(dataToCommit.nodes) + await commitValues(dataToCommit.values) + await merkleProofCase.seal({ from: primary }) + try { + await merkleProofCase.commitNode(testNode1, { from: primary }) + assert.fail('it should revert') + } catch (e) { + assert.ok('successfully reverted') + } + }) + }) + + describe('seal()', async () => { + it('should revert if it does not passes the merkle proof', async () => { + it('should have enough committed values which it refers', async () => { + await commitNodes(dataToCommit.nodes) + try { + await merkleProofCase.seal({ from: primary }) + assert.fail('should revert') + } catch (e) { + assert.ok('reverted successfully') + } + }) + it('should have enough committed nodes for its merkle proof', async () => { + await commitValues(dataToCommit.values) + try { + await merkleProofCase.seal({ from: primary }) + assert.fail('should revert') + } catch (e) { + assert.ok('reverted successfully') + } + }) + }) + + it('should change its status as ONGOING and emit an event for it', async () => { + // make the case have ONGOING status first + await commitNodes(dataToCommit.nodes) + await commitValues(dataToCommit.values) + let response = await merkleProofCase.seal({ from: primary }) + + // check it emits an event + assert.equal( + web3.toDecimal(response.receipt.logs[0].data), + Status.ONGOING, + 'event will be logged in the receipt' + ) + // check it returns its status as ONGOING + assert.equal( + (await merkleProofCase.status()).toNumber(), + Status.ONGOING, + 'it should return its status as ONGOING' + ) + }) + }) + + describe('insert()', async () => { + it('should be called only when the case is in ONGOING status', async () => { + try { + await merkleProofCase.insert('somekey', 'someval', { from: primary }) + assert.fail('should revert') + } catch (e) { + assert.ok('reverted successfully') + } + }) + it('should be called by only the primary account', async () => { + // make the case have ONGOING status first + await commitNodes(dataToCommit.nodes) + await commitValues(dataToCommit.values) + await merkleProofCase.seal({ from: primary }) + + // insert item with primary account + await merkleProofCase.insert('somekey', 'someval', { from: primary }) + assert.ok('primary account can insert item') + try { + // insert item with non primary account + await merkleProofCase.insert('somekey', 'someval', { from: nonPrimary }) + assert.fail('should revert') + } catch (e) { + assert.ok('non primary account can not insert item') + } + }) + }) + + describe('proof()', async () => { + it('should revert when the calculated root hash is not equal to the target hash', async () => { + // make the case have ONGOING status first + await commitNodes(dataToCommit.nodes) + await commitValues(dataToCommit.values) + await merkleProofCase.seal({ from: primary }) + + // insert manipulated items + await merkleProofCase.insert('key3', 'manipulatedval3', { from: primary }) // original value is 'val3' + await merkleProofCase.insert('key4', 'manipulatedval4', { from: primary }) // original value is 'val4' + + // try to proof + try { + await merkleProofCase.proof({ from: nonPrimary }) + assert.fail('should revert') + } catch (e) { + assert.ok('reverted because it was manipulated') + } + }) + + it('should change its state as SUCCESS when the calculated root hash is equal to the target hash', async () => { + // make the case have ONGOING status first + await commitNodes(dataToCommit.nodes) + await commitValues(dataToCommit.values) + await merkleProofCase.seal({ from: primary }) + + // insert correct items + await merkleProofCase.insert('key3', 'val3', { from: primary }) + await merkleProofCase.insert('key4', 'val4', { from: primary }) + + // try to proof + await merkleProofCase.proof({ from: primary }) + // check it changes its status as SUCCESS + assert.equal( + (await merkleProofCase.status()).toNumber(), + Status.SUCCESS, + 'it should return its status as SUCCESS' + ) + }) + }) + }) +}) + +const getDataToCommit = async function (tree, hash) { + let result = { + values: [], + nodes: [] + } + + let response = await tree.getNode(hash) + // response[2] means the first child's node value + // response[5] means the second child's node value + if (web3.toDecimal(response[2]) === 0 && web3.toDecimal(response[5]) === 0) { + // when if it is a leaf node, push the value to commit + let value = await tree.getValue(hash) + result.values.push(web3.toUtf8(value)) + } else { + // when if it is a branch node, push the node and repeat recursively + result.nodes.push([hash, ...response]) + let resultFromFirstChild = await getDataToCommit(tree, response[2]) + result.values = [...result.values, ...resultFromFirstChild.values] + result.nodes = [...result.nodes, ...resultFromFirstChild.nodes] + + if (response[5] != response[2]) { + let resultFromSecondChild = await getDataToCommit(tree, response[5]) + result.values = [...result.values, ...resultFromSecondChild.values] + result.nodes = [...result.nodes, ...resultFromSecondChild.nodes] + } + } + return result +}