From 91c9d0efc8f17e1769a30c4d413d38eafa254899 Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Wed, 29 Apr 2026 14:57:47 +0000 Subject: [PATCH 1/3] #58 Add shard id verification against state id --- .../InclusionProofVerificationRule.java | 14 ++++++- .../InclusionProofVerificationStatus.java | 2 + .../ShardIdMatchesStateIdRule.java | 42 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/unicitylabs/sdk/transaction/verification/ShardIdMatchesStateIdRule.java diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationRule.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationRule.java index bda3207..3b8f09e 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationRule.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationRule.java @@ -65,8 +65,20 @@ public static VerificationResult verify(RootTr InclusionProofVerificationStatus.PATH_INVALID); } + VerificationResult result = ShardIdMatchesStateIdRule.verify( + stateId, + inclusionProof.getUnicityCertificate().getShardTreeCertificate() + ); + if (result.getStatus() != VerificationStatus.OK) { + return new VerificationResult<>( + "InclusionProofVerificationRule", + InclusionProofVerificationStatus.SHARD_ID_MISMATCH, + "", + result + ); + } - VerificationResult result = UnicityCertificateVerification.verify(trustBase, inclusionProof); + result = UnicityCertificateVerification.verify(trustBase, inclusionProof); if (result.getStatus() != VerificationStatus.OK) { return new VerificationResult<>( "InclusionProofVerificationRule", diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationStatus.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationStatus.java index 5d3c7ca..c179fdd 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationStatus.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationStatus.java @@ -18,6 +18,8 @@ public enum InclusionProofVerificationStatus { INCLUSION_CERTIFICATE_MISSING, /** Proof path structure or hashes are invalid. */ PATH_INVALID, + /** Shard id of the unicity certificate does not match the transaction state id. */ + SHARD_ID_MISMATCH, /** Inclusion proof verification succeeded. */ OK } diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/ShardIdMatchesStateIdRule.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/ShardIdMatchesStateIdRule.java new file mode 100644 index 0000000..ff76825 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/ShardIdMatchesStateIdRule.java @@ -0,0 +1,42 @@ +package org.unicitylabs.sdk.transaction.verification; + +import org.unicitylabs.sdk.api.StateId; +import org.unicitylabs.sdk.api.bft.ShardId; +import org.unicitylabs.sdk.api.bft.ShardTreeCertificate; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +/** + * Rule to verify that the shard id of the shard tree certificate is a prefix of the transaction + * state id. An empty shard id matches any state id. + */ +public class ShardIdMatchesStateIdRule { + + private ShardIdMatchesStateIdRule() { + } + + /** + * Verify that the shard id is a prefix of the state id. + * + * @param stateId state id of the transaction being verified + * @param shardTreeCertificate shard tree certificate carrying the shard id + * + * @return verification result with {@link VerificationStatus#OK} on match (or empty shard id), + * otherwise {@link VerificationStatus#FAIL} + */ + public static VerificationResult verify( + StateId stateId, + ShardTreeCertificate shardTreeCertificate + ) { + ShardId shardId = shardTreeCertificate.getShard(); + if (shardId.getLength() == 0) { + return new VerificationResult<>("ShardIdMatchesStateIdRule", VerificationStatus.OK); + } + + if (!shardId.isPrefixOf(stateId.getData())) { + return new VerificationResult<>("ShardIdMatchesStateIdRule", VerificationStatus.FAIL); + } + + return new VerificationResult<>("ShardIdMatchesStateIdRule", VerificationStatus.OK); + } +} From 2d24eca9be1dec29a0d0a6ad5b00e622ee0ce9fa Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Wed, 29 Apr 2026 16:03:22 +0000 Subject: [PATCH 2/3] #58 Add unicityCertificate to certificateData equals method --- .../sdk/api/CertificationData.java | 15 ++- .../sdk/api/InclusionCertificate.java | 2 +- .../unicitylabs/sdk/api/InclusionProof.java | 10 +- .../unicitylabs/sdk/api/bft/InputRecord.java | 15 +-- .../sdk/api/bft/ShardTreeCertificate.java | 4 +- .../sdk/api/bft/UnicityCertificate.java | 12 +- .../unicitylabs/sdk/api/bft/UnicitySeal.java | 110 +++++++++--------- .../sdk/api/bft/UnicityTreeCertificate.java | 9 +- ...ySealQuorumSignaturesVerificationRule.java | 5 +- .../unicitylabs/sdk/predicate/Predicate.java | 20 ++-- .../builtin/PayToPublicKeyPredicate.java | 6 + .../sdk/api/bft/UnicityCertificateUtils.java | 14 ++- 12 files changed, 120 insertions(+), 102 deletions(-) diff --git a/src/main/java/org/unicitylabs/sdk/api/CertificationData.java b/src/main/java/org/unicitylabs/sdk/api/CertificationData.java index 2a43648..7ec2771 100644 --- a/src/main/java/org/unicitylabs/sdk/api/CertificationData.java +++ b/src/main/java/org/unicitylabs/sdk/api/CertificationData.java @@ -117,6 +117,8 @@ public static CertificationData fromCbor(byte[] bytes) { * @return certification data */ public static CertificationData fromMintTransaction(MintTransaction transaction) { + Objects.requireNonNull(transaction, "transaction cannot be null"); + SigningService signingService = MintSigningService.create(transaction.getTokenId()); return CertificationData.fromTransaction( @@ -135,6 +137,9 @@ public static CertificationData fromMintTransaction(MintTransaction transaction) * @return certification data */ public static CertificationData fromTransaction(Transaction transaction, UnlockScript unlockScript) { + Objects.requireNonNull(transaction, "transaction cannot be null"); + Objects.requireNonNull(unlockScript, "unlockScript cannot be null"); + return CertificationData.fromTransaction(transaction, unlockScript.encode()); } @@ -147,6 +152,9 @@ public static CertificationData fromTransaction(Transaction transaction, UnlockS * @return certification data */ public static CertificationData fromTransaction(Transaction transaction, byte[] unlockScript) { + Objects.requireNonNull(transaction, "transaction cannot be null"); + Objects.requireNonNull(unlockScript, "unlockScript cannot be null"); + return new CertificationData( transaction.getLockScript(), transaction.getSourceStateHash(), @@ -179,12 +187,17 @@ public boolean equals(Object o) { return false; } CertificationData that = (CertificationData) o; - return this.lockScript.isEqualTo(that.lockScript) + return Predicate.areEqual(this.lockScript, that.lockScript) && Objects.equals(this.sourceStateHash, that.sourceStateHash) && Objects.equals(this.transactionHash, that.transactionHash) && Arrays.equals(this.unlockScript, that.unlockScript); } + @Override + public int hashCode() { + return Objects.hash(EncodedPredicate.fromPredicate(this.lockScript), this.sourceStateHash, this.transactionHash, Arrays.hashCode(this.unlockScript)); + } + @Override public String toString() { return String.format( diff --git a/src/main/java/org/unicitylabs/sdk/api/InclusionCertificate.java b/src/main/java/org/unicitylabs/sdk/api/InclusionCertificate.java index 35b46ec..36b5a58 100644 --- a/src/main/java/org/unicitylabs/sdk/api/InclusionCertificate.java +++ b/src/main/java/org/unicitylabs/sdk/api/InclusionCertificate.java @@ -72,7 +72,7 @@ public static InclusionCertificate decode(byte[] bytes) { int siblingsCount = 0; for (int i = 0; i < InclusionCertificate.BITMAP_SIZE; i++) { - int x = bytes[i]; + int x = bytes[i] & 0xff; x = x - ((x >>> 1) & 0x55); x = (x & 0x33) + ((x >>> 2) & 0x33); x = (x + (x >>> 4)) & 0x0f; diff --git a/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java b/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java index d4388df..bab61ac 100644 --- a/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java +++ b/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java @@ -115,14 +115,12 @@ public boolean equals(Object o) { return false; } InclusionProof that = (InclusionProof) o; - return Objects.equals(this.inclusionCertificate, that.inclusionCertificate) && Objects.equals( - this.certificationData, - that.certificationData); + return Objects.equals(this.inclusionCertificate, that.inclusionCertificate) && Objects.equals(this.certificationData, that.certificationData) && Objects.equals(this.unicityCertificate, that.unicityCertificate); } @Override public int hashCode() { - return Objects.hash(InclusionProof.VERSION, this.inclusionCertificate, this.certificationData); + return Objects.hash(InclusionProof.VERSION, this.inclusionCertificate, this.certificationData, this.unicityCertificate); } @Override @@ -130,6 +128,8 @@ public String toString() { return String.format( "InclusionProof{certificationData=%s, inclusionCertificate=%s, unicityCertificate=%s}", this.inclusionCertificate, - this.certificationData, this.unicityCertificate); + this.certificationData, + this.unicityCertificate + ); } } diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java b/src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java index cb79a19..af1e455 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java @@ -199,14 +199,15 @@ public boolean equals(Object o) { return false; } InputRecord that = (InputRecord) o; - return Objects.equals(this.roundNumber, - that.roundNumber) && Objects.equals(this.epoch, that.epoch) + return Objects.equals(this.roundNumber, that.roundNumber) + && Objects.equals(this.epoch, that.epoch) && Objects.deepEquals(this.previousHash, that.previousHash) - && Objects.deepEquals(this.hash, that.hash) && Objects.deepEquals(this.summaryValue, - that.summaryValue) && Objects.equals(this.timestamp, that.timestamp) - && Objects.deepEquals(this.blockHash, that.blockHash) && Objects.equals( - this.sumOfEarnedFees, that.sumOfEarnedFees) && Objects.deepEquals( - this.executedTransactionsHash, that.executedTransactionsHash); + && Objects.deepEquals(this.hash, that.hash) + && Objects.deepEquals(this.summaryValue, that.summaryValue) + && Objects.equals(this.timestamp, that.timestamp) + && Objects.deepEquals(this.blockHash, that.blockHash) + && Objects.equals(this.sumOfEarnedFees, that.sumOfEarnedFees) + && Objects.deepEquals(this.executedTransactionsHash, that.executedTransactionsHash); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java b/src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java index 4b32070..babcc26 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java @@ -105,8 +105,8 @@ public boolean equals(Object o) { return false; } ShardTreeCertificate that = (ShardTreeCertificate) o; - return Objects.deepEquals(this.shard, that.shard) && Objects.equals( - this.siblingHashList, that.siblingHashList); + return Objects.deepEquals(this.shard, that.shard) + && Objects.equals(this.siblingHashList, that.siblingHashList); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java index 4cf16c8..09286d3 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java @@ -216,12 +216,12 @@ public boolean equals(Object o) { return false; } UnicityCertificate that = (UnicityCertificate) o; - return Objects.equals(this.inputRecord, - that.inputRecord) && Objects.deepEquals(this.technicalRecordHash, - that.technicalRecordHash) && Objects.deepEquals(this.shardConfigurationHash, - that.shardConfigurationHash) && Objects.equals(this.shardTreeCertificate, - that.shardTreeCertificate) && Objects.equals(this.unicityTreeCertificate, - that.unicityTreeCertificate) && Objects.equals(this.unicitySeal, that.unicitySeal); + return Objects.equals(this.inputRecord, that.inputRecord) + && Objects.deepEquals(this.technicalRecordHash, that.technicalRecordHash) + && Objects.deepEquals(this.shardConfigurationHash, that.shardConfigurationHash) + && Objects.equals(this.shardTreeCertificate, that.shardTreeCertificate) + && Objects.equals(this.unicityTreeCertificate, that.unicityTreeCertificate) + && Objects.equals(this.unicitySeal, that.unicitySeal); } @Override 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 9885eb4..c7ec830 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java @@ -23,7 +23,7 @@ public class UnicitySeal { private final long timestamp; private final byte[] previousHash; // nullable private final byte[] hash; - private final LinkedHashMap signatures; + private final Set signatures; UnicitySeal( short networkId, @@ -32,7 +32,7 @@ public class UnicitySeal { long timestamp, byte[] previousHash, byte[] hash, - Map signatures + Set signatures ) { Objects.requireNonNull(hash, "Hash cannot be null"); @@ -44,20 +44,7 @@ public class UnicitySeal { this.hash = hash; this.signatures = signatures == null ? null - : signatures.entrySet().stream() - .map(entry -> Map.entry( - entry.getKey(), - Arrays.copyOf(entry.getValue(), entry.getValue().length) - ) - ) - .collect( - Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue, - (e1, e2) -> e1, - LinkedHashMap::new - ) - ); + : Set.copyOf(signatures); } /** @@ -66,7 +53,7 @@ public class UnicitySeal { * @param signatures the signatures to include in the new UnicitySeal * @return a new UnicitySeal instance with the specified signatures */ - public UnicitySeal withSignatures(Map signatures) { + public UnicitySeal withSignatures(Set signatures) { return new UnicitySeal( this.networkId, this.rootChainRoundNumber, @@ -142,23 +129,8 @@ public byte[] getHash() { * * @return signatures */ - public Map getSignatures() { - return this.signatures == null - ? null - : this.signatures.entrySet().stream() - .map(entry -> Map.entry( - entry.getKey(), - Arrays.copyOf(entry.getValue(), entry.getValue().length) - ) - ) - .collect( - Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue, - (e1, e2) -> e1, - LinkedHashMap::new - ) - ); + public Set getSignatures() { + return this.signatures; } /** @@ -187,13 +159,11 @@ public static UnicitySeal fromCbor(byte[] bytes) { CborDeserializer.decodeNullable(data.get(5), CborDeserializer::decodeByteString), CborDeserializer.decodeByteString(data.get(6)), CborDeserializer.decodeMap(data.get(7)).stream() - .collect( - Collectors.toMap( - entry -> CborDeserializer.decodeTextString(entry.getKey()), - entry -> CborDeserializer.decodeByteString(entry.getValue() - ) - ) - ) + .map(entry -> new SignatureEntry( + CborDeserializer.decodeTextString(entry.getKey()), + CborDeserializer.decodeByteString(entry.getValue()) + )) + .collect(Collectors.toSet()) ); } @@ -217,10 +187,10 @@ public byte[] toCbor() { this.signatures, (signatures) -> CborSerializer.encodeMap( new CborMap( - signatures.entrySet().stream() + signatures.stream() .map(entry -> new CborMap.Entry( CborSerializer.encodeTextString(entry.getKey()), - CborSerializer.encodeByteString(entry.getValue()) + CborSerializer.encodeByteString(entry.getSignature()) ) ) .collect(Collectors.toSet()) @@ -254,12 +224,13 @@ public boolean equals(Object o) { return false; } UnicitySeal that = (UnicitySeal) o; - return Objects.equals(this.networkId, - that.networkId) && Objects.equals(this.rootChainRoundNumber, that.rootChainRoundNumber) - && Objects.equals(this.epoch, that.epoch) && Objects.equals(this.timestamp, - that.timestamp) && Objects.deepEquals(this.previousHash, that.previousHash) - && Objects.deepEquals(this.hash, that.hash) && Objects.equals(this.signatures, - that.signatures); + return Objects.equals(this.networkId, that.networkId) + && Objects.equals(this.rootChainRoundNumber, that.rootChainRoundNumber) + && Objects.equals(this.epoch, that.epoch) + && Objects.equals(this.timestamp, that.timestamp) + && Objects.deepEquals(this.previousHash, that.previousHash) + && Objects.deepEquals(this.hash, that.hash) + && Objects.equals(this.signatures, that.signatures); } @Override @@ -280,11 +251,42 @@ public String toString() { this.timestamp, this.previousHash != null ? HexConverter.encode(this.previousHash) : null, HexConverter.encode(this.hash), - this.signatures.entrySet() - .stream() - .map(entry -> String.format("%s: %s", entry.getKey(), - HexConverter.encode(entry.getValue()))) - .collect(Collectors.toList()) + this.signatures.stream().map(SignatureEntry::toString).collect(Collectors.toList()) ); } + + public static final class SignatureEntry { + private final String key; + private final byte[] signature; + + SignatureEntry(String key, byte[] signature) { + this.key = key; + this.signature = signature; + } + + public String getKey() { + return this.key; + } + + public byte[] getSignature() { + return Arrays.copyOf(this.signature, this.signature.length); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof SignatureEntry)) return false; + SignatureEntry that = (SignatureEntry) o; + return Objects.equals(this.key, that.key) && Objects.deepEquals(this.signature, that.signature); + } + + @Override + public int hashCode() { + return Objects.hash(this.key); + } + + @Override + public String toString() { + return String.format("SignatureEntry{key=%s, signature=%s}", this.key, HexConverter.encode(this.signature)); + } + } } diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/UnicityTreeCertificate.java b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityTreeCertificate.java index 5dc3b4a..a248f54 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/UnicityTreeCertificate.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityTreeCertificate.java @@ -106,9 +106,8 @@ public boolean equals(Object o) { return false; } UnicityTreeCertificate that = (UnicityTreeCertificate) o; - return Objects.equals( - this.partitionIdentifier, that.partitionIdentifier) && Objects.equals(this.steps, - that.steps); + return Objects.equals(this.partitionIdentifier, that.partitionIdentifier) + && Objects.equals(this.steps, that.steps); } @Override @@ -187,8 +186,8 @@ public boolean equals(Object o) { return false; } HashStep hashStep = (HashStep) o; - return Objects.equals(this.key, hashStep.key) && Objects.deepEquals(this.hash, - hashStep.hash); + return Objects.equals(this.key, hashStep.key) + && Objects.deepEquals(this.hash, hashStep.hash); } @Override 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 4bedac5..c0469ba 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 @@ -13,7 +13,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Map; /** * Rule to verify that the UnicitySeal contains valid quorum signatures. @@ -37,9 +36,9 @@ public static VerificationResult verify(RootTrustBase trustB .update(unicitySeal.toCborWithoutSignatures()) .digest(); int successful = 0; - for (Map.Entry entry : unicitySeal.getSignatures().entrySet()) { + for (UnicitySeal.SignatureEntry entry : unicitySeal.getSignatures()) { String nodeId = entry.getKey(); - byte[] signature = entry.getValue(); + byte[] signature = entry.getSignature(); VerificationResult result = UnicitySealQuorumSignaturesVerificationRule.verifySignature( trustBase, diff --git a/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java b/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java index 3e85ed9..540caaf 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java @@ -1,6 +1,7 @@ package org.unicitylabs.sdk.predicate; import java.util.Arrays; +import java.util.Objects; /** * Base contract for all predicate implementations. @@ -29,18 +30,13 @@ public interface Predicate { byte[] encodeParameters(); /** - * Compares this predicate with another predicate using encoded representation. - * - * @param other the predicate to compare against - * @return {@code true} when engine, code, and parameters are equal; otherwise {@code false} + * Checks if two predicates are equal. + * @param a first predicate + * @param b second predicate + * @return {@code true} if predicates are equal, {@code false} otherwise */ - default boolean isEqualTo(Predicate other) { - if (other == null) { - return false; - } - - return this.getEngine() == other.getEngine() - && Arrays.equals(this.encodeCode(), other.encodeCode()) - && Arrays.equals(this.encodeParameters(), other.encodeParameters()); + static boolean areEqual(Predicate a, Predicate b) { + return a.getEngine() == b.getEngine() && Arrays.equals(a.encodeCode(), b.encodeCode()) && Arrays.equals( + a.encodeParameters(), b.encodeParameters()); } } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java index b9cbd13..1cf6e72 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java @@ -4,6 +4,7 @@ import org.unicitylabs.sdk.predicate.Predicate; import org.unicitylabs.sdk.predicate.PredicateEngine; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.util.HexConverter; import java.util.Arrays; import java.util.Objects; @@ -92,4 +93,9 @@ public byte[] encodeParameters() { return this.getPublicKey(); } + @Override + public String toString() { + return String.format("PayToPublicKeyPredicate{publicKey=%s}", HexConverter.encode(this.publicKey)); + } + } 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 69e27cb..f367aa0 100644 --- a/src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateUtils.java +++ b/src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateUtils.java @@ -9,7 +9,7 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.List; -import java.util.Map; +import java.util.Set; public class UnicityCertificateUtils { @@ -82,11 +82,13 @@ public static UnicityCertificate generateCertificate( shardTreeCertificate, new UnicityTreeCertificate(0, List.of()), seal.withSignatures( - Map.of( - "NODE", - signingService.sign( - new DataHasher(HashAlgorithm.SHA256).update(seal.toCbor()).digest() - ).encode() + Set.of( + new UnicitySeal.SignatureEntry( + "NODE", + signingService.sign( + new DataHasher(HashAlgorithm.SHA256).update(seal.toCbor()).digest() + ).encode() + ) ) ) ); From ff9f377af92ff754b34a3ab3ced492a0dfb43383 Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Wed, 29 Apr 2026 17:51:54 +0000 Subject: [PATCH 3/3] #58 Add tests, fix isPrefixOf bug in shardId --- .../org/unicitylabs/sdk/api/bft/ShardId.java | 4 + .../ShardIdMatchesStateIdRule.java | 8 ++ .../sdk/api/InclusionProofTest.java | 40 +++++++- .../sdk/api/bft/UnicityCertificateUtils.java | 13 ++- .../ShardIdMatchesStateIdRuleTest.java | 94 +++++++++++++++++++ 5 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 src/test/java/org/unicitylabs/sdk/transaction/verification/ShardIdMatchesStateIdRuleTest.java diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/ShardId.java b/src/main/java/org/unicitylabs/sdk/api/bft/ShardId.java index 49356bd..0509ec0 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/ShardId.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/ShardId.java @@ -73,6 +73,10 @@ public int getBit(int index) { } public boolean isPrefixOf(byte[] data) { + if (data.length * 8 < this.length) { + return false; + } + int fullBytes = this.length / 8; int remainingBits = this.length % 8; diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/ShardIdMatchesStateIdRule.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/ShardIdMatchesStateIdRule.java index ff76825..2d2b36b 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/verification/ShardIdMatchesStateIdRule.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/ShardIdMatchesStateIdRule.java @@ -28,6 +28,14 @@ public static VerificationResult verify( StateId stateId, ShardTreeCertificate shardTreeCertificate ) { + if (stateId == null) { + return new VerificationResult<>("ShardIdMatchesStateIdRule", VerificationStatus.FAIL, "State ID is missing."); + } + + if (shardTreeCertificate == null) { + return new VerificationResult<>("ShardIdMatchesStateIdRule", VerificationStatus.FAIL, "Shard tree certificate is missing."); + } + ShardId shardId = shardTreeCertificate.getShard(); if (shardId.getLength() == 0) { return new VerificationResult<>("ShardIdMatchesStateIdRule", VerificationStatus.OK); diff --git a/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java b/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java index c16c07c..938d443 100644 --- a/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java +++ b/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.TestInstance; import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.api.bft.RootTrustBaseUtils; +import org.unicitylabs.sdk.api.bft.ShardId; import org.unicitylabs.sdk.api.bft.UnicityCertificate; import org.unicitylabs.sdk.api.bft.UnicityCertificateUtils; import org.unicitylabs.sdk.crypto.hash.DataHash; @@ -56,9 +57,9 @@ public void createMerkleTreePath() throws Exception { FinalizedNodeBranch root = smt.calculateRoot(); inclusionCertificate = InclusionCertificate.create(root, stateId.getData()); - SigningService ucSigningService = new SigningService(SigningService.generatePrivateKey()); - trustBase = RootTrustBaseUtils.generateRootTrustBase(ucSigningService.getPublicKey()); - unicityCertificate = UnicityCertificateUtils.generateCertificate(ucSigningService, root.getHash()); + // Reuse user signing service as unicity certificate signing service. + trustBase = RootTrustBaseUtils.generateRootTrustBase(signingService.getPublicKey()); + unicityCertificate = UnicityCertificateUtils.generateCertificate(signingService, root.getHash()); predicateVerifier = PredicateVerifierService.create(trustBase); } @@ -166,6 +167,39 @@ public void testItNotAuthenticated() { ); } + @Test + public void testItFailsWithShardIdMismatch() { + // 1-byte shard id whose first byte doesn't match the state id's first byte. The shard check + // runs before the trust base check, so the signing service used for the new certificate's seal + // is irrelevant — reuse the test's fixed key. + byte mismatchingByte = (byte) (this.stateId.getData()[0] ^ 0xFF); + ShardId mismatchingShardId = ShardId.decode(new byte[]{mismatchingByte, (byte) 0x80}); + DataHash rootHash = new DataHash(HashAlgorithm.SHA256, + this.unicityCertificate.getInputRecord().getHash()); + SigningService signingService = SigningService.generate(); + UnicityCertificate mismatchingCertificate = UnicityCertificateUtils.generateCertificate( + signingService, + rootHash, + mismatchingShardId + ); + + InclusionProof inclusionProof = new InclusionProof( + this.certificationData, + this.inclusionCertificate, + mismatchingCertificate + ); + + Assertions.assertEquals( + InclusionProofVerificationStatus.SHARD_ID_MISMATCH, + InclusionProofVerificationRule.verify( + RootTrustBaseUtils.generateRootTrustBase(signingService.getPublicKey()), + this.predicateVerifier, + inclusionProof, + this.transaction + ).getStatus() + ); + } + @Test public void testVerificationFailsWithInvalidTrustbase() { InclusionProof inclusionProof = new InclusionProof( 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 f367aa0..5a92481 100644 --- a/src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateUtils.java +++ b/src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateUtils.java @@ -16,6 +16,15 @@ public class UnicityCertificateUtils { public static UnicityCertificate generateCertificate( SigningService signingService, DataHash rootHash + ) { + return generateCertificate(signingService, rootHash, + ShardId.decode(new byte[]{(byte) 0b10000000})); + } + + public static UnicityCertificate generateCertificate( + SigningService signingService, + DataHash rootHash, + ShardId shardId ) { InputRecord inputRecord = new InputRecord( 0, @@ -31,9 +40,7 @@ public static UnicityCertificate generateCertificate( UnicityTreeCertificate unicityTreeCertificate = new UnicityTreeCertificate(0, List.of()); byte[] technicalRecordHash = new byte[32]; byte[] shardConfigurationHash = new byte[32]; - ShardTreeCertificate shardTreeCertificate = new ShardTreeCertificate( - ShardId.decode(new byte[]{(byte) 0b10000000}), List.of() - ); + ShardTreeCertificate shardTreeCertificate = new ShardTreeCertificate(shardId, List.of()); DataHash shardTreeCertificateRootHash = UnicityCertificate.calculateShardTreeCertificateRootHash( inputRecord, diff --git a/src/test/java/org/unicitylabs/sdk/transaction/verification/ShardIdMatchesStateIdRuleTest.java b/src/test/java/org/unicitylabs/sdk/transaction/verification/ShardIdMatchesStateIdRuleTest.java new file mode 100644 index 0000000..d14f927 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/transaction/verification/ShardIdMatchesStateIdRuleTest.java @@ -0,0 +1,94 @@ +package org.unicitylabs.sdk.transaction.verification; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.unicitylabs.sdk.api.StateId; +import org.unicitylabs.sdk.api.bft.ShardTreeCertificate; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.util.HexConverter; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +public class ShardIdMatchesStateIdRuleTest { + + /** 32 bytes of 0xAB — a valid SHA-256-shaped state id. */ + private static final byte[] STATE_ID_BYTES = + HexConverter.decode("ABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABAB"); + + @Test + public void verifyFailsWhenShardTreeCertificateIsNull() { + StateId stateId = StateId.fromCbor(CborSerializer.encodeByteString(STATE_ID_BYTES)); + + VerificationResult result = + ShardIdMatchesStateIdRule.verify(stateId, null); + + Assertions.assertEquals(VerificationStatus.FAIL, result.getStatus()); + Assertions.assertEquals("Shard tree certificate is missing.", result.getMessage()); + } + + @Test + public void verifyFailsWhenStateIdIsNull() { + // Empty shard id (length 0). + ShardTreeCertificate certificate = ShardTreeCertificate.fromCbor( + HexConverter.decode("d9985b8301418080")); + + VerificationResult result = + ShardIdMatchesStateIdRule.verify(null, certificate); + + Assertions.assertEquals(VerificationStatus.FAIL, result.getStatus()); + Assertions.assertEquals("State ID is missing.", result.getMessage()); + } + + @Test + public void verifyPassesWhenShardIdIsEmpty() { + StateId stateId = StateId.fromCbor(CborSerializer.encodeByteString(STATE_ID_BYTES)); + ShardTreeCertificate certificate = ShardTreeCertificate.fromCbor( + HexConverter.decode("d9985b8301418080")); + + VerificationResult result = + ShardIdMatchesStateIdRule.verify(stateId, certificate); + + Assertions.assertEquals(VerificationStatus.OK, result.getStatus()); + } + + @Test + public void verifyPassesWhenShardIdIsPrefixOfStateId() { + // Shard id of length 8 with bits=[0xAB]. + StateId stateId = StateId.fromCbor(CborSerializer.encodeByteString(STATE_ID_BYTES)); + ShardTreeCertificate certificate = ShardTreeCertificate.fromCbor( + HexConverter.decode("d9985b830142ab8080")); + + VerificationResult result = + ShardIdMatchesStateIdRule.verify(stateId, certificate); + + Assertions.assertEquals(VerificationStatus.OK, result.getStatus()); + } + + @Test + public void verifyFailsWhenShardIdIsNotPrefixOfStateId() { + // Shard id of length 8 with bits=[0x12] — does not match. + StateId stateId = StateId.fromCbor(CborSerializer.encodeByteString(STATE_ID_BYTES)); + ShardTreeCertificate certificate = ShardTreeCertificate.fromCbor( + HexConverter.decode("d9985b830142128080")); + + VerificationResult result = + ShardIdMatchesStateIdRule.verify(stateId, certificate); + + Assertions.assertEquals(VerificationStatus.FAIL, result.getStatus()); + } + + @Test + public void verifyFailsWhenStateIdIsShorterThanShardId() { + // SHA-256 state id is 32 bytes (256 bits). Use a 264-bit shard id (33 full bytes 0xAB + + // 0x80 end marker), so the state id has fewer bits than the shard id requires. + StateId stateId = StateId.fromCbor(CborSerializer.encodeByteString(STATE_ID_BYTES)); + ShardTreeCertificate certificate = ShardTreeCertificate.fromCbor( + HexConverter.decode( + "d9985b83015822ababababababababababababababababababababababababababababababababab8080")); + + VerificationResult result = + ShardIdMatchesStateIdRule.verify(stateId, certificate); + + Assertions.assertEquals(VerificationStatus.FAIL, result.getStatus()); + } +}