diff --git a/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/BTPMessageVerifier.java b/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/BTPMessageVerifier.java index db60d699..15c9ac54 100644 --- a/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/BTPMessageVerifier.java +++ b/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/BTPMessageVerifier.java @@ -18,6 +18,9 @@ import foundation.icon.btp.lib.BMV; import foundation.icon.btp.lib.BMVStatus; import foundation.icon.btp.lib.BTPAddress; +import foundation.icon.btp.lib.MTAException; +import foundation.icon.btp.lib.MerklePatriciaTree; +import foundation.icon.btp.lib.MerkleTreeAccumulator; import score.Address; import score.Context; import score.DictDB; @@ -48,9 +51,7 @@ public BTPMessageVerifier(Address bmc, BigInteger chainId, byte[] header, Context.require(config.isEpoch(head.getNumber()), "No epoch block"); verify(config, head); - MerkleTreeAccumulator mta = new MerkleTreeAccumulator(); - mta.setHeight(head.getNumber().longValue()); - mta.setOffset(head.getNumber().longValue()); + MerkleTreeAccumulator mta = new MerkleTreeAccumulator(head.getNumber().longValueExact()); mta.add(head.getHash().toBytes()); if (head.getNumber().compareTo(BigInteger.ZERO) == 0) { @@ -192,7 +193,7 @@ private Header handleBlockProof(BlockProof bp, MerkleTreeAccumulator mta) { try { mta.verify(bp.getWitness(), head.getHash().toBytes(), - head.getNumber().longValue()+1, bp.getHeight().intValue()); + head.getNumber().longValue(), bp.getHeight().intValue()); } catch (MTAException.InvalidWitnessOldException e) { throw BMVException.invalidBlockWitnessOld(e.getMessage()); } catch (MTAException e) { diff --git a/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/MTAException.java b/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/MTAException.java deleted file mode 100644 index e575d93c..00000000 --- a/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/MTAException.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2023 ICON Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package foundation.icon.btp.bmv.bsc; - -public class MTAException extends RuntimeException { - public MTAException(String message) { - super(message); - } - - public MTAException(String message, Throwable cause) { - super(message, cause); - } - - public static class InvalidWitnessOldException extends MTAException { - public InvalidWitnessOldException(String message) { - super(message); - } - } - - public static class InvalidWitnessNewerException extends MTAException { - public InvalidWitnessNewerException(String message) { - super(message); - } - } -} diff --git a/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/MerklePatriciaTree.java b/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/MerklePatriciaTree.java deleted file mode 100644 index cad0381c..00000000 --- a/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/MerklePatriciaTree.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright 2023 ICON Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package foundation.icon.btp.bmv.bsc; - -import foundation.icon.score.util.ArrayUtil; -import foundation.icon.score.util.StringUtil; -import score.ByteArrayObjectWriter; -import score.Context; -import score.ObjectReader; - -import java.util.Arrays; - -public class MerklePatriciaTree { - public static class MPTException extends RuntimeException { - public MPTException(String message) { - super(message); - } - - public MPTException(String message, Throwable cause) { - super(message, cause); - } - } - - public static byte[] encodeKey(Object key) { - ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLP"); - writer.write(key); - return writer.toByteArray(); - } - - public static byte[] prove(byte[] rootHash, byte[] key, byte[][] proofs) { - byte[] nibbles = bytesToNibbles(key, 0, null); - Node node = new Node(rootHash); - return node.prove(nibbles, proofs, 0); - } - - public static byte[] bytesToNibbles(byte[] bytes, int from, byte[] nibbles) { - int len = (bytes.length - from) * 2; - if (nibbles != null) { - len += nibbles.length; - } - byte[] ret = new byte[len]; - int j = 0; - if (nibbles != null) { - System.arraycopy(nibbles, 0, ret, 0, nibbles.length); - j = nibbles.length; - } - for (int i = from; i < bytes.length; i++) { - ret[j++] = (byte)(bytes[i] >> 4 & 0x0F); - ret[j++] = (byte)(bytes[i] & 0x0F); - } - return ret; - } - - public static class Node { - private byte[] hash; - private byte[] nibbles; - private Node[] children; - private byte[] serialized; - private byte[] data; - - public Node() {} - - public Node(byte[] hash) { - this.hash = hash; - } - - public static Node readObject(ObjectReader reader) { - Node obj = new Node(); - reader.beginList(); - Object[] arr = new Object[17]; - int i = 0; - while(reader.hasNext()) { - try { - arr[i] = reader.readByteArray(); - } catch (IllegalStateException e) { - if (i < 16) { - arr[i] = readObject(reader); - } else { - throw new MPTException("decode failure, branchNode.data required byte[]"); - } - } - i++; - } - reader.end(); - if (i == 2) { - if (arr[0] instanceof byte[] && arr[1] instanceof byte[]) { - byte[] header = (byte[])arr[0]; - int prefix = header[0] & 0xF0; - byte[] nibbles = null; - if ((prefix & 0x10) != 0) { - nibbles = new byte[]{(byte) (header[0] & 0x0F)}; - } - obj.nibbles = bytesToNibbles(header, 1, nibbles); - if ((prefix & 0x20) != 0) { - obj.data = (byte[])arr[1]; - } else { - Node node = new Node((byte[])arr[1]); - obj.children = new Node[]{node}; - } - } else { - throw new MPTException("decode failure, required byte[]"); - } - } else if (i == 17){ - obj.children = new Node[16]; - for(int j = 0; j < 16; j++) { - if (arr[j] instanceof Node) { - obj.children[j] = (Node) arr[j]; - } else if (arr[j] instanceof byte[]) { - byte[] bytes = (byte[]) arr[j]; - if (bytes.length > 0) { - obj.children[j] = new Node(bytes); - } - } else { - throw new MPTException("decode failure, required byte[] or Node"); - } - } - obj.data = (byte[])arr[16]; - } else { - throw new MPTException("decode failure, invalid list length "+i); - } - return obj; - } - - public static Node fromBytes(byte[] bytes) { - ObjectReader reader = Context.newByteArrayObjectReader("RLP", bytes); - return readObject(reader); - } - - public byte[] prove(byte[] nibbles, byte[][] proofs, int i) { - if (isHash()) { - byte[] serialized = proofs[i]; - byte[] hash = hash(serialized); - if (!Arrays.equals(this.hash, hash)) { - throw new MPTException("mismatch hash"); - } - Node node = Node.fromBytes(serialized); - node.hash = hash; - node.serialized = serialized; - return node.prove(nibbles, proofs, i+1); - } else if (isExtension()) { - int cnt = ArrayUtil.matchCount(this.nibbles, nibbles); - if (cnt < this.nibbles.length) { - throw new MPTException("mismatch nibbles on extension"); - } - return children[0].prove(Arrays.copyOfRange(nibbles, cnt, nibbles.length), proofs, i); - } else if (isBranch()) { - if(nibbles.length == 0) { - return data; - } else { - Node node = children[nibbles[0]]; - return node.prove(Arrays.copyOfRange(nibbles, 1, nibbles.length), proofs, i); - } - } else { - int cnt = ArrayUtil.matchCount(this.nibbles, nibbles); - if (cnt < nibbles.length) { - throw new MPTException("mismatch nibbles on leaf"); - } - return data; - } - } - - static byte[] hash(byte[] bytes) { - return Context.hash("keccak-256",bytes); - } - - private boolean isHash() { - return hash.length > 0 && serialized == null; - } - - private boolean isExtension() { - return children != null && children.length == 1; - } - - private boolean isBranch() { - return children != null && children.length == 16; - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder("Node{"); - sb.append("hash=").append(StringUtil.toString(hash)); - sb.append(", nibbles=").append(StringUtil.toString(nibbles)); - sb.append(", children=").append(StringUtil.toString(children)); - sb.append(", serialized=").append(StringUtil.toString(serialized)); - sb.append(", data=").append(StringUtil.toString(data)); - sb.append('}'); - return sb.toString(); - } - } - -} diff --git a/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/MerkleTreeAccumulator.java b/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/MerkleTreeAccumulator.java deleted file mode 100644 index 9309bca4..00000000 --- a/bmv/bsc/src/main/java/foundation/icon/btp/bmv/bsc/MerkleTreeAccumulator.java +++ /dev/null @@ -1,421 +0,0 @@ -/* - * Copyright 2023 ICON Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package foundation.icon.btp.bmv.bsc; - -import foundation.icon.score.util.StringUtil; -import score.ByteArrayObjectWriter; -import score.Context; -import score.ObjectReader; -import score.ObjectWriter; -import scorex.util.ArrayList; - -import java.util.Arrays; -import java.util.List; - -public class MerkleTreeAccumulator { - private static final int HASH_LEN = 32; - - private long height; - private byte[][] roots; - private long offset; - //optional reader.hasNext() - private Integer rootSize; - private Integer cacheSize; - private byte[][] cache; - private Boolean allowNewerWitness; - // - private Integer cacheIdx; - - public long getOffset() { - return offset; - } - - public void setOffset(long offset) { - this.offset = offset; - } - - public long getHeight() { - return height; - } - - public void setHeight(long height) { - this.height = height; - } - - public byte[][] getRoots() { - return roots; - } - - public void setRoots(byte[][] roots) { - this.roots = roots; - } - - public Integer getRootSize() { - return rootSize; - } - - public void setRootSize(Integer rootSize) { - this.rootSize = rootSize; - } - - public Integer getCacheSize() { - return cacheSize; - } - - public void setCacheSize(Integer cacheSize) { - this.cacheSize = cacheSize; - } - - public Integer getCacheIdx() { - return cacheIdx; - } - - public void setCacheIdx(Integer cacheIdx) { - this.cacheIdx = cacheIdx; - } - - public byte[][] getCache() { - return cache; - } - - public void setCache(byte[][] cache) { - this.cache = cache; - } - - public Boolean getAllowNewerWitness() { - return allowNewerWitness; - } - - public void setAllowNewerWitness(Boolean allowNewerWitness) { - this.allowNewerWitness = allowNewerWitness; - } - - public boolean isAllowNewerWitness() { - return allowNewerWitness != null && allowNewerWitness; - } - - private static byte[] concatAndHash(byte[] b1, byte[] b2) { - byte[] data = new byte[HASH_LEN * 2]; - System.arraycopy(b1, 0, data, 0, HASH_LEN); - System.arraycopy(b2, 0, data, HASH_LEN, HASH_LEN); - return Context.hash("sha3-256", data); - } - - private static void verify(byte[][] witness, int witnessLen, byte[] root, byte[] hash, long idx) { - for (int i = 0; i < witnessLen; i++) { - if (idx % 2 == 0) { - hash = concatAndHash(hash, witness[i]); - } else { - hash = concatAndHash(witness[i], hash); - } - idx = idx / 2; - } - if (!Arrays.equals(root, hash)) { - throw new MTAException("invalid witness"+ - ", root: "+StringUtil.toString(root) + ", hash: "+StringUtil.toString(hash)); - } - } - - public void verify(byte[][] witness, byte[] hash, long height, long at) { - if (this.height == at) { - byte[] root = getRoot(witness.length); - verify(witness, witness.length, root, hash, height - 1 - offset); - } else if (this.height < at) { - if (!isAllowNewerWitness()) { - throw new MTAException.InvalidWitnessNewerException("not allowed newer witness"); - } - if (this.height < height) { - throw new MTAException("given witness for newer node"); - } - int rootIdx = getRootIdxByHeight(height); - byte[] root = getRoot(rootIdx); - verify(witness, rootIdx, root, hash, height - 1 - offset); - } else { - // acc: new, wit: old - // rebuild witness is not supported, but able to verify by cache if enabled - if (isCacheEnabled() && (this.height - height - 1) < cacheSize) { - if (!hasCache(hash)) { - throw new MTAException("invalid old witness"); - } - } else { - throw new MTAException.InvalidWitnessOldException("not allowed old witness"); - } - } - } - - private int getRootIdxByHeight(long height) { - if (height <= offset) { - throw new MTAException("given height is out of range"); - } - long idx = height - 1 - offset; - int rootIdx = (roots == null ? 0 : roots.length) - 1; - while (rootIdx >= 0) { - if (roots[rootIdx] != null) { - long bitFlag = 1L << rootIdx; - if (idx < bitFlag) { - break; - } - idx -= bitFlag; - } - rootIdx--; - } - if (rootIdx < 0) { - throw new MTAException("given height is out of range"); - } - return rootIdx; - } - - private byte[] getRoot(int idx) { - if (idx < 0 || roots == null || idx >= roots.length) { - throw new MTAException("root idx is out of range"); - } else { - return roots[idx]; - } - } - - private void appendRoot(byte[] hash) { - int len = roots == null ? 0 : roots.length; - byte[][] roots = new byte[len + 1][]; - roots[len] = hash; - this.roots = roots; - } - - public boolean isRootSizeLimitEnabled() { - return rootSize != null && rootSize > 0; - } - - /** - * call after update rootSize - */ - public void ensureRoots() { - if (isRootSizeLimitEnabled() && rootSize < this.roots.length) { - byte[][] roots = new byte[rootSize][]; - int i = rootSize - 1; - int j = this.roots.length - 1; - while(i >= 0) { - roots[i--] = this.roots[j--]; - } - while(j >= 0) { - if (this.roots[j] != null) { - addOffset(j--); - } - } - this.roots = roots; - } - } - - public void add(byte[] hash) { - putCache(hash); - if (height == offset) { - appendRoot(hash); - } else { - boolean isAdded = false; - int len = roots == null ? 0 : roots.length; - int pruningIdx = (isRootSizeLimitEnabled() ? rootSize : 0) - 1; - for (int i = 0; i < len; i++) { - if (roots[i] == null) { - roots[i] = hash; - isAdded = true; - break; - } else { - if (i == pruningIdx) { - roots[i] = hash; - addOffset(i); - isAdded = true; - break; - } else { - hash = concatAndHash(roots[i], hash); - roots[i] = null; - } - } - } - if (!isAdded) { - appendRoot(hash); - } - } - height++; - } - - private void addOffset(int rootIdx) { - long offset = (long) StrictMath.pow(2, rootIdx); - this.offset += offset; - } - - public boolean isCacheEnabled() { - return cacheSize != null && cacheSize > 0; - } - - /** - * call after update cacheSize - */ - public void ensureCache() { - if (isCacheEnabled()) { - if (cache == null) { - cache = new byte[cacheSize][]; - cacheIdx = 0; - } else if (cache.length != cacheSize) { - byte[][] cache = new byte[cacheSize][]; - //copy this.cache to cache - int len = this.cache.length; - int src = this.cacheIdx; - int dst = 0; - for (int i = 0; i < len; i++) { - byte[] v = this.cache[src++]; - if (src >= len) { - src = 0; - } - if (v == null) { - continue; - } - cache[dst++] = v; - if (dst >= cacheSize) { - dst = 0; - break; - } - } - this.cache = cache; - this.cacheIdx = dst; - } - } else { - cache = null; - } - } - - private boolean hasCache(byte[] hash) { - if (isCacheEnabled()) { - for (byte[] v : cache) { - if (Arrays.equals(v, hash)) { - return true; - } - } - } - return false; - } - - private void putCache(byte[] hash) { - if (isCacheEnabled()) { - cache[cacheIdx++] = hash; - if (cacheIdx >= cache.length) { - cacheIdx = 0; - } - } - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder("MerkleTreeAccumulator{"); - sb.append("height=").append(height); - sb.append(", roots=").append(StringUtil.toString(roots)); - sb.append(", offset=").append(offset); - sb.append(", rootSize=").append(rootSize); - sb.append(", cacheSize=").append(cacheSize); - sb.append(", cache=").append(StringUtil.toString(cache)); - sb.append(", allowNewerWitness=").append(allowNewerWitness); - sb.append(", cacheIdx=").append(cacheIdx); - sb.append('}'); - return sb.toString(); - } - - - public static void writeObject(ObjectWriter writer, MerkleTreeAccumulator obj) { - obj.writeObject(writer); - } - - public static MerkleTreeAccumulator readObject(ObjectReader reader) { - MerkleTreeAccumulator obj = new MerkleTreeAccumulator(); - reader.beginList(); - obj.setHeight(reader.readLong()); - if (reader.beginNullableList()) { - byte[][] roots = null; - List rootsList = new ArrayList<>(); - while(reader.hasNext()) { - rootsList.add(reader.readNullable(byte[].class)); - } - roots = new byte[rootsList.size()][]; - for(int i=0; i cacheList = new ArrayList<>(); - while(reader.hasNext()) { - cacheList.add(reader.readNullable(byte[].class)); - } - cache = new byte[cacheList.size()][]; - for(int i=0; i> 4 & 0x0F); - ret[j++] = (byte)(bytes[i] & 0x0F); - } - return ret; - } - - public static class Node { - private byte[] hash; - private byte[] nibbles; - private Node[] children; - private byte[] serialized; - private byte[] data; - - public Node() {} - - public Node(byte[] hash) { - this.hash = hash; - } - - public static Node readObject(ObjectReader reader) { - Node obj = new Node(); - reader.beginList(); - Object[] arr = new Object[17]; - int i = 0; - while(reader.hasNext()) { - try { - arr[i] = reader.readByteArray(); - } catch (IllegalStateException e) { - if (i < 16) { - arr[i] = readObject(reader); - } else { - throw new MPTException("decode failure, branchNode.data required byte[]"); - } - } - i++; - } - reader.end(); - if (i == 2) { - if (arr[0] instanceof byte[] && arr[1] instanceof byte[]) { - byte[] header = (byte[])arr[0]; - int prefix = header[0] & 0xF0; - byte[] nibbles = null; - if ((prefix & 0x10) != 0) { - nibbles = new byte[]{(byte) (header[0] & 0x0F)}; - } - obj.nibbles = bytesToNibbles(header, 1, nibbles); - if ((prefix & 0x20) != 0) { - obj.data = (byte[])arr[1]; - } else { - Node node = new Node((byte[])arr[1]); - obj.children = new Node[]{node}; - } - } else { - throw new MPTException("decode failure, required byte[]"); - } - } else if (i == 17){ - obj.children = new Node[16]; - for(int j = 0; j < 16; j++) { - if (arr[j] instanceof Node) { - obj.children[j] = (Node) arr[j]; - } else if (arr[j] instanceof byte[]) { - byte[] bytes = (byte[]) arr[j]; - if (bytes.length > 0) { - obj.children[j] = new Node(bytes); - } - } else { - throw new MPTException("decode failure, required byte[] or Node"); - } - } - obj.data = (byte[])arr[16]; - } else { - throw new MPTException("decode failure, invalid list length "+i); - } - return obj; - } - - public static Node fromBytes(byte[] bytes) { - ObjectReader reader = Context.newByteArrayObjectReader("RLPn", bytes); - return readObject(reader); - } - - public byte[] prove(byte[] nibbles, byte[][] proofs, int i) { - if (isHash()) { - byte[] serialized = proofs[i]; - byte[] hash = hash(serialized); - if (!Arrays.equals(this.hash, hash)) { - throw new MPTException("mismatch hash"); - } - Node node = Node.fromBytes(serialized); - node.hash = hash; - node.serialized = serialized; - return node.prove(nibbles, proofs, i+1); - } else if (isExtension()) { - int cnt = ArrayUtil.matchCount(this.nibbles, nibbles); - if (cnt < this.nibbles.length) { - throw new MPTException("mismatch nibbles on extension"); - } - return children[0].prove(Arrays.copyOfRange(nibbles, cnt, nibbles.length), proofs, i); - } else if (isBranch()) { - if(nibbles.length == 0) { - return data; - } else { - Node node = children[nibbles[0]]; - return node.prove(Arrays.copyOfRange(nibbles, 1, nibbles.length), proofs, i); - } - } else { - int cnt = ArrayUtil.matchCount(this.nibbles, nibbles); - if (cnt < nibbles.length) { - throw new MPTException("mismatch nibbles on leaf"); - } - return data; - } - } - - static byte[] hash(byte[] bytes) { - return Context.hash("keccak-256",bytes); - } - - private boolean isHash() { - return hash.length > 0 && serialized == null; - } - - private boolean isExtension() { - return children != null && children.length == 1; - } - - private boolean isBranch() { - return children != null && children.length == 16; - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder("Node{"); - sb.append("hash=").append(StringUtil.toString(hash)); - sb.append(", nibbles=").append(StringUtil.toString(nibbles)); - sb.append(", children=").append(StringUtil.toString(children)); - sb.append(", serialized=").append(StringUtil.toString(serialized)); - sb.append(", data=").append(StringUtil.toString(data)); - sb.append('}'); - return sb.toString(); - } - } - -} diff --git a/bmv/icon/src/main/java/foundation/icon/btp/bmv/icon/BMVProperties.java b/bmv/icon/src/main/java/foundation/icon/btp/bmv/icon/BMVProperties.java index ed2178e8..d24b38c8 100644 --- a/bmv/icon/src/main/java/foundation/icon/btp/bmv/icon/BMVProperties.java +++ b/bmv/icon/src/main/java/foundation/icon/btp/bmv/icon/BMVProperties.java @@ -16,6 +16,7 @@ package foundation.icon.btp.bmv.icon; +import foundation.icon.btp.lib.MerkleTreeAccumulator; import score.*; public class BMVProperties { diff --git a/bmv/icon/src/main/java/foundation/icon/btp/bmv/icon/BTPMessageVerifier.java b/bmv/icon/src/main/java/foundation/icon/btp/bmv/icon/BTPMessageVerifier.java index 42fd6663..c20e60b5 100644 --- a/bmv/icon/src/main/java/foundation/icon/btp/bmv/icon/BTPMessageVerifier.java +++ b/bmv/icon/src/main/java/foundation/icon/btp/bmv/icon/BTPMessageVerifier.java @@ -19,9 +19,13 @@ import foundation.icon.btp.lib.BMV; import foundation.icon.btp.lib.BMVStatus; import foundation.icon.btp.lib.BTPAddress; +import foundation.icon.btp.lib.MTAException; +import foundation.icon.btp.lib.MerklePatriciaTree; +import foundation.icon.btp.lib.MerkleTreeAccumulator; import foundation.icon.score.util.Logger; import foundation.icon.score.util.StringUtil; import score.Address; +import score.ByteArrayObjectWriter; import score.Context; import score.VarDB; import score.annotation.External; @@ -33,29 +37,30 @@ public class BTPMessageVerifier implements BMV { private static final Logger logger = Logger.getLogger(BTPMessageVerifier.class); + private static final String SHA3_256 = "sha3-256"; private final VarDB properties = Context.newVarDB("properties", BMVProperties.class); - public BTPMessageVerifier(Address _bmc, String _net, String _validators, long _offset) { + public BTPMessageVerifier(Address _bmc, String _net, String _validators, byte[] _header) { BMVProperties properties = getProperties(); properties.setBmc(_bmc); properties.setNet(_net); Validators validators = Validators.fromString(_validators); + BlockHeader header = BlockHeader.fromBytes(_header); properties.setValidators(validators); if (properties.getLastHeight() == 0) { - properties.setLastHeight(_offset); + properties.setLastHeight(header.getHeight()); } if (properties.getMta() == null) { - MerkleTreeAccumulator mta = new MerkleTreeAccumulator(); - mta.setHeight(_offset); - mta.setOffset(_offset); + MerkleTreeAccumulator mta = new MerkleTreeAccumulator(header.getHeight()); + mta.add(hash(header.toBytes())); properties.setMta(mta); } setProperties(properties); } static byte[] hash(byte[] bytes) { - return Context.hash("sha3-256",bytes); + return Context.hash(SHA3_256, bytes); } static Address recoverAddress(byte[] msg, byte[] sig, boolean compressed) { @@ -144,11 +149,18 @@ public byte[][] handleRelayMessage(String _bmc, String _prev, BigInteger _seq, b return ret; } + private static byte[] encodeKey(Object value) { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + writer.write(value); + return writer.toByteArray(); + } + private Receipt proveReceiptProof(ReceiptProof receiptProof, byte[] receiptHash) { try { byte[] serializedReceipt = MerklePatriciaTree.prove( + SHA3_256, receiptHash, - MerklePatriciaTree.encodeKey(receiptProof.getIndex()), + encodeKey(receiptProof.getIndex()), receiptProof.getProofs().getProofs()); Receipt receipt = Receipt.fromBytes(serializedReceipt); byte[] eventLogsHash = receipt.getEventLogsHash(); @@ -158,8 +170,9 @@ private Receipt proveReceiptProof(ReceiptProof receiptProof, byte[] receiptHash) int i=0; for(MPTProof eventProof : eventProofs){ byte[] serializedEventLog = MerklePatriciaTree.prove( + SHA3_256, eventLogsHash, - MerklePatriciaTree.encodeKey(eventProof.getIndex()), + encodeKey(eventProof.getIndex()), eventProof.getProofs().getProofs()); EventLog eventLog = EventLog.fromBytes(serializedEventLog); eventLogs[i++] = eventLog; @@ -178,7 +191,7 @@ private Validators verifyBlockUpdates(BlockUpdate[] blockUpdates, MerkleTreeAccu for(BlockUpdate blockUpdate : blockUpdates) { BlockHeader blockHeader = blockUpdate.getBlockHeader(); long blockHeight = blockHeader.getHeight(); - long nextHeight = mta.getHeight() + 1; + long nextHeight = mta.getHeight(); if (nextHeight == blockHeight) { byte[] blockHash = hash(blockHeader.toBytes()); verifyVotes(blockUpdate.getVotes(), blockHeight, blockHash, validators); @@ -287,5 +300,4 @@ public BMVStatus getStatus() { mta.getOffset(), properties.getLastHeight()).toBytes()); return s; } - } diff --git a/bmv/icon/src/main/java/foundation/icon/btp/bmv/icon/MerklePatriciaTree.java b/bmv/icon/src/main/java/foundation/icon/btp/bmv/icon/MerklePatriciaTree.java deleted file mode 100644 index 36a00620..00000000 --- a/bmv/icon/src/main/java/foundation/icon/btp/bmv/icon/MerklePatriciaTree.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright 2021 ICON Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package foundation.icon.btp.bmv.icon; - -import foundation.icon.score.util.ArrayUtil; -import foundation.icon.score.util.StringUtil; -import score.ByteArrayObjectWriter; -import score.Context; -import score.ObjectReader; - -import java.util.Arrays; - -public class MerklePatriciaTree { - public static class MPTException extends RuntimeException { - public MPTException(String message) { - super(message); - } - - public MPTException(String message, Throwable cause) { - super(message, cause); - } - } - - public static byte[] encodeKey(Object key) { - ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); - writer.write(key); - return writer.toByteArray(); - } - - public static byte[] prove(byte[] rootHash, byte[] key, byte[][] proofs) { - byte[] nibbles = bytesToNibbles(key, 0, null); - Node node = new Node(rootHash); - return node.prove(nibbles, proofs, 0); - } - - public static byte[] bytesToNibbles(byte[] bytes, int from, byte[] nibbles) { - int len = (bytes.length - from) * 2; - if (nibbles != null) { - len += nibbles.length; - } - byte[] ret = new byte[len]; - int j = 0; - if (nibbles != null) { - System.arraycopy(nibbles, 0, ret, 0, nibbles.length); - j = nibbles.length; - } - for (int i = from; i < bytes.length; i++) { - ret[j++] = (byte)(bytes[i] >> 4 & 0x0F); - ret[j++] = (byte)(bytes[i] & 0x0F); - } - return ret; - } - - public static class Node { - private byte[] hash; - private byte[] nibbles; - private Node[] children; - private byte[] serialized; - private byte[] data; - - public Node() {} - - public Node(byte[] hash) { - this.hash = hash; - } - - public static Node readObject(ObjectReader reader) { - Node obj = new Node(); - reader.beginList(); - Object[] arr = new Object[17]; - int i = 0; - while(reader.hasNext()) { - try { - arr[i] = reader.readByteArray(); - } catch (IllegalStateException e) { - if (i < 16) { - arr[i] = readObject(reader); - } else { - throw new MPTException("decode failure, branchNode.data required byte[]"); - } - } - i++; - } - reader.end(); - if (i == 2) { - if (arr[0] instanceof byte[] && arr[1] instanceof byte[]) { - byte[] header = (byte[])arr[0]; - int prefix = header[0] & 0xF0; - byte[] nibbles = null; - if ((prefix & 0x10) != 0) { - nibbles = new byte[]{(byte) (header[0] & 0x0F)}; - } - obj.nibbles = bytesToNibbles(header, 1, nibbles); - if ((prefix & 0x20) != 0) { - obj.data = (byte[])arr[1]; - } else { - Node node = new Node((byte[])arr[1]); - obj.children = new Node[]{node}; - } - } else { - throw new MPTException("decode failure, required byte[]"); - } - } else if (i == 17){ - obj.children = new Node[16]; - for(int j = 0; j < 16; j++) { - if (arr[j] instanceof Node) { - obj.children[j] = (Node) arr[j]; - } else if (arr[j] instanceof byte[]) { - byte[] bytes = (byte[]) arr[j]; - if (bytes.length > 0) { - obj.children[j] = new Node(bytes); - } - } else { - throw new MPTException("decode failure, required byte[] or Node"); - } - } - obj.data = (byte[])arr[16]; - } else { - throw new MPTException("decode failure, invalid list length "+i); - } - return obj; - } - - public static Node fromBytes(byte[] bytes) { - ObjectReader reader = Context.newByteArrayObjectReader("RLPn", bytes); - return readObject(reader); - } - - public byte[] prove(byte[] nibbles, byte[][] proofs, int i) { - if (isHash()) { - byte[] serialized = proofs[i]; - byte[] hash = hash(serialized); - if (!Arrays.equals(this.hash, hash)) { - throw new MPTException("mismatch hash"); - } - Node node = Node.fromBytes(serialized); - node.hash = hash; - node.serialized = serialized; - return node.prove(nibbles, proofs, i+1); - } else if (isExtension()) { - int cnt = ArrayUtil.matchCount(this.nibbles, nibbles); - if (cnt < this.nibbles.length) { - throw new MPTException("mismatch nibbles on extension"); - } - return children[0].prove(Arrays.copyOfRange(nibbles, cnt, nibbles.length), proofs, i); - } else if (isBranch()) { - if(nibbles.length == 0) { - return data; - } else { - Node node = children[nibbles[0]]; - return node.prove(Arrays.copyOfRange(nibbles, 1, nibbles.length), proofs, i); - } - } else { - int cnt = ArrayUtil.matchCount(this.nibbles, nibbles); - if (cnt < nibbles.length) { - throw new MPTException("mismatch nibbles on leaf"); - } - return data; - } - } - - static byte[] hash(byte[] bytes) { - return Context.hash("sha3-256",bytes); - } - - private boolean isHash() { - return hash.length > 0 && serialized == null; - } - - private boolean isExtension() { - return children != null && children.length == 1; - } - - private boolean isBranch() { - return children != null && children.length == 16; - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder("Node{"); - sb.append("hash=").append(StringUtil.toString(hash)); - sb.append(", nibbles=").append(StringUtil.toString(nibbles)); - sb.append(", children=").append(StringUtil.toString(children)); - sb.append(", serialized=").append(StringUtil.toString(serialized)); - sb.append(", data=").append(StringUtil.toString(data)); - sb.append('}'); - return sb.toString(); - } - } - -} diff --git a/bmv/icon/src/main/java/foundation/icon/btp/bmv/icon/MerkleTreeAccumulator.java b/bmv/icon/src/main/java/foundation/icon/btp/bmv/icon/MerkleTreeAccumulator.java deleted file mode 100644 index 63ff2461..00000000 --- a/bmv/icon/src/main/java/foundation/icon/btp/bmv/icon/MerkleTreeAccumulator.java +++ /dev/null @@ -1,421 +0,0 @@ -/* - * Copyright 2021 ICON Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package foundation.icon.btp.bmv.icon; - -import foundation.icon.score.util.StringUtil; -import score.ByteArrayObjectWriter; -import score.Context; -import score.ObjectReader; -import score.ObjectWriter; -import scorex.util.ArrayList; - -import java.util.Arrays; -import java.util.List; - -public class MerkleTreeAccumulator { - private static final int HASH_LEN = 32; - - private long height; - private byte[][] roots; - private long offset; - //optional reader.hasNext() - private Integer rootSize; - private Integer cacheSize; - private byte[][] cache; - private Boolean allowNewerWitness; - // - private Integer cacheIdx; - - public long getOffset() { - return offset; - } - - public void setOffset(long offset) { - this.offset = offset; - } - - public long getHeight() { - return height; - } - - public void setHeight(long height) { - this.height = height; - } - - public byte[][] getRoots() { - return roots; - } - - public void setRoots(byte[][] roots) { - this.roots = roots; - } - - public Integer getRootSize() { - return rootSize; - } - - public void setRootSize(Integer rootSize) { - this.rootSize = rootSize; - } - - public Integer getCacheSize() { - return cacheSize; - } - - public void setCacheSize(Integer cacheSize) { - this.cacheSize = cacheSize; - } - - public Integer getCacheIdx() { - return cacheIdx; - } - - public void setCacheIdx(Integer cacheIdx) { - this.cacheIdx = cacheIdx; - } - - public byte[][] getCache() { - return cache; - } - - public void setCache(byte[][] cache) { - this.cache = cache; - } - - public Boolean getAllowNewerWitness() { - return allowNewerWitness; - } - - public void setAllowNewerWitness(Boolean allowNewerWitness) { - this.allowNewerWitness = allowNewerWitness; - } - - public boolean isAllowNewerWitness() { - return allowNewerWitness != null && allowNewerWitness; - } - - private static byte[] concatAndHash(byte[] b1, byte[] b2) { - byte[] data = new byte[HASH_LEN * 2]; - System.arraycopy(b1, 0, data, 0, HASH_LEN); - System.arraycopy(b2, 0, data, HASH_LEN, HASH_LEN); - return Context.hash("sha3-256", data); - } - - private static void verify(byte[][] witness, int witnessLen, byte[] root, byte[] hash, long idx) { - for (int i = 0; i < witnessLen; i++) { - if (idx % 2 == 0) { - hash = concatAndHash(hash, witness[i]); - } else { - hash = concatAndHash(witness[i], hash); - } - idx = idx / 2; - } - if (!Arrays.equals(root, hash)) { - throw new MTAException("invalid witness"+ - ", root: "+StringUtil.toString(root) + ", hash: "+StringUtil.toString(hash)); - } - } - - public void verify(byte[][] witness, byte[] hash, long height, long at) { - if (this.height == at) { - byte[] root = getRoot(witness.length); - verify(witness, witness.length, root, hash, height - 1 - offset); - } else if (this.height < at) { - if (!isAllowNewerWitness()) { - throw new MTAException.InvalidWitnessNewerException("not allowed newer witness"); - } - if (this.height < height) { - throw new MTAException("given witness for newer node"); - } - int rootIdx = getRootIdxByHeight(height); - byte[] root = getRoot(rootIdx); - verify(witness, rootIdx, root, hash, height - 1 - offset); - } else { - // acc: new, wit: old - // rebuild witness is not supported, but able to verify by cache if enabled - if (isCacheEnabled() && (this.height - height - 1) < cacheSize) { - if (!hasCache(hash)) { - throw new MTAException("invalid old witness"); - } - } else { - throw new MTAException.InvalidWitnessOldException("not allowed old witness"); - } - } - } - - private int getRootIdxByHeight(long height) { - if (height <= offset) { - throw new MTAException("given height is out of range"); - } - long idx = height - 1 - offset; - int rootIdx = (roots == null ? 0 : roots.length) - 1; - while (rootIdx >= 0) { - if (roots[rootIdx] != null) { - long bitFlag = 1L << rootIdx; - if (idx < bitFlag) { - break; - } - idx -= bitFlag; - } - rootIdx--; - } - if (rootIdx < 0) { - throw new MTAException("given height is out of range"); - } - return rootIdx; - } - - private byte[] getRoot(int idx) { - if (idx < 0 || roots == null || idx >= roots.length) { - throw new MTAException("root idx is out of range"); - } else { - return roots[idx]; - } - } - - private void appendRoot(byte[] hash) { - int len = roots == null ? 0 : roots.length; - byte[][] roots = new byte[len + 1][]; - roots[len] = hash; - this.roots = roots; - } - - public boolean isRootSizeLimitEnabled() { - return rootSize != null && rootSize > 0; - } - - /** - * call after update rootSize - */ - public void ensureRoots() { - if (isRootSizeLimitEnabled() && rootSize < this.roots.length) { - byte[][] roots = new byte[rootSize][]; - int i = rootSize - 1; - int j = this.roots.length - 1; - while(i >= 0) { - roots[i--] = this.roots[j--]; - } - while(j >= 0) { - if (this.roots[j] != null) { - addOffset(j--); - } - } - this.roots = roots; - } - } - - public void add(byte[] hash) { - putCache(hash); - if (height == offset) { - appendRoot(hash); - } else { - boolean isAdded = false; - int len = roots == null ? 0 : roots.length; - int pruningIdx = (isRootSizeLimitEnabled() ? rootSize : 0) - 1; - for (int i = 0; i < len; i++) { - if (roots[i] == null) { - roots[i] = hash; - isAdded = true; - break; - } else { - if (i == pruningIdx) { - roots[i] = hash; - addOffset(i); - isAdded = true; - break; - } else { - hash = concatAndHash(roots[i], hash); - roots[i] = null; - } - } - } - if (!isAdded) { - appendRoot(hash); - } - } - height++; - } - - private void addOffset(int rootIdx) { - long offset = (long) StrictMath.pow(2, rootIdx); - this.offset += offset; - } - - public boolean isCacheEnabled() { - return cacheSize != null && cacheSize > 0; - } - - /** - * call after update cacheSize - */ - public void ensureCache() { - if (isCacheEnabled()) { - if (cache == null) { - cache = new byte[cacheSize][]; - cacheIdx = 0; - } else if (cache.length != cacheSize) { - byte[][] cache = new byte[cacheSize][]; - //copy this.cache to cache - int len = this.cache.length; - int src = this.cacheIdx; - int dst = 0; - for (int i = 0; i < len; i++) { - byte[] v = this.cache[src++]; - if (src >= len) { - src = 0; - } - if (v == null) { - continue; - } - cache[dst++] = v; - if (dst >= cacheSize) { - dst = 0; - break; - } - } - this.cache = cache; - this.cacheIdx = dst; - } - } else { - cache = null; - } - } - - private boolean hasCache(byte[] hash) { - if (isCacheEnabled()) { - for (byte[] v : cache) { - if (Arrays.equals(v, hash)) { - return true; - } - } - } - return false; - } - - private void putCache(byte[] hash) { - if (isCacheEnabled()) { - cache[cacheIdx++] = hash; - if (cacheIdx >= cache.length) { - cacheIdx = 0; - } - } - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder("MerkleTreeAccumulator{"); - sb.append("height=").append(height); - sb.append(", roots=").append(StringUtil.toString(roots)); - sb.append(", offset=").append(offset); - sb.append(", rootSize=").append(rootSize); - sb.append(", cacheSize=").append(cacheSize); - sb.append(", cache=").append(StringUtil.toString(cache)); - sb.append(", allowNewerWitness=").append(allowNewerWitness); - sb.append(", cacheIdx=").append(cacheIdx); - sb.append('}'); - return sb.toString(); - } - - - public static void writeObject(ObjectWriter writer, MerkleTreeAccumulator obj) { - obj.writeObject(writer); - } - - public static MerkleTreeAccumulator readObject(ObjectReader reader) { - MerkleTreeAccumulator obj = new MerkleTreeAccumulator(); - reader.beginList(); - obj.setHeight(reader.readLong()); - if (reader.beginNullableList()) { - byte[][] roots = null; - List rootsList = new ArrayList<>(); - while(reader.hasNext()) { - rootsList.add(reader.readNullable(byte[].class)); - } - roots = new byte[rootsList.size()][]; - for(int i=0; i cacheList = new ArrayList<>(); - while(reader.hasNext()) { - cacheList.add(reader.readNullable(byte[].class)); - } - cache = new byte[cacheList.size()][]; - for(int i=0; i 0 && serialized == null; } diff --git a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/MerkleTreeAccumulator.java b/lib/src/main/java/foundation/icon/btp/lib/MerkleTreeAccumulator.java similarity index 57% rename from bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/MerkleTreeAccumulator.java rename to lib/src/main/java/foundation/icon/btp/lib/MerkleTreeAccumulator.java index 984376db..1710712e 100644 --- a/bmv/bsc2/src/main/java/foundation/icon/btp/bmv/bsc2/MerkleTreeAccumulator.java +++ b/lib/src/main/java/foundation/icon/btp/lib/MerkleTreeAccumulator.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 ICON Foundation + * Copyright 2021 ICON Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,10 @@ * limitations under the License. */ -package foundation.icon.btp.bmv.bsc2; +package foundation.icon.btp.lib; import foundation.icon.score.util.StringUtil; +import score.ByteArrayObjectWriter; import score.Context; import score.ObjectReader; import score.ObjectWriter; @@ -31,80 +32,25 @@ public class MerkleTreeAccumulator { private long height; private byte[][] roots; private long offset; - //optional reader.hasNext() private Integer rootSize; - private Integer cacheSize; - private byte[][] cache; - private Boolean allowNewerWitness; - // - private Integer cacheIdx; - public long getOffset() { - return offset; - } - - public void setOffset(long offset) { - this.offset = offset; + /** + * Constructor for decoding + */ + public MerkleTreeAccumulator() { } - public long getHeight() { - return height; - } - - public void setHeight(long height) { + public MerkleTreeAccumulator(long height) { this.height = height; + this.offset = height; } - public byte[][] getRoots() { - return roots; - } - - public void setRoots(byte[][] roots) { - this.roots = roots; - } - - public Integer getRootSize() { - return rootSize; - } - - public void setRootSize(Integer rootSize) { - this.rootSize = rootSize; - } - - public Integer getCacheSize() { - return cacheSize; - } - - public void setCacheSize(Integer cacheSize) { - this.cacheSize = cacheSize; - } - - public Integer getCacheIdx() { - return cacheIdx; - } - - public void setCacheIdx(Integer cacheIdx) { - this.cacheIdx = cacheIdx; - } - - public byte[][] getCache() { - return cache; - } - - public void setCache(byte[][] cache) { - this.cache = cache; - } - - public Boolean getAllowNewerWitness() { - return allowNewerWitness; - } - - public void setAllowNewerWitness(Boolean allowNewerWitness) { - this.allowNewerWitness = allowNewerWitness; + public long getOffset() { + return offset; } - public boolean isAllowNewerWitness() { - return allowNewerWitness != null && allowNewerWitness; + public long getHeight() { + return height; } private static byte[] concatAndHash(byte[] b1, byte[] b2) { @@ -132,35 +78,27 @@ private static void verify(byte[][] witness, int witnessLen, byte[] root, byte[] public void verify(byte[][] witness, byte[] hash, long height, long at) { if (this.height == at) { byte[] root = getRoot(witness.length); - verify(witness, witness.length, root, hash, height - 1 - offset); + verify(witness, witness.length, root, hash, height - offset); } else if (this.height < at) { - if (!isAllowNewerWitness()) { - throw new MTAException.InvalidWitnessNewerException("not allowed newer witness"); - } - if (this.height < height) { + if (this.height <= height) { throw new MTAException("given witness for newer node"); } + if (this.offset > height) { + throw new MTAException("not allowed old witness"); + } int rootIdx = getRootIdxByHeight(height); byte[] root = getRoot(rootIdx); - verify(witness, rootIdx, root, hash, height - 1 - offset); + verify(witness, rootIdx, root, hash, height - offset); } else { - // acc: new, wit: old - // rebuild witness is not supported, but able to verify by cache if enabled - if (isCacheEnabled() && (this.height - height - 1) < cacheSize) { - if (!hasCache(hash)) { - throw new MTAException("invalid old witness"); - } - } else { - throw new MTAException.InvalidWitnessOldException("not allowed old witness"); - } + throw new MTAException.InvalidWitnessOldException("not allowed old witness"); } } private int getRootIdxByHeight(long height) { - if (height <= offset) { + if (height < offset) { throw new MTAException("given height is out of range"); } - long idx = height - 1 - offset; + long idx = height - offset; int rootIdx = (roots == null ? 0 : roots.length) - 1; while (roots != null && rootIdx >= 0) { if (roots[rootIdx] != null) { @@ -197,8 +135,58 @@ public boolean isRootSizeLimitEnabled() { return rootSize != null && rootSize > 0; } + public void setRootSizeLimit(Integer size) { + this.setRootSizeLimit(size, false); + } + + /** + * Set root size limit + * @param size Root size limit(null for no limits). It limits size of roots by pruning old data. + * @param unsafe Set it as true for ignoring remaining tracked elements. Otherwise, + * It ensures that it tracks 2^(size-1) added elements even though it prunes old data. + * @throws IllegalArgumentException + */ + public void setRootSizeLimit(Integer size, boolean unsafe) { + if (size == null) { + this.rootSize = null; + } else { + if (size<1) { + throw new IllegalArgumentException("Invalid size value"); + } + this.updateRootsBySize(size, unsafe); + this.rootSize = size; + } + } + + /** + * Get root size limit + */ + public Integer getRootSizeLimit() { + return rootSize; + } + + private void updateRootsBySize(int rootSize, boolean unsafe) { + if (this.roots!= null && rootSize < this.roots.length) { + int size = this.roots.length; + long offset = this.offset; + while (size>rootSize) { + if (this.roots[size-1] != null) { + offset += (long)StrictMath.pow(2, size-1); + } + size -= 1; + } + if (!unsafe && roots[size-1] == null) { + throw new IllegalArgumentException("No way to keep required elements"); + } + while (size>0 && roots[size-1] == null) size--; + byte[][] roots = new byte[size][]; + System.arraycopy(this.roots, 0, roots, 0, size); + this.roots = roots; + this.offset = offset; + } + } + public void add(byte[] hash) { - putCache(hash); if (height == offset) { appendRoot(hash); } else { @@ -234,30 +222,6 @@ private void addOffset(int rootIdx) { this.offset += offset; } - public boolean isCacheEnabled() { - return cacheSize != null && cacheSize > 0; - } - - private boolean hasCache(byte[] hash) { - if (isCacheEnabled()) { - for (byte[] v : cache) { - if (Arrays.equals(v, hash)) { - return true; - } - } - } - return false; - } - - private void putCache(byte[] hash) { - if (isCacheEnabled()) { - cache[cacheIdx++] = hash; - if (cacheIdx >= cache.length) { - cacheIdx = 0; - } - } - } - @Override public String toString() { final StringBuilder sb = new StringBuilder("MerkleTreeAccumulator{"); @@ -265,10 +229,6 @@ public String toString() { sb.append(", roots=").append(StringUtil.toString(roots)); sb.append(", offset=").append(offset); sb.append(", rootSize=").append(rootSize); - sb.append(", cacheSize=").append(cacheSize); - sb.append(", cache=").append(StringUtil.toString(cache)); - sb.append(", allowNewerWitness=").append(allowNewerWitness); - sb.append(", cacheIdx=").append(cacheIdx); sb.append('}'); return sb.toString(); } @@ -281,48 +241,29 @@ public static void writeObject(ObjectWriter writer, MerkleTreeAccumulator obj) { public static MerkleTreeAccumulator readObject(ObjectReader reader) { MerkleTreeAccumulator obj = new MerkleTreeAccumulator(); reader.beginList(); - obj.setHeight(reader.readLong()); + obj.height = reader.readLong(); if (reader.beginNullableList()) { - byte[][] roots = null; List rootsList = new ArrayList<>(); while(reader.hasNext()) { rootsList.add(reader.readNullable(byte[].class)); } - roots = new byte[rootsList.size()][]; - for(int i=0; i cacheList = new ArrayList<>(); - while(reader.hasNext()) { - cacheList.add(reader.readNullable(byte[].class)); - } - cache = new byte[cacheList.size()][]; - for(int i=0; i 0) { + for (int i=0 ; i=cnt) { + throw new IllegalArgumentException("OffsetOutOfRange"); + } + return hashes[idx+offset]; + } + + private static byte[][] getProofAt(int height, int index) { + if (height > HASH_COUNT || index <0 || index >= height ) { + throw new IllegalArgumentException("InvalidHeightOrIndex"); + } + var proofs = new ArrayList(); + for (int i=0 ; (index|1)+1<=height ; i++, height/=2, index/=2) { + if (index%2 == 0) { + proofs.add(getHash(i, index+1)); + } else { + proofs.add(getHash(i, index-1)); + } + } + var ret = new byte[proofs.size()][]; + for (int i=0 ; i { + var proof = getProofAt(k+1, k); + mta.verify(proof, hashes[k], k, k+1); + }); + } + } + } + + @Test + public void testBasicOverflow() { + var mta = new MerkleTreeAccumulator(); + for (int i=0 ; i { + var proof = getProofAt(HASH_COUNT, 1); + mta.verify(proof, hashes[1], 1+OFFSET, OFFSET+HASH_COUNT); + }); + + Assertions.assertThrows(MTAException.class, () -> { + var proof = getProofAt(HASH_COUNT, 1); + mta.verify(proof, hashes[0], OFFSET, OFFSET+HASH_COUNT); + }); + + Assertions.assertThrows(MTAException.class, () -> { + var proof = getProofAt(HASH_COUNT, 1); + mta.verify(proof, hashes[0], OFFSET-1, OFFSET+HASH_COUNT+1); + }); + + Assertions.assertThrows(MTAException.class, () -> { + var proof = getProofAt(HASH_COUNT, 1); + mta.verify(proof, hashes[0], OFFSET-1, OFFSET); + }); + } + + @Test + public void testRootSizeLimit() { + var mta = new MerkleTreeAccumulator(); + mta.setRootSizeLimit(HASH_DEPTH); + for (int i=0 ; i { + mta.verify(proof, hashes[k], k, HASH_COUNT); + }); + } else { + Assertions.assertDoesNotThrow(() -> { + var proof2 = Arrays.copyOf(proof, proof.length-1); + mta.verify(proof2, hashes[k], k, HASH_COUNT); + }); + } + } + } + + private void testValuesWithReducedProofs(MerkleTreeAccumulator mta, int reduce) { + int mod = 1< { + mta.verify(proof, hashes[k], k, HASH_COUNT); + }); + Assertions.assertThrows(MTAException.class, () -> { + var proof2 = Arrays.copyOf(proof, proof.length-reduce); + mta.verify(proof2, hashes[k], k, HASH_COUNT); + }); + } else { + Assertions.assertDoesNotThrow(() -> { + var proof2 = Arrays.copyOf(proof, proof.length-reduce); + mta.verify(proof2, hashes[k], k, HASH_COUNT); + }); + } + } + } + + private static MerkleTreeAccumulator testSerializeDeserialize(MerkleTreeAccumulator mta) { + var bs = mta.toBytes(); + var mta2 = MerkleTreeAccumulator.fromBytes(bs); + Assertions.assertEquals(mta.toString(), mta2.toString()); + Assertions.assertArrayEquals(bs, mta2.toBytes()); + return mta2; + } + + @Test + public void testSetRootSizeOnTheFly() { + for (int l=0 ; l=(HASH_COUNT/4) && i%(HASH_COUNT/2) < (HASH_COUNT/4)) { + var mta2 = testSerializeDeserialize(mta); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + mta2.setRootSizeLimit(HASH_DEPTH - 1); + }); + Assertions.assertArrayEquals(mta2.toBytes(), mta.toBytes()); + mta.setRootSizeLimit(HASH_DEPTH-1, true); + } else { + mta.setRootSizeLimit(HASH_DEPTH-1); + } + } + testSerializeDeserialize(mta); + mta.add(hashes[i]); + } + var mta2 = testSerializeDeserialize(mta); + testValuesWithReducedProofs(mta2, 2); + } + } + + @Test + public void testSetGetRootSizeLimit() { + final int OFFSET = 10; + var mta = new MerkleTreeAccumulator(OFFSET); + Assertions.assertEquals(OFFSET, mta.getOffset()); + Assertions.assertNull(mta.getRootSizeLimit()); + + Assertions.assertThrows(IllegalArgumentException.class, ()-> { + mta.setRootSizeLimit(0); + }); + Assertions.assertThrows(IllegalArgumentException.class, ()-> { + mta.setRootSizeLimit(-1); + }); + + mta.setRootSizeLimit(4); + Assertions.assertEquals(Integer.valueOf(4), mta.getRootSizeLimit()); + + mta.setRootSizeLimit(null); + Assertions.assertNull(mta.getRootSizeLimit()); + } +}