diff --git a/circuits/transaction.circom b/circuits/transaction.circom index a66cc01..8d3b1fa 100644 --- a/circuits/transaction.circom +++ b/circuits/transaction.circom @@ -17,7 +17,6 @@ nullifier = hash(commitment, privKey, merklePath) // Universal JoinSplit transaction with nIns inputs and 2 outputs template Transaction(levels, nIns, nOuts, zeroLeaf) { signal input root; - signal input newRoot; // extAmount = external amount used for deposits and withdrawals // correct extAmount range is enforced on the smart contract // publicAmount = extAmount - fee @@ -37,7 +36,7 @@ template Transaction(levels, nIns, nOuts, zeroLeaf) { signal private input outAmount[nOuts]; signal private input outBlinding[nOuts]; signal private input outPubkey[nOuts]; - signal input outPathIndices; + signal private input outPathIndices; signal private input outPathElements[levels - 1]; component inKeypair[nIns]; @@ -118,17 +117,6 @@ template Transaction(levels, nIns, nOuts, zeroLeaf) { // verify amount invariant sumIns + publicAmount === sumOuts; - // Check merkle tree update with inserted transaction outputs - component treeUpdater = TreeUpdater(levels, 1 /* log2(nOuts) */, zeroLeaf); - treeUpdater.oldRoot <== root; - treeUpdater.newRoot <== newRoot; - for (var i = 0; i < nOuts; i++) { - treeUpdater.leaves[i] <== outputCommitment[i]; - } - treeUpdater.pathIndices <== outPathIndices; - for (var i = 0; i < levels - 1; i++) { - treeUpdater.pathElements[i] <== outPathElements[i]; - } - + // optional safety constraint to make sure extDataHash cannot be changed signal extDataSquare <== extDataHash * extDataHash; } diff --git a/contracts/MerkleTreeWithHistory.sol b/contracts/MerkleTreeWithHistory.sol new file mode 100644 index 0000000..2037347 --- /dev/null +++ b/contracts/MerkleTreeWithHistory.sol @@ -0,0 +1,159 @@ +// https://tornado.cash +/* + * d888888P dP a88888b. dP + * 88 88 d8' `88 88 + * 88 .d8888b. 88d888b. 88d888b. .d8888b. .d888b88 .d8888b. 88 .d8888b. .d8888b. 88d888b. + * 88 88' `88 88' `88 88' `88 88' `88 88' `88 88' `88 88 88' `88 Y8ooooo. 88' `88 + * 88 88. .88 88 88 88 88. .88 88. .88 88. .88 dP Y8. .88 88. .88 88 88 88 + * dP `88888P' dP dP dP `88888P8 `88888P8 `88888P' 88 Y88888P' `88888P8 `88888P' dP dP + * ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + */ + +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; + +import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; + +interface IHasher { + function poseidon(bytes32[2] calldata inputs) external pure returns (bytes32); +} + +contract MerkleTreeWithHistory is Initializable { + uint256 public constant FIELD_SIZE = 21888242871839275222246405745257275088548364400416034343698204186575808495617; + uint256 public constant ZERO_VALUE = 21663839004416932945382355908790599225266501822907911457504978515578255421292; // = keccak256("tornado") % FIELD_SIZE + + IHasher public immutable hasher; + uint32 public immutable levels; + + // the following variables are made public for easier testing and debugging and + // are not supposed to be accessed in regular code + + // filledSubtrees and roots could be bytes32[size], but using mappings makes it cheaper because + // it removes index range check on every interaction + mapping(uint256 => bytes32) public filledSubtrees; + mapping(uint256 => bytes32) public roots; + uint32 public constant ROOT_HISTORY_SIZE = 30; + uint32 public currentRootIndex = 0; // todo remove + uint32 public nextIndex = 0; + + constructor(uint32 _levels, address _hasher) { + require(_levels > 0, "_levels should be greater than zero"); + require(_levels < 31, "_levels should be less than 31"); + levels = _levels; + hasher = IHasher(_hasher); + } + + function initialize() external initializer { + for (uint32 i = 0; i < levels; i++) { + filledSubtrees[i] = zeros(i); + } + + roots[0] = zeros(levels); + } + + /** + @dev Hash 2 tree leaves, returns Poseidon(_left, _right) + */ + function hashLeftRight(bytes32 _left, bytes32 _right) public view returns (bytes32) { + require(uint256(_left) < FIELD_SIZE, "_left should be inside the field"); + require(uint256(_right) < FIELD_SIZE, "_right should be inside the field"); + bytes32[2] memory input; + input[0] = _left; + input[1] = _right; + return hasher.poseidon(input); + } + + // Modified to insert pairs of leaves for better efficiency + function _insert(bytes32 _leaf1, bytes32 _leaf2) internal returns (uint32 index) { + uint32 _nextIndex = nextIndex; + require(_nextIndex != uint32(2)**levels, "Merkle tree is full. No more leaves can be added"); + uint32 currentIndex = _nextIndex / 2; + bytes32 currentLevelHash = hashLeftRight(_leaf1, _leaf2); + bytes32 left; + bytes32 right; + + for (uint32 i = 1; i < levels; i++) { + if (currentIndex % 2 == 0) { + left = currentLevelHash; + right = zeros(i); + filledSubtrees[i] = currentLevelHash; + } else { + left = filledSubtrees[i]; + right = currentLevelHash; + } + currentLevelHash = hashLeftRight(left, right); + currentIndex /= 2; + } + + uint32 newRootIndex = (currentRootIndex + 1) % ROOT_HISTORY_SIZE; + currentRootIndex = newRootIndex; + roots[newRootIndex] = currentLevelHash; + nextIndex = _nextIndex + 2; + return _nextIndex; + } + + /** + @dev Whether the root is present in the root history + */ + function isKnownRoot(bytes32 _root) public view returns (bool) { + if (_root == 0) { + return false; + } + uint32 _currentRootIndex = currentRootIndex; + uint32 i = _currentRootIndex; + do { + if (_root == roots[i]) { + return true; + } + if (i == 0) { + i = ROOT_HISTORY_SIZE; + } + i--; + } while (i != _currentRootIndex); + return false; + } + + /** + @dev Returns the last root + */ + function getLastRoot() public view returns (bytes32) { + return roots[currentRootIndex]; + } + + /// @dev provides Zero (Empty) elements for a MiMC MerkleTree. Up to 32 levels + function zeros(uint256 i) public pure returns (bytes32) { + if (i == 0) return bytes32(0x2fe54c60d3acabf3343a35b6eba15db4821b340f76e741e2249685ed4899af6c); + else if (i == 1) return bytes32(0x1a332ca2cd2436bdc6796e6e4244ebf6f7e359868b7252e55342f766e4088082); + else if (i == 2) return bytes32(0x2fb19ac27499bdf9d7d3b387eff42b6d12bffbc6206e81d0ef0b0d6b24520ebd); + else if (i == 3) return bytes32(0x18d0d6e282d4eacbf18efc619a986db763b75095ed122fac7d4a49418daa42e1); + else if (i == 4) return bytes32(0x054dec40f76a0f5aaeff1a85a4a3721b92b4ad244362d30b0ef8ed7033de11d3); + else if (i == 5) return bytes32(0x1d24c91f8d40f1c2591edec19d392905cf5eb01eada48d71836177ef11aea5b2); + else if (i == 6) return bytes32(0x0fb63621cfc047eba2159faecfa55b120d7c81c0722633ef94e20e27675e378f); + else if (i == 7) return bytes32(0x277b08f214fe8c5504a79614cdec5abd7b6adc9133fe926398684c82fd798b44); + else if (i == 8) return bytes32(0x2633613437c1fd97f7c798e2ea30d52cfddee56d74f856a541320ae86ddaf2de); + else if (i == 9) return bytes32(0x00768963fa4b993fbfece3619bfaa3ca4afd7e3864f11b09a0849dbf4ad25807); + else if (i == 10) return bytes32(0x0e63ff9df484c1a21478bd27111763ef203177ec0a7ef3a3cd43ec909f587bb0); + else if (i == 11) return bytes32(0x0e6a4bfb0dd0ac8bf5517eaac48a95ba783dabe9f64494f9c892d3e8431eaab3); + else if (i == 12) return bytes32(0x0164a46b3ffff8baca00de7a130a63d105f1578076838502b99488505d5b3d35); + else if (i == 13) return bytes32(0x145a6f1521c02b250cc76eb35cd67c9b0b22473577de3778e4c51903836c8957); + else if (i == 14) return bytes32(0x29849fc5b55303a660bad33d986fd156d48516ec58a0f0a561a03b704a802254); + else if (i == 15) return bytes32(0x26639dd486b374e98ac6da34e8651b3fca58c51f1c2f857dd82045f27fc8dbe6); + else if (i == 16) return bytes32(0x2aa39214b887ee877e60afdb191390344c68177c30a0b8646649774174de5e33); + else if (i == 17) return bytes32(0x09b397d253e41a521d042ffe01f8c33ae37d4c7da21af68693aafb63d599d708); + else if (i == 18) return bytes32(0x02fbfd397ad901cea38553239aefec016fcb6a19899038503f04814cbb79a511); + else if (i == 19) return bytes32(0x266640a877ec97a91f6c95637f843eeac8718f53f311bac9cba7d958df646f9d); + else if (i == 20) return bytes32(0x29f9a0a07a22ab214d00aaa0190f54509e853f3119009baecb0035347606b0a9); + else if (i == 21) return bytes32(0x0a1fda67bffa0ab3a755f23fdcf922720820b6a96616a5ca34643cd0b935e3d6); + else if (i == 22) return bytes32(0x19507199eb76b5ec5abe538a01471d03efb6c6984739c77ec61ada2ba2afb389); + else if (i == 23) return bytes32(0x26bd93d26b751484942282e27acfb6d193537327a831df6927e19cdfc73c3e64); + else if (i == 24) return bytes32(0x2eb88a9c6b00a4bc6ea253268090fe1d255f6fe02d2eb745517723aae44d7386); + else if (i == 25) return bytes32(0x13e50d0bda78be97792df40273cbb16f0dc65c0697d81a82d07d0f6eee80a164); + else if (i == 26) return bytes32(0x2ea95776929000133246ff8d9fdcba179d0b262b9e910558309bac1c1ec03d7a); + else if (i == 27) return bytes32(0x1a640d6ef66e356c795396c0957b06a99891afe0c493f4d0bdfc0450764bae60); + else if (i == 28) return bytes32(0x2b17979f2c2048dd9e4ee5f482cced21435ea8cc54c32f80562e39a5016b0496); + else if (i == 29) return bytes32(0x29ba6a30de50542e261abfc7ee0c68911002d3acd4dd4c02ad59aa96805b20bb); + else if (i == 30) return bytes32(0x103fcf1c8a98ebe50285f6e669077a579308311fd44bb6895d5da7ba7fd3564e); + else if (i == 31) return bytes32(0x166bdd01780976e655f5278260c638dcf10fe7c136f37c9152cbcaabef901f4d); + else revert("Index out of bounds"); + } +} diff --git a/contracts/Mocks/MerkleTreeWithHistoryMock.sol b/contracts/Mocks/MerkleTreeWithHistoryMock.sol new file mode 100644 index 0000000..60eef68 --- /dev/null +++ b/contracts/Mocks/MerkleTreeWithHistoryMock.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; + +import "../MerkleTreeWithHistory.sol"; + +contract MerkleTreeWithHistoryMock is MerkleTreeWithHistory { + constructor(uint32 _levels, address _hasher) MerkleTreeWithHistory(_levels, _hasher) {} + + function insert(bytes32 _leaf1, bytes32 _leaf2) public returns (uint32 index) { + return _insert(_leaf1, _leaf2); + } +} diff --git a/contracts/TornadoPool.sol b/contracts/TornadoPool.sol index 98946f4..6dbf40e 100644 --- a/contracts/TornadoPool.sol +++ b/contracts/TornadoPool.sol @@ -12,26 +12,23 @@ pragma solidity ^0.7.0; pragma experimental ABIEncoderV2; -import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; +import "./MerkleTreeWithHistory.sol"; interface IVerifier { - function verifyProof(bytes memory _proof, uint256[9] memory _input) external view returns (bool); + function verifyProof(bytes memory _proof, uint256[7] memory _input) external view returns (bool); - function verifyProof(bytes memory _proof, uint256[23] memory _input) external view returns (bool); + function verifyProof(bytes memory _proof, uint256[21] memory _input) external view returns (bool); } interface ERC20 { function transfer(address to, uint256 value) external returns (bool); } -contract TornadoPool is Initializable { - uint256 public constant FIELD_SIZE = 21888242871839275222246405745257275088548364400416034343698204186575808495617; +contract TornadoPool is MerkleTreeWithHistory { int256 public constant MAX_EXT_AMOUNT = 2**248; uint256 public constant MAX_FEE = 2**248; mapping(bytes32 => bool) public nullifierHashes; - bytes32 public currentRoot; - uint256 public currentCommitmentIndex; IVerifier public immutable verifier2; IVerifier public immutable verifier16; @@ -47,10 +44,8 @@ contract TornadoPool is Initializable { struct Proof { bytes proof; bytes32 root; - bytes32 newRoot; bytes32[] inputNullifiers; bytes32[2] outputCommitments; - uint256 outPathIndices; uint256 publicAmount; bytes32 extDataHash; } @@ -70,28 +65,25 @@ contract TornadoPool is Initializable { @param _verifier2 the address of SNARK verifier for 2 inputs @param _verifier16 the address of SNARK verifier for 16 inputs */ - constructor(IVerifier _verifier2, IVerifier _verifier16) { + constructor( + IVerifier _verifier2, + IVerifier _verifier16, + uint32 _levels, + address _hasher + ) MerkleTreeWithHistory(_levels, _hasher) { verifier2 = _verifier2; verifier16 = _verifier16; } - function initialize(bytes32 _currentRoot) external initializer { - currentRoot = _currentRoot; - } - function transaction(Proof calldata _args, ExtData calldata _extData) public payable { - require(currentRoot == _args.root, "Invalid merkle root"); + require(isKnownRoot(_args.root), "Invalid merkle root"); for (uint256 i = 0; i < _args.inputNullifiers.length; i++) { require(!isSpent(_args.inputNullifiers[i]), "Input is already spent"); } require(uint256(_args.extDataHash) == uint256(keccak256(abi.encode(_extData))) % FIELD_SIZE, "Incorrect external data hash"); - uint256 cachedCommitmentIndex = currentCommitmentIndex; - require(_args.outPathIndices == cachedCommitmentIndex >> 1, "Invalid merkle tree insert position"); require(_args.publicAmount == calculatePublicAmount(_extData.extAmount, _extData.fee), "Invalid public amount"); require(verifyProof(_args), "Invalid transaction proof"); - currentRoot = _args.newRoot; - currentCommitmentIndex = cachedCommitmentIndex + 2; for (uint256 i = 0; i < _args.inputNullifiers.length; i++) { nullifierHashes[_args.inputNullifiers[i]] = true; } @@ -110,8 +102,9 @@ contract TornadoPool is Initializable { _transfer(_extData.relayer, _extData.fee); } - emit NewCommitment(_args.outputCommitments[0], cachedCommitmentIndex, _extData.encryptedOutput1); - emit NewCommitment(_args.outputCommitments[1], cachedCommitmentIndex + 1, _extData.encryptedOutput2); + _insert(_args.outputCommitments[0], _args.outputCommitments[1]); + emit NewCommitment(_args.outputCommitments[0], nextIndex - 2, _extData.encryptedOutput1); + emit NewCommitment(_args.outputCommitments[1], nextIndex - 1, _extData.encryptedOutput2); for (uint256 i = 0; i < _args.inputNullifiers.length; i++) { emit NewNullifier(_args.inputNullifiers[i]); } @@ -148,14 +141,12 @@ contract TornadoPool is Initializable { _args.proof, [ uint256(_args.root), - uint256(_args.newRoot), _args.publicAmount, uint256(_args.extDataHash), uint256(_args.inputNullifiers[0]), uint256(_args.inputNullifiers[1]), uint256(_args.outputCommitments[0]), - uint256(_args.outputCommitments[1]), - _args.outPathIndices + uint256(_args.outputCommitments[1]) ] ); } else if (_args.inputNullifiers.length == 16) { @@ -164,7 +155,6 @@ contract TornadoPool is Initializable { _args.proof, [ uint256(_args.root), - uint256(_args.newRoot), _args.publicAmount, uint256(_args.extDataHash), uint256(_args.inputNullifiers[0]), @@ -184,8 +174,7 @@ contract TornadoPool is Initializable { uint256(_args.inputNullifiers[14]), uint256(_args.inputNullifiers[15]), uint256(_args.outputCommitments[0]), - uint256(_args.outputCommitments[1]), - _args.outPathIndices + uint256(_args.outputCommitments[1]) ] ); } else { diff --git a/scripts/compileHasher.js b/scripts/compileHasher.js new file mode 100644 index 0000000..c978f3d --- /dev/null +++ b/scripts/compileHasher.js @@ -0,0 +1,22 @@ +// Generates Hasher artifact at compile-time using external compilermechanism +const path = require('path') +const fs = require('fs') +const genContract = require('circomlib/src/poseidon_gencontract.js') +const outputPath = path.join(__dirname, '..', 'artifacts', 'contracts') +const outputFile = path.join(outputPath, 'Hasher.json') + +if (!fs.existsSync(outputPath)) { + fs.mkdirSync(outputPath, { recursive: true }) +} + +const contract = { + _format: 'hh-sol-artifact-1', + sourceName: 'contracts/Hasher.sol', + linkReferences: {}, + deployedLinkReferences: {}, + contractName: 'Hasher', + abi: genContract.generateABI(2), + bytecode: genContract.createCode(2), +} + +fs.writeFileSync(outputFile, JSON.stringify(contract, null, 2)) diff --git a/src/index.js b/src/index.js index 80e461e..b2523f1 100644 --- a/src/index.js +++ b/src/index.js @@ -58,7 +58,6 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela const extDataHash = getExtDataHash(extData) let input = { root: oldRoot, - newRoot: tree.root(), inputNullifier: inputs.map((x) => x.getNullifier()), outputCommitment: outputs.map((x) => x.getCommitment()), publicAmount: BigNumber.from(extAmount).sub(fee).add(FIELD_SIZE).mod(FIELD_SIZE).toString(), @@ -84,10 +83,8 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela const args = { proof, root: toFixedHex(input.root), - newRoot: toFixedHex(input.newRoot), inputNullifiers: inputs.map((x) => toFixedHex(x.getNullifier())), outputCommitments: outputs.map((x) => toFixedHex(x.getCommitment())), - outPathIndices: toFixedHex(outputIndex >> outputBatchBits), publicAmount: toFixedHex(input.publicAmount), extDataHash: toFixedHex(extDataHash), } diff --git a/test/full.test.js b/test/full.test.js index e0c7eeb..cfe82bd 100644 --- a/test/full.test.js +++ b/test/full.test.js @@ -3,15 +3,13 @@ const { ethers, waffle } = hre const { loadFixture } = waffle const { expect } = require('chai') -const { poseidonHash2 } = require('../src/utils') +const { toFixedHex } = require('../src/utils') const Utxo = require('../src/utxo') - -const MERKLE_TREE_HEIGHT = 5 -const MerkleTree = require('fixed-merkle-tree') - const { transaction, registerAndTransact } = require('../src/index') const { Keypair } = require('../src/keypair') +const MERKLE_TREE_HEIGHT = 5 + describe('TornadoPool', function () { this.timeout(20000) @@ -22,14 +20,19 @@ describe('TornadoPool', function () { } async function fixture() { + require('../scripts/compileHasher') const verifier2 = await deploy('Verifier2') const verifier16 = await deploy('Verifier16') - - const tree = new MerkleTree(MERKLE_TREE_HEIGHT, [], { hashFunction: poseidonHash2 }) - + const hasher = await deploy('Hasher') /** @type {TornadoPool} */ - const tornadoPool = await deploy('TornadoPool', verifier2.address, verifier16.address) - await tornadoPool.initialize(tree.root()) + const tornadoPool = await deploy( + 'TornadoPool', + verifier2.address, + verifier16.address, + MERKLE_TREE_HEIGHT, + hasher.address, + ) + await tornadoPool.initialize() return { tornadoPool } } @@ -48,7 +51,7 @@ describe('TornadoPool', function () { const TornadoPool = await ethers.getContractFactory('TornadoPool') /** @type {TornadoPool} */ const tornadoPoolProxied = TornadoPool.attach(proxy.address) - await tornadoPoolProxied.initialize(await tornadoPool.currentRoot()) + await tornadoPoolProxied.initialize() return { tornadoPool: tornadoPoolProxied, proxy, gov, messenger } } diff --git a/test/tree.test.js b/test/tree.test.js new file mode 100644 index 0000000..62a303f --- /dev/null +++ b/test/tree.test.js @@ -0,0 +1,124 @@ +const hre = require('hardhat') +const { ethers, waffle } = hre +const { loadFixture } = waffle +const { expect } = require('chai') + +const { poseidonHash2, toFixedHex } = require('../src/utils') + +const MERKLE_TREE_HEIGHT = 5 +const MerkleTree = require('fixed-merkle-tree') + +describe('MerkleTreeWithHistory', function () { + this.timeout(20000) + + async function deploy(contractName, ...args) { + const Factory = await ethers.getContractFactory(contractName) + const instance = await Factory.deploy(...args) + return instance.deployed() + } + + function getNewTree() { + return new MerkleTree(MERKLE_TREE_HEIGHT, [], { hashFunction: poseidonHash2 }) + } + + async function fixture() { + require('../scripts/compileHasher') + const hasher = await deploy('Hasher') + const merkleTreeWithHistory = await deploy( + 'MerkleTreeWithHistoryMock', + MERKLE_TREE_HEIGHT, + hasher.address, + ) + await merkleTreeWithHistory.initialize() + return { hasher, merkleTreeWithHistory } + } + + // it('should return cloned tree in fixture', async () => { + // const { tree: tree1 } = await loadFixture(fixture) + // tree1.insert(1) + // const { tree: tree2 } = await loadFixture(fixture) + // expect(tree1.root()).to.not.equal(tree2.root()) + // }) + + describe('#constructor', () => { + it('should correctly hash 2 leaves', async () => { + const { hasher, merkleTreeWithHistory } = await loadFixture(fixture) + //console.log(hasher) + const hash0 = await merkleTreeWithHistory.hashLeftRight(toFixedHex(123), toFixedHex(456)) + // const hash1 = await hasher.poseidon([123, 456]) + const hash2 = poseidonHash2(123, 456) + expect(hash0).to.equal(hash2) + }) + + it('should initialize', async () => { + const { merkleTreeWithHistory } = await loadFixture(fixture) + const zeroValue = await merkleTreeWithHistory.ZERO_VALUE() + const firstSubtree = await merkleTreeWithHistory.filledSubtrees(0) + const firstZero = await merkleTreeWithHistory.zeros(0) + expect(firstSubtree).to.be.equal(zeroValue) + expect(firstZero).to.be.equal(zeroValue) + }) + + it('should have correct merkle root', async () => { + const { merkleTreeWithHistory } = await loadFixture(fixture) + const tree = getNewTree() + const contractRoot = await merkleTreeWithHistory.getLastRoot() + expect(tree.root()).to.equal(contractRoot) + }) + }) + + describe('#insert', () => { + it('should insert', async () => { + const { merkleTreeWithHistory } = await loadFixture(fixture) + const tree = getNewTree() + merkleTreeWithHistory.insert(toFixedHex(123), toFixedHex(456)) + tree.bulkInsert([123, 456]) + expect(tree.root()).to.be.be.equal(await merkleTreeWithHistory.getLastRoot()) + + merkleTreeWithHistory.insert(toFixedHex(678), toFixedHex(876)) + tree.bulkInsert([678, 876]) + expect(tree.root()).to.be.be.equal(await merkleTreeWithHistory.getLastRoot()) + }) + + it.skip('hasher gas', async () => { + const { hasher } = await loadFixture(fixture) + const gas = await hasher.estimateGas.poseidon([123, 456]) + console.log('hasher gas', gas - 21000) + }) + }) + + describe('#isKnownRoot', () => { + async function fixtureFilled() { + const { merkleTreeWithHistory, hasher } = await loadFixture(fixture) + await merkleTreeWithHistory.insert(toFixedHex(123), toFixedHex(456)) + return { merkleTreeWithHistory, hasher } + } + + it('should return last root', async () => { + const { merkleTreeWithHistory } = await fixtureFilled(fixture) + const tree = getNewTree() + tree.bulkInsert([123, 456]) + expect(await merkleTreeWithHistory.isKnownRoot(tree.root())).to.equal(true) + }) + + it('should return older root', async () => { + const { merkleTreeWithHistory } = await fixtureFilled(fixture) + const tree = getNewTree() + tree.bulkInsert([123, 456]) + await merkleTreeWithHistory.insert(toFixedHex(234), toFixedHex(432)) + expect(await merkleTreeWithHistory.isKnownRoot(tree.root())).to.equal(true) + }) + + it('should fail on unknown root', async () => { + const { merkleTreeWithHistory } = await fixtureFilled(fixture) + const tree = getNewTree() + tree.bulkInsert([456, 654]) + expect(await merkleTreeWithHistory.isKnownRoot(tree.root())).to.equal(false) + }) + + it('should not return uninitialized roots', async () => { + const { merkleTreeWithHistory } = await fixtureFilled(fixture) + expect(await merkleTreeWithHistory.isKnownRoot(toFixedHex(0))).to.equal(false) + }) + }) +})