Commit 5f3b2cfd authored by smartcontracts's avatar smartcontracts Committed by GitHub

tests[contracts]: Improve Lib_MerkleTrie test coverage (#783)

* wip: Improve Lib_MerkleTrie test coverage

* Add a few more JSON tests

* fix[contracts]: Separate function for extension node value changes

* test: Add some additional JSON tests

* Update packages/contracts/test/contracts/libraries/trie/Lib_MerkleTrie.spec.ts
Co-authored-by: default avatarMaurelian <maurelian@protonmail.ch>

* fix: trailing newlines
Co-authored-by: default avatarMaurelian <maurelian@protonmail.ch>
parent 67e153e1
...@@ -122,7 +122,7 @@ library Lib_MerkleTrie { ...@@ -122,7 +122,7 @@ library Lib_MerkleTrie {
TrieNode[] memory proof = _parseProof(_proof); TrieNode[] memory proof = _parseProof(_proof);
(uint256 pathLength, bytes memory keyRemainder, ) = _walkNodePath(proof, _key, _root); (uint256 pathLength, bytes memory keyRemainder, ) = _walkNodePath(proof, _key, _root);
TrieNode[] memory newPath = _getNewPath(proof, pathLength, keyRemainder, _value); TrieNode[] memory newPath = _getNewPath(proof, pathLength, _key, keyRemainder, _value);
return _getUpdatedTrieRoot(newPath, _key); return _getUpdatedTrieRoot(newPath, _key);
} }
...@@ -312,20 +312,20 @@ library Lib_MerkleTrie { ...@@ -312,20 +312,20 @@ library Lib_MerkleTrie {
} }
/** /**
* @notice Creates new nodes to support a k/v pair insertion into a given * @notice Creates new nodes to support a k/v pair insertion into a given Merkle trie path.
* Merkle trie path.
* @param _path Path to the node nearest the k/v pair. * @param _path Path to the node nearest the k/v pair.
* @param _pathLength Length of the path. Necessary because the provided * @param _pathLength Length of the path. Necessary because the provided path may include
* path may include additional nodes (e.g., it comes directly from a proof) * additional nodes (e.g., it comes directly from a proof) and we can't resize in-memory
* and we can't resize in-memory arrays without costly duplication. * arrays without costly duplication.
* @param _keyRemainder Portion of the initial key that must be inserted * @param _key Full original key.
* into the trie. * @param _keyRemainder Portion of the initial key that must be inserted into the trie.
* @param _value Value to insert at the given key. * @param _value Value to insert at the given key.
* @return _newPath A new path with the inserted k/v pair and extra supporting nodes. * @return _newPath A new path with the inserted k/v pair and extra supporting nodes.
*/ */
function _getNewPath( function _getNewPath(
TrieNode[] memory _path, TrieNode[] memory _path,
uint256 _pathLength, uint256 _pathLength,
bytes memory _key,
bytes memory _keyRemainder, bytes memory _keyRemainder,
bytes memory _value bytes memory _value
) )
...@@ -348,7 +348,32 @@ library Lib_MerkleTrie { ...@@ -348,7 +348,32 @@ library Lib_MerkleTrie {
TrieNode[] memory newNodes = new TrieNode[](3); TrieNode[] memory newNodes = new TrieNode[](3);
uint256 totalNewNodes = 0; uint256 totalNewNodes = 0;
if (keyRemainder.length == 0 && lastNodeType == NodeType.LeafNode) { // Reference: https://github.com/ethereumjs/merkle-patricia-tree/blob/c0a10395aab37d42c175a47114ebfcbd7efcf059/src/baseTrie.ts#L294-L313
bool matchLeaf = false;
if (lastNodeType == NodeType.LeafNode) {
uint256 l = 0;
if (_path.length > 0) {
for (uint256 i = 0; i < _path.length - 1; i++) {
if (_getNodeType(_path[i]) == NodeType.BranchNode) {
l++;
} else {
l += _getNodeKey(_path[i]).length;
}
}
}
if (
_getSharedNibbleLength(
_getNodeKey(lastNode),
Lib_BytesUtils.slice(Lib_BytesUtils.toNibbles(_key), l)
) == _getNodeKey(lastNode).length
&& keyRemainder.length == 0
) {
matchLeaf = true;
}
}
if (matchLeaf) {
// We've found a leaf node with the given key. // We've found a leaf node with the given key.
// Simply need to update the value of the node to match. // Simply need to update the value of the node to match.
newNodes[totalNewNodes] = _makeLeafNode(_getNodeKey(lastNode), _value); newNodes[totalNewNodes] = _makeLeafNode(_getNodeKey(lastNode), _value);
...@@ -488,7 +513,7 @@ library Lib_MerkleTrie { ...@@ -488,7 +513,7 @@ library Lib_MerkleTrie {
// and we can skip this part. // and we can skip this part.
if (previousNodeHash.length > 0) { if (previousNodeHash.length > 0) {
// Re-encode the node based on the previous node. // Re-encode the node based on the previous node.
currentNode = _makeExtensionNode(nodeKey, previousNodeHash); currentNode = _editExtensionNodeValue(currentNode, previousNodeHash);
} }
} else if (currentNodeType == NodeType.BranchNode) { } else if (currentNodeType == NodeType.BranchNode) {
// If this node is the last element in the path, it'll be correctly encoded // If this node is the last element in the path, it'll be correctly encoded
...@@ -761,6 +786,33 @@ library Lib_MerkleTrie { ...@@ -761,6 +786,33 @@ library Lib_MerkleTrie {
return _makeNode(raw); return _makeNode(raw);
} }
/**
* Creates a new extension node with the same key but a different value.
* @param _node Extension node to copy and modify.
* @param _value New value for the extension node.
* @return New node with the same key and different value.
*/
function _editExtensionNodeValue(
TrieNode memory _node,
bytes memory _value
)
private
pure
returns (
TrieNode memory
)
{
bytes[] memory raw = new bytes[](2);
bytes memory key = _addHexPrefix(_getNodeKey(_node), false);
raw[0] = Lib_RLPWriter.writeBytes(Lib_BytesUtils.fromNibbles(key));
if (_value.length < 32) {
raw[1] = _value;
} else {
raw[1] = Lib_RLPWriter.writeBytes(_value);
}
return _makeNode(raw);
}
/** /**
* @notice Creates a new leaf node. * @notice Creates a new leaf node.
* @dev This function is essentially identical to `_makeExtensionNode`. * @dev This function is essentially identical to `_makeExtensionNode`.
......
...@@ -93,7 +93,7 @@ library Lib_BytesUtils { ...@@ -93,7 +93,7 @@ library Lib_BytesUtils {
bytes memory bytes memory
) )
{ {
if (_bytes.length - _start == 0) { if (_start >= _bytes.length) {
return bytes(''); return bytes('');
} }
......
...@@ -4,12 +4,15 @@ import { expect } from '../../../setup' ...@@ -4,12 +4,15 @@ import { expect } from '../../../setup'
import * as rlp from 'rlp' import * as rlp from 'rlp'
import { ethers } from 'hardhat' import { ethers } from 'hardhat'
import { Contract } from 'ethers' import { Contract } from 'ethers'
import { toHexString } from '@eth-optimism/core-utils' import { fromHexString, toHexString } from '@eth-optimism/core-utils'
import { Trie } from 'merkle-patricia-tree/dist/baseTrie'
/* Internal Imports */ /* Internal Imports */
import { TrieTestGenerator } from '../../../helpers' import { TrieTestGenerator } from '../../../helpers'
import * as officialTestJson from '../../../data/json/libraries/trie/trietest.json'
import * as officialTestAnyOrderJson from '../../../data/json/libraries/trie/trieanyorder.json'
const NODE_COUNTS = [1, 2, 128] const NODE_COUNTS = [1, 2, 32, 128]
describe('Lib_MerkleTrie', () => { describe('Lib_MerkleTrie', () => {
let Lib_MerkleTrie: Contract let Lib_MerkleTrie: Contract
...@@ -19,15 +22,112 @@ describe('Lib_MerkleTrie', () => { ...@@ -19,15 +22,112 @@ describe('Lib_MerkleTrie', () => {
).deploy() ).deploy()
}) })
// Eth-foundation tests: https://github.com/ethereum/tests/tree/develop/TrieTests
describe('official tests', () => {
for (const testName of Object.keys(officialTestJson.tests)) {
it(`should perform official test: ${testName}`, async () => {
const trie = new Trie()
const inputs = officialTestJson.tests[testName].in
const expected = officialTestJson.tests[testName].root
for (const input of inputs) {
let key: Buffer
if (input[0].startsWith('0x')) {
key = fromHexString(input[0])
} else {
key = fromHexString(
ethers.utils.hexlify(ethers.utils.toUtf8Bytes(input[0]))
)
}
let val: Buffer
if (input[1] === null) {
throw new Error('deletions not supported, check your tests')
} else if (input[1].startsWith('0x')) {
val = fromHexString(input[1])
} else {
val = fromHexString(
ethers.utils.hexlify(ethers.utils.toUtf8Bytes(input[1]))
)
}
const proof = await Trie.createProof(trie, key)
const root = trie.root
await trie.put(key, val)
const out = await Lib_MerkleTrie.update(
toHexString(key),
toHexString(val),
toHexString(rlp.encode(proof)),
root
)
expect(out).to.equal(toHexString(trie.root))
}
expect(toHexString(trie.root)).to.equal(expected)
})
}
})
describe('official tests - trie any order', () => {
for (const testName of Object.keys(officialTestAnyOrderJson.tests)) {
it(`should perform official test: ${testName}`, async () => {
const trie = new Trie()
const inputs = officialTestAnyOrderJson.tests[testName].in
const expected = officialTestAnyOrderJson.tests[testName].root
for (const input of Object.keys(inputs)) {
let key: Buffer
if (input.startsWith('0x')) {
key = fromHexString(input)
} else {
key = fromHexString(
ethers.utils.hexlify(ethers.utils.toUtf8Bytes(input))
)
}
let val: Buffer
if (inputs[input] === null) {
throw new Error('deletions not supported, check your tests')
} else if (inputs[input].startsWith('0x')) {
val = fromHexString(inputs[input])
} else {
val = fromHexString(
ethers.utils.hexlify(ethers.utils.toUtf8Bytes(inputs[input]))
)
}
const proof = await Trie.createProof(trie, key)
const root = trie.root
await trie.put(key, val)
const out = await Lib_MerkleTrie.update(
toHexString(key),
toHexString(val),
toHexString(rlp.encode(proof)),
root
)
expect(out).to.equal(toHexString(trie.root))
}
expect(toHexString(trie.root)).to.equal(expected)
})
}
})
describe('verifyInclusionProof', () => { describe('verifyInclusionProof', () => {
for (const nodeCount of NODE_COUNTS) { for (const nodeCount of NODE_COUNTS) {
describe(`inside a trie with ${nodeCount} nodes`, () => { describe(`inside a trie with ${nodeCount} nodes and keys/vals of size ${nodeCount} bytes`, () => {
let generator: TrieTestGenerator let generator: TrieTestGenerator
before(async () => { before(async () => {
generator = await TrieTestGenerator.fromRandom({ generator = await TrieTestGenerator.fromRandom({
seed: `seed.incluson.${nodeCount}`, seed: `seed.incluson.${nodeCount}`,
nodeCount, nodeCount,
secure: false, secure: false,
keySize: nodeCount,
valSize: nodeCount,
}) })
}) })
...@@ -55,13 +155,15 @@ describe('Lib_MerkleTrie', () => { ...@@ -55,13 +155,15 @@ describe('Lib_MerkleTrie', () => {
describe('update', () => { describe('update', () => {
for (const nodeCount of NODE_COUNTS) { for (const nodeCount of NODE_COUNTS) {
describe(`inside a trie with ${nodeCount} nodes`, () => { describe(`inside a trie with ${nodeCount} nodes and keys/vals of size ${nodeCount} bytes`, () => {
let generator: TrieTestGenerator let generator: TrieTestGenerator
before(async () => { before(async () => {
generator = await TrieTestGenerator.fromRandom({ generator = await TrieTestGenerator.fromRandom({
seed: `seed.update.${nodeCount}`, seed: `seed.update.${nodeCount}`,
nodeCount, nodeCount,
secure: false, secure: false,
keySize: nodeCount,
valSize: nodeCount,
}) })
}) })
...@@ -88,17 +190,36 @@ describe('Lib_MerkleTrie', () => { ...@@ -88,17 +190,36 @@ describe('Lib_MerkleTrie', () => {
} }
}) })
} }
it('should return the single-node root hash if the trie was previously empty', async () => {
const key = '0x1234'
const val = '0x5678'
const trie = new Trie()
await trie.put(fromHexString(key), fromHexString(val))
expect(
await Lib_MerkleTrie.update(
key,
val,
'0x', // Doesn't require a proof
ethers.utils.keccak256('0x80') // Empty Merkle trie root hash
)
).to.equal(toHexString(trie.root))
})
}) })
describe('get', () => { describe('get', () => {
for (const nodeCount of NODE_COUNTS) { for (const nodeCount of NODE_COUNTS) {
describe(`inside a trie with ${nodeCount} nodes`, () => { describe(`inside a trie with ${nodeCount} nodes and keys/vals of size ${nodeCount} bytes`, () => {
let generator: TrieTestGenerator let generator: TrieTestGenerator
before(async () => { before(async () => {
generator = await TrieTestGenerator.fromRandom({ generator = await TrieTestGenerator.fromRandom({
seed: `seed.get.${nodeCount}`, seed: `seed.get.${nodeCount}`,
nodeCount, nodeCount,
secure: false, secure: false,
keySize: nodeCount,
valSize: nodeCount,
}) })
}) })
......
{
"source": "https://github.com/ethereum/tests/blob/develop/TrieTests/trieanyorder.json",
"commit": "7d66cbfff1e6561d1046e45df8b7918d186b136f",
"date": "2019-01-10",
"tests": {
"singleItem": {
"in": {
"A": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"root": "0xd23786fb4a010da3ce639d66d5e904a11dbc02746d1ce25029e53290cabf28ab"
},
"dogs": {
"in": {
"doe": "reindeer",
"dog": "puppy",
"dogglesworth": "cat"
},
"root": "0x8aad789dff2f538bca5d8ea56e8abe10f4c7ba3a5dea95fea4cd6e7c3a1168d3"
},
"puppy": {
"in": {
"do": "verb",
"horse": "stallion",
"doge": "coin",
"dog": "puppy"
},
"root": "0x5991bb8c6514148a29db676a14ac506cd2cd5775ace63c30a4fe457715e9ac84"
},
"foo": {
"in": {
"foo": "bar",
"food": "bass"
},
"root": "0x17beaa1648bafa633cda809c90c04af50fc8aed3cb40d16efbddee6fdf63c4c3"
},
"smallValues": {
"in": {
"be": "e",
"dog": "puppy",
"bed": "d"
},
"root": "0x3f67c7a47520f79faa29255d2d3c084a7a6df0453116ed7232ff10277a8be68b"
},
"testy": {
"in": {
"test": "test",
"te": "testy"
},
"root": "0x8452568af70d8d140f58d941338542f645fcca50094b20f3c3d8c3df49337928"
},
"hex": {
"in": {
"0x0045": "0x0123456789",
"0x4500": "0x9876543210"
},
"root": "0x285505fcabe84badc8aa310e2aae17eddc7d120aabec8a476902c8184b3a3503"
}
}
}
{
"source": "https://github.com/ethereum/tests/blob/develop/TrieTests/trietest.json",
"commit": "7d66cbfff1e6561d1046e45df8b7918d186b136f",
"date": "2019-01-10",
"tests": {
"insert-middle-leaf": {
"in": [
[ "key1aa", "0123456789012345678901234567890123456789xxx"],
[ "key1", "0123456789012345678901234567890123456789Very_Long"],
[ "key2bb", "aval3"],
[ "key2", "short"],
[ "key3cc", "aval3"],
[ "key3","1234567890123456789012345678901"]
],
"root": "0xcb65032e2f76c48b82b5c24b3db8f670ce73982869d38cd39a624f23d62a9e89"
},
"branch-value-update": {
"in": [
[ "abc", "123" ],
[ "abcd", "abcd" ],
[ "abc", "abc" ]
],
"root": "0x7a320748f780ad9ad5b0837302075ce0eeba6c26e3d8562c67ccc0f1b273298a"
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment