diff --git a/src/main/java/org/unicitylabs/sdk/api/InclusionCertificate.java b/src/main/java/org/unicitylabs/sdk/api/InclusionCertificate.java index 324f0ee7..0ef8f403 100644 --- a/src/main/java/org/unicitylabs/sdk/api/InclusionCertificate.java +++ b/src/main/java/org/unicitylabs/sdk/api/InclusionCertificate.java @@ -3,15 +3,11 @@ import org.unicitylabs.sdk.crypto.hash.DataHash; import org.unicitylabs.sdk.crypto.hash.DataHasher; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; -import org.unicitylabs.sdk.smt.radix.FinalizedBranch; -import org.unicitylabs.sdk.smt.radix.FinalizedLeafBranch; import org.unicitylabs.sdk.smt.SparseMerkleTreePathUtils; -import org.unicitylabs.sdk.smt.radix.FinalizedNodeBranch; -import org.unicitylabs.sdk.util.BitString; +import org.unicitylabs.sdk.smt.radix.SparseMerkleTreeRootNode; import org.unicitylabs.sdk.util.HexConverter; import org.unicitylabs.sdk.util.LongConverter; -import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -30,35 +26,23 @@ private InclusionCertificate(byte[] bitmap, List siblings) { this.siblings = siblings; } - public static InclusionCertificate create(FinalizedNodeBranch root, byte[] key) { - FinalizedBranch node = root; + public static InclusionCertificate create(SparseMerkleTreeRootNode root, byte[] key) { + Objects.requireNonNull(root, "root cannot be null"); + Objects.requireNonNull(key, "key cannot be null"); + if (key.length != 32) { + throw new IllegalArgumentException("Key must be 32 bytes long."); + } ArrayList siblings = new ArrayList<>(); byte[] bitmap = new byte[InclusionCertificate.BITMAP_SIZE]; - BigInteger keyPath = BitString.fromBytesReversedLSB(key).toBigInteger(); - - while (node != null) { - if (node instanceof FinalizedLeafBranch) { - FinalizedLeafBranch leaf = (FinalizedLeafBranch) node; - if (!Arrays.equals(leaf.getKey(), key)) { - throw new RuntimeException(String.format("Leaf not found for key: %s", HexConverter.encode(key))); - } - - return new InclusionCertificate(bitmap, siblings); - } - FinalizedNodeBranch nodeBranch = (FinalizedNodeBranch) node; - boolean isRight = keyPath.testBit(nodeBranch.getDepth()); - FinalizedBranch sibling = isRight ? nodeBranch.getLeft() : nodeBranch.getRight(); - if (sibling != null) { - bitmap[nodeBranch.getDepth() / 8] |= (byte) (1 << nodeBranch.getDepth() % 8); - siblings.add(sibling.getHash()); - } - - node = isRight ? nodeBranch.getRight() : nodeBranch.getLeft(); + for (SparseMerkleTreeRootNode.Sibling sibling : root.getPath(key)) { + int depth = sibling.getDepth(); + bitmap[depth >> 3] |= (byte) (0x80 >> (depth & 7)); + siblings.add(sibling.getHash()); } - throw new RuntimeException("Could not construct inclusion certificate: Invalid path"); + return new InclusionCertificate(bitmap, siblings); } public static InclusionCertificate decode(byte[] bytes) { @@ -114,12 +98,9 @@ public boolean verify(StateId leafKey, DataHash leafValue, DataHash expectedRoot .update(value) .digest(); - BigInteger keyPath = BitString.fromBytesReversedLSB(key).toBigInteger(); - BigInteger bitmapPath = BitString.fromBytesReversedLSB(this.bitmap).toBigInteger(); - int position = this.siblings.size(); for (int depth = InclusionCertificate.MAX_DEPTH; depth >= 0; depth--) { - if (!bitmapPath.testBit(depth)) continue; + if (SparseMerkleTreePathUtils.getBitAtDepth(this.bitmap, depth) == 0) continue; position -= 1; if (position < 0) return false; @@ -127,7 +108,7 @@ public boolean verify(StateId leafKey, DataHash leafValue, DataHash expectedRoot DataHash sibling = this.siblings.get(position); byte[] left, right; - if (keyPath.testBit(depth)) { + if (SparseMerkleTreePathUtils.getBitAtDepth(key, depth) == 1) { left = sibling.getData(); right = hash.getData(); } else { @@ -138,7 +119,7 @@ public boolean verify(StateId leafKey, DataHash leafValue, DataHash expectedRoot hash = new DataHasher(HashAlgorithm.SHA256) .update(new byte[]{0x01}) .update(LongConverter.encode(depth)) - .update(SparseMerkleTreePathUtils.pathToRegion(keyPath, depth)) + .update(SparseMerkleTreePathUtils.regionFromKey(key, depth)) .update(left) .update(right) .digest(); diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java b/src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java index f3e41371..0356c53a 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java @@ -1,6 +1,7 @@ package org.unicitylabs.sdk.api.bft; import org.unicitylabs.sdk.api.NetworkId; +import org.unicitylabs.sdk.crypto.secp256k1.Signature; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer.CborTag; import org.unicitylabs.sdk.serializer.cbor.CborSerializationException; @@ -162,7 +163,7 @@ public static UnicitySeal fromCbor(byte[] bytes) { CborDeserializer.decodeMap(data.get(7)).stream() .map(entry -> new SignatureEntry( CborDeserializer.decodeTextString(entry.getKey()), - CborDeserializer.decodeByteString(entry.getValue()) + Signature.fromCbor(entry.getValue()) )) .collect(Collectors.toSet()) ); @@ -191,7 +192,7 @@ public byte[] toCbor() { signatures.stream() .map(entry -> new CborMap.Entry( CborSerializer.encodeTextString(entry.getKey()), - CborSerializer.encodeByteString(entry.getSignature()) + entry.getSignature().toCbor() ) ) .collect(Collectors.toSet()) @@ -258,9 +259,9 @@ public String toString() { public static final class SignatureEntry { private final String key; - private final byte[] signature; + private final Signature signature; - SignatureEntry(String key, byte[] signature) { + SignatureEntry(String key, Signature signature) { this.key = key; this.signature = signature; } @@ -269,8 +270,8 @@ public String getKey() { return this.key; } - public byte[] getSignature() { - return Arrays.copyOf(this.signature, this.signature.length); + public Signature getSignature() { + return this.signature; } @Override @@ -287,7 +288,7 @@ public int hashCode() { @Override public String toString() { - return String.format("SignatureEntry{key=%s, signature=%s}", this.key, HexConverter.encode(this.signature)); + return String.format("SignatureEntry{key=%s, signature=%s}", this.key, this.signature); } } } diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.java b/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.java index 1909fdd1..ca7fc90e 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.java @@ -6,12 +6,12 @@ import org.unicitylabs.sdk.crypto.hash.DataHash; import org.unicitylabs.sdk.crypto.hash.DataHasher; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.crypto.secp256k1.Signature; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; /** @@ -38,13 +38,13 @@ public static VerificationResult verify(RootTrustBase trustB int successful = 0; for (UnicitySeal.SignatureEntry entry : unicitySeal.getSignatures()) { String nodeId = entry.getKey(); - byte[] signature = entry.getSignature(); + Signature signature = entry.getSignature(); VerificationResult result = UnicitySealQuorumSignaturesVerificationRule.verifySignature( trustBase, nodeId, signature, - hash.getData() + hash ); results.add(result); @@ -73,8 +73,8 @@ public static VerificationResult verify(RootTrustBase trustB private static VerificationResult verifySignature( RootTrustBase trustBase, String nodeId, - byte[] signature, - byte[] hash + Signature signature, + DataHash hash ) { NodeInfo node = trustBase.getRootNode(nodeId); if (node == null) { @@ -87,7 +87,7 @@ private static VerificationResult verifySignature( if (!SigningService.verifyWithPublicKey( hash, - Arrays.copyOf(signature, signature.length - 1), + signature, node.getSigningKey() )) { return new VerificationResult<>( diff --git a/src/main/java/org/unicitylabs/sdk/crypto/secp256k1/SigningService.java b/src/main/java/org/unicitylabs/sdk/crypto/secp256k1/SigningService.java index 855b32d7..6112df5f 100644 --- a/src/main/java/org/unicitylabs/sdk/crypto/secp256k1/SigningService.java +++ b/src/main/java/org/unicitylabs/sdk/crypto/secp256k1/SigningService.java @@ -146,30 +146,30 @@ public Signature sign(DataHash hash) { * @return true if successful */ public boolean verify(DataHash hash, Signature signature) { - return verifyWithPublicKey(hash, signature.getBytes(), this.publicKey); + return SigningService.verifyWithPublicKey(hash, signature, this.publicKey); } /** - * Verify signature with public key. + * Verify secp256k1 signature against the given public key. * * @param hash data hash - * @param signature signature bytes + * @param signature compact signature bytes * @param publicKey public key * @return true if successful */ - public static boolean verifyWithPublicKey(DataHash hash, byte[] signature, byte[] publicKey) { - return SigningService.verifyWithPublicKey(hash.getData(), signature, publicKey); + public static boolean verify(DataHash hash, byte[] signature, byte[] publicKey) { + return SigningService.verify(hash.getData(), signature, publicKey); } /** - * Verify signature with public key and data hash bytes. + * Verify secp256k1 signature against the given public key and data hash bytes. * * @param hash hash bytes - * @param signature signature bytes + * @param signature compact signature bytes * @param publicKey public key * @return true if successful */ - public static boolean verifyWithPublicKey(byte[] hash, byte[] signature, byte[] publicKey) { + public static boolean verify(byte[] hash, byte[] signature, byte[] publicKey) { ECPoint pubPoint = EC_SPEC.getCurve().decodePoint(publicKey); ECPublicKeyParameters pubKey = new ECPublicKeyParameters(pubPoint, EC_DOMAIN_PARAMETERS); @@ -183,6 +183,25 @@ public static boolean verifyWithPublicKey(byte[] hash, byte[] signature, byte[] return verifier.verifySignature(hash, r, s); } + /** + * Verify a recoverable signature against an expected public key. Unlike {@link #verify(DataHash, + * byte[], byte[])}, this binds the signature's recovery byte: the public key is recovered from + * the signature and must equal {@code publicKey}, so a tampered recovery byte fails verification. + * + * @param hash data hash + * @param signature recoverable signature + * @param publicKey expected compressed public key + * @return true if the signature verifies and the recovered public key matches {@code publicKey} + */ + public static boolean verifyWithPublicKey(DataHash hash, Signature signature, byte[] publicKey) { + byte[] recoveredPublicKey = SigningService.recoverPublicKey(hash, signature); + if (recoveredPublicKey == null || !Arrays.equals(publicKey, recoveredPublicKey)) { + return false; + } + + return SigningService.verify(hash, signature.getBytes(), publicKey); + } + private byte[] toFixedLength(BigInteger value, int length) { byte[] bytes = value.toByteArray(); @@ -261,22 +280,43 @@ private static ECPoint decompressKey(BigInteger x, boolean ybit, ECCurve curve) } /** - * Verify signature with recovered public key - extract public key from signature. + * Recover the public key from the signature's recovery byte and verify the signature against + * {@code hash}. The recovered key defines the signer's identity; no expected key is supplied. * * @param hash data hash - * @param signature signature + * @param signature recoverable signature * @return true if successful */ - public static boolean verifySignatureWithRecoveredPublicKey(DataHash hash, Signature signature) { - // Extract r and s from signature - BigInteger r = new BigInteger(1, Arrays.copyOfRange(signature.getBytes(), 0, 32)); - BigInteger s = new BigInteger(1, Arrays.copyOfRange(signature.getBytes(), 32, 64)); - - ECPoint recovered = recoverFromSignature(signature.getRecovery(), r, s, hash.getData()); - if (recovered == null || !recovered.isValid()) { + public static boolean verifyWithRecoveredPublicKey(DataHash hash, Signature signature) { + byte[] recoveredPublicKey = SigningService.recoverPublicKey(hash, signature); + if (recoveredPublicKey == null) { return false; } - return verifyWithPublicKey(hash, signature.getBytes(), recovered.getEncoded(true)); + return SigningService.verify(hash, signature.getBytes(), recoveredPublicKey); + } + + /** + * Recover the compressed public key that produced {@code signature} over {@code hash}, using the + * signature's recovery byte. + * + * @param hash data hash + * @param signature recoverable signature + * @return recovered compressed public key, or {@code null} if the signature is not recoverable + */ + private static byte[] recoverPublicKey(DataHash hash, Signature signature) { + try { + BigInteger r = new BigInteger(1, Arrays.copyOfRange(signature.getBytes(), 0, 32)); + BigInteger s = new BigInteger(1, Arrays.copyOfRange(signature.getBytes(), 32, 64)); + + ECPoint recovered = recoverFromSignature(signature.getRecovery(), r, s, hash.getData()); + if (recovered == null || !recovered.isValid()) { + return null; + } + + return recovered.getEncoded(true); + } catch (Exception e) { + return null; + } } } \ No newline at end of file diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitAllocationProof.java b/src/main/java/org/unicitylabs/sdk/payment/SplitAllocationProof.java index c9c2df3b..8e12893c 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitAllocationProof.java +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitAllocationProof.java @@ -6,18 +6,12 @@ import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.serializer.cbor.CborSerializationException; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.smt.radixsum.FinalizedBranch; -import org.unicitylabs.sdk.smt.radixsum.FinalizedLeafBranch; -import org.unicitylabs.sdk.smt.radixsum.FinalizedNodeBranch; +import org.unicitylabs.sdk.smt.SparseMerkleTreePathUtils; import org.unicitylabs.sdk.smt.radixsum.SparseMerkleSumTreeRootNode; import org.unicitylabs.sdk.util.BigIntegerConverter; -import org.unicitylabs.sdk.util.BitString; -import org.unicitylabs.sdk.util.HexConverter; import java.math.BigInteger; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Objects; @@ -65,37 +59,11 @@ public static SplitAllocationProof create(SparseMerkleSumTreeRootNode root, byte throw new IllegalArgumentException("Key must be 32 bytes long."); } - BigInteger keyPath = BitString.fromBytesReversedLSB(key).toBigInteger(); List siblings = new ArrayList<>(); - - boolean isRight = keyPath.testBit(0); - FinalizedBranch sibling = isRight ? root.getLeft() : root.getRight(); - FinalizedBranch node = isRight ? root.getRight() : root.getLeft(); - if (sibling != null) { - siblings.add(new Sibling(0, sibling.getHash(), sibling.getValue())); + for (SparseMerkleSumTreeRootNode.Sibling sibling : root.getPath(key)) { + siblings.add(new Sibling(sibling.getDepth(), sibling.getHash(), sibling.getValue())); } - while (node instanceof FinalizedNodeBranch) { - FinalizedNodeBranch branch = (FinalizedNodeBranch) node; - isRight = keyPath.testBit(branch.getDepth()); - sibling = isRight ? branch.getLeft() : branch.getRight(); - node = isRight ? branch.getRight() : branch.getLeft(); - if (sibling != null) { - siblings.add(new Sibling(branch.getDepth(), sibling.getHash(), sibling.getValue())); - } - } - - if (!(node instanceof FinalizedLeafBranch)) { - throw new IllegalArgumentException( - "Could not construct split allocation proof: invalid path."); - } - - if (!Arrays.equals(((FinalizedLeafBranch) node).getKey(), key)) { - throw new IllegalArgumentException( - String.format("Leaf not found for key: %s", HexConverter.encode(key))); - } - - Collections.reverse(siblings); return new SplitAllocationProof(siblings); } @@ -170,8 +138,6 @@ public Root calculateRoot(byte[] key, byte[] data, BigInteger value) { throw new IllegalArgumentException("Data must be 32 bytes long."); } - BigInteger keyPath = BitString.fromBytesReversedLSB(key).toBigInteger(); - DataHash hash = new DataHasher(HashAlgorithm.SHA256) .update(new byte[]{0x10}) .update(key) @@ -186,7 +152,7 @@ public Root calculateRoot(byte[] key, byte[] data, BigInteger value) { throw new ArithmeticException("Reconstructed sum overflows 256 bits."); } - boolean isRight = keyPath.testBit(sibling.depth); + boolean isRight = SparseMerkleTreePathUtils.getBitAtDepth(key, sibling.depth) == 1; DataHash leftHash = isRight ? sibling.hash : hash; BigInteger leftValue = isRight ? sibling.sum : sum; DataHash rightHash = isRight ? hash : sibling.hash; diff --git a/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java b/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java index 9acb719d..e2b185f0 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java +++ b/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java @@ -9,8 +9,7 @@ import org.unicitylabs.sdk.payment.asset.PaymentAssetCollection; import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.builtin.BurnPredicate; -import org.unicitylabs.sdk.smt.BranchExistsException; -import org.unicitylabs.sdk.smt.LeafOutOfBoundsException; +import org.unicitylabs.sdk.smt.LeafExistsException; import org.unicitylabs.sdk.smt.radixsum.SparseMerkleSumTree; import org.unicitylabs.sdk.smt.radixsum.SparseMerkleSumTreeRootNode; import org.unicitylabs.sdk.transaction.StateMask; @@ -43,14 +42,13 @@ private TokenSplit() { * @param paymentDataDeserializer decoder for the source token's payment data * @param requests per-output mint requests; each carries its own payment data * @return burn predicate, burn transaction and split tokens ready to mint - * @throws LeafOutOfBoundsException if a leaf path is invalid for merkle tree insertion - * @throws BranchExistsException if duplicate branches are inserted into a merkle tree + * @throws LeafExistsException if duplicate leaves are inserted into a merkle tree */ public static SplitResult split( Token token, PaymentDataDeserializer paymentDataDeserializer, List requests - ) throws LeafOutOfBoundsException, BranchExistsException { + ) throws LeafExistsException { return TokenSplit.split(token, paymentDataDeserializer, requests, StateMask.generate()); } @@ -64,15 +62,14 @@ public static SplitResult split( * (re-buildable) split supply a deterministically derived mask so the identical burn * transaction can be reconstructed after a failure * @return burn predicate, burn transaction and split tokens ready to mint - * @throws LeafOutOfBoundsException if a leaf path is invalid for merkle tree insertion - * @throws BranchExistsException if duplicate branches are inserted into a merkle tree + * @throws LeafExistsException if duplicate leaves are inserted into a merkle tree */ public static SplitResult split( Token token, PaymentDataDeserializer paymentDataDeserializer, List requests, StateMask burnStateMask - ) throws LeafOutOfBoundsException, BranchExistsException { + ) throws LeafExistsException { Objects.requireNonNull(token, "Token cannot be null"); Objects.requireNonNull(paymentDataDeserializer, "Payment data deserializer cannot be null"); Objects.requireNonNull(requests, "Requests cannot be null"); diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java index ffb0222a..13d9125f 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java @@ -7,9 +7,7 @@ public enum BuiltInPredicateType { /** Predicate that locks state to a public key. */ SIGNATURE(0x01), /** Predicate that marks state as unspendable (burned). */ - BURN(0x02), - /** Predicate that references a Unicity identifier. */ - UNICITY_ID(0x100); + BURN(0x02); private final int id; diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/SignaturePredicateVerifier.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/SignaturePredicateVerifier.java index f3c0c543..5848c6e5 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/SignaturePredicateVerifier.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/SignaturePredicateVerifier.java @@ -3,11 +3,11 @@ import org.unicitylabs.sdk.crypto.hash.DataHash; import org.unicitylabs.sdk.crypto.hash.DataHasher; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; -import org.unicitylabs.sdk.crypto.secp256k1.Signature; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.builtin.BuiltInPredicateType; import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicateUnlockScript; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; @@ -32,8 +32,11 @@ public BuiltInPredicateType getType() { @Override public VerificationResult verify(EncodedPredicate encodedPredicate, DataHash sourceStateHash, - DataHash transactionHash, byte[] unlockScript) { + DataHash transactionHash, + byte[] unlockScriptBytes) { SignaturePredicate predicate = SignaturePredicate.fromPredicate(encodedPredicate); + SignaturePredicateUnlockScript unlockScript = + SignaturePredicateUnlockScript.decode(unlockScriptBytes); boolean result = SigningService.verifyWithPublicKey( new DataHasher(HashAlgorithm.SHA256) @@ -44,7 +47,7 @@ public VerificationResult verify(EncodedPredicate encodedPre ) ) .digest(), - Signature.decode(unlockScript).getBytes(), + unlockScript.getSignature(), predicate.getPublicKey() ); diff --git a/src/main/java/org/unicitylabs/sdk/smt/BranchExistsException.java b/src/main/java/org/unicitylabs/sdk/smt/BranchExistsException.java deleted file mode 100644 index c6c87aa9..00000000 --- a/src/main/java/org/unicitylabs/sdk/smt/BranchExistsException.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.unicitylabs.sdk.smt; - -/** - * Exception thrown when a branch already exists at a given path in the merkle tree. - */ -public class BranchExistsException extends Exception { - - /** - * Create exception indicating that a branch already exists at the specified path. - */ - public BranchExistsException() { - super("Branch already exists at this path."); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/smt/CommonPath.java b/src/main/java/org/unicitylabs/sdk/smt/CommonPath.java deleted file mode 100644 index 6a4f1a6f..00000000 --- a/src/main/java/org/unicitylabs/sdk/smt/CommonPath.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.unicitylabs.sdk.smt; - -import java.math.BigInteger; -import java.util.Objects; - -/** - * Common path for two nodes in a merkle tree. - */ -public class CommonPath { - - private final BigInteger path; - private final int length; - - CommonPath(BigInteger path, int length) { - this.path = path; - this.length = length; - } - - /** - * Get common path. - * - * @return common path - */ - public BigInteger getPath() { - return this.path; - } - - /** - * Get length of the common path. - * - * @return length of the common path - */ - public int getLength() { - return this.length; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof CommonPath)) { - return false; - } - CommonPath that = (CommonPath) o; - return length == that.length && Objects.equals(path, that.path); - } - - @Override - public int hashCode() { - return Objects.hash(path, length); - } - - /** - * Create common path for two paths. - * - * @param path1 first path - * @param path2 second path - * @return common path - */ - public static CommonPath create(BigInteger path1, BigInteger path2) { - BigInteger path = BigInteger.ONE; - BigInteger mask = BigInteger.ONE; - int length = 0; - - while (Objects.equals(path1.and(mask), path2.and(mask)) && path.compareTo(path1) < 0 - && path.compareTo(path2) < 0) { - mask = mask.shiftLeft(1); - length += 1; - path = mask.or(mask.subtract(BigInteger.ONE).and(path1)); - } - - return new CommonPath(path, length); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/smt/LeafExistsException.java b/src/main/java/org/unicitylabs/sdk/smt/LeafExistsException.java new file mode 100644 index 00000000..e4a21423 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/smt/LeafExistsException.java @@ -0,0 +1,15 @@ +package org.unicitylabs.sdk.smt; + +/** + * Exception thrown when a sparse Merkle tree insertion targets a key that is already present in the + * tree. + */ +public class LeafExistsException extends Exception { + + /** + * Create exception indicating that a leaf already exists for the inserted key. + */ + public LeafExistsException() { + super("Leaf already exists."); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/smt/LeafOutOfBoundsException.java b/src/main/java/org/unicitylabs/sdk/smt/LeafOutOfBoundsException.java deleted file mode 100644 index 1516ae5e..00000000 --- a/src/main/java/org/unicitylabs/sdk/smt/LeafOutOfBoundsException.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.unicitylabs.sdk.smt; - -/** - * Exception when leaf is out of bounds. - */ -public class LeafOutOfBoundsException extends Exception { - - /** - * Create exception. - */ - public LeafOutOfBoundsException() { - super("Cannot extend tree through leaf."); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/smt/SparseMerkleTreePathUtils.java b/src/main/java/org/unicitylabs/sdk/smt/SparseMerkleTreePathUtils.java index bf7eefae..0d778c82 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/SparseMerkleTreePathUtils.java +++ b/src/main/java/org/unicitylabs/sdk/smt/SparseMerkleTreePathUtils.java @@ -1,6 +1,5 @@ package org.unicitylabs.sdk.smt; -import java.math.BigInteger; import java.util.Objects; /** @@ -18,30 +17,79 @@ private SparseMerkleTreePathUtils() { } /** - * Region committed by an interior node: the `depth`-bit common prefix of all leaves in the node's - * sub-tree. The `i`th lowest bit of path will be the `i mod 8`th lowest bit in the `i div 8`th - * byte of the returned 32-byte array (so the packing is little-endian); the remaining bits of the array - * are set to zero. + * Length of the common big-endian bit prefix shared by keys {@code a} and {@code b}, capped at + * {@code maxDepth} (depth 0 is the most significant bit of byte 0). Used to find where a new key + * bifurcates from an existing branch: pass {@code 256} for a leaf (compare the whole key) or the + * node's depth for an interior branch (its stored region is only meaningful up to that depth). * - * @param path absolute node or key path + * @param a first key + * @param b second key + * @param maxDepth cap on the returned prefix length + * @return common-prefix bit length + * @throws NullPointerException if {@code a} or {@code b} is {@code null} + */ + public static int commonPrefixLength(byte[] a, byte[] b, int maxDepth) { + Objects.requireNonNull(a, "a cannot be null"); + Objects.requireNonNull(b, "b cannot be null"); + + int fullBytes = maxDepth / 8; + for (int i = 0; i < fullBytes; i++) { + if (a[i] != b[i]) { + return (i * 8) + Integer.numberOfLeadingZeros((a[i] ^ b[i]) & 0xff) - 24; + } + } + + int remainderBits = maxDepth % 8; + if (remainderBits > 0) { + int diff = (a[fullBytes] ^ b[fullBytes]) & (0xff << (8 - remainderBits)) & 0xff; + if (diff != 0) { + return (fullBytes * 8) + Integer.numberOfLeadingZeros(diff) - 24; + } + } + + return maxDepth; + } + + /** + * The key's first {@code depth} bits, with the remaining bits of the 32-byte array zeroed. + * + * @param key routing key * @param depth bifurcation depth of the node * @return 32-byte region - * @throws NullPointerException if {@code path} is {@code null} + * @throws NullPointerException if {@code key} is {@code null} */ - public static byte[] pathToRegion(BigInteger path, int depth) { - Objects.requireNonNull(path, "path cannot be null"); + public static byte[] regionFromKey(byte[] key, int depth) { + Objects.requireNonNull(key, "key cannot be null"); byte[] region = new byte[REGION_LENGTH]; int fullBytes = depth / 8; int remainderBits = depth % 8; - BigInteger bits = path; - for (int j = 0; j < fullBytes && j < REGION_LENGTH; j++, bits = bits.shiftRight(8)) { - region[j] = bits.byteValue(); - } - if (remainderBits > 0 && fullBytes < REGION_LENGTH) { - region[fullBytes] = (byte) (bits.byteValue() & ((1 << remainderBits) - 1)); + System.arraycopy(key, 0, region, 0, fullBytes); + if (remainderBits > 0) { + region[fullBytes] = (byte) (key[fullBytes] & (0xff << (8 - remainderBits))); } return region; } + + /** + * Big-endian bit of {@code data} at the given depth per the Yellowpaper: depth 0 is the most + * significant bit of {@code data[0]} ({@code data[0] & 0x80}) and depth 255 is the least + * significant bit of {@code data[31]}. + * + * @param data value to read a bit from + * @param depth bit index + * @return {@code 0} or {@code 1} + * @throws IllegalArgumentException if {@code depth} is out of bounds for {@code data} + */ + public static int getBitAtDepth(byte[] data, int depth) { + Objects.requireNonNull(data, "data cannot be null"); + if (depth < 0 || depth >= data.length * 8) { + throw new IllegalArgumentException( + String.format("Depth %d is out of bounds for a %d-byte value.", depth, data.length)); + } + int byteIndex = depth / 8; + int bitInByte = depth % 8; + return ((data[byteIndex] & 0xff) >> (7 - bitInByte)) & 1; + } } diff --git a/src/main/java/org/unicitylabs/sdk/smt/radix/Branch.java b/src/main/java/org/unicitylabs/sdk/smt/radix/Branch.java index 0b3beb09..e7a9f386 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/radix/Branch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/radix/Branch.java @@ -2,19 +2,25 @@ import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; -import java.math.BigInteger; - /** * Sparse merkle tree branch structure. */ -public interface Branch { +interface Branch { + + /** + * Get the absolute bifurcation depth of this branch. + * + * @return depth + */ + int getDepth(); /** - * Get branch path from leaf to root. + * Depth at which {@code key} diverges from this branch, capped at the branch's own depth. * - * @return path + * @param key key being inserted + * @return common-prefix depth */ - BigInteger getPath(); + int calculateSplitDepth(byte[] key); /** * Finalize current branch. diff --git a/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedBranch.java index 603090b1..55f9ac7f 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedBranch.java @@ -5,7 +5,7 @@ /** * Finalized branch in sparse merkle tree. */ -public interface FinalizedBranch extends Branch { +interface FinalizedBranch extends Branch { /** * Get hash of the branch. diff --git a/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedLeafBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedLeafBranch.java index 6e282c5a..109961a0 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedLeafBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedLeafBranch.java @@ -3,28 +3,33 @@ import org.unicitylabs.sdk.crypto.hash.DataHash; import org.unicitylabs.sdk.crypto.hash.DataHasher; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.smt.SparseMerkleTreePathUtils; -import java.math.BigInteger; import java.util.Arrays; import java.util.Objects; -public class FinalizedLeafBranch implements LeafBranch, FinalizedBranch { +class FinalizedLeafBranch implements LeafBranch, FinalizedBranch { - private final BigInteger path; private final byte[] key; private final byte[] value; + private final int depth; private final DataHash hash; - private FinalizedLeafBranch(BigInteger path, byte[] key, byte[] value, DataHash hash) { - this.path = path; - this.key = Arrays.copyOf(key, key.length); - this.value = Arrays.copyOf(value, value.length); + private FinalizedLeafBranch(byte[] key, byte[] value, DataHash hash) { + this.key = key; + this.value = value; + this.depth = key.length * 8; this.hash = hash; } @Override - public BigInteger getPath() { - return this.path; + public int getDepth() { + return this.depth; + } + + @Override + public int calculateSplitDepth(byte[] key) { + return SparseMerkleTreePathUtils.commonPrefixLength(key, this.key, this.depth); } @Override @@ -53,12 +58,12 @@ public boolean equals(Object o) { return false; } FinalizedLeafBranch that = (FinalizedLeafBranch) o; - return Objects.equals(this.path, that.path) && Arrays.equals(this.value, that.value); + return Arrays.equals(this.key, that.key) && Arrays.equals(this.value, that.value); } @Override public int hashCode() { - return Objects.hash(this.path, Arrays.hashCode(this.value)); + return Objects.hash(Arrays.hashCode(this.key), Arrays.hashCode(this.value)); } public static FinalizedLeafBranch fromPendingLeaf( @@ -75,6 +80,6 @@ public static FinalizedLeafBranch fromPendingLeaf( .update(value) .digest(); - return new FinalizedLeafBranch(leaf.getPath(), key, value, hash); + return new FinalizedLeafBranch(key, value, hash); } } diff --git a/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedNodeBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedNodeBranch.java index d12604e7..aaff293b 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedNodeBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedNodeBranch.java @@ -6,17 +6,17 @@ import org.unicitylabs.sdk.smt.SparseMerkleTreePathUtils; import org.unicitylabs.sdk.util.LongConverter; -import java.math.BigInteger; +import java.util.Arrays; -public class FinalizedNodeBranch implements NodeBranch, FinalizedBranch { - private final BigInteger path; +class FinalizedNodeBranch implements NodeBranch, FinalizedBranch { + private final byte[] path; private final int depth; private final FinalizedBranch left; private final FinalizedBranch right; private final DataHash hash; private FinalizedNodeBranch( - BigInteger path, + byte[] path, int depth, FinalizedBranch left, FinalizedBranch right, @@ -30,8 +30,8 @@ private FinalizedNodeBranch( } @Override - public BigInteger getPath() { - return this.path; + public byte[] getPath() { + return Arrays.copyOf(this.path, this.path.length); } @Override @@ -39,6 +39,11 @@ public int getDepth() { return this.depth; } + @Override + public int calculateSplitDepth(byte[] key) { + return SparseMerkleTreePathUtils.commonPrefixLength(key, this.path, this.depth); + } + @Override public FinalizedBranch getLeft() { return this.left; @@ -73,7 +78,7 @@ public static FinalizedNodeBranch fromPendingNode(HashAlgorithm hashAlgorithm, P DataHash hash = new DataHasher(hashAlgorithm) .update(new byte[]{0x01}) .update(LongConverter.encode(node.getDepth())) - .update(SparseMerkleTreePathUtils.pathToRegion(node.getPath(), node.getDepth())) + .update(node.getPath()) .update(left.getHash().getData()) .update(right.getHash().getData()) .digest(); @@ -81,6 +86,16 @@ public static FinalizedNodeBranch fromPendingNode(HashAlgorithm hashAlgorithm, P return new FinalizedNodeBranch(node.getPath(), node.getDepth(), left, right, hash); } + @Override + public PendingNodeBranch withLeftBranch(Branch left) { + return new PendingNodeBranch(this.path, this.depth, left, this.right); + } + + @Override + public PendingNodeBranch withRightBranch(Branch right) { + return new PendingNodeBranch(this.path, this.depth, this.left, right); + } + @Override public FinalizedNodeBranch finalize(HashAlgorithm hashAlgorithm) { return this; diff --git a/src/main/java/org/unicitylabs/sdk/smt/radix/LeafBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radix/LeafBranch.java index 4cda9b5d..ab8517dc 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/radix/LeafBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/radix/LeafBranch.java @@ -3,7 +3,7 @@ /** * Leaf branch in a sparse merkle tree. */ -public interface LeafBranch extends Branch { +interface LeafBranch extends Branch { byte[] getKey(); diff --git a/src/main/java/org/unicitylabs/sdk/smt/radix/NodeBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radix/NodeBranch.java index 109130fd..f0c4091f 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/radix/NodeBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/radix/NodeBranch.java @@ -3,9 +3,14 @@ /** * Node branch in merkle tree. */ -public interface NodeBranch extends Branch { +interface NodeBranch extends Branch { - int getDepth(); + /** + * Get the node's committed region: its {@code depth}-bit key prefix with the suffix zeroed. + * + * @return region + */ + byte[] getPath(); /** * Get left branch. @@ -20,4 +25,21 @@ public interface NodeBranch extends Branch { * @return right branch */ Branch getRight(); + + /** + * Derive a pending node with {@code left} as its left child, reusing this node's committed region. + * + * @param left replacement left child + * @return new pending node + */ + PendingNodeBranch withLeftBranch(Branch left); + + /** + * Derive a pending node with {@code right} as its right child, reusing this node's committed + * region. + * + * @param right replacement right child + * @return new pending node + */ + PendingNodeBranch withRightBranch(Branch right); } diff --git a/src/main/java/org/unicitylabs/sdk/smt/radix/PendingLeafBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radix/PendingLeafBranch.java index 3b4c9d1d..e447b4d9 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/radix/PendingLeafBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/radix/PendingLeafBranch.java @@ -1,25 +1,30 @@ package org.unicitylabs.sdk.smt.radix; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.smt.SparseMerkleTreePathUtils; -import java.math.BigInteger; import java.util.Arrays; import java.util.Objects; -public class PendingLeafBranch implements LeafBranch { - private final BigInteger path; +class PendingLeafBranch implements LeafBranch { private final byte[] key; private final byte[] value; + private final int depth; - public PendingLeafBranch(BigInteger path, byte[] key, byte[] value) { - this.path = path; - this.key = Arrays.copyOf(key, key.length); - this.value = Arrays.copyOf(value, value.length); + public PendingLeafBranch(byte[] key, byte[] value) { + this.key = key; + this.value = value; + this.depth = key.length * 8; } @Override - public BigInteger getPath() { - return this.path; + public int getDepth() { + return this.depth; + } + + @Override + public int calculateSplitDepth(byte[] key) { + return SparseMerkleTreePathUtils.commonPrefixLength(key, this.key, this.depth); } @Override @@ -44,11 +49,11 @@ public boolean equals(Object o) { } PendingLeafBranch that = (PendingLeafBranch) o; - return Objects.equals(this.path, that.path) && Objects.deepEquals(this.value, that.value); + return Arrays.equals(this.key, that.key) && Arrays.equals(this.value, that.value); } @Override public int hashCode() { - return Objects.hash(this.path, Arrays.hashCode(this.value)); + return Objects.hash(Arrays.hashCode(this.key), Arrays.hashCode(this.value)); } } diff --git a/src/main/java/org/unicitylabs/sdk/smt/radix/PendingNodeBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radix/PendingNodeBranch.java index 6c34f49c..523efca9 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/radix/PendingNodeBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/radix/PendingNodeBranch.java @@ -1,16 +1,17 @@ package org.unicitylabs.sdk.smt.radix; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.smt.SparseMerkleTreePathUtils; -import java.math.BigInteger; +import java.util.Arrays; -public class PendingNodeBranch implements NodeBranch { - private final BigInteger path; +class PendingNodeBranch implements NodeBranch { + private final byte[] path; private final int depth; private final Branch left; private final Branch right; - public PendingNodeBranch(BigInteger path, int depth, Branch left, Branch right) { + public PendingNodeBranch(byte[] path, int depth, Branch left, Branch right) { this.path = path; this.depth = depth; this.left = left; @@ -18,8 +19,8 @@ public PendingNodeBranch(BigInteger path, int depth, Branch left, Branch right) } @Override - public BigInteger getPath() { - return this.path; + public byte[] getPath() { + return Arrays.copyOf(this.path, this.path.length); } @Override @@ -27,6 +28,11 @@ public int getDepth() { return this.depth; } + @Override + public int calculateSplitDepth(byte[] key) { + return SparseMerkleTreePathUtils.commonPrefixLength(key, this.path, this.depth); + } + @Override public Branch getLeft() { return this.left; @@ -37,6 +43,16 @@ public Branch getRight() { return this.right; } + @Override + public PendingNodeBranch withLeftBranch(Branch left) { + return new PendingNodeBranch(this.path, this.depth, left, this.right); + } + + @Override + public PendingNodeBranch withRightBranch(Branch right) { + return new PendingNodeBranch(this.path, this.depth, this.left, right); + } + @Override public FinalizedNodeBranch finalize(HashAlgorithm hashAlgorithm) { return FinalizedNodeBranch.fromPendingNode( diff --git a/src/main/java/org/unicitylabs/sdk/smt/radix/SparseMerkleTree.java b/src/main/java/org/unicitylabs/sdk/smt/radix/SparseMerkleTree.java index f86c0414..ad4e83c0 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/radix/SparseMerkleTree.java +++ b/src/main/java/org/unicitylabs/sdk/smt/radix/SparseMerkleTree.java @@ -1,12 +1,10 @@ package org.unicitylabs.sdk.smt.radix; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; -import org.unicitylabs.sdk.smt.BranchExistsException; -import org.unicitylabs.sdk.smt.CommonPath; -import org.unicitylabs.sdk.smt.LeafOutOfBoundsException; -import org.unicitylabs.sdk.util.BitString; +import org.unicitylabs.sdk.smt.LeafExistsException; +import org.unicitylabs.sdk.smt.SparseMerkleTreePathUtils; -import java.math.BigInteger; +import java.util.Arrays; import java.util.Objects; /** @@ -33,12 +31,10 @@ public SparseMerkleTree(HashAlgorithm hashAlgorithm) { * * @param key path of the leaf; must be a 32-byte key * @param data data of the leaf; must be 32 bytes - * @throws BranchExistsException if branch already exists at the path - * @throws LeafOutOfBoundsException if leaf is out of bounds - * @throws IllegalArgumentException if the key or data is not 32 bytes, or the path is less than 1 + * @throws LeafExistsException if a leaf already exists for the key + * @throws IllegalArgumentException if the key or data is not 32 bytes */ - public synchronized void addLeaf(byte[] key, byte[] data) - throws BranchExistsException, LeafOutOfBoundsException { + public synchronized void addLeaf(byte[] key, byte[] data) throws LeafExistsException { Objects.requireNonNull(key, "key cannot be null"); Objects.requireNonNull(data, "data cannot be null"); @@ -50,17 +46,14 @@ public synchronized void addLeaf(byte[] key, byte[] data) throw new IllegalArgumentException("Data must be 32 bytes long."); } - BigInteger path = BitString.fromBytesReversedLSB(key).toBigInteger(); + key = Arrays.copyOf(key, key.length); + data = Arrays.copyOf(data, data.length); - if (path.compareTo(BigInteger.ONE) <= 0) { - throw new IllegalArgumentException("Path must be greater than 0"); - } - - boolean isRight = path.testBit(0); + boolean isRight = SparseMerkleTreePathUtils.getBitAtDepth(key, 0) == 1; Branch branch = isRight ? this.right : this.left; Branch result = branch != null - ? SparseMerkleTree.buildTree(branch, path, key, data) - : new PendingLeafBranch(path, key, data); + ? SparseMerkleTree.buildTree(branch, key, data) + : new PendingLeafBranch(key, data); if (isRight) { this.right = result; @@ -74,52 +67,48 @@ public synchronized void addLeaf(byte[] key, byte[] data) * * @return root node and its state */ - public synchronized FinalizedNodeBranch calculateRoot() { + public synchronized SparseMerkleTreeRootNode calculateRoot() { FinalizedBranch left = this.left != null ? this.left.finalize(this.hashAlgorithm) : null; FinalizedBranch right = this.right != null ? this.right.finalize(this.hashAlgorithm) : null; this.left = left; this.right = right; - return new PendingNodeBranch(BigInteger.ONE, 0, left, right).finalize(hashAlgorithm); + FinalizedNodeBranch root = new PendingNodeBranch(new byte[32], 0, left, right) + .finalize(hashAlgorithm); + return SparseMerkleTreeRootNode.create(root); } - private static Branch buildTree(Branch branch, BigInteger keyPath, byte[] key, byte[] value) - throws BranchExistsException, LeafOutOfBoundsException { - CommonPath commonPath = CommonPath.create(keyPath, branch.getPath()); - boolean isRight = keyPath.shiftRight(commonPath.getLength()).testBit(0); - - if (commonPath.getPath().equals(keyPath)) { - throw new BranchExistsException(); - } - + private static Branch buildTree(Branch branch, byte[] key, byte[] value) + throws LeafExistsException { if (branch instanceof LeafBranch) { - if (commonPath.getPath().equals(branch.getPath())) { - throw new LeafOutOfBoundsException(); + int depth = branch.calculateSplitDepth(key); + if (depth == branch.getDepth()) { + throw new LeafExistsException(); } - LeafBranch newBranch = new PendingLeafBranch(keyPath, key, value); - return new PendingNodeBranch(commonPath.getPath(), commonPath.getLength(), + boolean isRight = SparseMerkleTreePathUtils.getBitAtDepth(key, depth) == 1; + LeafBranch newBranch = new PendingLeafBranch(key, value); + return new PendingNodeBranch(SparseMerkleTreePathUtils.regionFromKey(key, depth), depth, isRight ? branch : newBranch, isRight ? newBranch : branch); } NodeBranch nodeBranch = (NodeBranch) branch; + int depth = branch.calculateSplitDepth(key); + boolean isRight = SparseMerkleTreePathUtils.getBitAtDepth(key, depth) == 1; // if node branch is split in the middle - if (!commonPath.getPath().equals(branch.getPath())) { - LeafBranch newBranch = new PendingLeafBranch(keyPath, key, value); - return new PendingNodeBranch(commonPath.getPath(), commonPath.getLength(), + if (depth < branch.getDepth()) { + LeafBranch newBranch = new PendingLeafBranch(key, value); + return new PendingNodeBranch(SparseMerkleTreePathUtils.regionFromKey(key, depth), depth, isRight ? branch : newBranch, isRight ? newBranch : branch); } if (isRight) { - return new PendingNodeBranch(nodeBranch.getPath(), nodeBranch.getDepth(), - nodeBranch.getLeft(), - SparseMerkleTree.buildTree(nodeBranch.getRight(), keyPath, key, value)); + return nodeBranch.withRightBranch( + SparseMerkleTree.buildTree(nodeBranch.getRight(), key, value)); } - return new PendingNodeBranch(nodeBranch.getPath(), nodeBranch.getDepth(), - SparseMerkleTree.buildTree(nodeBranch.getLeft(), keyPath, key, value), - nodeBranch.getRight()); + return nodeBranch.withLeftBranch( + SparseMerkleTree.buildTree(nodeBranch.getLeft(), key, value)); } } - diff --git a/src/main/java/org/unicitylabs/sdk/smt/radix/SparseMerkleTreeRootNode.java b/src/main/java/org/unicitylabs/sdk/smt/radix/SparseMerkleTreeRootNode.java new file mode 100644 index 00000000..df657d81 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/smt/radix/SparseMerkleTreeRootNode.java @@ -0,0 +1,104 @@ +package org.unicitylabs.sdk.smt.radix; + +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.smt.SparseMerkleTreePathUtils; +import org.unicitylabs.sdk.util.HexConverter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Root of a radix sparse Merkle tree: the committed root hash plus the ability to enumerate the + * root-to-leaf sibling path for a key. The tree's branch nodes are an internal detail of this + * package; callers see only the root hash and the sibling path. + */ +public class SparseMerkleTreeRootNode { + + private final FinalizedNodeBranch root; + + private SparseMerkleTreeRootNode(FinalizedNodeBranch root) { + this.root = root; + } + + static SparseMerkleTreeRootNode create(FinalizedNodeBranch root) { + return new SparseMerkleTreeRootNode(root); + } + + /** + * Get the root hash. + * + * @return root hash + */ + public DataHash getHash() { + return this.root.getHash(); + } + + /** + * Enumerate the sibling entries on the path from the root to the leaf with the given key, ordered + * from the root down to the leaf. + * + * @param key 32-byte leaf key + * @return sibling entries from the root down to the leaf + * @throws IllegalArgumentException if the key is not present in the tree + */ + public List getPath(byte[] key) { + Objects.requireNonNull(key, "key cannot be null"); + + List siblings = new ArrayList<>(); + FinalizedBranch node = this.root; + while (node != null) { + if (node instanceof FinalizedLeafBranch) { + if (!Arrays.equals(((FinalizedLeafBranch) node).getKey(), key)) { + throw new IllegalArgumentException( + String.format("Leaf not found for key: %s", HexConverter.encode(key))); + } + return siblings; + } + + FinalizedNodeBranch branch = (FinalizedNodeBranch) node; + int depth = branch.getDepth(); + boolean isRight = SparseMerkleTreePathUtils.getBitAtDepth(key, depth) == 1; + FinalizedBranch sibling = isRight ? branch.getLeft() : branch.getRight(); + if (sibling != null) { + siblings.add(new Sibling(depth, sibling.getHash())); + } + node = isRight ? branch.getRight() : branch.getLeft(); + } + + throw new IllegalArgumentException( + String.format("Leaf not found for key: %s", HexConverter.encode(key))); + } + + /** + * One sibling entry of a radix sparse Merkle tree inclusion path. + */ + public static final class Sibling { + private final int depth; + private final DataHash hash; + + private Sibling(int depth, DataHash hash) { + this.depth = depth; + this.hash = hash; + } + + /** + * Get the bifurcation depth at which this sibling hangs. + * + * @return depth + */ + public int getDepth() { + return this.depth; + } + + /** + * Get the sibling subtree hash. + * + * @return hash + */ + public DataHash getHash() { + return this.hash; + } + } +} diff --git a/src/main/java/org/unicitylabs/sdk/smt/radixsum/Branch.java b/src/main/java/org/unicitylabs/sdk/smt/radixsum/Branch.java index 0b1b18de..b5d24329 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/radixsum/Branch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/radixsum/Branch.java @@ -2,19 +2,25 @@ import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; -import java.math.BigInteger; - /** * Radix sparse Merkle sum tree branch structure. */ -public interface Branch { +interface Branch { + + /** + * Get the absolute bifurcation depth of this branch. + * + * @return depth + */ + int getDepth(); /** - * Get branch path from leaf to root. + * Depth at which {@code key} diverges from this branch, capped at the branch's own depth. * - * @return path + * @param key key being inserted + * @return common-prefix depth */ - BigInteger getPath(); + int calculateSplitDepth(byte[] key); /** * Finalize current branch. diff --git a/src/main/java/org/unicitylabs/sdk/smt/radixsum/FinalizedBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radixsum/FinalizedBranch.java index 6ebc1231..ca254ddd 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/radixsum/FinalizedBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/radixsum/FinalizedBranch.java @@ -7,7 +7,7 @@ /** * Finalized branch in a radix sparse Merkle sum tree. */ -public interface FinalizedBranch extends Branch { +interface FinalizedBranch extends Branch { /** * Get hash of the branch. diff --git a/src/main/java/org/unicitylabs/sdk/smt/radixsum/FinalizedLeafBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radixsum/FinalizedLeafBranch.java index a08c9709..22c6b6c9 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/radixsum/FinalizedLeafBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/radixsum/FinalizedLeafBranch.java @@ -3,6 +3,7 @@ import org.unicitylabs.sdk.crypto.hash.DataHash; import org.unicitylabs.sdk.crypto.hash.DataHasher; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.smt.SparseMerkleTreePathUtils; import org.unicitylabs.sdk.util.BigIntegerConverter; import java.math.BigInteger; @@ -13,24 +14,23 @@ * {@code SHA-256(0x10 || key || data || u256(value))}, where {@code u256} is the 32-byte * big-endian encoding of the leaf amount. */ -public class FinalizedLeafBranch implements LeafBranch, FinalizedBranch { +class FinalizedLeafBranch implements LeafBranch, FinalizedBranch { - private final BigInteger path; private final byte[] key; private final byte[] data; private final BigInteger value; + private final int depth; private final DataHash hash; - private FinalizedLeafBranch(BigInteger path, byte[] key, byte[] data, BigInteger value, - DataHash hash) { - this.path = path; - this.key = Arrays.copyOf(key, key.length); - this.data = Arrays.copyOf(data, data.length); + private FinalizedLeafBranch(byte[] key, byte[] data, BigInteger value, DataHash hash) { + this.key = key; + this.data = data; this.value = value; + this.depth = key.length * 8; this.hash = hash; } - static FinalizedLeafBranch fromPendingLeaf(HashAlgorithm hashAlgorithm, PendingLeafBranch leaf) { + public static FinalizedLeafBranch fromPendingLeaf(HashAlgorithm hashAlgorithm, PendingLeafBranch leaf) { byte[] key = leaf.getKey(); byte[] data = leaf.getData(); @@ -41,12 +41,17 @@ static FinalizedLeafBranch fromPendingLeaf(HashAlgorithm hashAlgorithm, PendingL .update(BigIntegerConverter.encode(leaf.getValue(), 32)) .digest(); - return new FinalizedLeafBranch(leaf.getPath(), key, data, leaf.getValue(), hash); + return new FinalizedLeafBranch(key, data, leaf.getValue(), hash); } @Override - public BigInteger getPath() { - return this.path; + public int getDepth() { + return this.depth; + } + + @Override + public int calculateSplitDepth(byte[] key) { + return SparseMerkleTreePathUtils.commonPrefixLength(key, this.key, this.depth); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/smt/radixsum/FinalizedNodeBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radixsum/FinalizedNodeBranch.java index a7f4a993..1d2b21cf 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/radixsum/FinalizedNodeBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/radixsum/FinalizedNodeBranch.java @@ -3,27 +3,29 @@ import org.unicitylabs.sdk.crypto.hash.DataHash; import org.unicitylabs.sdk.crypto.hash.DataHasher; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.smt.SparseMerkleTreePathUtils; import org.unicitylabs.sdk.util.BigIntegerConverter; import java.math.BigInteger; +import java.util.Arrays; /** * Finalized interior node in a radix sparse Merkle sum tree. The node hash is * {@code SHA-256(0x11 || u8(depth) || hL || u256(vL) || hR || u256(vR))} and the node sum is * {@code vL + vR}, computed with a checked 256-bit addition. */ -public class FinalizedNodeBranch implements NodeBranch, FinalizedBranch { +class FinalizedNodeBranch implements NodeBranch, FinalizedBranch { private static final BigInteger SUM_LIMIT = BigInteger.ONE.shiftLeft(256); - private final BigInteger path; + private final byte[] path; private final int depth; private final FinalizedBranch left; private final FinalizedBranch right; private final BigInteger value; private final DataHash hash; - private FinalizedNodeBranch(BigInteger path, int depth, FinalizedBranch left, + private FinalizedNodeBranch(byte[] path, int depth, FinalizedBranch left, FinalizedBranch right, BigInteger value, DataHash hash) { this.path = path; this.depth = depth; @@ -33,7 +35,7 @@ private FinalizedNodeBranch(BigInteger path, int depth, FinalizedBranch left, this.hash = hash; } - static FinalizedNodeBranch fromPendingNode(HashAlgorithm hashAlgorithm, PendingNodeBranch node) { + public static FinalizedNodeBranch fromPendingNode(HashAlgorithm hashAlgorithm, PendingNodeBranch node) { FinalizedBranch left = node.getLeft().finalize(hashAlgorithm); FinalizedBranch right = node.getRight().finalize(hashAlgorithm); @@ -54,8 +56,18 @@ static FinalizedNodeBranch fromPendingNode(HashAlgorithm hashAlgorithm, PendingN } @Override - public BigInteger getPath() { - return this.path; + public PendingNodeBranch withLeftBranch(Branch left) { + return new PendingNodeBranch(this.path, this.depth, left, this.right); + } + + @Override + public PendingNodeBranch withRightBranch(Branch right) { + return new PendingNodeBranch(this.path, this.depth, this.left, right); + } + + @Override + public byte[] getPath() { + return Arrays.copyOf(this.path, this.path.length); } @Override @@ -63,6 +75,11 @@ public int getDepth() { return this.depth; } + @Override + public int calculateSplitDepth(byte[] key) { + return SparseMerkleTreePathUtils.commonPrefixLength(key, this.path, this.depth); + } + @Override public FinalizedBranch getLeft() { return this.left; diff --git a/src/main/java/org/unicitylabs/sdk/smt/radixsum/LeafBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radixsum/LeafBranch.java index fc050330..a5a71faf 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/radixsum/LeafBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/radixsum/LeafBranch.java @@ -5,7 +5,7 @@ /** * Leaf branch in a radix sparse Merkle sum tree. */ -public interface LeafBranch extends Branch { +interface LeafBranch extends Branch { /** * Get the 32-byte leaf key. diff --git a/src/main/java/org/unicitylabs/sdk/smt/radixsum/NodeBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radixsum/NodeBranch.java index 92b897a0..7c10919b 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/radixsum/NodeBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/radixsum/NodeBranch.java @@ -3,14 +3,14 @@ /** * Node branch in a radix sparse Merkle sum tree. */ -public interface NodeBranch extends Branch { +interface NodeBranch extends Branch { /** - * Get the absolute bifurcation depth of this node. + * Get the node's committed region: its {@code depth}-bit key prefix with the suffix zeroed. * - * @return depth + * @return region */ - int getDepth(); + byte[] getPath(); /** * Get left branch. @@ -25,4 +25,21 @@ public interface NodeBranch extends Branch { * @return right branch */ Branch getRight(); + + /** + * Derive a pending node with {@code left} as its left child, reusing this node's committed region. + * + * @param left replacement left child + * @return new pending node + */ + PendingNodeBranch withLeftBranch(Branch left); + + /** + * Derive a pending node with {@code right} as its right child, reusing this node's committed + * region. + * + * @param right replacement right child + * @return new pending node + */ + PendingNodeBranch withRightBranch(Branch right); } diff --git a/src/main/java/org/unicitylabs/sdk/smt/radixsum/PendingLeafBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radixsum/PendingLeafBranch.java index 3a4c77b6..fb618c93 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/radixsum/PendingLeafBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/radixsum/PendingLeafBranch.java @@ -1,6 +1,7 @@ package org.unicitylabs.sdk.smt.radixsum; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.smt.SparseMerkleTreePathUtils; import java.math.BigInteger; import java.util.Arrays; @@ -8,22 +9,27 @@ /** * Pending leaf in a radix sparse Merkle sum tree, awaiting hashing. */ -public class PendingLeafBranch implements LeafBranch { - private final BigInteger path; +class PendingLeafBranch implements LeafBranch { private final byte[] key; private final byte[] data; private final BigInteger value; + private final int depth; - PendingLeafBranch(BigInteger path, byte[] key, byte[] data, BigInteger value) { - this.path = path; - this.key = Arrays.copyOf(key, key.length); - this.data = Arrays.copyOf(data, data.length); + public PendingLeafBranch(byte[] key, byte[] data, BigInteger value) { + this.key = key; + this.data = data; this.value = value; + this.depth = key.length * 8; } @Override - public BigInteger getPath() { - return this.path; + public int getDepth() { + return this.depth; + } + + @Override + public int calculateSplitDepth(byte[] key) { + return SparseMerkleTreePathUtils.commonPrefixLength(key, this.key, this.depth); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/smt/radixsum/PendingNodeBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radixsum/PendingNodeBranch.java index 2a93d416..c0c1c268 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/radixsum/PendingNodeBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/radixsum/PendingNodeBranch.java @@ -1,19 +1,20 @@ package org.unicitylabs.sdk.smt.radixsum; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.smt.SparseMerkleTreePathUtils; -import java.math.BigInteger; +import java.util.Arrays; /** * Pending interior node in a radix sparse Merkle sum tree, awaiting hashing. */ -public class PendingNodeBranch implements NodeBranch { - private final BigInteger path; +class PendingNodeBranch implements NodeBranch { + private final byte[] path; private final int depth; private final Branch left; private final Branch right; - PendingNodeBranch(BigInteger path, int depth, Branch left, Branch right) { + public PendingNodeBranch(byte[] path, int depth, Branch left, Branch right) { this.path = path; this.depth = depth; this.left = left; @@ -21,8 +22,8 @@ public class PendingNodeBranch implements NodeBranch { } @Override - public BigInteger getPath() { - return this.path; + public byte[] getPath() { + return Arrays.copyOf(this.path, this.path.length); } @Override @@ -30,6 +31,11 @@ public int getDepth() { return this.depth; } + @Override + public int calculateSplitDepth(byte[] key) { + return SparseMerkleTreePathUtils.commonPrefixLength(key, this.path, this.depth); + } + @Override public Branch getLeft() { return this.left; @@ -40,6 +46,16 @@ public Branch getRight() { return this.right; } + @Override + public PendingNodeBranch withLeftBranch(Branch left) { + return new PendingNodeBranch(this.path, this.depth, left, this.right); + } + + @Override + public PendingNodeBranch withRightBranch(Branch right) { + return new PendingNodeBranch(this.path, this.depth, this.left, right); + } + @Override public FinalizedNodeBranch finalize(HashAlgorithm hashAlgorithm) { return FinalizedNodeBranch.fromPendingNode(hashAlgorithm, this); diff --git a/src/main/java/org/unicitylabs/sdk/smt/radixsum/SparseMerkleSumTree.java b/src/main/java/org/unicitylabs/sdk/smt/radixsum/SparseMerkleSumTree.java index b4d1a1ce..aad5d509 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/radixsum/SparseMerkleSumTree.java +++ b/src/main/java/org/unicitylabs/sdk/smt/radixsum/SparseMerkleSumTree.java @@ -1,16 +1,15 @@ package org.unicitylabs.sdk.smt.radixsum; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; -import org.unicitylabs.sdk.smt.BranchExistsException; -import org.unicitylabs.sdk.smt.CommonPath; -import org.unicitylabs.sdk.smt.LeafOutOfBoundsException; -import org.unicitylabs.sdk.util.BitString; +import org.unicitylabs.sdk.smt.LeafExistsException; +import org.unicitylabs.sdk.smt.SparseMerkleTreePathUtils; import java.math.BigInteger; +import java.util.Arrays; import java.util.Objects; /** - * Radix sparse Merkle sum tree. It reuses the radix sparse Merkle tree structure (LSB-first key + * Radix sparse Merkle sum tree. It reuses the radix sparse Merkle tree structure (big-endian key * routing, path-compressed binary trie, absolute bifurcation depths) and additionally commits a * positive amount at every leaf and the accumulated sum at every internal node, so any inclusion * proof also proves the leaf amount is part of the committed root total. @@ -39,17 +38,19 @@ public SparseMerkleSumTree(HashAlgorithm hashAlgorithm) { * @param key 32-byte leaf key * @param data 32-byte leaf data * @param value leaf amount in the range {@code [1, 2^256)} - * @throws BranchExistsException if a branch already exists at the key's path - * @throws LeafOutOfBoundsException if the leaf is out of bounds + * @throws LeafExistsException if a leaf already exists for the key * @throws IllegalArgumentException if the value is not a positive 256-bit integer, or the key * or data is not 32 bytes */ public synchronized void addLeaf(byte[] key, byte[] data, BigInteger value) - throws BranchExistsException, LeafOutOfBoundsException { + throws LeafExistsException { Objects.requireNonNull(key, "key cannot be null"); Objects.requireNonNull(data, "data cannot be null"); Objects.requireNonNull(value, "value cannot be null"); + key = Arrays.copyOf(key, key.length); + data = Arrays.copyOf(data, data.length); + if (value.signum() <= 0 || value.compareTo(SparseMerkleSumTree.VALUE_LIMIT) >= 0) { throw new IllegalArgumentException("Value must be a positive 256-bit integer."); } @@ -62,13 +63,11 @@ public synchronized void addLeaf(byte[] key, byte[] data, BigInteger value) throw new IllegalArgumentException("Data must be 32 bytes long."); } - BigInteger path = BitString.fromBytesReversedLSB(key).toBigInteger(); - - boolean isRight = path.testBit(0); + boolean isRight = SparseMerkleTreePathUtils.getBitAtDepth(key, 0) == 1; Branch branch = isRight ? this.right : this.left; Branch result = branch != null - ? SparseMerkleSumTree.buildTree(branch, path, key, data, value) - : new PendingLeafBranch(path, key, data, value); + ? SparseMerkleSumTree.buildTree(branch, key, data, value) + : new PendingLeafBranch(key, data, value); if (isRight) { this.right = result; @@ -91,43 +90,37 @@ public synchronized SparseMerkleSumTreeRootNode calculateRoot() { return SparseMerkleSumTreeRootNode.create(left, right, this.hashAlgorithm); } - private static Branch buildTree(Branch branch, BigInteger keyPath, byte[] key, byte[] data, - BigInteger value) - throws BranchExistsException, LeafOutOfBoundsException { - CommonPath commonPath = CommonPath.create(keyPath, branch.getPath()); - boolean isRight = keyPath.shiftRight(commonPath.getLength()).testBit(0); - - if (commonPath.getPath().equals(keyPath)) { - throw new BranchExistsException(); - } - + private static Branch buildTree(Branch branch, byte[] key, byte[] data, BigInteger value) + throws LeafExistsException { if (branch instanceof LeafBranch) { - if (commonPath.getPath().equals(branch.getPath())) { - throw new LeafOutOfBoundsException(); + int depth = branch.calculateSplitDepth(key); + if (depth == branch.getDepth()) { + throw new LeafExistsException(); } - LeafBranch newBranch = new PendingLeafBranch(keyPath, key, data, value); - return new PendingNodeBranch(commonPath.getPath(), commonPath.getLength(), + boolean isRight = SparseMerkleTreePathUtils.getBitAtDepth(key, depth) == 1; + LeafBranch newBranch = new PendingLeafBranch(key, data, value); + return new PendingNodeBranch(SparseMerkleTreePathUtils.regionFromKey(key, depth), depth, isRight ? branch : newBranch, isRight ? newBranch : branch); } NodeBranch nodeBranch = (NodeBranch) branch; + int depth = branch.calculateSplitDepth(key); + boolean isRight = SparseMerkleTreePathUtils.getBitAtDepth(key, depth) == 1; // if node branch is split in the middle - if (!commonPath.getPath().equals(branch.getPath())) { - LeafBranch newBranch = new PendingLeafBranch(keyPath, key, data, value); - return new PendingNodeBranch(commonPath.getPath(), commonPath.getLength(), + if (depth < branch.getDepth()) { + LeafBranch newBranch = new PendingLeafBranch(key, data, value); + return new PendingNodeBranch(SparseMerkleTreePathUtils.regionFromKey(key, depth), depth, isRight ? branch : newBranch, isRight ? newBranch : branch); } if (isRight) { - return new PendingNodeBranch(nodeBranch.getPath(), nodeBranch.getDepth(), - nodeBranch.getLeft(), - SparseMerkleSumTree.buildTree(nodeBranch.getRight(), keyPath, key, data, value)); + return nodeBranch.withRightBranch( + SparseMerkleSumTree.buildTree(nodeBranch.getRight(), key, data, value)); } - return new PendingNodeBranch(nodeBranch.getPath(), nodeBranch.getDepth(), - SparseMerkleSumTree.buildTree(nodeBranch.getLeft(), keyPath, key, data, value), - nodeBranch.getRight()); + return nodeBranch.withLeftBranch( + SparseMerkleSumTree.buildTree(nodeBranch.getLeft(), key, data, value)); } } diff --git a/src/main/java/org/unicitylabs/sdk/smt/radixsum/SparseMerkleSumTreeRootNode.java b/src/main/java/org/unicitylabs/sdk/smt/radixsum/SparseMerkleSumTreeRootNode.java index 57edf7d0..926065ba 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/radixsum/SparseMerkleSumTreeRootNode.java +++ b/src/main/java/org/unicitylabs/sdk/smt/radixsum/SparseMerkleSumTreeRootNode.java @@ -2,12 +2,21 @@ import org.unicitylabs.sdk.crypto.hash.DataHash; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.smt.SparseMerkleTreePathUtils; +import org.unicitylabs.sdk.util.HexConverter; import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; /** * Radix sparse Merkle sum tree root node. If the tree holds a single leaf the root hash and sum - * are that leaf's; otherwise the root is the top internal node, which bifurcates at depth 0. + * are that leaf's; otherwise the root is the top internal node, which bifurcates at depth 0. The + * tree's branch nodes are an internal detail of this package; callers see only the root hash, the + * root sum and the sibling path. */ public class SparseMerkleSumTreeRootNode { @@ -34,7 +43,7 @@ static SparseMerkleSumTreeRootNode create(FinalizedBranch left, FinalizedBranch } if (left != null) { - FinalizedNodeBranch node = new PendingNodeBranch(BigInteger.ONE, 0, left, right) + FinalizedNodeBranch node = new PendingNodeBranch(new byte[32], 0, left, right) .finalize(hashAlgorithm); return new SparseMerkleSumTreeRootNode(node.getLeft(), node.getRight(), node.getValue(), node.getHash()); @@ -45,38 +54,105 @@ static SparseMerkleSumTreeRootNode create(FinalizedBranch left, FinalizedBranch } /** - * Get root node left branch. + * Get the total sum committed by the tree. * - * @return left branch, or {@code null} + * @return root sum */ - public FinalizedBranch getLeft() { - return this.left; + public BigInteger getValue() { + return this.value; } /** - * Get root node right branch. + * Get root hash. * - * @return right branch, or {@code null} + * @return root hash */ - public FinalizedBranch getRight() { - return this.right; + public DataHash getHash() { + return this.hash; } /** - * Get the total sum committed by the tree. + * Enumerate the sibling entries on the path from the leaf with the given key up to the root, + * ordered from the leaf up to the root (strictly decreasing depth). * - * @return root sum + * @param key 32-byte leaf key + * @return sibling entries from the leaf up to the root + * @throws IllegalArgumentException if the key is not present in the tree */ - public BigInteger getValue() { - return this.value; + public List getPath(byte[] key) { + Objects.requireNonNull(key, "key cannot be null"); + + List siblings = new ArrayList<>(); + boolean isRight = SparseMerkleTreePathUtils.getBitAtDepth(key, 0) == 1; + FinalizedBranch sibling = isRight ? this.left : this.right; + FinalizedBranch node = isRight ? this.right : this.left; + if (sibling != null) { + siblings.add(new Sibling(0, sibling.getHash(), sibling.getValue())); + } + + while (node instanceof FinalizedNodeBranch) { + FinalizedNodeBranch branch = (FinalizedNodeBranch) node; + int depth = branch.getDepth(); + isRight = SparseMerkleTreePathUtils.getBitAtDepth(key, depth) == 1; + sibling = isRight ? branch.getLeft() : branch.getRight(); + node = isRight ? branch.getRight() : branch.getLeft(); + if (sibling != null) { + siblings.add(new Sibling(depth, sibling.getHash(), sibling.getValue())); + } + } + + if (!(node instanceof FinalizedLeafBranch)) { + throw new IllegalArgumentException( + "Could not construct split allocation proof: invalid path."); + } + if (!Arrays.equals(((FinalizedLeafBranch) node).getKey(), key)) { + throw new IllegalArgumentException( + String.format("Leaf not found for key: %s", HexConverter.encode(key))); + } + + Collections.reverse(siblings); + return siblings; } /** - * Get root hash. - * - * @return root hash + * One sibling entry of a radix sparse Merkle sum tree inclusion path. */ - public DataHash getHash() { - return this.hash; + public static final class Sibling { + private final int depth; + private final DataHash hash; + private final BigInteger value; + + private Sibling(int depth, DataHash hash, BigInteger value) { + this.depth = depth; + this.hash = hash; + this.value = value; + } + + /** + * Get the bifurcation depth at which this sibling hangs. + * + * @return depth + */ + public int getDepth() { + return this.depth; + } + + /** + * Get the sibling subtree hash. + * + * @return hash + */ + public DataHash getHash() { + return this.hash; + } + + /** + * Get the sum committed by the sibling subtree. + * + * @return sum + */ + public BigInteger getValue() { + return this.value; + } } } diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedUnicityIdMintTransactionVerificationRule.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedUnicityIdMintTransactionVerificationRule.java deleted file mode 100644 index ac05ca27..00000000 --- a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedUnicityIdMintTransactionVerificationRule.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.unicitylabs.sdk.transaction.verification; - -import org.unicitylabs.sdk.predicate.EncodedPredicate; -import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; -import org.unicitylabs.sdk.unicityid.CertifiedUnicityIdMintTransaction; -import org.unicitylabs.sdk.util.verification.VerificationResult; -import org.unicitylabs.sdk.util.verification.VerificationStatus; - -import java.util.ArrayList; -import java.util.List; - -/** - * Verification rule for the genesis (mint) of a unicity id token. Validates the inclusion proof of - * the certified mint transaction, and optionally checks that the genesis lock script matches an - * expected issuer public key. - */ -public class CertifiedUnicityIdMintTransactionVerificationRule { - - private CertifiedUnicityIdMintTransactionVerificationRule() { - } - - /** - * Verify the certified unicity id mint transaction. - * - * @param genesis certified unicity id mint transaction to verify - * @param context shared verification context (trust base + registries) - * @param issuerPublicKey expected issuer public key, or {@code null} to skip the lock-script - * issuer check (e.g., when minting a fresh token where no external issuer is being asserted) - * - * @return verification result - */ - public static VerificationResult verify( - CertifiedUnicityIdMintTransaction genesis, - VerificationContext context, - byte[] issuerPublicKey - ) { - List> results = new ArrayList<>(); - - if (!genesis.getNetworkId().equals(context.getTrustBase().getNetworkId())) { - results.add(new VerificationResult<>("MintNetworkMatchesTrustBaseRule", - VerificationStatus.FAIL)); - return new VerificationResult<>( - "CertifiedUnicityIdMintTransactionVerificationRule", - VerificationStatus.FAIL, - "Mint network does not match trust base.", - results - ); - } - results.add(new VerificationResult<>("MintNetworkMatchesTrustBaseRule", - VerificationStatus.OK)); - - if (issuerPublicKey != null) { - EncodedPredicate expectedLockScript = EncodedPredicate.fromPredicate( - SignaturePredicate.create(issuerPublicKey)); - if (!expectedLockScript.equals(genesis.getLockScript())) { - results.add(new VerificationResult<>("IsLockScriptValidVerificationRule", - VerificationStatus.FAIL)); - return new VerificationResult<>( - "CertifiedUnicityIdMintTransactionVerificationRule", - VerificationStatus.FAIL, - "Lock script does not match expected unicity-id issuer.", - results - ); - } - results.add(new VerificationResult<>("IsLockScriptValidVerificationRule", - VerificationStatus.OK)); - } - - VerificationResult result = InclusionProofVerificationRule.verify( - context.getTrustBase(), - context.getPredicateVerifier(), - genesis.getInclusionProof(), - genesis - ); - results.add(result); - if (result.getStatus() != InclusionProofVerificationStatus.OK) { - return new VerificationResult<>( - "CertifiedUnicityIdMintTransactionVerificationRule", - VerificationStatus.FAIL, - String.format("Inclusion proof verification failed: %s", result.getStatus()), - results - ); - } - - return new VerificationResult<>( - "CertifiedUnicityIdMintTransactionVerificationRule", - VerificationStatus.OK, - "", - results - ); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java b/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java deleted file mode 100644 index 8c2c93dd..00000000 --- a/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java +++ /dev/null @@ -1,189 +0,0 @@ -package org.unicitylabs.sdk.unicityid; - -import org.unicitylabs.sdk.api.InclusionProof; -import org.unicitylabs.sdk.api.NetworkId; -import org.unicitylabs.sdk.api.bft.RootTrustBase; -import org.unicitylabs.sdk.crypto.hash.DataHash; -import org.unicitylabs.sdk.predicate.EncodedPredicate; -import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; -import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.transaction.StateMask; -import org.unicitylabs.sdk.transaction.TokenId; -import org.unicitylabs.sdk.transaction.TokenType; -import org.unicitylabs.sdk.transaction.Transaction; -import org.unicitylabs.sdk.transaction.verification.InclusionProofVerificationRule; -import org.unicitylabs.sdk.transaction.verification.InclusionProofVerificationStatus; -import org.unicitylabs.sdk.util.verification.VerificationException; -import org.unicitylabs.sdk.util.verification.VerificationResult; - -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -/** - * Unicity id mint transaction bundled with a verified inclusion proof. - */ -public final class CertifiedUnicityIdMintTransaction implements Transaction { - - private final UnicityIdMintTransaction transaction; - private final InclusionProof inclusionProof; - - private CertifiedUnicityIdMintTransaction(UnicityIdMintTransaction transaction, - InclusionProof inclusionProof) { - this.transaction = transaction; - this.inclusionProof = inclusionProof; - } - - @Override - public Optional getData() { - return this.transaction.getData(); - } - - @Override - public EncodedPredicate getLockScript() { - return this.transaction.getLockScript(); - } - - @Override - public EncodedPredicate getRecipient() { - return this.transaction.getRecipient(); - } - - /** - * Returns the network identifier. - * - * @return network identifier - */ - public NetworkId getNetworkId() { - return this.transaction.getNetworkId(); - } - - @Override - public DataHash getSourceStateHash() { - return this.transaction.getSourceStateHash(); - } - - @Override - public StateMask getStateMask() { - return this.transaction.getStateMask(); - } - - /** - * Returns the token id derived from the unicity id. - * - * @return token id - */ - public TokenId getTokenId() { - return this.transaction.getTokenId(); - } - - /** - * Returns the token type. - * - * @return token type - */ - public TokenType getTokenType() { - return this.transaction.getTokenType(); - } - - /** - * Returns the target predicate. - * - * @return target predicate - */ - public SignaturePredicate getTargetPredicate() { - return this.transaction.getTargetPredicate(); - } - - /** - * Returns the unicity id. - * - * @return unicity id - */ - public UnicityId getUnicityId() { - return this.transaction.getUnicityId(); - } - - /** - * Returns the inclusion proof certifying this transaction. - * - * @return inclusion proof - */ - public InclusionProof getInclusionProof() { - return this.inclusionProof; - } - - /** - * Deserializes a certified unicity id mint transaction from CBOR. - * - * @param bytes CBOR-encoded certified mint transaction - * - * @return decoded certified mint transaction - */ - public static CertifiedUnicityIdMintTransaction fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes, 2); - return new CertifiedUnicityIdMintTransaction( - UnicityIdMintTransaction.fromCbor(data.get(0)), - InclusionProof.fromCbor(data.get(1)) - ); - } - - /** - * Creates a certified unicity id mint transaction after verifying its inclusion proof. - * - * @param trustBase trust base used to verify inclusion proof signatures - * @param predicateVerifier predicate verifier service - * @param transaction unicity id mint transaction to certify - * @param inclusionProof inclusion proof for the transaction - * - * @return certified mint transaction - * - * @throws VerificationException if inclusion proof verification fails - */ - public static CertifiedUnicityIdMintTransaction fromTransaction( - RootTrustBase trustBase, - PredicateVerifierService predicateVerifier, - UnicityIdMintTransaction transaction, - InclusionProof inclusionProof - ) { - Objects.requireNonNull(trustBase, "trustBase cannot be null"); - Objects.requireNonNull(predicateVerifier, "predicateVerifier cannot be null"); - Objects.requireNonNull(transaction, "transaction cannot be null"); - Objects.requireNonNull(inclusionProof, "inclusionProof cannot be null"); - - VerificationResult result = InclusionProofVerificationRule.verify( - trustBase, - predicateVerifier, - inclusionProof, - transaction - ); - if (result.getStatus() != InclusionProofVerificationStatus.OK) { - throw new VerificationException("Inclusion proof verification failed", result); - } - - return new CertifiedUnicityIdMintTransaction(transaction, inclusionProof); - } - - @Override - public DataHash calculateStateHash() { - return this.transaction.calculateStateHash(); - } - - @Override - public DataHash calculateTransactionHash() { - return this.transaction.calculateTransactionHash(); - } - - @Override - public byte[] toCbor() { - return CborSerializer.encodeArray(this.transaction.toCbor(), this.inclusionProof.toCbor()); - } - - @Override - public String toString() { - return String.format("CertifiedUnicityIdMintTransaction{transaction=%s, inclusionProof=%s}", - this.transaction, this.inclusionProof); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityId.java b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityId.java deleted file mode 100644 index d783e9bd..00000000 --- a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityId.java +++ /dev/null @@ -1,127 +0,0 @@ -package org.unicitylabs.sdk.unicityid; - -import org.unicitylabs.sdk.crypto.hash.DataHash; -import org.unicitylabs.sdk.crypto.hash.DataHasher; -import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.transaction.TokenSalt; - -import java.util.List; -import java.util.Objects; - -/** - * Human-readable identifier for a unicity token. The pair (domain, name) is hashed deterministically - * to derive the corresponding {@link TokenSalt}. - */ -public final class UnicityId { - - private final String name; - private final String domain; - - /** - * Create a unicity id with name only (no domain). - * - * @param name token name - */ - public UnicityId(String name) { - this(name, null); - } - - /** - * Create a unicity id. - * - * @param name token name - * @param domain optional domain; may be null - */ - public UnicityId(String name, String domain) { - this.name = Objects.requireNonNull(name, "name cannot be null"); - this.domain = domain; - } - - /** - * Get the token name. - * - * @return name - */ - public String getName() { - return this.name; - } - - /** - * Get the optional domain. - * - * @return domain, or null if not set - */ - public String getDomain() { - return this.domain; - } - - /** - * Deserialize a unicity id from CBOR bytes. - * - * @param bytes CBOR bytes - * - * @return unicity id - */ - public static UnicityId fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes, 2); - return new UnicityId( - CborDeserializer.decodeTextString(data.get(0)), - CborDeserializer.decodeNullable(data.get(1), CborDeserializer::decodeTextString) - ); - } - - /** - * Serialize the unicity id to CBOR bytes. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeArray( - CborSerializer.encodeTextString(this.name), - CborSerializer.encodeNullable(this.domain, CborSerializer::encodeTextString) - ); - } - - /** - * Derive the token salt from this unicity id by hashing the tagged ("NAMETAG_", domain, name) - * tuple with SHA-256. - * - * @return derived token salt - */ - public TokenSalt toTokenSalt() { - DataHash hash = new DataHasher(HashAlgorithm.SHA256) - .update( - CborSerializer.encodeArray( - CborSerializer.encodeTextString("NAMETAG_"), - CborSerializer.encodeNullable(this.domain, CborSerializer::encodeTextString), - CborSerializer.encodeTextString(this.name) - ) - ) - .digest(); - return TokenSalt.fromBytes(hash.getData()); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof UnicityId)) { - return false; - } - UnicityId that = (UnicityId) o; - return this.name.equals(that.name) && Objects.equals(this.domain, that.domain); - } - - @Override - public int hashCode() { - return Objects.hash(this.name, this.domain); - } - - @Override - public String toString() { - return "@" + (this.domain != null ? this.domain + "/" : "") + this.name; - } -} diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java deleted file mode 100644 index e3c3b464..00000000 --- a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java +++ /dev/null @@ -1,280 +0,0 @@ -package org.unicitylabs.sdk.unicityid; - -import org.unicitylabs.sdk.api.InclusionProof; -import org.unicitylabs.sdk.api.NetworkId; -import org.unicitylabs.sdk.api.bft.RootTrustBase; -import org.unicitylabs.sdk.crypto.hash.DataHash; -import org.unicitylabs.sdk.crypto.hash.DataHasher; -import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; -import org.unicitylabs.sdk.predicate.EncodedPredicate; -import org.unicitylabs.sdk.predicate.Predicate; -import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; -import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializationException; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.transaction.StateMask; -import org.unicitylabs.sdk.transaction.MintTransactionState; -import org.unicitylabs.sdk.transaction.TokenId; -import org.unicitylabs.sdk.transaction.TokenType; -import org.unicitylabs.sdk.transaction.Transaction; - -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -/** - * Mint transaction that derives its token id from a {@link UnicityId}. The token's data field is - * the encoded target predicate. - */ -public final class UnicityIdMintTransaction implements Transaction { - public static final long CBOR_TAG = 39041; - private static final int VERSION = 1; - - private final MintTransactionState sourceStateHash; - private final EncodedPredicate lockScript; - private final NetworkId networkId; - private final EncodedPredicate recipient; - private final TokenId tokenId; - private final TokenType tokenType; - private final SignaturePredicate targetPredicate; - private final UnicityId unicityId; - - private UnicityIdMintTransaction( - MintTransactionState sourceStateHash, - EncodedPredicate lockScript, - NetworkId networkId, - EncodedPredicate recipient, - TokenId tokenId, - TokenType tokenType, - SignaturePredicate targetPredicate, - UnicityId unicityId - ) { - this.sourceStateHash = sourceStateHash; - this.lockScript = lockScript; - this.networkId = networkId; - this.recipient = recipient; - this.tokenId = tokenId; - this.tokenType = tokenType; - this.targetPredicate = targetPredicate; - this.unicityId = unicityId; - } - - /** - * Get the version number. - * - * @return version - */ - public int getVersion() { - return UnicityIdMintTransaction.VERSION; - } - - @Override - public MintTransactionState getSourceStateHash() { - return this.sourceStateHash; - } - - @Override - public EncodedPredicate getLockScript() { - return this.lockScript; - } - - @Override - public EncodedPredicate getRecipient() { - return this.recipient; - } - - /** - * Get the network identifier. - * - * @return network identifier - */ - public NetworkId getNetworkId() { - return this.networkId; - } - - /** - * Get the token id derived from the unicity id. - * - * @return token id - */ - public TokenId getTokenId() { - return this.tokenId; - } - - /** - * Get the token type. - * - * @return token type - */ - public TokenType getTokenType() { - return this.tokenType; - } - - /** - * Get the target predicate (the predicate the minted token is locked to). - * - * @return target predicate - */ - public SignaturePredicate getTargetPredicate() { - return this.targetPredicate; - } - - /** - * Get the unicity id. - * - * @return unicity id - */ - public UnicityId getUnicityId() { - return this.unicityId; - } - - @Override - public Optional getData() { - return Optional.of(EncodedPredicate.fromPredicate(this.targetPredicate).toCbor()); - } - - @Override - public StateMask getStateMask() { - return StateMask.fromBytes(this.tokenId.getBytes()); - } - - /** - * Create a unicity id mint transaction. The token id is derived from the unicity id; the lock - * script is supplied by the caller. - * - * @param networkId network identifier - * @param lockScript lock script predicate (the predicate that must be unlocked to spend this - * transaction) - * @param recipient recipient predicate - * @param unicityId unicity id producing the token id - * @param tokenType token type identifier - * @param targetPredicate target predicate the minted token will be locked to - * - * @return mint transaction - */ - public static UnicityIdMintTransaction create( - NetworkId networkId, - SignaturePredicate lockScript, - Predicate recipient, - UnicityId unicityId, - TokenType tokenType, - SignaturePredicate targetPredicate - ) { - Objects.requireNonNull(networkId, "Network ID must not be null"); - Objects.requireNonNull(lockScript, "lockScript cannot be null"); - Objects.requireNonNull(recipient, "recipient cannot be null"); - Objects.requireNonNull(unicityId, "unicityId cannot be null"); - Objects.requireNonNull(tokenType, "tokenType cannot be null"); - Objects.requireNonNull(targetPredicate, "targetPredicate cannot be null"); - - TokenId tokenId = TokenId.fromSalt(networkId, unicityId.toTokenSalt()); - - return new UnicityIdMintTransaction( - MintTransactionState.create(tokenId), - EncodedPredicate.fromPredicate(lockScript), - networkId, - EncodedPredicate.fromPredicate(recipient), - tokenId, - tokenType, - targetPredicate, - unicityId - ); - } - - /** - * Deserialize a unicity id mint transaction from CBOR bytes. - * - * @param bytes CBOR bytes - * - * @return mint transaction - * - * @throws CborSerializationException if the bytes do not carry the expected tag, version, or if - * the encoded token id does not match the unicity id - */ - public static UnicityIdMintTransaction fromCbor(byte[] bytes) { - CborDeserializer.CborTag tag = CborDeserializer.decodeTag(bytes); - if (tag.getTag() != UnicityIdMintTransaction.CBOR_TAG) { - throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); - } - List data = CborDeserializer.decodeArray(tag.getData(), 7); - - int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); - if (version != UnicityIdMintTransaction.VERSION) { - throw new CborSerializationException(String.format("Unsupported version: %s", version)); - } - - return UnicityIdMintTransaction.create( - NetworkId.fromId(CborDeserializer.decodeUnsignedInteger(data.get(1)).asShort()), - SignaturePredicate.fromPredicate( - EncodedPredicate.fromCbor(data.get(2)) - ), - EncodedPredicate.fromCbor(data.get(3)), - UnicityId.fromCbor(data.get(4)), - TokenType.fromCbor(data.get(5)), - SignaturePredicate.fromPredicate( - EncodedPredicate.fromCbor(data.get(6)) - ) - ); - } - - @Override - public DataHash calculateStateHash() { - return new DataHasher(HashAlgorithm.SHA256) - .update( - CborSerializer.encodeArray( - CborSerializer.encodeByteString(this.sourceStateHash.getImprint()), - this.getStateMask().toCbor() - ) - ) - .digest(); - } - - @Override - public DataHash calculateTransactionHash() { - return new DataHasher(HashAlgorithm.SHA256).update(this.toCbor()).digest(); - } - - @Override - public byte[] toCbor() { - return CborSerializer.encodeTag( - UnicityIdMintTransaction.CBOR_TAG, - CborSerializer.encodeArray( - CborSerializer.encodeUnsignedInteger(UnicityIdMintTransaction.VERSION), - CborSerializer.encodeUnsignedInteger(this.networkId.getId()), - this.lockScript.toCbor(), - this.recipient.toCbor(), - this.unicityId.toCbor(), - this.tokenType.toCbor(), - EncodedPredicate.fromPredicate(this.targetPredicate).toCbor() - ) - ); - } - - /** - * Build the certified version by attaching and verifying an inclusion proof. - * - * @param trustBase root trust base - * @param predicateVerifier predicate verifier - * @param inclusionProof inclusion proof - * - * @return certified mint transaction - */ - public CertifiedUnicityIdMintTransaction toCertifiedTransaction( - RootTrustBase trustBase, - PredicateVerifierService predicateVerifier, - InclusionProof inclusionProof - ) { - return CertifiedUnicityIdMintTransaction.fromTransaction(trustBase, predicateVerifier, this, - inclusionProof); - } - - @Override - public String toString() { - return String.format( - "UnicityIdMintTransaction{networkId=%s, lockScript=%s, recipient=%s, tokenId=%s, tokenType=%s, unicityId=%s, targetPredicate=%s}", - this.networkId, this.lockScript, this.recipient, this.tokenId, this.tokenType, - this.unicityId, this.targetPredicate - ); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java deleted file mode 100644 index 51963628..00000000 --- a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java +++ /dev/null @@ -1,150 +0,0 @@ -package org.unicitylabs.sdk.unicityid; - -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.transaction.TokenId; -import org.unicitylabs.sdk.transaction.TokenType; -import org.unicitylabs.sdk.transaction.verification.CertifiedUnicityIdMintTransactionVerificationRule; -import org.unicitylabs.sdk.transaction.verification.VerificationContext; -import org.unicitylabs.sdk.util.verification.VerificationException; -import org.unicitylabs.sdk.util.verification.VerificationResult; -import org.unicitylabs.sdk.util.verification.VerificationStatus; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** - * Token whose genesis is a {@link CertifiedUnicityIdMintTransaction}. The token's identifier is - * deterministically derived from a {@link UnicityId}. - */ -public final class UnicityIdToken { - - private final CertifiedUnicityIdMintTransaction genesis; - - private UnicityIdToken(CertifiedUnicityIdMintTransaction genesis) { - this.genesis = genesis; - } - - /** - * Returns the certified genesis mint transaction. - * - * @return genesis transaction - */ - public CertifiedUnicityIdMintTransaction getGenesis() { - return this.genesis; - } - - /** - * Returns the token id. - * - * @return token id - */ - public TokenId getId() { - return this.genesis.getTokenId(); - } - - /** - * Returns the token type. - * - * @return token type - */ - public TokenType getType() { - return this.genesis.getTokenType(); - } - - /** - * Returns the unicity id used to derive this token's identifier. - * - * @return unicity id - */ - public UnicityId getUnicityId() { - return this.genesis.getUnicityId(); - } - - /** - * Deserialize a unicity id token from CBOR bytes. - * - * @param bytes CBOR bytes - * - * @return decoded token - */ - public static UnicityIdToken fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes, 1); - return new UnicityIdToken(CertifiedUnicityIdMintTransaction.fromCbor(data.get(0))); - } - - /** - * Build a unicity id token from a certified genesis transaction and verify it. - * - * @param genesis certified mint transaction - * @param context shared verification context (trust base + registries) - * - * @return verified token - * - * @throws VerificationException if genesis verification fails - */ - public static UnicityIdToken mint( - CertifiedUnicityIdMintTransaction genesis, - VerificationContext context - ) { - Objects.requireNonNull(genesis, "genesis cannot be null"); - Objects.requireNonNull(context, "context cannot be null"); - - VerificationResult result = - CertifiedUnicityIdMintTransactionVerificationRule.verify( - genesis, - context, - null - ); - if (result.getStatus() != VerificationStatus.OK) { - throw new VerificationException("Invalid token genesis", result); - } - - return new UnicityIdToken(genesis); - } - - /** - * Serialize this token to CBOR bytes. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeArray(this.genesis.toCbor()); - } - - /** - * Verify the token by validating its certified mint transaction against an expected issuer. - * - * @param context shared verification context (trust base + registries) - * @param issuerPublicKey expected issuer public key - * - * @return verification result - * @throws NullPointerException if {@code issuerPublicKey} is {@code null} - */ - public VerificationResult verify( - VerificationContext context, - byte[] issuerPublicKey - ) { - Objects.requireNonNull(context, "context cannot be null"); - Objects.requireNonNull(issuerPublicKey, "issuerPublicKey cannot be null"); - - List> results = new ArrayList<>(); - VerificationResult result = CertifiedUnicityIdMintTransactionVerificationRule.verify( - this.genesis, - context, - issuerPublicKey - ); - results.add(result); - if (result.getStatus() != VerificationStatus.OK) { - return new VerificationResult<>("TokenVerification", VerificationStatus.FAIL, "", results); - } - - return new VerificationResult<>("TokenVerification", VerificationStatus.OK, "", results); - } - - @Override - public String toString() { - return String.format("UnicityIdToken{genesis=%s}", this.genesis); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/util/BitString.java b/src/main/java/org/unicitylabs/sdk/util/BitString.java index 98e7bf7c..9ef6aca7 100644 --- a/src/main/java/org/unicitylabs/sdk/util/BitString.java +++ b/src/main/java/org/unicitylabs/sdk/util/BitString.java @@ -29,45 +29,6 @@ public static BitString fromBytes(byte[] data) { return new BitString(Arrays.copyOf(data, data.length)); } - /** - * Creates a BitString for LSB-first tree routing with reversed byte order. BigInteger bit 0 is - * bit 0 (LSB) of data[0], matching getBitAtDepth LSB convention. - * - * @param data input bytes - * @return BitString - */ - public static BitString fromBytesReversedLSB(byte[] data) { - byte[] reversed = new byte[data.length]; - for (int i = 0; i < data.length; i++) { - reversed[i] = data[data.length - 1 - i]; - } - return new BitString(reversed); - } - - /** - * Creates a BitString for MSB-first tree routing with reversed byte order. BigInteger bit 0 is - * bit 7 (MSB) of data[0], matching getBitAtDepth MSB convention. - * - * @param data input bytes - * @return BitString - */ - public static BitString fromBytesReversedMSB(byte[] data) { - byte[] reversed = new byte[data.length]; - for (int i = 0; i < data.length; i++) { - int b = data[data.length - 1 - i] & 0xFF; - int bitReversed = ((b & 0x80) >> 7) - | ((b & 0x40) >> 5) - | ((b & 0x20) >> 3) - | ((b & 0x10) >> 1) - | ((b & 0x08) << 1) - | ((b & 0x04) << 3) - | ((b & 0x02) << 5) - | ((b & 0x01) << 7); - reversed[i] = (byte) bitReversed; - } - return new BitString(reversed); - } - /** * Converts BitString to BigInteger by adding a leading byte 1 to input byte array. This is to * ensure that the BigInteger will retain the leading zero bits. diff --git a/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java b/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java index 7e8149bb..3f0d7467 100644 --- a/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java +++ b/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java @@ -7,8 +7,8 @@ import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; -import org.unicitylabs.sdk.smt.radix.FinalizedNodeBranch; import org.unicitylabs.sdk.smt.radix.SparseMerkleTree; +import org.unicitylabs.sdk.smt.radix.SparseMerkleTreeRootNode; import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; @@ -84,7 +84,7 @@ public CompletableFuture submitCertificationRequest(Certi @Override public CompletableFuture getInclusionProof(StateId stateId) { - FinalizedNodeBranch root = this.sparseMerkleTree.calculateRoot(); + SparseMerkleTreeRootNode root = this.sparseMerkleTree.calculateRoot(); if (!requests.containsKey(stateId)) { return CompletableFuture.completedFuture(InclusionProofFixture.createResponse(null, null, root.getHash(), this.signingService)); diff --git a/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java b/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java index 8ead4eb3..2a1a070c 100644 --- a/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java +++ b/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java @@ -4,7 +4,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; -import org.unicitylabs.sdk.api.NetworkId; import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.api.bft.RootTrustBaseUtils; import org.unicitylabs.sdk.api.bft.ShardId; @@ -16,8 +15,8 @@ import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; import org.unicitylabs.sdk.predicate.builtin.SignaturePredicateUnlockScript; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; -import org.unicitylabs.sdk.smt.radix.FinalizedNodeBranch; import org.unicitylabs.sdk.smt.radix.SparseMerkleTree; +import org.unicitylabs.sdk.smt.radix.SparseMerkleTreeRootNode; import org.unicitylabs.sdk.transaction.MintTransaction; import org.unicitylabs.sdk.transaction.verification.InclusionProofVerificationRule; import org.unicitylabs.sdk.transaction.verification.InclusionProofVerificationStatus; @@ -51,7 +50,7 @@ public void createMerkleTreePath() throws Exception { SparseMerkleTree smt = new SparseMerkleTree(HashAlgorithm.SHA256); smt.addLeaf(stateId.getData(), certificationData.getTransactionHash().getData()); - FinalizedNodeBranch root = smt.calculateRoot(); + SparseMerkleTreeRootNode root = smt.calculateRoot(); inclusionCertificate = InclusionCertificate.create(root, stateId.getData()); // Reuse user signing service as unicity certificate signing service. trustBase = RootTrustBaseUtils.generateRootTrustBase(signingService.getPublicKey()); diff --git a/src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateUtils.java b/src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateUtils.java index 24d397a3..1ea13549 100644 --- a/src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateUtils.java +++ b/src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateUtils.java @@ -95,7 +95,7 @@ public static UnicityCertificate generateCertificate( "NODE", signingService.sign( new DataHasher(HashAlgorithm.SHA256).update(seal.toCbor()).digest() - ).encode() + ) ) ) ) diff --git a/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java b/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java index 74989a6b..c7e75386 100644 --- a/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java +++ b/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java @@ -3,19 +3,10 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.unicitylabs.sdk.StateTransitionClient; -import org.unicitylabs.sdk.api.CertificationData; -import org.unicitylabs.sdk.api.CertificationResponse; -import org.unicitylabs.sdk.api.CertificationStatus; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; -import org.unicitylabs.sdk.predicate.builtin.SignaturePredicateUnlockScript; import org.unicitylabs.sdk.transaction.Token; -import org.unicitylabs.sdk.transaction.TokenType; import org.unicitylabs.sdk.transaction.verification.VerificationContext; -import org.unicitylabs.sdk.unicityid.UnicityId; -import org.unicitylabs.sdk.unicityid.UnicityIdMintTransaction; -import org.unicitylabs.sdk.unicityid.UnicityIdToken; -import org.unicitylabs.sdk.util.InclusionProofUtils; import org.unicitylabs.sdk.util.verification.VerificationStatus; import org.unicitylabs.sdk.utils.TokenUtils; @@ -62,64 +53,4 @@ public void testTransferFlow() throws Exception { Assertions.assertEquals(VerificationStatus.OK, carolToken.verify(this.context).getStatus()); } - - /** - * Default successful flow: mint a unicity-id token and then mint a regular token whose recipient - * is the unicity-id token's target predicate. - */ - @Test - public void testUnicityIdMintFlow() throws Exception { - SigningService unicityIdSigningService = SigningService.generate(); - SignaturePredicate targetPredicate = SignaturePredicate.create( - ALICE_SIGNING_SERVICE.getPublicKey()); - - UnicityId unicityId = new UnicityId("testuser", "unicity-labs/test"); - UnicityIdMintTransaction unicityIdMintTransaction = UnicityIdMintTransaction.create( - this.context.getTrustBase().getNetworkId(), - SignaturePredicate.fromSigningService(unicityIdSigningService), - targetPredicate, - unicityId, - TokenType.generate(), - targetPredicate - ); - - CertificationData unicityIdCertificationData = CertificationData.fromTransaction( - unicityIdMintTransaction, - SignaturePredicateUnlockScript.create(unicityIdMintTransaction, unicityIdSigningService) - ); - - CertificationResponse unicityIdResponse = this.client - .submitCertificationRequest(unicityIdCertificationData).get(); - Assertions.assertEquals(CertificationStatus.SUCCESS, unicityIdResponse.getStatus()); - - UnicityIdToken aliceUnicityIdToken = UnicityIdToken.mint( - unicityIdMintTransaction.toCertifiedTransaction( - this.context.getTrustBase(), - this.context.getPredicateVerifier(), - InclusionProofUtils.waitInclusionProof(this.client, - this.context.getTrustBase(), - this.context.getPredicateVerifier(), unicityIdMintTransaction).get() - ), - this.context - ); - - Assertions.assertEquals(VerificationStatus.OK, - aliceUnicityIdToken.verify(this.context, unicityIdSigningService.getPublicKey()).getStatus()); - - UnicityIdToken decodedUnicityIdToken = UnicityIdToken.fromCbor(aliceUnicityIdToken.toCbor()); - Assertions.assertArrayEquals(aliceUnicityIdToken.toCbor(), decodedUnicityIdToken.toCbor()); - Assertions.assertEquals(aliceUnicityIdToken.getId(), decodedUnicityIdToken.getId()); - Assertions.assertEquals(VerificationStatus.OK, - decodedUnicityIdToken.verify(this.context, unicityIdSigningService.getPublicKey()).getStatus()); - - Token aliceToken = TokenUtils.mintToken( - this.client, - this.context, - aliceUnicityIdToken.getGenesis().getTargetPredicate() - ); - - Assertions.assertEquals(VerificationStatus.OK, - aliceToken.verify(this.context) - .getStatus()); - } } \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SignatureRecoveryTest.java b/src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SignatureRecoveryTest.java index 99dacf64..682ac0e9 100644 --- a/src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SignatureRecoveryTest.java +++ b/src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SignatureRecoveryTest.java @@ -35,7 +35,7 @@ void testSignatureRecoveryId() { // Verify signature with known public key byte[] publicKey = signingService.getPublicKey(); - assertTrue(SigningService.verifyWithPublicKey(hash, signature.getBytes(), publicKey)); + assertTrue(SigningService.verify(hash, signature.getBytes(), publicKey)); } @Test @@ -55,7 +55,7 @@ void testPublicKeyRecovery() { Signature signature = signingService.sign(hash); // Verify signature using recovered public key - assertTrue(SigningService.verifySignatureWithRecoveredPublicKey(hash, signature), + assertTrue(SigningService.verifyWithRecoveredPublicKey(hash, signature), "Signature verification with recovered public key should succeed"); } @@ -82,7 +82,7 @@ void testSignatureFormatCompliance() { DataHash transactionHash = DataHash.fromImprint(HexConverter.decode(transactionHashHex)); // Verify using recovered public key - assertTrue(SigningService.verifySignatureWithRecoveredPublicKey(transactionHash, signature), + assertTrue(SigningService.verifyWithRecoveredPublicKey(transactionHash, signature), "Should verify with recovered public key"); } } \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SigningServiceTest.java b/src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SigningServiceTest.java index 8ed558f9..d81a8502 100644 --- a/src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SigningServiceTest.java +++ b/src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SigningServiceTest.java @@ -54,7 +54,7 @@ public void testVerifyWithPublicKey() { Signature signature = service.sign(hash); // Verify with public key - boolean isValid = SigningService.verifyWithPublicKey(hash, signature.getBytes(), publicKey); + boolean isValid = SigningService.verify(hash, signature.getBytes(), publicKey); assertTrue(isValid); } diff --git a/src/test/java/org/unicitylabs/sdk/predicate/builtin/verification/SignaturePredicateVerifierTest.java b/src/test/java/org/unicitylabs/sdk/predicate/builtin/verification/SignaturePredicateVerifierTest.java new file mode 100644 index 00000000..dd86c316 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/predicate/builtin/verification/SignaturePredicateVerifierTest.java @@ -0,0 +1,70 @@ +package org.unicitylabs.sdk.predicate.builtin.verification; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.DataHasher; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.crypto.secp256k1.Signature; +import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.predicate.EncodedPredicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +public class SignaturePredicateVerifierTest { + + private final SignaturePredicateVerifier verifier = new SignaturePredicateVerifier(); + private final SigningService signingService = SigningService.generate(); + private final EncodedPredicate encodedPredicate = EncodedPredicate.fromPredicate( + SignaturePredicate.fromSigningService(this.signingService)); + + private static DataHash hash(byte[] data) { + return new DataHasher(HashAlgorithm.SHA256).update(data).digest(); + } + + private final DataHash sourceStateHash = hash(new byte[]{1}); + private final DataHash transactionHash = hash(new byte[]{2}); + + private Signature signUnlock() { + DataHash digest = hash( + CborSerializer.encodeArray( + CborSerializer.encodeByteString(this.sourceStateHash.getData()), + CborSerializer.encodeByteString(this.transactionHash.getData()) + ) + ); + return this.signingService.sign(digest); + } + + @Test + public void shouldAcceptValidUnlockScript() { + Signature signature = signUnlock(); + + Assertions.assertEquals( + VerificationStatus.OK, + this.verifier.verify(this.encodedPredicate, this.sourceStateHash, this.transactionHash, + signature.encode()).getStatus()); + } + + @Test + public void shouldRejectTamperedRecoveryByte() { + byte[] tampered = signUnlock().encode(); + tampered[64] ^= 1; + + Assertions.assertEquals( + VerificationStatus.FAIL, + this.verifier.verify(this.encodedPredicate, this.sourceStateHash, this.transactionHash, + tampered).getStatus()); + } + + @Test + public void shouldFailWhenRecoveryByteMakesSignatureUnrecoverable() { + byte[] tampered = signUnlock().encode(); + tampered[64] = 2; + + Assertions.assertEquals( + VerificationStatus.FAIL, + this.verifier.verify(this.encodedPredicate, this.sourceStateHash, this.transactionHash, + tampered).getStatus()); + } +} diff --git a/src/test/java/org/unicitylabs/sdk/smt/CommonPathTest.java b/src/test/java/org/unicitylabs/sdk/smt/CommonPathTest.java deleted file mode 100644 index 650352ae..00000000 --- a/src/test/java/org/unicitylabs/sdk/smt/CommonPathTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.unicitylabs.sdk.smt; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.math.BigInteger; - -public class CommonPathTest { - - @Test - public void shouldCalculateCommonPath() { - Assertions.assertEquals(CommonPath.create( - BigInteger.valueOf(0b11), - BigInteger.valueOf(0b111101111) - ), new CommonPath(BigInteger.valueOf(0b11), 1)); - Assertions.assertEquals(CommonPath.create( - BigInteger.valueOf(0b111101111), - BigInteger.valueOf(0b11) - ), new CommonPath(BigInteger.valueOf(0b11), 1)); - Assertions.assertEquals(CommonPath.create( - BigInteger.valueOf(0b110010000), - BigInteger.valueOf(0b100010000) - ), new CommonPath(BigInteger.valueOf(0b10010000), 7)); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/smt/SparseMerkleTreePathUtilsTest.java b/src/test/java/org/unicitylabs/sdk/smt/SparseMerkleTreePathUtilsTest.java new file mode 100644 index 00000000..81e430cf --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/smt/SparseMerkleTreePathUtilsTest.java @@ -0,0 +1,68 @@ +package org.unicitylabs.sdk.smt; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class SparseMerkleTreePathUtilsTest { + + @Test + public void calculatesCommonPrefixLength() { + byte[] a = new byte[32]; + byte[] b = new byte[32]; + + Assertions.assertEquals(256, SparseMerkleTreePathUtils.commonPrefixLength(a, b, 256)); + Assertions.assertEquals(10, SparseMerkleTreePathUtils.commonPrefixLength(a, b, 10)); + + b[0] = (byte) 0x80; + Assertions.assertEquals(0, SparseMerkleTreePathUtils.commonPrefixLength(a, b, 256)); + + b[0] = (byte) 0x01; + Assertions.assertEquals(7, SparseMerkleTreePathUtils.commonPrefixLength(a, b, 256)); + + b[0] = (byte) 0x00; + b[2] = (byte) 0x01; + Assertions.assertEquals(23, SparseMerkleTreePathUtils.commonPrefixLength(a, b, 256)); + Assertions.assertEquals(10, SparseMerkleTreePathUtils.commonPrefixLength(a, b, 10)); + } + + @Test + public void getBitAtDepthFollowsBigEndianConvention() { + byte[] key = new byte[32]; + key[0] = (byte) 0b1000_0001; + key[31] = (byte) 0x01; + + Assertions.assertEquals(1, SparseMerkleTreePathUtils.getBitAtDepth(key, 0)); + Assertions.assertEquals(0, SparseMerkleTreePathUtils.getBitAtDepth(key, 1)); + Assertions.assertEquals(1, SparseMerkleTreePathUtils.getBitAtDepth(key, 7)); + Assertions.assertEquals(0, SparseMerkleTreePathUtils.getBitAtDepth(key, 254)); + Assertions.assertEquals(1, SparseMerkleTreePathUtils.getBitAtDepth(key, 255)); + } + + @Test + public void getBitAtDepthRejectsOutOfBounds() { + byte[] key = new byte[32]; + Assertions.assertThrows(IllegalArgumentException.class, + () -> SparseMerkleTreePathUtils.getBitAtDepth(key, -1)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> SparseMerkleTreePathUtils.getBitAtDepth(key, 256)); + } + + @Test + public void regionFromKeyPacksPrefixIntoHighOrderBits() { + byte[] key = new byte[32]; + + key[0] = (byte) 0b1010_1111; + byte[] region = SparseMerkleTreePathUtils.regionFromKey(key, 3); + byte[] expected = new byte[32]; + expected[0] = (byte) 0b1010_0000; + Assertions.assertArrayEquals(expected, region); + + key[0] = (byte) 0b1000_0001; + key[1] = (byte) 0b1100_0000; + byte[] spill = SparseMerkleTreePathUtils.regionFromKey(key, 9); + byte[] spillExpected = new byte[32]; + spillExpected[0] = (byte) 0b1000_0001; + spillExpected[1] = (byte) 0b1000_0000; + Assertions.assertArrayEquals(spillExpected, spill); + } +} diff --git a/src/test/java/org/unicitylabs/sdk/smt/radix/SparseMerkleTreeTest.java b/src/test/java/org/unicitylabs/sdk/smt/radix/SparseMerkleTreeTest.java index 7a08e58a..f3e2206e 100644 --- a/src/test/java/org/unicitylabs/sdk/smt/radix/SparseMerkleTreeTest.java +++ b/src/test/java/org/unicitylabs/sdk/smt/radix/SparseMerkleTreeTest.java @@ -35,7 +35,7 @@ void deepSplitAtDepth255VerifiesWithRegion() throws Exception { tree.addLeaf(a, valueA); tree.addLeaf(b, valueB); - FinalizedNodeBranch root = tree.calculateRoot(); + SparseMerkleTreeRootNode root = tree.calculateRoot(); for (byte[][] entry : new byte[][][]{{a, valueA}, {b, valueB}}) { InclusionCertificate certificate = InclusionCertificate.create(root, entry[0]); @@ -61,7 +61,7 @@ void everyLeafVerifiesThroughNonZeroRegions() throws Exception { tree.addLeaf(key, value); } - FinalizedNodeBranch root = tree.calculateRoot(); + SparseMerkleTreeRootNode root = tree.calculateRoot(); for (int i = 0; i < keys.length; i++) { InclusionCertificate certificate = InclusionCertificate.create(root, keys[i]); diff --git a/src/test/java/org/unicitylabs/sdk/smt/radixsum/SparseMerkleSumTreeTest.java b/src/test/java/org/unicitylabs/sdk/smt/radixsum/SparseMerkleSumTreeTest.java index 41edf960..8eb89d9f 100644 --- a/src/test/java/org/unicitylabs/sdk/smt/radixsum/SparseMerkleSumTreeTest.java +++ b/src/test/java/org/unicitylabs/sdk/smt/radixsum/SparseMerkleSumTreeTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.unicitylabs.sdk.payment.SplitAllocationProof; +import org.unicitylabs.sdk.smt.LeafExistsException; import java.math.BigInteger; import java.util.Arrays; @@ -69,6 +70,15 @@ public void rejectsTamperedLeafAmount() throws Exception { leaf.key, leaf.data, leaf.value.add(BigInteger.ONE), root.getHash(), leaf.value)); } + @Test + public void rejectsDuplicateKey() throws Exception { + Leaf leaf = LEAVES.get(0); + SparseMerkleSumTree tree = build(List.of(leaf)); + LeafExistsException exception = Assertions.assertThrows(LeafExistsException.class, + () -> tree.addLeaf(leaf.key, leaf.data, leaf.value)); + Assertions.assertEquals("Leaf already exists.", exception.getMessage()); + } + @Test public void producesEmptyProofForSingleLeafTree() throws Exception { Leaf leaf = LEAVES.get(0);