From 00e7f7216ab444f4cff4c7e5c850efd80c417faf Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Tue, 28 Apr 2026 18:45:49 +0000 Subject: [PATCH 01/11] #60 Use predicates instead of address, move mint justification to mint root --- .../unicitylabs/sdk/api/InclusionProof.java | 4 +- .../unicitylabs/sdk/api/bft/InputRecord.java | 6 +- .../sdk/api/bft/UnicityCertificate.java | 4 +- .../unicitylabs/sdk/api/bft/UnicitySeal.java | 4 +- ...tReasonProof.java => SplitAssetProof.java} | 25 +- .../sdk/payment/SplitMintJustification.java | 101 +++ .../SplitMintJustificationVerifier.java | 200 ++++++ .../sdk/payment/SplitPaymentData.java | 2 +- .../unicitylabs/sdk/payment/SplitReason.java | 91 --- .../unicitylabs/sdk/payment/SplitResult.java | 6 +- .../unicitylabs/sdk/payment/TokenSplit.java | 190 +----- .../sdk/payment/asset/AssetId.java | 2 +- .../sdk/serializer/cbor/CborSerializer.java | 2 +- .../sdk/smt/plain/FinalizedNodeBranch.java | 4 +- .../sdk/smt/plain/SparseMerkleTreePath.java | 6 +- .../smt/plain/SparseMerkleTreePathStep.java | 2 +- .../sdk/smt/sum/FinalizedNodeBranch.java | 4 +- .../sdk/smt/sum/SparseMerkleSumTreePath.java | 6 +- .../smt/sum/SparseMerkleSumTreePathStep.java | 2 +- .../unicitylabs/sdk/transaction/Address.java | 100 --- .../transaction/CertifiedMintTransaction.java | 9 +- .../CertifiedTransferTransaction.java | 39 +- .../sdk/transaction/MintTransaction.java | 69 +- .../unicitylabs/sdk/transaction/Token.java | 48 +- .../sdk/transaction/Transaction.java | 10 +- .../sdk/transaction/TransferTransaction.java | 64 +- ...tifiedMintTransactionVerificationRule.java | 27 +- ...edTransferTransactionVerificationRule.java | 30 +- .../MintJustificationVerifier.java | 33 + .../MintJustificationVerifierService.java | 70 +++ .../sdk/TestApiKeyIntegration.java | 6 +- .../sdk/api/InclusionProofTest.java | 6 +- .../sdk/common/CommonTestFlow.java | 14 +- .../org/unicitylabs/sdk/e2e/TokenE2ETest.java | 2 + .../functional/FunctionalCommonFlowTest.java | 2 + .../functional/payment/SplitBuilderTest.java | 588 ++---------------- .../SplitMintJustificationVerifierTest.java | 474 ++++++++++++++ .../payment/TestSplitPaymentData.java | 95 --- .../functional/payment/TokenSplitTest.java | 111 ++++ .../org/unicitylabs/sdk/utils/TokenUtils.java | 102 +-- 40 files changed, 1280 insertions(+), 1280 deletions(-) rename src/main/java/org/unicitylabs/sdk/payment/{SplitReasonProof.java => SplitAssetProof.java} (79%) create mode 100644 src/main/java/org/unicitylabs/sdk/payment/SplitMintJustification.java create mode 100644 src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java delete mode 100644 src/main/java/org/unicitylabs/sdk/payment/SplitReason.java delete mode 100644 src/main/java/org/unicitylabs/sdk/transaction/Address.java create mode 100644 src/main/java/org/unicitylabs/sdk/transaction/verification/MintJustificationVerifier.java create mode 100644 src/main/java/org/unicitylabs/sdk/transaction/verification/MintJustificationVerifierService.java create mode 100644 src/test/java/org/unicitylabs/sdk/functional/payment/SplitMintJustificationVerifierTest.java delete mode 100644 src/test/java/org/unicitylabs/sdk/functional/payment/TestSplitPaymentData.java create mode 100644 src/test/java/org/unicitylabs/sdk/functional/payment/TokenSplitTest.java diff --git a/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java b/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java index 0237ee2..d4388df 100644 --- a/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java +++ b/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java @@ -100,8 +100,8 @@ public byte[] toCbor() { InclusionProof.CBOR_TAG, CborSerializer.encodeArray( CborSerializer.encodeUnsignedInteger(InclusionProof.VERSION), - CborSerializer.encodeOptional(this.certificationData, CertificationData::toCbor), - CborSerializer.encodeOptional(this.inclusionCertificate, (inclusionCertificate) -> + CborSerializer.encodeNullable(this.certificationData, CertificationData::toCbor), + CborSerializer.encodeNullable(this.inclusionCertificate, (inclusionCertificate) -> CborSerializer.encodeByteString(inclusionCertificate.encode()) ), this.unicityCertificate.toCbor() 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 a7bc2dd..cb79a19 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java @@ -182,13 +182,13 @@ public byte[] toCbor() { CborSerializer.encodeUnsignedInteger(InputRecord.VERSION), CborSerializer.encodeUnsignedInteger(this.roundNumber), CborSerializer.encodeUnsignedInteger(this.epoch), - CborSerializer.encodeOptional(this.previousHash, CborSerializer::encodeByteString), + CborSerializer.encodeNullable(this.previousHash, CborSerializer::encodeByteString), CborSerializer.encodeByteString(this.hash), CborSerializer.encodeByteString(this.summaryValue), CborSerializer.encodeUnsignedInteger(this.timestamp), - CborSerializer.encodeOptional(this.blockHash, CborSerializer::encodeByteString), + CborSerializer.encodeNullable(this.blockHash, CborSerializer::encodeByteString), CborSerializer.encodeUnsignedInteger(this.sumOfEarnedFees), - CborSerializer.encodeOptional(this.executedTransactionsHash, + CborSerializer.encodeNullable(this.executedTransactionsHash, CborSerializer::encodeByteString) )); } 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 81ff6a2..4cf16c8 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java @@ -137,7 +137,7 @@ public static DataHash calculateShardTreeCertificateRootHash( DataHash rootHash = new DataHasher(HashAlgorithm.SHA256) .update(inputRecord.toCbor()) .update( - CborSerializer.encodeOptional(technicalRecordHash, CborSerializer::encodeByteString)) + CborSerializer.encodeNullable(technicalRecordHash, CborSerializer::encodeByteString)) .update(CborSerializer.encodeByteString(shardConfigurationHash)) .digest(); @@ -201,7 +201,7 @@ public byte[] toCbor() { CborSerializer.encodeArray( CborSerializer.encodeUnsignedInteger(UnicityCertificate.VERSION), this.inputRecord.toCbor(), - CborSerializer.encodeOptional(this.technicalRecordHash, + CborSerializer.encodeNullable(this.technicalRecordHash, CborSerializer::encodeByteString), CborSerializer.encodeByteString(this.shardConfigurationHash), this.shardTreeCertificate.toCbor(), 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 73f89e6..9885eb4 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java @@ -211,9 +211,9 @@ public byte[] toCbor() { CborSerializer.encodeUnsignedInteger(this.rootChainRoundNumber), CborSerializer.encodeUnsignedInteger(this.epoch), CborSerializer.encodeUnsignedInteger(this.timestamp), - CborSerializer.encodeOptional(this.previousHash, CborSerializer::encodeByteString), + CborSerializer.encodeNullable(this.previousHash, CborSerializer::encodeByteString), CborSerializer.encodeByteString(this.hash), - CborSerializer.encodeOptional( + CborSerializer.encodeNullable( this.signatures, (signatures) -> CborSerializer.encodeMap( new CborMap( diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitReasonProof.java b/src/main/java/org/unicitylabs/sdk/payment/SplitAssetProof.java similarity index 79% rename from src/main/java/org/unicitylabs/sdk/payment/SplitReasonProof.java rename to src/main/java/org/unicitylabs/sdk/payment/SplitAssetProof.java index ae3c2c7..e756b95 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitReasonProof.java +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitAssetProof.java @@ -7,16 +7,17 @@ import org.unicitylabs.sdk.smt.sum.SparseMerkleSumTreePath; import java.util.List; +import java.util.Objects; /** * Proof material for one split reason entry. */ -public class SplitReasonProof { +public class SplitAssetProof { private final AssetId assetId; private final SparseMerkleTreePath aggregationPath; private final SparseMerkleSumTreePath assetTreePath; - private SplitReasonProof( + private SplitAssetProof( AssetId assetId, SparseMerkleTreePath aggregationPath, SparseMerkleSumTreePath assetTreePath @@ -62,12 +63,12 @@ public SparseMerkleSumTreePath getAssetTreePath() { * * @return split reason proof */ - public static SplitReasonProof create( + public static SplitAssetProof create( AssetId assetId, SparseMerkleTreePath aggregationPath, SparseMerkleSumTreePath assetTreePath ) { - return new SplitReasonProof(assetId, aggregationPath, assetTreePath); + return new SplitAssetProof(assetId, aggregationPath, assetTreePath); } /** @@ -77,10 +78,10 @@ public static SplitReasonProof create( * * @return split reason proof */ - public static SplitReasonProof fromCbor(byte[] bytes) { + public static SplitAssetProof fromCbor(byte[] bytes) { List data = CborDeserializer.decodeArray(bytes); - return new SplitReasonProof( + return new SplitAssetProof( AssetId.fromCbor(data.get(0)), SparseMerkleTreePath.fromCbor(data.get(1)), SparseMerkleSumTreePath.fromCbor(data.get(2)) @@ -99,4 +100,16 @@ public byte[] toCbor() { this.assetTreePath.toCbor() ); } + + @Override + public boolean equals(Object o) { + if (!(o instanceof SplitAssetProof)) return false; + SplitAssetProof that = (SplitAssetProof) o; + return Objects.equals(this.assetId, that.assetId); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.assetId); + } } diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustification.java b/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustification.java new file mode 100644 index 0000000..7f64f21 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustification.java @@ -0,0 +1,101 @@ +package org.unicitylabs.sdk.payment; + +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.Token; + +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Mint justification for a split-output token, carrying the burn token of the source and the + * inclusion proofs that link each output asset back to the burned source aggregation tree. + */ +public final class SplitMintJustification { + public static final long CBOR_TAG = 39044; + + private final Token token; + private final List proofs; + + private SplitMintJustification( + Token token, + List proofs + ) { + this.token = token; + this.proofs = proofs; + } + + /** + * Get the burn token whose split produced this justification. + * + * @return burn token + */ + public Token getToken() { + return this.token; + } + + /** + * Get the inclusion proofs supporting this split mint justification. + * + * @return proofs + */ + public List getProofs() { + return this.proofs; + } + + /** + * Create a split mint justification. + * + * @param token burn token of the source token being split + * @param proofs inclusion proofs supporting split eligibility + * + * @return split mint justification + */ + public static SplitMintJustification create(Token token, Set proofs) { + Objects.requireNonNull(token, "token cannot be null"); + Objects.requireNonNull(proofs, "proofs cannot be null"); + + if (proofs.isEmpty()) { + throw new IllegalArgumentException("proofs cannot be empty"); + } + + return new SplitMintJustification(token, List.copyOf(proofs)); + } + + /** + * Deserialize split mint justification from CBOR bytes. + * + * @param bytes CBOR bytes + * + * @return split mint justification + */ + public static SplitMintJustification fromCbor(byte[] bytes) { + CborDeserializer.CborTag tag = CborDeserializer.decodeTag(bytes); + if (tag.getTag() != SplitMintJustification.CBOR_TAG) { + throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); + } + List data = CborDeserializer.decodeArray(tag.getData()); + return SplitMintJustification.create( + Token.fromCbor(data.get(0)), + CborDeserializer.decodeArray(data.get(1)).stream().map(SplitAssetProof::fromCbor).collect(Collectors.toSet()) + ); + } + + /** + * Serialize split mint justification to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeTag( + SplitMintJustification.CBOR_TAG, + CborSerializer.encodeArray( + this.token.toCbor(), + CborSerializer.encodeArray(this.proofs.stream().map(SplitAssetProof::toCbor).toArray(byte[][]::new)) + ) + ); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java b/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java new file mode 100644 index 0000000..96b6423 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java @@ -0,0 +1,200 @@ +package org.unicitylabs.sdk.payment; + +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.payment.asset.Asset; +import org.unicitylabs.sdk.payment.asset.AssetId; +import org.unicitylabs.sdk.predicate.EncodedPredicate; +import org.unicitylabs.sdk.predicate.builtin.BurnPredicate; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.smt.MerkleTreePathVerificationResult; +import org.unicitylabs.sdk.transaction.CertifiedMintTransaction; +import org.unicitylabs.sdk.transaction.Transaction; +import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifier; +import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifierService; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +import java.math.BigInteger; +import java.util.*; + +public class SplitMintJustificationVerifier implements MintJustificationVerifier { + private final RootTrustBase trustBase; + private final PredicateVerifierService predicateVerifier; + private final PaymentDataDeserializer decodePaymentData; + + public SplitMintJustificationVerifier( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + PaymentDataDeserializer decodePaymentData + ) { + this.trustBase = Objects.requireNonNull(trustBase, "trustBase cannot be null"); + this.predicateVerifier = Objects.requireNonNull(predicateVerifier, "predicateVerifier cannot be null"); + this.decodePaymentData = Objects.requireNonNull(decodePaymentData, "decodePaymentData cannot be null"); + } + + @Override + public long getTag() { + return SplitMintJustification.CBOR_TAG; + } + + @Override + public VerificationResult verify(CertifiedMintTransaction transaction, MintJustificationVerifierService mintJustificationVerifier) { + Objects.requireNonNull(transaction, "transaction cannot be null"); + Objects.requireNonNull(mintJustificationVerifier, "mintJustificationVerifierService cannot be null"); + + var justificationBytes = transaction.getJustification().orElse(null); + if (justificationBytes == null) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Transaction has no justification." + ); + } + + var justification = SplitMintJustification.fromCbor(justificationBytes); + var paymentDataBytes = transaction.getData().orElse(null); + var paymentData = paymentDataBytes != null ? this.decodePaymentData.decode(paymentDataBytes) : null; + + if (paymentData == null || paymentData.getAssets() == null) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Assets data is missing." + ); + } + + VerificationResult verificationResult = justification.getToken() + .verify(trustBase, predicateVerifier, mintJustificationVerifier); + if (verificationResult.getStatus() != VerificationStatus.OK) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Burn token verification failed.", + verificationResult + ); + } + + if (paymentData.getAssets().size() != justification.getProofs().size()) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Total amount of assets differ in token and proofs." + ); + } + + Map assets = new HashMap<>(); + for (Asset asset : paymentData.getAssets()) { + if (asset == null) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Asset data is missing." + ); + } + + AssetId assetId = asset.getId(); + if (assets.putIfAbsent(assetId, asset) != null) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + String.format("Duplicate asset id %s found in asset data.", assetId) + ); + } + } + + var validatedAssets = new HashSet(); + Transaction burnTokenLastTransaction = justification.getToken().getLatestTransaction(); + DataHash root = justification.getProofs().get(0).getAggregationPath().getRootHash(); + for (SplitAssetProof proof : justification.getProofs()) { + MerkleTreePathVerificationResult aggregationPathResult = proof.getAggregationPath() + .verify(proof.getAssetId().toBitString().toBigInteger()); + if (!aggregationPathResult.isSuccessful()) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + String.format("Aggregation path verification failed for asset: %s", proof.getAssetId()) + ); + } + + MerkleTreePathVerificationResult assetTreePathResult = proof.getAssetTreePath() + .verify(transaction.getTokenId().toBitString().toBigInteger()); + if (!assetTreePathResult.isSuccessful()) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + String.format("Asset tree path verification failed for token: %s", transaction.getTokenId()) + ); + } + + if (!proof.getAggregationPath().getRootHash().equals(root)) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Current proof is not derived from the same asset tree as other proofs." + ); + } + + if (!Arrays.equals( + proof.getAssetTreePath().getRootHash().getImprint(), + proof.getAggregationPath().getSteps().get(0).getData().orElse(null) + )) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Asset tree root does not match aggregation path leaf." + ); + } + + Asset asset = assets.get(proof.getAssetId()); + + if (asset == null) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + String.format("Asset id %s not found in asset data.", proof.getAssetId()) + ); + } + + BigInteger amount = asset.getValue(); + + if (!proof.getAssetTreePath().getSteps().get(0).getValue().equals(amount)) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + String.format("Asset amount for asset id %s does not match asset tree leaf.", proof.getAssetId()) + ); + } + + var recipient = burnTokenLastTransaction != null + ? EncodedPredicate.fromPredicate(burnTokenLastTransaction.getRecipient()) + : null; + var expectedRecipient = EncodedPredicate.fromPredicate( + BurnPredicate.create(proof.getAggregationPath().getRootHash().getImprint()) + ); + + if (!expectedRecipient.equals(recipient)) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Aggregation path root does not match burn predicate." + ); + } + + validatedAssets.add(proof.getAssetId()); + } + + if (validatedAssets.size() != paymentData.getAssets().size()) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Some assets proofs are missing from the token." + ); + } + + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.OK + ); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentData.java b/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentData.java index a3b2804..8170439 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentData.java +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentData.java @@ -9,5 +9,5 @@ public interface SplitPaymentData extends PaymentData { * * @return split reason */ - SplitReason getReason(); + SplitMintJustification getReason(); } diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitReason.java b/src/main/java/org/unicitylabs/sdk/payment/SplitReason.java deleted file mode 100644 index aa7760e..0000000 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitReason.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.unicitylabs.sdk.payment; - -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.transaction.Token; - -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; - -/** - * The reason for token splitting represented by an input token and inclusion proofs. - */ -public final class SplitReason { - - private final Token token; - private final List proofs; - - private SplitReason( - Token token, - List proofs - ) { - this.token = token; - this.proofs = List.copyOf(proofs); - } - - /** - * Get the token being split. - * - * @return token - */ - public Token getToken() { - return this.token; - } - - /** - * Get proofs supporting the split reason. - * - * @return proof list - */ - public List getProofs() { - return this.proofs; - } - - /** - * Create a split reason. - * - * @param token token being split - * @param proofs proofs supporting split eligibility - * - * @return split reason - */ - public static SplitReason create(Token token, List proofs) { - Objects.requireNonNull(token, "token cannot be null"); - Objects.requireNonNull(proofs, "proofs cannot be null"); - - if (proofs.isEmpty()) { - throw new IllegalArgumentException("proofs cannot be empty"); - } - - return new SplitReason(token, proofs); - } - - /** - * Deserialize split reason from CBOR bytes. - * - * @param bytes CBOR bytes - * - * @return split reason - */ - public static SplitReason fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); - - return new SplitReason( - Token.fromCbor(data.get(0)), - CborDeserializer.decodeArray(data.get(1)).stream().map(SplitReasonProof::fromCbor).collect(Collectors.toList()) - ); - } - - /** - * Serialize split reason to CBOR bytes. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeArray( - this.token.toCbor(), - CborSerializer.encodeArray(this.proofs.stream().map(SplitReasonProof::toCbor).toArray(byte[][]::new)) - ); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitResult.java b/src/main/java/org/unicitylabs/sdk/payment/SplitResult.java index fa9bfb4..ac2afb0 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitResult.java +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitResult.java @@ -14,9 +14,9 @@ public class SplitResult { private final TransferTransaction burnTransaction; - private final Map> proofs; + private final Map> proofs; - SplitResult(TransferTransaction burnTransaction, Map> proofs) { + SplitResult(TransferTransaction burnTransaction, Map> proofs) { this.burnTransaction = burnTransaction; this.proofs = Map.copyOf( proofs.entrySet().stream() @@ -40,7 +40,7 @@ public TransferTransaction getBurnTransaction() { * * @return split proofs map */ - public Map> getProofs() { + public Map> getProofs() { return this.proofs; } } diff --git a/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java b/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java index 377ebc0..10d16bc 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java +++ b/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java @@ -1,26 +1,20 @@ package org.unicitylabs.sdk.payment; -import org.unicitylabs.sdk.api.bft.RootTrustBase; -import org.unicitylabs.sdk.crypto.hash.DataHash; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.unicitylabs.sdk.payment.asset.Asset; import org.unicitylabs.sdk.payment.asset.AssetId; -import org.unicitylabs.sdk.predicate.Predicate; import org.unicitylabs.sdk.predicate.builtin.BurnPredicate; -import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.smt.BranchExistsException; import org.unicitylabs.sdk.smt.LeafOutOfBoundsException; -import org.unicitylabs.sdk.smt.MerkleTreePathVerificationResult; import org.unicitylabs.sdk.smt.plain.SparseMerkleTree; import org.unicitylabs.sdk.smt.plain.SparseMerkleTreeRootNode; import org.unicitylabs.sdk.smt.sum.SparseMerkleSumTree; import org.unicitylabs.sdk.smt.sum.SparseMerkleSumTreeRootNode; -import org.unicitylabs.sdk.transaction.*; -import org.unicitylabs.sdk.util.verification.VerificationResult; -import org.unicitylabs.sdk.util.verification.VerificationStatus; +import org.unicitylabs.sdk.transaction.Token; +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.transaction.TransferTransaction; -import java.math.BigInteger; import java.security.SecureRandom; import java.util.*; import java.util.Map.Entry; @@ -40,7 +34,6 @@ private TokenSplit() { * Create split proofs and burn transaction for provided target token distributions. * * @param token source token being split - * @param ownerPredicate owner predicate of the source token * @param paymentDataDeserializer payment data decoder for source token payload * @param splitTokens destination token ids and their asset allocations * @@ -51,14 +44,16 @@ private TokenSplit() { */ public static SplitResult split( Token token, - Predicate ownerPredicate, PaymentDataDeserializer paymentDataDeserializer, Map> splitTokens ) throws LeafOutOfBoundsException, BranchExistsException { Objects.requireNonNull(token, "Token cannot be null"); - Objects.requireNonNull(ownerPredicate, "Owner predicate cannot be null"); Objects.requireNonNull(paymentDataDeserializer, "Payment data deserializer cannot be null"); Objects.requireNonNull(splitTokens, "Split tokens cannot be null"); + byte[] paymentDataBytes = token.getGenesis().getData().orElse(null); + if (paymentDataBytes == null) { + throw new IllegalArgumentException("Token genesis data must be present"); + } HashMap trees = new HashMap(); for (Entry> entry : splitTokens.entrySet()) { @@ -76,7 +71,7 @@ public static SplitResult split( } } - PaymentData paymentData = paymentDataDeserializer.decode(token.getGenesis().getData()); + PaymentData paymentData = paymentDataDeserializer.decode(paymentDataBytes); Map assets = paymentData.getAssets().stream() .collect(Collectors.toMap( Asset::getId, @@ -118,23 +113,22 @@ public static SplitResult split( SparseMerkleTreeRootNode aggregationRoot = aggregationTree.calculateRoot(); BurnPredicate burnPredicate = BurnPredicate.create(aggregationRoot.getRootHash().getImprint()); - byte[] x = new byte[32]; - RANDOM.nextBytes(x); + byte[] nonce = new byte[32]; + RANDOM.nextBytes(nonce); TransferTransaction burnTransaction = TransferTransaction.create( token, - ownerPredicate, - Address.fromPredicate(burnPredicate), - x, + burnPredicate, + nonce, CborSerializer.encodeNull() ); - HashMap> proofs = new HashMap>(); + HashMap> proofs = new HashMap>(); for (Entry> entry : splitTokens.entrySet()) { proofs.put( entry.getKey(), List.copyOf( - entry.getValue().stream().map(asset -> SplitReasonProof.create( + entry.getValue().stream().map(asset -> SplitAssetProof.create( asset.getId(), aggregationRoot.getPath(asset.getId().toBitString().toBigInteger()), assetTreeRoots.get(asset.getId()).getPath(entry.getKey().toBitString().toBigInteger()) @@ -147,160 +141,4 @@ public static SplitResult split( return new SplitResult(burnTransaction, proofs); } - /** - * Verify split reason and proofs embedded in a token. - * - * @param token token to verify - * @param paymentDataDeserializer split payment data deserializer - * @param trustBase trust base for token certification verification - * @param predicateVerifier predicate verifier service - * - * @return verification result - */ - public static VerificationResult verify( - Token token, - SplitPaymentDataDeserializer paymentDataDeserializer, - RootTrustBase trustBase, - PredicateVerifierService predicateVerifier - ) { - Objects.requireNonNull(token, "Token cannot be null"); - Objects.requireNonNull(paymentDataDeserializer, "Payment data deserializer cannot be null"); - Objects.requireNonNull(trustBase, "Trust base cannot be null"); - Objects.requireNonNull(predicateVerifier, "Predicate verifier cannot be null"); - - SplitPaymentData data = paymentDataDeserializer.decode(token.getGenesis().getData()); - - if (data.getAssets() == null) { - return new VerificationResult<>( - "TokenSplitReasonVerificationRule", - VerificationStatus.FAIL, - "Assets data is missing." - ); - } - - if (data.getReason() == null) { - return new VerificationResult<>( - "TokenSplitReasonVerificationRule", - VerificationStatus.FAIL, - "Reason is missing." - ); - } - - VerificationResult verificationResult = data.getReason().getToken() - .verify(trustBase, predicateVerifier); - if (verificationResult.getStatus() != VerificationStatus.OK) { - return new VerificationResult<>( - "TokenSplitReasonVerificationRule", - VerificationStatus.FAIL, - "Burn token verification failed.", - verificationResult - ); - } - - if (data.getAssets().size() != data.getReason().getProofs().size()) { - return new VerificationResult<>( - "TokenSplitReasonVerificationRule", - VerificationStatus.FAIL, - "Total amount of assets differ in token and proofs." - ); - } - - Map assets = new HashMap<>(); - for (Asset asset : data.getAssets()) { - if (asset == null) { - return new VerificationResult<>( - "TokenSplitReasonVerificationRule", - VerificationStatus.FAIL, - "Asset data is missing." - ); - } - - AssetId assetId = asset.getId(); - if (assets.putIfAbsent(assetId, asset) != null) { - return new VerificationResult<>( - "TokenSplitReasonVerificationRule", - VerificationStatus.FAIL, - String.format("Duplicate asset id %s found in asset data.", assetId) - ); - } - } - - Transaction burnTokenLastTransaction = data.getReason().getToken().getLatestTransaction(); - DataHash root = data.getReason().getProofs().get(0).getAggregationPath().getRootHash(); - for (SplitReasonProof proof : data.getReason().getProofs()) { - MerkleTreePathVerificationResult aggregationPathResult = proof.getAggregationPath() - .verify(proof.getAssetId().toBitString().toBigInteger()); - if (!aggregationPathResult.isSuccessful()) { - return new VerificationResult<>( - "TokenSplitReasonVerificationRule", - VerificationStatus.FAIL, - String.format("Aggregation path verification failed for asset: %s", proof.getAssetId()) - ); - } - - MerkleTreePathVerificationResult assetTreePathResult = proof.getAssetTreePath() - .verify(token.getId().toBitString().toBigInteger()); - if (!assetTreePathResult.isSuccessful()) { - return new VerificationResult<>( - "TokenSplitReasonVerificationRule", - VerificationStatus.FAIL, - String.format("Asset tree path verification failed for token: %s", token.getId()) - ); - } - - if (!proof.getAggregationPath().getRootHash().equals(root)) { - return new VerificationResult<>( - "TokenSplitReasonVerificationRule", - VerificationStatus.FAIL, - "Current proof is not derived from the same asset tree as other proofs." - ); - } - - if (!Arrays.equals( - proof.getAssetTreePath().getRootHash().getImprint(), - proof.getAggregationPath().getSteps().get(0).getData().orElse(null) - )) { - return new VerificationResult<>( - "TokenSplitReasonVerificationRule", - VerificationStatus.FAIL, - "Asset tree root does not match aggregation path leaf." - ); - } - - Asset asset = assets.get(proof.getAssetId()); - - if (asset == null) { - return new VerificationResult<>( - "TokenSplitReasonVerificationRule", - VerificationStatus.FAIL, - String.format("Asset id %s not found in asset data.", proof.getAssetId()) - ); - } - - BigInteger amount = asset.getValue(); - - if (!proof.getAssetTreePath().getSteps().get(0).getValue().equals(amount)) { - return new VerificationResult<>( - "TokenSplitReasonVerificationRule", - VerificationStatus.FAIL, - String.format("Asset amount for asset id %s does not match asset tree leaf.", proof.getAssetId()) - ); - } - - if (!burnTokenLastTransaction.getRecipient() - .equals(Address.fromPredicate(BurnPredicate.create(proof.getAggregationPath().getRootHash().getImprint())))) { - return new VerificationResult<>( - "TokenSplitReasonVerificationRule", - VerificationStatus.FAIL, - "Aggregation path root does not match burn predicate." - ); - } - } - - return new VerificationResult<>( - "TokenSplitReasonVerificationRule", - VerificationStatus.OK - ); - } - } diff --git a/src/main/java/org/unicitylabs/sdk/payment/asset/AssetId.java b/src/main/java/org/unicitylabs/sdk/payment/asset/AssetId.java index febf8f1..0d8ad4d 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/asset/AssetId.java +++ b/src/main/java/org/unicitylabs/sdk/payment/asset/AssetId.java @@ -74,7 +74,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Arrays.hashCode(bytes); + return Arrays.hashCode(this.bytes); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborSerializer.java b/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborSerializer.java index 9c52220..db5e3b6 100644 --- a/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborSerializer.java +++ b/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborSerializer.java @@ -22,7 +22,7 @@ private CborSerializer() { * @param value type * @return bytes */ - public static byte[] encodeOptional(T data, Function encoder) { + public static byte[] encodeNullable(T data, Function encoder) { if (data == null) { return new byte[]{(byte) 0xf6}; } diff --git a/src/main/java/org/unicitylabs/sdk/smt/plain/FinalizedNodeBranch.java b/src/main/java/org/unicitylabs/sdk/smt/plain/FinalizedNodeBranch.java index 7badfc9..f176570 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/plain/FinalizedNodeBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/plain/FinalizedNodeBranch.java @@ -50,13 +50,13 @@ public static FinalizedNodeBranch create( .update( CborSerializer.encodeArray( CborSerializer.encodeByteString(BigIntegerConverter.encode(path)), - CborSerializer.encodeOptional( + CborSerializer.encodeNullable( left == null ? null : left.getHash().getData(), CborSerializer::encodeByteString ), - CborSerializer.encodeOptional( + CborSerializer.encodeNullable( right == null ? null : right.getHash().getData(), diff --git a/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePath.java b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePath.java index 339ed76..a8ff834 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePath.java +++ b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePath.java @@ -65,7 +65,7 @@ public MerkleTreePathVerificationResult verify(BigInteger stateId) { .update( CborSerializer.encodeArray( CborSerializer.encodeByteString(BigIntegerConverter.encode(step.getPath())), - CborSerializer.encodeOptional( + CborSerializer.encodeNullable( step.getData().orElse(null), CborSerializer::encodeByteString ) @@ -91,8 +91,8 @@ public MerkleTreePathVerificationResult verify(BigInteger stateId) { .update( CborSerializer.encodeArray( CborSerializer.encodeByteString(BigIntegerConverter.encode(step.getPath())), - CborSerializer.encodeOptional(left, CborSerializer::encodeByteString), - CborSerializer.encodeOptional(right, CborSerializer::encodeByteString) + CborSerializer.encodeNullable(left, CborSerializer::encodeByteString), + CborSerializer.encodeNullable(right, CborSerializer::encodeByteString) ) ) .digest(); diff --git a/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePathStep.java b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePathStep.java index e86f23b..e0fefb4 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePathStep.java +++ b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePathStep.java @@ -79,7 +79,7 @@ public static SparseMerkleTreePathStep fromCbor(byte[] bytes) { public byte[] toCbor() { return CborSerializer.encodeArray( CborSerializer.encodeByteString(BigIntegerConverter.encode(this.path)), - CborSerializer.encodeOptional(this.data, CborSerializer::encodeByteString) + CborSerializer.encodeNullable(this.data, CborSerializer::encodeByteString) ); } diff --git a/src/main/java/org/unicitylabs/sdk/smt/sum/FinalizedNodeBranch.java b/src/main/java/org/unicitylabs/sdk/smt/sum/FinalizedNodeBranch.java index 58b308e..da42dcd 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/sum/FinalizedNodeBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/FinalizedNodeBranch.java @@ -58,9 +58,9 @@ public static FinalizedNodeBranch create( .update( CborSerializer.encodeArray( CborSerializer.encodeByteString(BigIntegerConverter.encode(path)), - CborSerializer.encodeOptional(leftHash, CborSerializer::encodeByteString), + CborSerializer.encodeNullable(leftHash, CborSerializer::encodeByteString), CborSerializer.encodeByteString(BigIntegerConverter.encode(leftCounter)), - CborSerializer.encodeOptional(rightHash, CborSerializer::encodeByteString), + CborSerializer.encodeNullable(rightHash, CborSerializer::encodeByteString), CborSerializer.encodeByteString(BigIntegerConverter.encode(rightCounter)) ) ) diff --git a/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePath.java b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePath.java index 1adf448..907d2cb 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePath.java +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePath.java @@ -69,7 +69,7 @@ public MerkleTreePathVerificationResult verify(BigInteger stateId) { .update( CborSerializer.encodeArray( CborSerializer.encodeByteString(BigIntegerConverter.encode(step.getPath())), - CborSerializer.encodeOptional( + CborSerializer.encodeNullable( step.getData().orElse(null), CborSerializer::encodeByteString ), @@ -98,9 +98,9 @@ public MerkleTreePathVerificationResult verify(BigInteger stateId) { .update( CborSerializer.encodeArray( CborSerializer.encodeByteString(BigIntegerConverter.encode(step.getPath())), - CborSerializer.encodeOptional(leftHash, CborSerializer::encodeByteString), + CborSerializer.encodeNullable(leftHash, CborSerializer::encodeByteString), CborSerializer.encodeByteString(BigIntegerConverter.encode(leftCounter)), - CborSerializer.encodeOptional(rightHash, CborSerializer::encodeByteString), + CborSerializer.encodeNullable(rightHash, CborSerializer::encodeByteString), CborSerializer.encodeByteString(BigIntegerConverter.encode(rightCounter)) ) ) diff --git a/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePathStep.java b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePathStep.java index 06dc725..dc0168f 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePathStep.java +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePathStep.java @@ -84,7 +84,7 @@ public static SparseMerkleSumTreePathStep fromCbor(byte[] bytes) { public byte[] toCbor() { return CborSerializer.encodeArray( CborSerializer.encodeByteString(BigIntegerConverter.encode(this.path)), - CborSerializer.encodeOptional(this.data, CborSerializer::encodeByteString), + CborSerializer.encodeNullable(this.data, CborSerializer::encodeByteString), CborSerializer.encodeByteString(BigIntegerConverter.encode(this.value)) ); } diff --git a/src/main/java/org/unicitylabs/sdk/transaction/Address.java b/src/main/java/org/unicitylabs/sdk/transaction/Address.java deleted file mode 100644 index c25c724..0000000 --- a/src/main/java/org/unicitylabs/sdk/transaction/Address.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.unicitylabs.sdk.transaction; - -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.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.util.HexConverter; - -import java.util.Arrays; - -/** - * Transaction address. - */ -public class Address { - - private final byte[] bytes; - - private Address(byte[] bytes) { - this.bytes = bytes; - } - - /** - * Returns a copy of the address bytes. - * - * @return address bytes - */ - public byte[] getBytes() { - return Arrays.copyOf(this.bytes, this.bytes.length); - } - - /** - * Create an address from bytes. - * - * @param bytes address bytes - * - * @return address - */ - public static Address fromBytes(byte[] bytes) { - if (bytes == null || bytes.length != 32) { - throw new IllegalArgumentException("Invalid address length"); - } - - return new Address(Arrays.copyOf(bytes, bytes.length)); - } - - /** - * Deserialize an address from CBOR bytes. - * - * @param bytes CBOR bytes - * - * @return address - */ - public static Address fromCbor(byte[] bytes) { - return Address.fromBytes(CborDeserializer.decodeByteString(bytes)); - } - - /** - * Create an address from predicate. - * - * @param predicate predicate - * - * @return address - */ - public static Address fromPredicate(Predicate predicate) { - DataHash hash = new DataHasher(HashAlgorithm.SHA256).update( - EncodedPredicate.fromPredicate(predicate).toCbor()).digest(); - return new Address(hash.getData()); - } - - /** - * Serialize address to CBOR bytes. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeByteString(this.bytes); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof Address)) { - return false; - } - Address address = (Address) o; - return Arrays.equals(this.bytes, address.bytes); - } - - @Override - public int hashCode() { - return Arrays.hashCode(this.bytes); - } - - @Override - public String toString() { - return String.format("Address{bytes=%s}", HexConverter.encode(this.bytes)); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java index f9ff5e7..4486032 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java @@ -13,6 +13,7 @@ import org.unicitylabs.sdk.util.verification.VerificationResult; import java.util.List; +import java.util.Optional; /** * Mint transaction bundled with an inclusion proof. @@ -28,7 +29,7 @@ private CertifiedMintTransaction(MintTransaction transaction, InclusionProof inc } @Override - public byte[] getData() { + public Optional getData() { return this.transaction.getData(); } @@ -38,7 +39,7 @@ public Predicate getLockScript() { } @Override - public Address getRecipient() { + public Predicate getRecipient() { return this.transaction.getRecipient(); } @@ -65,6 +66,10 @@ public TokenType getTokenType() { return this.transaction.getTokenType(); } + public Optional getJustification() { + return this.transaction.getJustification(); + } + @Override public byte[] getNonce() { return this.transaction.getNonce(); diff --git a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java index a74d318..973713b 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java @@ -13,6 +13,7 @@ import org.unicitylabs.sdk.util.verification.VerificationResult; import java.util.List; +import java.util.Optional; /** * Transfer transaction with a verified inclusion proof. @@ -30,51 +31,26 @@ private CertifiedTransferTransaction( this.inclusionProof = inclusionProof; } - /** - * Get transaction payload data. - * - * @return payload data bytes - */ @Override - public byte[] getData() { + public Optional getData() { return this.transaction.getData(); } - /** - * Get predicate locking script for this transaction output. - * - * @return lock script predicate - */ @Override public Predicate getLockScript() { return this.transaction.getLockScript(); } - /** - * Get recipient address of this transaction. - * - * @return recipient address - */ @Override - public Address getRecipient() { + public Predicate getRecipient() { return this.transaction.getRecipient(); } - /** - * Get source state hash of this transaction. - * - * @return source state hash - */ @Override public DataHash getSourceStateHash() { return this.transaction.getSourceStateHash(); } - /** - * Get transaction chosen random bytes. - * - * @return random bytes - */ @Override public byte[] getNonce() { return this.transaction.getNonce(); @@ -93,14 +69,17 @@ public InclusionProof getInclusionProof() { * Deserialize a certified transfer transaction from CBOR bytes. * * @param bytes CBOR encoded certified transfer transaction + * @param token token providing the source state for the deserialized transfer * * @return certified transfer transaction */ - public static CertifiedTransferTransaction fromCbor(byte[] bytes) { + public static CertifiedTransferTransaction fromCbor(byte[] bytes, Token token) { List data = CborDeserializer.decodeArray(bytes); - return new CertifiedTransferTransaction(TransferTransaction.fromCbor(data.get(0)), - InclusionProof.fromCbor(data.get(1))); + return new CertifiedTransferTransaction( + TransferTransaction.fromCbor(data.get(0), token), + InclusionProof.fromCbor(data.get(1)) + ); } /** diff --git a/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java index 2cd8675..2e8faa1 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java @@ -7,6 +7,7 @@ import org.unicitylabs.sdk.crypto.hash.DataHasher; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.Predicate; import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; @@ -18,6 +19,7 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.Optional; /** @@ -32,17 +34,19 @@ public class MintTransaction implements Transaction { private final MintTransactionState sourceStateHash; private final Predicate lockScript; - private final Address recipient; + private final Predicate recipient; private final TokenId tokenId; private final TokenType tokenType; + private final byte[] justification; private final byte[] data; private MintTransaction( MintTransactionState sourceStateHash, Predicate lockScript, - Address recipient, + Predicate recipient, TokenId tokenId, TokenType tokenType, + byte[] justification, byte[] data ) { this.sourceStateHash = sourceStateHash; @@ -50,6 +54,7 @@ private MintTransaction( this.recipient = recipient; this.tokenId = tokenId; this.tokenType = tokenType; + this.justification = justification; this.data = data; } @@ -58,30 +63,18 @@ public int getVersion() { } - /** - * Retrieves the state hash of the source state. - * - * @return the source state hash represented as a {@code MintTransactionState}. - */ + @Override public MintTransactionState getSourceStateHash() { return this.sourceStateHash; } - /** - * Retrieves the lock script. - * - * @return a {@code Predicate} representing the lock script. - */ + @Override public Predicate getLockScript() { return this.lockScript; } - /** - * Retrieves the initial owner address. - * - * @return the recipient address as an {@code Address}. - */ - public Address getRecipient() { + @Override + public Predicate getRecipient() { return this.recipient; } @@ -103,9 +96,18 @@ public TokenType getTokenType() { return this.tokenType; } + /** + * Retrieves the justification for the mint transaction, if any. + * + * @return optional justification bytes + */ + public Optional getJustification() { + return Optional.ofNullable(this.justification != null ? Arrays.copyOf(this.justification, this.justification.length) : null); + } + @Override - public byte[] getData() { - return this.data; + public Optional getData() { + return Optional.ofNullable(this.data != null ? Arrays.copyOf(this.data, this.data.length) : null); } @Override @@ -116,23 +118,24 @@ public byte[] getNonce() { /** * Create a mint transaction. * - * @param recipient recipient address + * @param recipient recipient predicate * @param tokenId token identifier * @param tokenType token type identifier - * @param data payload bytes + * @param justification mint justification bytes, may be null + * @param data payload bytes, may be null * * @return mint transaction */ public static MintTransaction create( - Address recipient, + Predicate recipient, TokenId tokenId, TokenType tokenType, + byte[] justification, byte[] data ) { Objects.requireNonNull(recipient, "Recipient cannot be null"); Objects.requireNonNull(tokenId, "Token ID cannot be null"); Objects.requireNonNull(tokenType, "Token type cannot be null"); - Objects.requireNonNull(data, "Data cannot be null"); SigningService signingService = MintSigningService.create(tokenId); return new MintTransaction( @@ -141,7 +144,8 @@ public static MintTransaction create( recipient, tokenId, tokenType, - Arrays.copyOf(data, data.length) + justification != null ? Arrays.copyOf(justification, justification.length) : null, + data != null ? Arrays.copyOf(data, data.length) : null ); } @@ -163,13 +167,13 @@ public static MintTransaction fromCbor(byte[] bytes) { if (version != MintTransaction.VERSION) { throw new CborSerializationException(String.format("Unsupported version: %s", version)); } - List aux = CborDeserializer.decodeArray(data.get(3)); return MintTransaction.create( - Address.fromCbor(data.get(1)), + EncodedPredicate.fromCbor(data.get(1)), TokenId.fromCbor(data.get(2)), - TokenType.fromCbor(aux.get(0)), - CborDeserializer.decodeByteString(aux.get(1)) + TokenType.fromCbor(data.get(3)), + CborDeserializer.decodeNullable(data.get(4), CborDeserializer::decodeByteString), + CborDeserializer.decodeNullable(data.get(5), CborDeserializer::decodeByteString) ); } @@ -211,10 +215,11 @@ public byte[] toCbor() { MintTransaction.CBOR_TAG, CborSerializer.encodeArray( CborSerializer.encodeUnsignedInteger(MintTransaction.VERSION), - this.recipient.toCbor(), + EncodedPredicate.fromPredicate(this.recipient).toCbor(), this.tokenId.toCbor(), - CborSerializer.encodeArray(this.tokenType.toCbor(), - CborSerializer.encodeByteString(this.data)) + this.tokenType.toCbor(), + CborSerializer.encodeNullable(this.justification, CborSerializer::encodeByteString), + CborSerializer.encodeNullable(this.data, CborSerializer::encodeByteString) ) ); } diff --git a/src/main/java/org/unicitylabs/sdk/transaction/Token.java b/src/main/java/org/unicitylabs/sdk/transaction/Token.java index 5bb8326..be45c07 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/Token.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/Token.java @@ -7,13 +7,13 @@ import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.transaction.verification.CertifiedMintTransactionVerificationRule; import org.unicitylabs.sdk.transaction.verification.CertifiedTransferTransactionVerificationRule; +import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifierService; 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.stream.Collectors; /** * Immutable token aggregate containing the certified genesis mint transaction and transfer history. @@ -104,13 +104,16 @@ public static Token fromCbor(byte[] bytes) { if (version != Token.VERSION) { throw new CborSerializationException(String.format("Unsupported version: %s", version)); } - List transactions = CborDeserializer.decodeArray(data.get(2)); - return new Token( - CertifiedMintTransaction.fromCbor(data.get(1)), - transactions.stream().map(CertifiedTransferTransaction::fromCbor) - .collect(Collectors.toList()) - ); + CertifiedMintTransaction genesis = CertifiedMintTransaction.fromCbor(data.get(1)); + List transactionsCbor = CborDeserializer.decodeArray(data.get(2)); + + List transactions = new ArrayList<>(); + for (byte[] transaction : transactionsCbor) { + transactions.add(CertifiedTransferTransaction.fromCbor(transaction, new Token(genesis, transactions))); + } + + return new Token(genesis, transactions); } /** @@ -118,14 +121,19 @@ public static Token fromCbor(byte[] bytes) { * * @param trustBase trust base used for certification checks * @param predicateVerifier predicate verifier service + * @param mintJustificationVerifier mint justification verifier service * @param genesis certified mint transaction * @return verified token instance * @throws VerificationException if genesis verification fails */ - public static Token mint(RootTrustBase trustBase, PredicateVerifierService predicateVerifier, - CertifiedMintTransaction genesis) { + public static Token mint( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + MintJustificationVerifierService mintJustificationVerifier, + CertifiedMintTransaction genesis + ) { Token token = new Token(genesis); - VerificationResult result = token.verify(trustBase, predicateVerifier); + VerificationResult result = token.verify(trustBase, predicateVerifier, mintJustificationVerifier); if (result.getStatus() != VerificationStatus.OK) { throw new VerificationException("Invalid token genesis", result); } @@ -147,7 +155,6 @@ public Token transfer(RootTrustBase trustBase, PredicateVerifierService predicat VerificationResult result = CertifiedTransferTransactionVerificationRule.verify( trustBase, predicateVerifier, - this.getLatestTransaction(), transaction ); if (result.getStatus() != VerificationStatus.OK) { @@ -164,13 +171,21 @@ public Token transfer(RootTrustBase trustBase, PredicateVerifierService predicat * * @param trustBase trust base used for certification checks * @param predicateVerifier predicate verifier service + * @param mintJustificationVerifier mint justification verifier service * @return verification result with nested per-step verification details */ - public VerificationResult verify(RootTrustBase trustBase, - PredicateVerifierService predicateVerifier) { + public VerificationResult verify( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + MintJustificationVerifierService mintJustificationVerifier + ) { List> results = new ArrayList<>(); - VerificationResult result = CertifiedMintTransactionVerificationRule.verify(trustBase, - predicateVerifier, this.genesis); + VerificationResult result = CertifiedMintTransactionVerificationRule.verify( + trustBase, + predicateVerifier, + mintJustificationVerifier, + this.genesis + ); results.add(result); if (result.getStatus() != VerificationStatus.OK) { return new VerificationResult<>("TokenVerification", VerificationStatus.FAIL, @@ -180,8 +195,7 @@ public VerificationResult verify(RootTrustBase trustBase, List> transferResults = new ArrayList<>(); for (int i = 0; i < this.transactions.size(); i++) { CertifiedTransferTransaction transaction = this.transactions.get(i); - result = CertifiedTransferTransactionVerificationRule.verify(trustBase, predicateVerifier, - i == 0 ? this.genesis : this.transactions.get(i - 1), transaction); + result = CertifiedTransferTransactionVerificationRule.verify(trustBase, predicateVerifier, transaction); transferResults.add(result); if (result.getStatus() != VerificationStatus.OK) { results.add( diff --git a/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java b/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java index 3cdea0e..19cd038 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java @@ -3,6 +3,8 @@ import org.unicitylabs.sdk.crypto.hash.DataHash; import org.unicitylabs.sdk.predicate.Predicate; +import java.util.Optional; + /** * Common interface for token transactions. */ @@ -13,7 +15,7 @@ public interface Transaction { * * @return payload bytes */ - byte[] getData(); + Optional getData(); /** * Gets the predicate that locks this transaction. @@ -23,11 +25,11 @@ public interface Transaction { Predicate getLockScript(); /** - * Gets the transaction recipient address. + * Gets the transaction recipient. * - * @return recipient address + * @return recipient predicate */ - Address getRecipient(); + Predicate getRecipient(); /** * Gets the source state hash. diff --git a/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java index 2f798c7..7a80e51 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java @@ -15,6 +15,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Optional; /** * Transfer transaction that moves token ownership from a source state to a recipient. @@ -25,14 +26,14 @@ public class TransferTransaction implements Transaction { private final DataHash sourceStateHash; private final Predicate lockScript; - private final Address recipient; + private final Predicate recipient; private final byte[] nonce; private final byte[] data; private TransferTransaction( DataHash sourceStateHash, Predicate lockScript, - Address recipient, + Predicate recipient, byte[] nonce, byte[] data ) { @@ -49,8 +50,8 @@ public int getVersion() { @Override - public byte[] getData() { - return Arrays.copyOf(this.data, this.data.length); + public Optional getData() { + return Optional.ofNullable(this.data != null ? Arrays.copyOf(this.data, this.data.length) : null); } @Override @@ -59,7 +60,7 @@ public Predicate getLockScript() { } @Override - public Address getRecipient() { + public Predicate getRecipient() { return this.recipient; } @@ -77,25 +78,20 @@ public byte[] getNonce() { * Creates a transfer transaction from the latest state of the provided token. * * @param token token whose latest transaction is used as the source - * @param owner current owner predicate - * @param recipient recipient address - * @param x transaction randomness component + * @param recipient recipient predicate + * @param nonce transaction randomness component * @param data transfer payload * @return created transfer transaction - * @throws RuntimeException if the owner predicate does not match the latest recipient */ - public static TransferTransaction create(Token token, Predicate owner, Address recipient, - byte[] x, byte[] data) { + public static TransferTransaction create(Token token, Predicate recipient, + byte[] nonce, byte[] data) { Transaction transaction = token.getLatestTransaction(); - if (!transaction.getRecipient().equals(Address.fromPredicate(owner))) { - throw new RuntimeException("Predicate does not match pay to script hash."); - } return new TransferTransaction( transaction.calculateStateHash(), - owner, + transaction.getRecipient(), recipient, - x, + nonce, data ); } @@ -104,9 +100,10 @@ public static TransferTransaction create(Token token, Predicate owner, Address r * Deserializes a transfer transaction from CBOR bytes. * * @param bytes CBOR-encoded transfer transaction + * @param token token providing the source state for the deserialized transfer * @return decoded transfer transaction */ - public static TransferTransaction fromCbor(byte[] bytes) { + public static TransferTransaction fromCbor(byte[] bytes, Token token) { CborDeserializer.CborTag tag = CborDeserializer.decodeTag(bytes); if (tag.getTag() != TransferTransaction.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); @@ -118,12 +115,11 @@ public static TransferTransaction fromCbor(byte[] bytes) { throw new CborSerializationException(String.format("Unsupported version: %s", version)); } - return new TransferTransaction( - new DataHash(HashAlgorithm.SHA256, CborDeserializer.decodeByteString(data.get(1))), - EncodedPredicate.fromCbor(data.get(2)), - Address.fromCbor(data.get(3)), - CborDeserializer.decodeByteString(data.get(4)), - CborDeserializer.decodeByteString(data.get(5)) + return TransferTransaction.create( + token, + EncodedPredicate.fromCbor(data.get(1)), + CborDeserializer.decodeByteString(data.get(2)), + CborDeserializer.decodeNullable(data.get(3), CborDeserializer::decodeByteString) ); } @@ -142,13 +138,7 @@ public DataHash calculateStateHash() { @Override public DataHash calculateTransactionHash() { return new DataHasher(HashAlgorithm.SHA256) - .update( - CborSerializer.encodeArray( - this.recipient.toCbor(), - CborSerializer.encodeByteString(this.nonce), - CborSerializer.encodeByteString(this.data) - ) - ) + .update(this.toCbor()) .digest(); } @@ -158,11 +148,9 @@ public byte[] toCbor() { TransferTransaction.CBOR_TAG, CborSerializer.encodeArray( CborSerializer.encodeUnsignedInteger(TransferTransaction.VERSION), - CborSerializer.encodeByteString(this.sourceStateHash.getData()), - EncodedPredicate.fromPredicate(this.lockScript).toCbor(), - this.recipient.toCbor(), + EncodedPredicate.fromPredicate(this.recipient).toCbor(), CborSerializer.encodeByteString(this.nonce), - CborSerializer.encodeByteString(this.data) + CborSerializer.encodeNullable(this.data, CborSerializer::encodeByteString) ) ); } @@ -180,8 +168,12 @@ public CertifiedTransferTransaction toCertifiedTransaction( PredicateVerifierService predicateVerifier, InclusionProof inclusionProof ) { - return CertifiedTransferTransaction.fromTransaction(trustBase, predicateVerifier, this, - inclusionProof); + return CertifiedTransferTransaction.fromTransaction( + trustBase, + predicateVerifier, + this, + inclusionProof + ); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedMintTransactionVerificationRule.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedMintTransactionVerificationRule.java index 19748f8..c4a4b72 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedMintTransactionVerificationRule.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedMintTransactionVerificationRule.java @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.List; /** * Verification rule set for certified mint transactions. @@ -27,15 +28,20 @@ private CertifiedMintTransactionVerificationRule() { /** * Verify a certified mint transaction. * - * @param trustBase root trust base used for inclusion proof verification - * @param predicateVerifier predicate verifier used by inclusion proof verification + * @param trustBase root trust base + * @param predicateVerifier predicate verifier + * @param mintJustificationVerifier mint justification verifier * @param transaction certified mint transaction to verify * * @return verification result with child results for each validation step */ - public static VerificationResult verify(RootTrustBase trustBase, - PredicateVerifierService predicateVerifier, CertifiedMintTransaction transaction) { - ArrayList> results = new ArrayList>(); + public static VerificationResult verify( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + MintJustificationVerifierService mintJustificationVerifier, + CertifiedMintTransaction transaction + ) { + List> results = new ArrayList<>(); SigningService signingService = MintSigningService.create(transaction.getTokenId()); VerificationResult result = Arrays.equals( @@ -62,6 +68,17 @@ public static VerificationResult verify(RootTrustBase trustB VerificationStatus.FAIL, "Inclusion proof verification failed", results); } + result = mintJustificationVerifier.verify(transaction); + results.add(result); + if (result.getStatus() != VerificationStatus.OK) { + return new VerificationResult<>( + "CertifiedMintTransactionVerificationRule", + VerificationStatus.FAIL, + "Invalid mint justification", + results + ); + } + return new VerificationResult<>("CertifiedMintTransactionVerificationRule", VerificationStatus.OK, "", results); } diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedTransferTransactionVerificationRule.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedTransferTransactionVerificationRule.java index a15adff..37612ed 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedTransferTransactionVerificationRule.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedTransferTransactionVerificationRule.java @@ -2,9 +2,7 @@ import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; -import org.unicitylabs.sdk.transaction.Address; import org.unicitylabs.sdk.transaction.CertifiedTransferTransaction; -import org.unicitylabs.sdk.transaction.Transaction; import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; @@ -13,8 +11,8 @@ /** * Verification rule set for certified transfer transactions. * - *

The verification checks inclusion proof validity, validates that the current transaction - * is spent by previous recipient and ensures source-state-hash continuity. + *

The verification checks that the certified transfer transaction's inclusion proof is valid + * against the trust base. */ public class CertifiedTransferTransactionVerificationRule { @@ -26,7 +24,6 @@ private CertifiedTransferTransactionVerificationRule() { * * @param trustBase root trust base used for inclusion proof verification * @param predicateVerifier predicate verifier used by inclusion proof verification - * @param latestTransaction latest transaction in token history * @param transaction certified transfer transaction to verify * * @return verification result with child results for each validation step @@ -34,7 +31,6 @@ private CertifiedTransferTransactionVerificationRule() { public static VerificationResult verify( RootTrustBase trustBase, PredicateVerifierService predicateVerifier, - Transaction latestTransaction, CertifiedTransferTransaction transaction) { ArrayList> results = new ArrayList>(); @@ -46,28 +42,6 @@ public static VerificationResult verify( VerificationStatus.FAIL, "Inclusion proof verification failed", results); } - Address payToScriptHash = Address.fromPredicate(transaction.getLockScript()); - result = new VerificationResult<>("RecipientVerificationRule", - latestTransaction.getRecipient().equals(payToScriptHash) ? VerificationStatus.OK - : VerificationStatus.FAIL); - results.add(result); - - if (result.getStatus() != VerificationStatus.OK) { - return new VerificationResult<>("CertifiedTransferTransactionVerificationRule", - VerificationStatus.FAIL, - "Transaction owner does not match the previous transaction recipient", results); - } - - result = new VerificationResult<>("SourceStateHashVerificationRule", - latestTransaction.calculateStateHash().equals(transaction.getSourceStateHash()) - ? VerificationStatus.OK : VerificationStatus.FAIL); - results.add(result); - if (result.getStatus() != VerificationStatus.OK) { - return new VerificationResult<>("CertifiedTransferTransactionVerificationRule", - VerificationStatus.FAIL, - "Source state hash does not match the previous transaction state hash", results); - } - return new VerificationResult<>("CertifiedTransferTransactionVerificationRule", VerificationStatus.OK, "", results); } diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/MintJustificationVerifier.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/MintJustificationVerifier.java new file mode 100644 index 0000000..ae94df2 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/MintJustificationVerifier.java @@ -0,0 +1,33 @@ +package org.unicitylabs.sdk.transaction.verification; + +import org.unicitylabs.sdk.transaction.CertifiedMintTransaction; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +/** + * Verifier for a specific kind of certified mint transaction justification, identified by a CBOR + * tag. Implementations are registered with {@link MintJustificationVerifierService} and dispatched + * based on the tag of the bytes stored in the mint transaction's justification field. + */ +public interface MintJustificationVerifier { + + /** + * Get the CBOR tag identifying the justification kind handled by this verifier. + * + * @return CBOR tag + */ + long getTag(); + + /** + * Verify the justification of the given certified mint transaction. + * + * @param transaction certified mint transaction whose justification is being verified + * @param mintJustificationVerifierService dispatcher used to recursively verify nested mint + * justifications (for example, the burn token's mint chain) + * + * @return verification result + */ + VerificationResult verify( + CertifiedMintTransaction transaction, + MintJustificationVerifierService mintJustificationVerifierService); +} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/MintJustificationVerifierService.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/MintJustificationVerifierService.java new file mode 100644 index 0000000..d5e06af --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/MintJustificationVerifierService.java @@ -0,0 +1,70 @@ +package org.unicitylabs.sdk.transaction.verification; + +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.transaction.CertifiedMintTransaction; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +import java.util.HashMap; +import java.util.Map; + +/** + * Dispatcher for {@link MintJustificationVerifier} implementations. Verifiers are registered + * by their CBOR tag; on {@link #verify(CertifiedMintTransaction)} the service reads the tag of + * the mint transaction's justification and routes the verification to the matching verifier. + * + *

Mint transactions with no justification are accepted as OK without further checks. + */ +public class MintJustificationVerifierService { + private final Map verifiers = new HashMap<>(); + + /** + * Register a verifier for its declared tag. Each tag may be registered only once. + * + * @param verifier verifier to register + * + * @return this service for fluent chaining + * + * @throws IllegalArgumentException if a verifier for the same tag is already registered + */ + public MintJustificationVerifierService register(MintJustificationVerifier verifier) { + if (this.verifiers.containsKey(verifier.getTag())) { + throw new IllegalArgumentException(String.format("Duplicate mint justification verifier for tag %s.", verifier.getTag())); + } + + this.verifiers.put(verifier.getTag(), verifier); + return this; + } + + /** + * Verify the mint justification carried by the given transaction. + * + * @param transaction certified mint transaction to verify + * + * @return verification result; OK if the transaction has no justification, otherwise the result + * of the verifier registered for the justification's CBOR tag + */ + public VerificationResult verify(CertifiedMintTransaction transaction) { + var bytes = transaction.getJustification().orElse(null); + if (bytes == null) { + return new VerificationResult<>("MintJustificationVerification", VerificationStatus.OK); + } + + var tag = CborDeserializer.decodeTag(bytes); + var verifier = this.verifiers.get(tag.getTag()); + if (verifier == null) { + return new VerificationResult<>( + "MintJustificationVerification", + VerificationStatus.FAIL, + String.format("Unsupported mint justification tag: %s", tag.getTag()) + ); + } + + var result = verifier.verify(transaction, this); + if (result.getStatus() != VerificationStatus.OK) { + return new VerificationResult<>("MintJustificationVerification", VerificationStatus.FAIL, String.format("Verification failed for tag %s", tag.getTag()), result); + } + + return new VerificationResult<>("MintJustificationVerification", VerificationStatus.OK, "", result); + } +} diff --git a/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java b/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java index 8e0df16..2cbd995 100644 --- a/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java +++ b/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java @@ -8,7 +8,6 @@ import org.unicitylabs.sdk.api.jsonrpc.JsonRpcNetworkException; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; -import org.unicitylabs.sdk.transaction.Address; import org.unicitylabs.sdk.transaction.MintTransaction; import org.unicitylabs.sdk.transaction.TokenId; import org.unicitylabs.sdk.transaction.TokenType; @@ -44,10 +43,11 @@ void setUp() throws Exception { HexConverter.decode("0000000000000000000000000000000000000000000000000000000000000001")); MintTransaction transaction = MintTransaction.create( - Address.fromPredicate(PayToPublicKeyPredicate.fromSigningService(signingService)), + PayToPublicKeyPredicate.fromSigningService(signingService), TokenId.generate(), TokenType.generate(), - new byte[32] + null, + null ); certificationData = CertificationData.fromMintTransaction(transaction); } diff --git a/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java b/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java index 22a68b7..c16c07c 100644 --- a/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java +++ b/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java @@ -16,7 +16,6 @@ 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.transaction.Address; import org.unicitylabs.sdk.transaction.MintTransaction; import org.unicitylabs.sdk.transaction.TokenId; import org.unicitylabs.sdk.transaction.TokenType; @@ -42,10 +41,11 @@ public void createMerkleTreePath() throws Exception { transaction = MintTransaction.create( - Address.fromPredicate(PayToPublicKeyPredicate.fromSigningService(signingService)), + PayToPublicKeyPredicate.fromSigningService(signingService), TokenId.generate(), TokenType.generate(), - new byte[32] + null, + null ); certificationData = CertificationData.fromMintTransaction(transaction); diff --git a/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java b/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java index 32b5e15..44b326d 100644 --- a/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java +++ b/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java @@ -7,8 +7,8 @@ import org.unicitylabs.sdk.crypto.secp256k1.SigningService; import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; -import org.unicitylabs.sdk.transaction.Address; import org.unicitylabs.sdk.transaction.Token; +import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifierService; import org.unicitylabs.sdk.util.verification.VerificationStatus; import org.unicitylabs.sdk.utils.TokenUtils; @@ -20,6 +20,7 @@ public abstract class CommonTestFlow { protected StateTransitionClient client; protected RootTrustBase trustBase; protected PredicateVerifierService predicateVerifier; + protected MintJustificationVerifierService mintJustificationVerifier; private static final SigningService ALICE_SIGNING_SERVICE = SigningService.generate(); private static final SigningService BOB_SIGNING_SERVICE = SigningService.generate(); @@ -34,15 +35,17 @@ public void testTransferFlow() throws Exception { this.client, this.trustBase, this.predicateVerifier, - Address.fromPredicate(PayToPublicKeyPredicate.create(ALICE_SIGNING_SERVICE.getPublicKey())) + this.mintJustificationVerifier, + PayToPublicKeyPredicate.create(ALICE_SIGNING_SERVICE.getPublicKey()) ); Token bobToken = TokenUtils.transferToken( this.client, this.trustBase, this.predicateVerifier, + this.mintJustificationVerifier, aliceToken.toCbor(), - Address.fromPredicate(PayToPublicKeyPredicate.create(BOB_SIGNING_SERVICE.getPublicKey())), + PayToPublicKeyPredicate.create(BOB_SIGNING_SERVICE.getPublicKey()), ALICE_SIGNING_SERVICE ); @@ -50,12 +53,13 @@ public void testTransferFlow() throws Exception { this.client, this.trustBase, this.predicateVerifier, + this.mintJustificationVerifier, bobToken.toCbor(), - Address.fromPredicate(PayToPublicKeyPredicate.create(CAROL_SIGNING_SERVICE.getPublicKey())), + PayToPublicKeyPredicate.create(CAROL_SIGNING_SERVICE.getPublicKey()), BOB_SIGNING_SERVICE ); Assertions.assertEquals(VerificationStatus.OK, - carolToken.verify(this.trustBase, this.predicateVerifier).getStatus()); + carolToken.verify(this.trustBase, this.predicateVerifier, this.mintJustificationVerifier).getStatus()); } } \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/e2e/TokenE2ETest.java b/src/test/java/org/unicitylabs/sdk/e2e/TokenE2ETest.java index bafab7e..032950b 100644 --- a/src/test/java/org/unicitylabs/sdk/e2e/TokenE2ETest.java +++ b/src/test/java/org/unicitylabs/sdk/e2e/TokenE2ETest.java @@ -9,6 +9,7 @@ import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.common.CommonTestFlow; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifierService; import java.io.IOException; import java.io.InputStream; @@ -38,6 +39,7 @@ void setUp() throws IOException { assertNotNull(stream, "trust-base.json not found"); this.trustBase = RootTrustBase.fromJson(new String(stream.readAllBytes())); this.predicateVerifier = PredicateVerifierService.create(this.trustBase); + this.mintJustificationVerifier = new MintJustificationVerifierService(); } } diff --git a/src/test/java/org/unicitylabs/sdk/functional/FunctionalCommonFlowTest.java b/src/test/java/org/unicitylabs/sdk/functional/FunctionalCommonFlowTest.java index 2a8612c..3d0b676 100644 --- a/src/test/java/org/unicitylabs/sdk/functional/FunctionalCommonFlowTest.java +++ b/src/test/java/org/unicitylabs/sdk/functional/FunctionalCommonFlowTest.java @@ -5,6 +5,7 @@ import org.unicitylabs.sdk.TestAggregatorClient; import org.unicitylabs.sdk.common.CommonTestFlow; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifierService; public class FunctionalCommonFlowTest extends CommonTestFlow { @@ -14,5 +15,6 @@ void setUp() { this.client = new StateTransitionClient(aggregatorClient); this.trustBase = aggregatorClient.getTrustBase(); this.predicateVerifier = PredicateVerifierService.create(this.trustBase); + this.mintJustificationVerifier = new MintJustificationVerifierService(); } } \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java index c88ac3f..0e2a369 100644 --- a/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java @@ -1,597 +1,105 @@ package org.unicitylabs.sdk.functional.payment; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; import org.unicitylabs.sdk.StateTransitionClient; import org.unicitylabs.sdk.TestAggregatorClient; import org.unicitylabs.sdk.api.bft.RootTrustBase; -import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; -import org.unicitylabs.sdk.payment.*; +import org.unicitylabs.sdk.payment.SplitMintJustification; +import org.unicitylabs.sdk.payment.SplitMintJustificationVerifier; +import org.unicitylabs.sdk.payment.SplitResult; +import org.unicitylabs.sdk.payment.TokenSplit; import org.unicitylabs.sdk.payment.asset.Asset; import org.unicitylabs.sdk.payment.asset.AssetId; import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; 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.smt.plain.SparseMerkleTree; -import org.unicitylabs.sdk.smt.plain.SparseMerkleTreeRootNode; -import org.unicitylabs.sdk.smt.sum.SparseMerkleSumTree; -import org.unicitylabs.sdk.smt.sum.SparseMerkleSumTreeRootNode; -import org.unicitylabs.sdk.transaction.Address; import org.unicitylabs.sdk.transaction.Token; import org.unicitylabs.sdk.transaction.TokenId; -import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.transaction.TokenType; +import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifierService; import org.unicitylabs.sdk.util.verification.VerificationStatus; import org.unicitylabs.sdk.utils.TokenUtils; import java.math.BigInteger; import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.Map.Entry; -import java.util.stream.Collectors; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; /** - * Functional tests for minting and splitting tokens with proof verification. + * End-to-end functional test for the token split flow: mint a source token, split it, burn the + * source, mint the split output token with the resulting justification, and verify the split + * output through {@link Token#verify}. */ -@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class SplitBuilderTest { - private StateTransitionClient client; - private RootTrustBase trustBase; - private PredicateVerifierService predicateVerifier; - private Asset asset1; - private Asset asset2; - private Token splitToken; - - @BeforeAll - public void setupFixture() throws Exception { + @Test + public void buildAndVerifySplitToken() throws Exception { TestAggregatorClient aggregatorClient = TestAggregatorClient.create(); - this.trustBase = aggregatorClient.getTrustBase(); + RootTrustBase trustBase = aggregatorClient.getTrustBase(); + StateTransitionClient client = new StateTransitionClient(aggregatorClient); + PredicateVerifierService predicateVerifier = PredicateVerifierService.create(trustBase); - this.client = new StateTransitionClient(aggregatorClient); - this.predicateVerifier = PredicateVerifierService.create(this.trustBase); + MintJustificationVerifierService mintJustificationVerifier = new MintJustificationVerifierService(); + mintJustificationVerifier.register(new SplitMintJustificationVerifier( + trustBase, predicateVerifier, TestPaymentData::decode)); SigningService signingService = SigningService.generate(); PayToPublicKeyPredicate ownerPredicate = PayToPublicKeyPredicate.fromSigningService(signingService); - this.asset1 = new Asset(new AssetId("ASSET_1".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); - this.asset2 = new Asset(new AssetId("ASSET_2".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); - - this.splitToken = createSplitToken( - this.client, - signingService, - ownerPredicate, - Set.of(this.asset1, this.asset2), - Set.of(this.asset1, this.asset2) - ); - } - - /** - * Verifies end-to-end mint, split, burn and validation flow. - * - * @throws Exception when async client interactions fail - */ - @Test - public void verifyTokenSplitIsSuccessful() throws Exception { - SigningService signingService = SigningService.generate(); - PayToPublicKeyPredicate predicate = PayToPublicKeyPredicate.fromSigningService(signingService); - - Set assets = Set.of(this.asset1, this.asset2); - TestPaymentData paymentData = new TestPaymentData(assets); - - Token token = TokenUtils.mintToken( - this.client, - this.trustBase, - this.predicateVerifier, - Address.fromPredicate(predicate), - paymentData.encode() - ); - - IllegalArgumentException exception = Assertions.assertThrows( - IllegalArgumentException.class, - () -> TokenSplit.split( - token, - predicate, - TestPaymentData::decode, - Map.of(TokenId.generate(), Set.of(this.asset1)) - ) - ); - - Assertions.assertEquals("Token and split tokens asset counts differ.", exception.getMessage()); - - exception = Assertions.assertThrows( - IllegalArgumentException.class, - () -> TokenSplit.split( - token, - predicate, - TestPaymentData::decode, - Map.of( - TokenId.generate(), - Set.of(this.asset1, new Asset(this.asset2.getId(), BigInteger.valueOf(400))) - ) - ) - ); - - Assertions.assertEquals("Token contained 500 AssetId{bytes=41535345545f32} assets, but tree has 400", - exception.getMessage()); - - exception = Assertions.assertThrows( - IllegalArgumentException.class, - () -> TokenSplit.split( - token, - predicate, - TestPaymentData::decode, - Map.of( - TokenId.generate(), - Set.of(this.asset1, new Asset(this.asset2.getId(), BigInteger.valueOf(1500))) - ) - ) - ); - - Assertions.assertEquals("Token contained 500 AssetId{bytes=41535345545f32} assets, but tree has 1500", - exception.getMessage()); - - Map> splitTokens = Map.of( - TokenId.generate(), Set.of(this.asset1), - TokenId.generate(), Set.of(this.asset2) - ); - - SplitResult result = TokenSplit.split(token, predicate, TestPaymentData::decode, splitTokens); - - Token burnToken = TokenUtils.transferToken( - this.client, - this.trustBase, - this.predicateVerifier, - token, - result.getBurnTransaction(), - PayToPublicKeyPredicateUnlockScript.create(result.getBurnTransaction(), signingService) - ); - - for (Entry> entry : splitTokens.entrySet()) { - List proofs = result.getProofs().get(entry.getKey()); - Assertions.assertNotNull(proofs); - - Token splitToken = TokenUtils.mintToken( - this.client, - this.trustBase, - this.predicateVerifier, - entry.getKey(), - Address.fromPredicate(predicate), - new TestSplitPaymentData( - entry.getValue(), - SplitReason.create( - burnToken, - proofs - ) - ).encode() - ); - - Assertions.assertEquals( - VerificationStatus.OK, - splitToken.verify(this.trustBase, this.predicateVerifier).getStatus() - ); - Assertions.assertEquals(VerificationStatus.OK, - TokenSplit.verify( - Token.fromCbor(splitToken.toCbor()), - TestSplitPaymentData::decode, - this.trustBase, - this.predicateVerifier - ).getStatus()); - } - } - - @Test - public void verifyFailsWhenTokenIsNull() { - assertNpe("Token cannot be null", - () -> TokenSplit.verify(null, TestSplitPaymentData::decode, this.trustBase, this.predicateVerifier)); - } - - @Test - public void verifyFailsWhenDeserializerIsNull() { - assertNpe("Payment data deserializer cannot be null", - () -> TokenSplit.verify(this.splitToken, null, this.trustBase, this.predicateVerifier)); - } - - @Test - public void verifyFailsWhenTrustBaseIsNull() { - assertNpe("Trust base cannot be null", - () -> TokenSplit.verify(this.splitToken, TestSplitPaymentData::decode, null, this.predicateVerifier)); - } - - @Test - public void verifyFailsWhenPredicateVerifierIsNull() { - assertNpe("Predicate verifier cannot be null", - () -> TokenSplit.verify(this.splitToken, TestSplitPaymentData::decode, this.trustBase, null)); - } - - @Test - public void verifyFailsWhenAssetsAreMissing() { - VerificationResult result = verifyWithData( - this.splitToken, - new TestSplitPaymentData(null, TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()).getReason()) - ); - - assertFailWithMessage(result, "Assets data is missing."); - } - - @Test - public void verifyFailsWhenReasonIsMissing() { - VerificationResult result = verifyWithData( - this.splitToken, - new TestSplitPaymentData(Set.of(this.asset1), null) - ); - - assertFailWithMessage(result, "Reason is missing."); - } - - @Test - public void verifyFailsWhenBurnTokenVerificationFails() { - List payloadData = CborDeserializer.decodeArray(this.splitToken.getGenesis().getData()); - List reasonData = CborDeserializer.decodeArray(payloadData.get(1)); - - CborDeserializer.CborTag reasonTokenTag = CborDeserializer.decodeTag(reasonData.get(0)); - List reasonTokenData = CborDeserializer.decodeArray(reasonTokenTag.getData()); - List transactions = CborDeserializer.decodeArray(reasonTokenData.get(2)); - List certifiedTransfer = CborDeserializer.decodeArray(transactions.get(0)); - - CborDeserializer.CborTag transferTag = CborDeserializer.decodeTag(certifiedTransfer.get(0)); - List transfer = CborDeserializer.decodeArray(transferTag.getData()); - - // Corrupt burn transaction recipient address so burn token verification fails. - byte[] invalidRecipient = new byte[32]; - invalidRecipient[0] = 1; - transfer.set(3, Address.fromBytes(invalidRecipient).toCbor()); - - certifiedTransfer.set(0, CborSerializer.encodeTag(transferTag.getTag(), encodeArray(transfer))); - transactions.set(0, encodeArray(certifiedTransfer)); - reasonTokenData.set(2, encodeArray(transactions)); - reasonData.set(0, CborSerializer.encodeTag(reasonTokenTag.getTag(), encodeArray(reasonTokenData))); - payloadData.set(1, encodeArray(reasonData)); - byte[] payload = encodeArray(payloadData); - - VerificationResult result = verifyWithPayload(this.splitToken, payload); - assertFailWithMessage(result, "Burn token verification failed."); - Assertions.assertFalse(result.getResults().isEmpty()); - } - - @Test - public void verifyFailsWhenAssetAndProofCountsDiffer() { - VerificationResult result = verifyWithData( - this.splitToken, - new TestSplitPaymentData(Set.of(this.asset1), - TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()).getReason()) - ); - - assertFailWithMessage(result, "Total amount of assets differ in token and proofs."); - } - - @Test - public void verifyFailsWhenAssetEntryIsNull() { - Set invalidAssets = new NonUniqueAssetSet(Arrays.asList(null, this.asset1)); - VerificationResult result = verifyWithData( - this.splitToken, - new TestSplitPaymentData(invalidAssets, - TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()).getReason()) - ); - - assertFailWithMessage(result, "Asset data is missing."); - } - - @Test - public void verifyFailsWhenAssetIdsAreDuplicated() { - Asset duplicate = new Asset(this.asset1.getId(), this.asset1.getValue().add(BigInteger.ONE)); - Set duplicatedAssets = new NonUniqueAssetSet(List.of(this.asset1, duplicate)); - - VerificationResult result = verifyWithData( - this.splitToken, - new TestSplitPaymentData(duplicatedAssets, - TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()).getReason()) - ); - - assertFailWithMessage(result, - String.format("Duplicate asset id %s found in asset data.", this.asset1.getId())); - } - - @Test - public void verifyFailsWhenAggregationPathVerificationFails() throws Exception { - SplitPaymentData splitPaymentData = TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()); - SplitReason splitReason = splitPaymentData.getReason(); - List proofs = new ArrayList<>(splitReason.getProofs()); - SplitReasonProof proof = proofs.get(0); - SparseMerkleTreeRootNode aggregationRoot = new SparseMerkleTree(HashAlgorithm.SHA256).calculateRoot(); - - proofs.set( - 0, - SplitReasonProof.create( - proof.getAssetId(), - aggregationRoot.getPath(proof.getAssetId().toBitString().toBigInteger()), - proof.getAssetTreePath() - ) - ); - - byte[] payload = new TestSplitPaymentData( - splitPaymentData.getAssets(), - SplitReason.create(splitReason.getToken(), proofs) - ).encode(); - - VerificationResult result = verifyWithPayload(this.splitToken, payload); - - assertFailWithMessage(result, - String.format("Aggregation path verification failed for asset: %s", proof.getAssetId())); - } - - @Test - public void verifyFailsWhenAssetTreePathVerificationFails() throws Exception { - SplitPaymentData splitPaymentData = TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()); - SplitReason splitReason = splitPaymentData.getReason(); - List proofs = new ArrayList<>(splitReason.getProofs()); - SplitReasonProof proof = proofs.get(0); - - SparseMerkleSumTreeRootNode assetTreeRoot = new SparseMerkleSumTree(HashAlgorithm.SHA256).calculateRoot(); - - SplitReasonProof mutated = SplitReasonProof.create( - proof.getAssetId(), - proof.getAggregationPath(), - assetTreeRoot.getPath(this.splitToken.getId().toBitString().toBigInteger()) - ); - proofs.set(0, mutated); - - byte[] payload = new TestSplitPaymentData( - splitPaymentData.getAssets(), - SplitReason.create(splitReason.getToken(), proofs) - ).encode(); - - VerificationResult result = verifyWithPayload(this.splitToken, payload); - - assertFailWithMessage(result, - String.format("Asset tree path verification failed for token: %s", this.splitToken.getId())); - } - - @Test - public void verifyFailsWhenProofsUseDifferentAssetTrees() throws Exception { - SplitPaymentData splitPaymentData = TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()); - SplitReason splitReason = splitPaymentData.getReason(); - List proofs = new ArrayList<>(splitReason.getProofs()); - SplitReasonProof secondProof = proofs.get(1); - - SparseMerkleTree aggregationTree = new SparseMerkleTree(HashAlgorithm.SHA256); - aggregationTree.addLeaf( - secondProof.getAssetId().toBitString().toBigInteger(), - secondProof.getAssetTreePath().getRootHash().getImprint() - ); - SparseMerkleTreeRootNode otherAggregationRoot = aggregationTree.calculateRoot(); - - SplitReasonProof mutated = SplitReasonProof.create( - secondProof.getAssetId(), - otherAggregationRoot.getPath(secondProof.getAssetId().toBitString().toBigInteger()), - secondProof.getAssetTreePath() + Set assets = Set.of( + new Asset(new AssetId("ASSET_1".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)), + new Asset(new AssetId("ASSET_2".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)) ); - proofs.set(1, mutated); - byte[] payload = new TestSplitPaymentData( - splitPaymentData.getAssets(), - SplitReason.create(splitReason.getToken(), proofs) - ).encode(); - - VerificationResult result = verifyWithPayload(this.splitToken, payload); - assertFailWithMessage(result, "Current proof is not derived from the same asset tree as other proofs."); - } - - @Test - public void verifyFailsWhenAssetTreeRootDoesNotMatchAggregationLeaf() throws Exception { - SplitPaymentData splitPaymentData = TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()); - SplitReason splitReason = splitPaymentData.getReason(); - List proofs = new ArrayList<>(splitReason.getProofs()); - SplitReasonProof proof = proofs.get(0); - - SparseMerkleSumTree assetTree = new SparseMerkleSumTree(HashAlgorithm.SHA256); - assetTree.addLeaf( - this.splitToken.getId().toBitString().toBigInteger(), - new SparseMerkleSumTree.LeafValue( - proof.getAssetId().getBytes(), - proof.getAssetTreePath().getSteps().get(0).getValue().add(BigInteger.ONE) - ) - ); - - SplitReasonProof mutated = SplitReasonProof.create( - proof.getAssetId(), - proof.getAggregationPath(), - assetTree.calculateRoot().getPath(this.splitToken.getId().toBitString().toBigInteger()) - ); - proofs.set(0, mutated); - - byte[] payload = new TestSplitPaymentData( - splitPaymentData.getAssets(), - SplitReason.create(splitReason.getToken(), proofs) - ).encode(); - - VerificationResult result = verifyWithPayload(this.splitToken, payload); - assertFailWithMessage(result, "Asset tree root does not match aggregation path leaf."); - } - - @Test - public void verifyFailsWhenProofAssetIdIsMissingFromAssetData() { - SplitPaymentData splitPaymentData = TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()); - SplitReason splitReason = splitPaymentData.getReason(); - List proofs = List.of(splitReason.getProofs().get(0)); - Set assets = splitPaymentData.getAssets().stream() - .filter(asset -> !asset.getId().equals(proofs.get(0).getAssetId())) - .collect(Collectors.toSet()); - byte[] payload = new TestSplitPaymentData( - assets, - SplitReason.create(splitReason.getToken(), proofs) - ).encode(); - - VerificationResult result = verifyWithPayload(this.splitToken, payload); - assertFailWithMessage(result, - String.format("Asset id %s not found in asset data.", proofs.get(0).getAssetId())); - } - - @Test - public void verifyFailsWhenAssetAmountDoesNotMatchLeafAmount() { - SplitPaymentData splitPaymentData = TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()); - SplitReason splitReason = splitPaymentData.getReason(); - List assets = new ArrayList<>(splitPaymentData.getAssets()); - Asset asset = assets.get(0); - Asset modified = new Asset(asset.getId(), asset.getValue().add(BigInteger.ONE)); - assets.set(0, modified); - - byte[] payload = new TestSplitPaymentData(Set.copyOf(assets), splitReason).encode(); - - VerificationResult result = verifyWithPayload(this.splitToken, payload); - assertFailWithMessage(result, - String.format("Asset amount for asset id %s does not match asset tree leaf.", asset.getId())); - } - - @Test - public void verifyFailsWhenAggregationRootDoesNotMatchBurnPredicate() throws Exception { - SplitPaymentData splitPaymentData = TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()); - SplitReason splitReason = splitPaymentData.getReason(); - List proofs = new ArrayList<>(splitReason.getProofs()); - SplitReasonProof proof = proofs.get(0); - - SparseMerkleTree aggregationTree = new SparseMerkleTree(HashAlgorithm.SHA256); - aggregationTree.addLeaf( - proof.getAssetId().toBitString().toBigInteger(), - proof.getAssetTreePath().getRootHash().getImprint() - ); - SparseMerkleTreeRootNode aggregationRoot = aggregationTree.calculateRoot(); - - SplitReasonProof mutated = SplitReasonProof.create( - proof.getAssetId(), - aggregationRoot.getPath(proof.getAssetId().toBitString().toBigInteger()), - proof.getAssetTreePath() - ); - proofs.set(0, mutated); - - byte[] payload = new TestSplitPaymentData( - splitPaymentData.getAssets(), - SplitReason.create(splitReason.getToken(), proofs) - ).encode(); - - VerificationResult result = verifyWithPayload(this.splitToken, payload); - assertFailWithMessage(result, "Aggregation path root does not match burn predicate."); - } - - private Token createSplitToken( - StateTransitionClient client, - SigningService signingService, - PayToPublicKeyPredicate ownerPredicate, - Set sourceAssets, - Set outputAssets - ) throws Exception { Token sourceToken = TokenUtils.mintToken( client, - this.trustBase, - this.predicateVerifier, - Address.fromPredicate(ownerPredicate), - new TestPaymentData(sourceAssets).encode() + trustBase, + predicateVerifier, + mintJustificationVerifier, + ownerPredicate, + null, + new TestPaymentData(assets).encode() ); TokenId outputTokenId = TokenId.generate(); SplitResult split = TokenSplit.split( sourceToken, - ownerPredicate, TestPaymentData::decode, - Map.of(outputTokenId, outputAssets) + Map.of(outputTokenId, assets) ); Token burnToken = TokenUtils.transferToken( client, - this.trustBase, - this.predicateVerifier, + trustBase, + predicateVerifier, sourceToken, split.getBurnTransaction(), PayToPublicKeyPredicateUnlockScript.create(split.getBurnTransaction(), signingService) ); - return TokenUtils.mintToken( - client, - this.trustBase, - this.predicateVerifier, - outputTokenId, - Address.fromPredicate(ownerPredicate), - new TestSplitPaymentData( - outputAssets, - SplitReason.create(burnToken, split.getProofs().get(outputTokenId)) - ).encode() + SplitMintJustification justification = SplitMintJustification.create( + burnToken, + new LinkedHashSet<>(split.getProofs().get(outputTokenId)) ); - } - private VerificationResult verify(Token token) { - return TokenSplit.verify( - Token.fromCbor(token.toCbor()), - TestSplitPaymentData::decode, - this.trustBase, - this.predicateVerifier + Token splitToken = TokenUtils.mintToken( + client, + trustBase, + predicateVerifier, + mintJustificationVerifier, + outputTokenId, + TokenType.generate(), + ownerPredicate, + justification.toCbor(), + new TestPaymentData(assets).encode() ); - } - private VerificationResult verifyWithData(Token token, SplitPaymentData paymentData) { - return TokenSplit.verify( - Token.fromCbor(token.toCbor()), - ignored -> paymentData, - this.trustBase, - this.predicateVerifier + Assertions.assertEquals( + VerificationStatus.OK, + splitToken.verify(trustBase, predicateVerifier, mintJustificationVerifier).getStatus() ); } - - private VerificationResult verifyWithPayload(Token token, byte[] payload) { - return this.verify(withPayload(token, payload)); - } - - private Token withPayload(Token token, byte[] payload) { - CborDeserializer.CborTag tokenTag = CborDeserializer.decodeTag(token.toCbor()); - List tokenData = CborDeserializer.decodeArray(tokenTag.getData()); - - List certifiedGenesis = CborDeserializer.decodeArray(tokenData.get(1)); - - CborDeserializer.CborTag mintTag = CborDeserializer.decodeTag(certifiedGenesis.get(0)); - List mint = CborDeserializer.decodeArray(mintTag.getData()); - List aux = CborDeserializer.decodeArray(mint.get(3)); - - aux.set(1, CborSerializer.encodeByteString(payload)); - mint.set(3, encodeArray(aux)); - certifiedGenesis.set(0, CborSerializer.encodeTag(mintTag.getTag(), encodeArray(mint))); - tokenData.set(1, encodeArray(certifiedGenesis)); - - return Token.fromCbor(CborSerializer.encodeTag(tokenTag.getTag(), encodeArray(tokenData))); - } - - private void assertFailWithMessage(VerificationResult result, String message) { - Assertions.assertEquals(VerificationStatus.FAIL, result.getStatus()); - Assertions.assertEquals(message, result.getMessage()); - } - - private void assertNpe(String message, Runnable callback) { - NullPointerException error = Assertions.assertThrows(NullPointerException.class, callback::run); - Assertions.assertEquals(message, error.getMessage()); - } - - private byte[] encodeArray(List data) { - return CborSerializer.encodeArray(data.toArray(new byte[0][])); - } - - private static final class NonUniqueAssetSet extends AbstractSet { - - private final List items; - - private NonUniqueAssetSet(List items) { - this.items = new ArrayList<>(items); - } - - @Override - public Iterator iterator() { - return this.items.iterator(); - } - - @Override - public int size() { - return this.items.size(); - } - } } diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/SplitMintJustificationVerifierTest.java b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitMintJustificationVerifierTest.java new file mode 100644 index 0000000..419c2fb --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitMintJustificationVerifierTest.java @@ -0,0 +1,474 @@ +package org.unicitylabs.sdk.functional.payment; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.unicitylabs.sdk.StateTransitionClient; +import org.unicitylabs.sdk.TestAggregatorClient; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.payment.PaymentData; +import org.unicitylabs.sdk.payment.SplitMintJustification; +import org.unicitylabs.sdk.payment.SplitMintJustificationVerifier; +import org.unicitylabs.sdk.payment.SplitAssetProof; +import org.unicitylabs.sdk.payment.SplitResult; +import org.unicitylabs.sdk.payment.TokenSplit; +import org.unicitylabs.sdk.payment.asset.Asset; +import org.unicitylabs.sdk.payment.asset.AssetId; +import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; +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.smt.plain.SparseMerkleTree; +import org.unicitylabs.sdk.smt.plain.SparseMerkleTreeRootNode; +import org.unicitylabs.sdk.smt.sum.SparseMerkleSumTree; +import org.unicitylabs.sdk.smt.sum.SparseMerkleSumTreeRootNode; +import org.unicitylabs.sdk.transaction.Token; +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.transaction.TokenType; +import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifierService; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; +import org.unicitylabs.sdk.utils.TokenUtils; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Unit tests for the failure branches of {@link SplitMintJustificationVerifier}. Each test drives + * one specific reject path inside the verifier by handing it a corrupted or mismatched fixture. + * The verifier is invoked directly; integration through the dispatcher and {@link Token#verify} + * is covered by {@link SplitBuilderTest}. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class SplitMintJustificationVerifierTest { + + private RootTrustBase trustBase; + private PredicateVerifierService predicateVerifier; + private MintJustificationVerifierService mintJustificationVerifier; + private SplitMintJustificationVerifier splitMintJustificationVerifier; + private Asset asset1; + private Asset asset2; + private Token splitToken; + private SplitMintJustification splitJustification; + + @BeforeAll + public void setupFixture() throws Exception { + TestAggregatorClient aggregatorClient = TestAggregatorClient.create(); + this.trustBase = aggregatorClient.getTrustBase(); + + StateTransitionClient client = new StateTransitionClient(aggregatorClient); + this.predicateVerifier = PredicateVerifierService.create(this.trustBase); + + this.splitMintJustificationVerifier = new SplitMintJustificationVerifier( + this.trustBase, this.predicateVerifier, TestPaymentData::decode); + this.mintJustificationVerifier = new MintJustificationVerifierService(); + this.mintJustificationVerifier.register(this.splitMintJustificationVerifier); + + SigningService signingService = SigningService.generate(); + PayToPublicKeyPredicate ownerPredicate = PayToPublicKeyPredicate.fromSigningService(signingService); + + this.asset1 = new Asset(new AssetId("ASSET_1".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); + this.asset2 = new Asset(new AssetId("ASSET_2".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); + + Set assets = Set.of(this.asset1, this.asset2); + + Token sourceToken = TokenUtils.mintToken( + client, + this.trustBase, + this.predicateVerifier, + this.mintJustificationVerifier, + ownerPredicate, + null, + new TestPaymentData(assets).encode() + ); + + TokenId outputTokenId = TokenId.generate(); + SplitResult split = TokenSplit.split( + sourceToken, + TestPaymentData::decode, + Map.of(outputTokenId, assets) + ); + + Token burnToken = TokenUtils.transferToken( + client, + this.trustBase, + this.predicateVerifier, + sourceToken, + split.getBurnTransaction(), + PayToPublicKeyPredicateUnlockScript.create(split.getBurnTransaction(), signingService) + ); + + this.splitJustification = SplitMintJustification.create( + burnToken, + new LinkedHashSet<>(split.getProofs().get(outputTokenId)) + ); + + this.splitToken = TokenUtils.mintToken( + client, + this.trustBase, + this.predicateVerifier, + this.mintJustificationVerifier, + outputTokenId, + TokenType.generate(), + ownerPredicate, + this.splitJustification.toCbor(), + new TestPaymentData(assets).encode() + ); + } + + @Test + public void verifyFailsWhenTransactionIsNull() { + assertNpe("transaction cannot be null", + () -> this.splitMintJustificationVerifier.verify(null, this.mintJustificationVerifier)); + } + + @Test + public void verifyFailsWhenDeserializerIsNull() { + assertNpe("decodePaymentData cannot be null", + () -> new SplitMintJustificationVerifier(this.trustBase, this.predicateVerifier, null)); + } + + @Test + public void verifyFailsWhenTrustBaseIsNull() { + assertNpe("trustBase cannot be null", + () -> new SplitMintJustificationVerifier(null, this.predicateVerifier, TestPaymentData::decode)); + } + + @Test + public void verifyFailsWhenPredicateVerifierIsNull() { + assertNpe("predicateVerifier cannot be null", + () -> new SplitMintJustificationVerifier(this.trustBase, null, TestPaymentData::decode)); + } + + @Test + public void verifyFailsWhenJustificationIsMissing() { + VerificationResult result = verifyWith(null, originalDataBytes()); + assertFailWithMessage(result, "Transaction has no justification."); + } + + @Test + public void verifyFailsWhenAssetsAreMissing() { + VerificationResult result = verifyWithPaymentData( + this.splitJustification.toCbor(), paymentDataOf(null)); + assertFailWithMessage(result, "Assets data is missing."); + } + + @Test + public void verifyFailsWhenBurnTokenVerificationFails() { + byte[] corruptedJustification = corruptBurnTokenInJustification(this.splitJustification.toCbor()); + + VerificationResult result = verifyWith(corruptedJustification, originalDataBytes()); + assertFailWithMessage(result, "Burn token verification failed."); + Assertions.assertFalse(result.getResults().isEmpty()); + } + + @Test + public void verifyFailsWhenAssetAndProofCountsDiffer() { + byte[] data = new TestPaymentData(Set.of(this.asset1)).encode(); + + VerificationResult result = verifyWith(this.splitJustification.toCbor(), data); + assertFailWithMessage(result, "Total amount of assets differ in token and proofs."); + } + + @Test + public void verifyFailsWhenAssetEntryIsNull() { + Set invalidAssets = new NonUniqueAssetSet(Arrays.asList(null, this.asset1)); + + VerificationResult result = verifyWithPaymentData( + this.splitJustification.toCbor(), paymentDataOf(invalidAssets)); + assertFailWithMessage(result, "Asset data is missing."); + } + + @Test + public void verifyFailsWhenAssetIdsAreDuplicated() { + Asset duplicate = new Asset(this.asset1.getId(), this.asset1.getValue().add(BigInteger.ONE)); + Set duplicatedAssets = new NonUniqueAssetSet(List.of(this.asset1, duplicate)); + + VerificationResult result = verifyWithPaymentData( + this.splitJustification.toCbor(), paymentDataOf(duplicatedAssets)); + assertFailWithMessage(result, + String.format("Duplicate asset id %s found in asset data.", this.asset1.getId())); + } + + @Test + public void verifyFailsWhenAggregationPathVerificationFails() throws Exception { + List proofs = new ArrayList<>(this.splitJustification.getProofs()); + SplitAssetProof proof = proofs.get(0); + SparseMerkleTreeRootNode aggregationRoot = new SparseMerkleTree(HashAlgorithm.SHA256).calculateRoot(); + + proofs.set( + 0, + SplitAssetProof.create( + proof.getAssetId(), + aggregationRoot.getPath(proof.getAssetId().toBitString().toBigInteger()), + proof.getAssetTreePath() + ) + ); + + SplitMintJustification mutated = SplitMintJustification.create( + this.splitJustification.getToken(), new LinkedHashSet<>(proofs)); + + VerificationResult result = verifyWith(mutated.toCbor(), originalDataBytes()); + assertFailWithMessage(result, + String.format("Aggregation path verification failed for asset: %s", proof.getAssetId())); + } + + @Test + public void verifyFailsWhenAssetTreePathVerificationFails() throws Exception { + List proofs = new ArrayList<>(this.splitJustification.getProofs()); + SplitAssetProof proof = proofs.get(0); + + SparseMerkleSumTreeRootNode assetTreeRoot = new SparseMerkleSumTree(HashAlgorithm.SHA256).calculateRoot(); + + SplitAssetProof mutatedProof = SplitAssetProof.create( + proof.getAssetId(), + proof.getAggregationPath(), + assetTreeRoot.getPath(this.splitToken.getId().toBitString().toBigInteger()) + ); + proofs.set(0, mutatedProof); + + SplitMintJustification mutated = SplitMintJustification.create( + this.splitJustification.getToken(), new LinkedHashSet<>(proofs)); + + VerificationResult result = verifyWith(mutated.toCbor(), originalDataBytes()); + assertFailWithMessage(result, + String.format("Asset tree path verification failed for token: %s", this.splitToken.getId())); + } + + @Test + public void verifyFailsWhenProofsUseDifferentAssetTrees() throws Exception { + List proofs = new ArrayList<>( + SplitMintJustification.fromCbor(this.splitJustification.toCbor()).getProofs()); + SplitAssetProof lastProof = proofs.get(proofs.size() - 1); + + SparseMerkleTree aggregationTree = new SparseMerkleTree(HashAlgorithm.SHA256); + aggregationTree.addLeaf( + lastProof.getAssetId().toBitString().toBigInteger(), + lastProof.getAssetTreePath().getRootHash().getImprint() + ); + SparseMerkleTreeRootNode otherAggregationRoot = aggregationTree.calculateRoot(); + + proofs.set(proofs.size() - 1, SplitAssetProof.create( + lastProof.getAssetId(), + otherAggregationRoot.getPath(lastProof.getAssetId().toBitString().toBigInteger()), + lastProof.getAssetTreePath() + )); + + SplitMintJustification mutated = SplitMintJustification.create( + this.splitJustification.getToken(), new LinkedHashSet<>(proofs)); + + VerificationResult result = verifyWith(mutated.toCbor(), originalDataBytes()); + assertFailWithMessage(result, "Current proof is not derived from the same asset tree as other proofs."); + } + + @Test + public void verifyFailsWhenAssetTreeRootDoesNotMatchAggregationLeaf() throws Exception { + List proofs = new ArrayList<>(this.splitJustification.getProofs()); + SplitAssetProof proof = proofs.get(0); + + SparseMerkleSumTree assetTree = new SparseMerkleSumTree(HashAlgorithm.SHA256); + assetTree.addLeaf( + this.splitToken.getId().toBitString().toBigInteger(), + new SparseMerkleSumTree.LeafValue( + proof.getAssetId().getBytes(), + proof.getAssetTreePath().getSteps().get(0).getValue().add(BigInteger.ONE) + ) + ); + + SplitAssetProof mutatedProof = SplitAssetProof.create( + proof.getAssetId(), + proof.getAggregationPath(), + assetTree.calculateRoot().getPath(this.splitToken.getId().toBitString().toBigInteger()) + ); + proofs.set(0, mutatedProof); + + SplitMintJustification mutated = SplitMintJustification.create( + this.splitJustification.getToken(), new LinkedHashSet<>(proofs)); + + VerificationResult result = verifyWith(mutated.toCbor(), originalDataBytes()); + assertFailWithMessage(result, "Asset tree root does not match aggregation path leaf."); + } + + @Test + public void verifyFailsWhenProofAssetIdIsMissingFromAssetData() { + List proofs = List.of(this.splitJustification.getProofs().get(0)); + PaymentData originalPaymentData = TestPaymentData.decode(originalDataBytes()); + Set assets = originalPaymentData.getAssets().stream() + .filter(asset -> !asset.getId().equals(proofs.get(0).getAssetId())) + .collect(Collectors.toSet()); + + SplitMintJustification mutated = SplitMintJustification.create( + this.splitJustification.getToken(), new LinkedHashSet<>(proofs)); + byte[] data = new TestPaymentData(assets).encode(); + + VerificationResult result = verifyWith(mutated.toCbor(), data); + assertFailWithMessage(result, + String.format("Asset id %s not found in asset data.", proofs.get(0).getAssetId())); + } + + @Test + public void verifyFailsWhenAssetAmountDoesNotMatchLeafAmount() { + PaymentData originalPaymentData = TestPaymentData.decode(originalDataBytes()); + List assets = new ArrayList<>(originalPaymentData.getAssets()); + Asset asset = assets.get(0); + Asset modified = new Asset(asset.getId(), asset.getValue().add(BigInteger.ONE)); + assets.set(0, modified); + + byte[] data = new TestPaymentData(Set.copyOf(assets)).encode(); + + VerificationResult result = verifyWith(this.splitJustification.toCbor(), data); + assertFailWithMessage(result, + String.format("Asset amount for asset id %s does not match asset tree leaf.", asset.getId())); + } + + @Test + public void verifyFailsWhenAggregationRootDoesNotMatchBurnPredicate() throws Exception { + List originalProofs = new ArrayList<>(this.splitJustification.getProofs()); + + SparseMerkleTree aggregationTree = new SparseMerkleTree(HashAlgorithm.SHA256); + for (SplitAssetProof proof : originalProofs) { + aggregationTree.addLeaf( + proof.getAssetId().toBitString().toBigInteger(), + proof.getAssetTreePath().getRootHash().getImprint() + ); + } + aggregationTree.addLeaf( + new BigInteger(1, "extra-leaf-marker".getBytes(StandardCharsets.UTF_8)), + new byte[]{0x01} + ); + SparseMerkleTreeRootNode aggregationRoot = aggregationTree.calculateRoot(); + + List mutatedProofs = new ArrayList<>(); + for (SplitAssetProof proof : originalProofs) { + mutatedProofs.add(SplitAssetProof.create( + proof.getAssetId(), + aggregationRoot.getPath(proof.getAssetId().toBitString().toBigInteger()), + proof.getAssetTreePath() + )); + } + + SplitMintJustification mutated = SplitMintJustification.create( + this.splitJustification.getToken(), new LinkedHashSet<>(mutatedProofs)); + + VerificationResult result = verifyWith(mutated.toCbor(), originalDataBytes()); + assertFailWithMessage(result, "Aggregation path root does not match burn predicate."); + } + + private byte[] originalDataBytes() { + return this.splitToken.getGenesis().getData().orElseThrow(); + } + + private VerificationResult verifyWith(byte[] justification, byte[] data) { + Token modified = withJustificationAndData(this.splitToken, justification, data); + return this.splitMintJustificationVerifier.verify(modified.getGenesis(), this.mintJustificationVerifier); + } + + private VerificationResult verifyWithPaymentData(byte[] justification, + PaymentData paymentData) { + Token modified = withJustificationAndData(this.splitToken, justification, originalDataBytes()); + SplitMintJustificationVerifier verifier = new SplitMintJustificationVerifier( + this.trustBase, this.predicateVerifier, ignored -> paymentData); + return verifier.verify(modified.getGenesis(), this.mintJustificationVerifier); + } + + private Token withJustificationAndData(Token token, byte[] justification, byte[] data) { + CborDeserializer.CborTag tokenTag = CborDeserializer.decodeTag(token.toCbor()); + List tokenData = CborDeserializer.decodeArray(tokenTag.getData()); + + List certifiedGenesis = CborDeserializer.decodeArray(tokenData.get(1)); + + CborDeserializer.CborTag mintTag = CborDeserializer.decodeTag(certifiedGenesis.get(0)); + List mint = CborDeserializer.decodeArray(mintTag.getData()); + + mint.set(4, CborSerializer.encodeNullable(justification, CborSerializer::encodeByteString)); + mint.set(5, CborSerializer.encodeNullable(data, CborSerializer::encodeByteString)); + + certifiedGenesis.set(0, CborSerializer.encodeTag(mintTag.getTag(), encodeArray(mint))); + tokenData.set(1, encodeArray(certifiedGenesis)); + + return Token.fromCbor(CborSerializer.encodeTag(tokenTag.getTag(), encodeArray(tokenData))); + } + + private byte[] corruptBurnTokenInJustification(byte[] justificationBytes) { + CborDeserializer.CborTag justificationTag = CborDeserializer.decodeTag(justificationBytes); + List reasonData = CborDeserializer.decodeArray(justificationTag.getData()); + + CborDeserializer.CborTag tokenTag = CborDeserializer.decodeTag(reasonData.get(0)); + List tokenData = CborDeserializer.decodeArray(tokenTag.getData()); + List transactions = CborDeserializer.decodeArray(tokenData.get(2)); + List certifiedTransfer = CborDeserializer.decodeArray(transactions.get(0)); + + CborDeserializer.CborTag transferTag = CborDeserializer.decodeTag(certifiedTransfer.get(0)); + List transfer = CborDeserializer.decodeArray(transferTag.getData()); + + byte[] differentNonce = new byte[32]; + differentNonce[0] = 1; + transfer.set(2, CborSerializer.encodeByteString(differentNonce)); + + certifiedTransfer.set(0, CborSerializer.encodeTag(transferTag.getTag(), encodeArray(transfer))); + transactions.set(0, encodeArray(certifiedTransfer)); + tokenData.set(2, encodeArray(transactions)); + reasonData.set(0, CborSerializer.encodeTag(tokenTag.getTag(), encodeArray(tokenData))); + return CborSerializer.encodeTag(justificationTag.getTag(), encodeArray(reasonData)); + } + + private static PaymentData paymentDataOf(Set assets) { + return new PaymentData() { + @Override + public Set getAssets() { + return assets; + } + + @Override + public byte[] encode() { + return new byte[0]; + } + }; + } + + private void assertFailWithMessage(VerificationResult result, String message) { + Assertions.assertEquals(VerificationStatus.FAIL, result.getStatus()); + Assertions.assertEquals(message, result.getMessage()); + } + + private void assertNpe(String message, Runnable callback) { + NullPointerException error = Assertions.assertThrows(NullPointerException.class, callback::run); + Assertions.assertEquals(message, error.getMessage()); + } + + private byte[] encodeArray(List data) { + return CborSerializer.encodeArray(data.toArray(new byte[0][])); + } + + static final class NonUniqueAssetSet extends AbstractSet { + + private final List items; + + NonUniqueAssetSet(List items) { + this.items = new ArrayList<>(items); + } + + @Override + public Iterator iterator() { + return this.items.iterator(); + } + + @Override + public int size() { + return this.items.size(); + } + } +} diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/TestSplitPaymentData.java b/src/test/java/org/unicitylabs/sdk/functional/payment/TestSplitPaymentData.java deleted file mode 100644 index 3e27450..0000000 --- a/src/test/java/org/unicitylabs/sdk/functional/payment/TestSplitPaymentData.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.unicitylabs.sdk.functional.payment; - -import org.unicitylabs.sdk.payment.SplitPaymentData; -import org.unicitylabs.sdk.payment.SplitReason; -import org.unicitylabs.sdk.payment.asset.Asset; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; - -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Test implementation of split payment payload used by functional tests. - */ -public class TestSplitPaymentData implements SplitPaymentData { - - private final Set assets; - private final SplitReason reason; - - /** - * Create test split payment data. - * - * @param assets split assets - * @param reason split reason with proofs - */ - public TestSplitPaymentData(Set assets, SplitReason reason) { - this.assets = assets; - this.reason = reason; - } - - /** - * Get split assets. - * - * @return split assets - */ - public Set getAssets() { - return this.assets; - } - - /** - * Get split reason. - * - * @return split reason - */ - @Override - public SplitReason getReason() { - return this.reason; - } - - /** - * Decode split payment data from CBOR bytes. - * - * @param bytes encoded split payment data - * - * @return decoded split payment data - */ - public static TestSplitPaymentData decode(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); - - Set assets = CborDeserializer.decodeNullable( - data.get(0), - result -> CborDeserializer.decodeArray(result).stream() - .map(asset -> CborDeserializer.decodeNullable(asset, Asset::fromCbor)) - .collect(Collectors.toSet()) - ); - - SplitReason reason = CborDeserializer.decodeNullable(data.get(1), SplitReason::fromCbor); - - return new TestSplitPaymentData(assets, reason); - } - - /** - * Encode split payment data to CBOR bytes. - * - * @return encoded payload - */ - @Override - public byte[] encode() { - return CborSerializer.encodeArray( - CborSerializer.encodeOptional( - this.assets, - assets -> CborSerializer.encodeArray( - assets.stream().map(asset -> CborSerializer.encodeOptional(asset, Asset::toCbor)).toArray(byte[][]::new) - ) - ), - CborSerializer.encodeOptional(this.reason, SplitReason::toCbor) - ); - } - - @Override - public String toString() { - return String.format("SplitPaymentData{assets=%s, reason=%s}", this.assets, this.reason); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/TokenSplitTest.java b/src/test/java/org/unicitylabs/sdk/functional/payment/TokenSplitTest.java new file mode 100644 index 0000000..78194a4 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/TokenSplitTest.java @@ -0,0 +1,111 @@ +package org.unicitylabs.sdk.functional.payment; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.unicitylabs.sdk.StateTransitionClient; +import org.unicitylabs.sdk.TestAggregatorClient; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.payment.SplitMintJustificationVerifier; +import org.unicitylabs.sdk.payment.TokenSplit; +import org.unicitylabs.sdk.payment.asset.Asset; +import org.unicitylabs.sdk.payment.asset.AssetId; +import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.transaction.Token; +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifierService; +import org.unicitylabs.sdk.utils.TokenUtils; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Set; + +/** + * Unit tests for the precondition (IAE) branches of {@link TokenSplit#split}. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class TokenSplitTest { + + private Asset asset1; + private Asset asset2; + private Token sourceToken; + + @BeforeAll + public void setupFixture() throws Exception { + TestAggregatorClient aggregatorClient = TestAggregatorClient.create(); + RootTrustBase trustBase = aggregatorClient.getTrustBase(); + StateTransitionClient client = new StateTransitionClient(aggregatorClient); + PredicateVerifierService predicateVerifier = PredicateVerifierService.create(trustBase); + + MintJustificationVerifierService mintJustificationVerifier = new MintJustificationVerifierService(); + mintJustificationVerifier.register(new SplitMintJustificationVerifier( + trustBase, predicateVerifier, TestPaymentData::decode)); + + SigningService signingService = SigningService.generate(); + PayToPublicKeyPredicate ownerPredicate = PayToPublicKeyPredicate.fromSigningService(signingService); + + this.asset1 = new Asset(new AssetId("ASSET_1".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); + this.asset2 = new Asset(new AssetId("ASSET_2".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); + + this.sourceToken = TokenUtils.mintToken( + client, + trustBase, + predicateVerifier, + mintJustificationVerifier, + ownerPredicate, + null, + new TestPaymentData(Set.of(this.asset1, this.asset2)).encode() + ); + } + + @Test + public void splitFailsWhenAssetCountsDiffer() { + IllegalArgumentException exception = Assertions.assertThrows( + IllegalArgumentException.class, + () -> TokenSplit.split( + this.sourceToken, + TestPaymentData::decode, + Map.of(TokenId.generate(), Set.of(this.asset1)) + ) + ); + Assertions.assertEquals("Token and split tokens asset counts differ.", exception.getMessage()); + } + + @Test + public void splitFailsWhenAssetTreeAmountIsLess() { + IllegalArgumentException exception = Assertions.assertThrows( + IllegalArgumentException.class, + () -> TokenSplit.split( + this.sourceToken, + TestPaymentData::decode, + Map.of( + TokenId.generate(), + Set.of(this.asset1, new Asset(this.asset2.getId(), BigInteger.valueOf(400))) + ) + ) + ); + Assertions.assertEquals("Token contained 500 AssetId{bytes=41535345545f32} assets, but tree has 400", + exception.getMessage()); + } + + @Test + public void splitFailsWhenAssetTreeAmountIsMore() { + IllegalArgumentException exception = Assertions.assertThrows( + IllegalArgumentException.class, + () -> TokenSplit.split( + this.sourceToken, + TestPaymentData::decode, + Map.of( + TokenId.generate(), + Set.of(this.asset1, new Asset(this.asset2.getId(), BigInteger.valueOf(1500))) + ) + ) + ); + Assertions.assertEquals("Token contained 500 AssetId{bytes=41535345545f32} assets, but tree has 1500", + exception.getMessage()); + } +} diff --git a/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java b/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java index c28c31d..f5c7c10 100644 --- a/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java +++ b/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java @@ -7,12 +7,13 @@ import org.unicitylabs.sdk.api.CertificationStatus; import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.predicate.Predicate; import org.unicitylabs.sdk.predicate.UnlockScript; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.transaction.*; +import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifierService; import org.unicitylabs.sdk.util.InclusionProofUtils; import org.unicitylabs.sdk.util.verification.VerificationStatus; @@ -23,124 +24,64 @@ */ public class TokenUtils { - /** - * Mint a token with empty payload. - * - * @param client state transition client - * @param trustBase trust base - * @param predicateVerifier predicate verifier - * @param recipient recipient address - * - * @return minted token - * - * @throws Exception when request or verification fails - */ - public static Token mintToken( - StateTransitionClient client, - RootTrustBase trustBase, - PredicateVerifierService predicateVerifier, - Address recipient - ) throws Exception { - return TokenUtils.mintToken( - client, - trustBase, - predicateVerifier, - recipient, - CborSerializer.encodeArray() - ); - } - - /** - * Mint a token with explicit payload. - * - * @param client state transition client - * @param trustBase trust base - * @param predicateVerifier predicate verifier - * @param recipient recipient address - * @param data token payload - * - * @return minted token - * - * @throws Exception when request or verification fails - */ public static Token mintToken( StateTransitionClient client, RootTrustBase trustBase, PredicateVerifierService predicateVerifier, - Address recipient, - byte[] data + MintJustificationVerifierService mintJustificationVerifier, + Predicate recipient ) throws Exception { return TokenUtils.mintToken( client, trustBase, predicateVerifier, + mintJustificationVerifier, TokenId.generate(), + TokenType.generate(), recipient, - data + null, + null ); } - /** - * Mint a token with provided token id and generated type. - * - * @param client state transition client - * @param trustBase trust base - * @param predicateVerifier predicate verifier - * @param tokenId token id - * @param recipient recipient address - * @param data token payload - * - * @return minted token - * - * @throws Exception when request or verification fails - */ public static Token mintToken( StateTransitionClient client, RootTrustBase trustBase, PredicateVerifierService predicateVerifier, - TokenId tokenId, - Address recipient, + MintJustificationVerifierService mintJustificationVerifier, + Predicate recipient, + byte[] justification, byte[] data ) throws Exception { return TokenUtils.mintToken( client, trustBase, predicateVerifier, - tokenId, + mintJustificationVerifier, + TokenId.generate(), TokenType.generate(), recipient, + justification, data ); } - /** - * Mint a token with fully specified token id and type. - * - * @param client state transition client - * @param trustBase trust base - * @param predicateVerifier predicate verifier - * @param tokenId token id - * @param tokenType token type - * @param recipient recipient address - * @param data token payload - * - * @return minted token - * - * @throws Exception when request or verification fails - */ public static Token mintToken( StateTransitionClient client, RootTrustBase trustBase, PredicateVerifierService predicateVerifier, + MintJustificationVerifierService mintJustificationVerifier, TokenId tokenId, TokenType tokenType, - Address recipient, + Predicate recipient, + byte[] justification, byte[] data ) throws Exception { MintTransaction transaction = MintTransaction.create( recipient, tokenId, tokenType, + justification, data ); @@ -155,6 +96,7 @@ public static Token mintToken( return Token.mint( trustBase, predicateVerifier, + mintJustificationVerifier, transaction.toCertifiedTransaction( trustBase, predicateVerifier, @@ -182,19 +124,19 @@ public static Token transferToken( StateTransitionClient client, RootTrustBase trustBase, PredicateVerifierService predicateVerifier, + MintJustificationVerifierService mintJustificationVerifier, byte[] tokenBytes, - Address recipient, + Predicate recipient, SigningService signingService ) throws Exception { Token token = Token.fromCbor(tokenBytes); - Assertions.assertEquals(VerificationStatus.OK, token.verify(trustBase, predicateVerifier).getStatus()); + Assertions.assertEquals(VerificationStatus.OK, token.verify(trustBase, predicateVerifier, mintJustificationVerifier).getStatus()); byte[] x = new byte[32]; new SecureRandom().nextBytes(x); TransferTransaction transaction = TransferTransaction.create( token, - PayToPublicKeyPredicate.create(signingService.getPublicKey()), recipient, x, CborSerializer.encodeArray() From 122e6700c9c12f190dd716a2f29468c737291b11 Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Wed, 29 Apr 2026 14:44:09 +0000 Subject: [PATCH 02/11] #60 Rename nonce to stateMask, remove unused files --- .../sdk/payment/SplitAssetProof.java | 2 +- .../SplitMintJustificationVerifier.java | 41 ++++++++++--------- .../sdk/payment/SplitPaymentData.java | 13 ------ .../payment/SplitPaymentDataDeserializer.java | 15 ------- .../unicitylabs/sdk/payment/TokenSplit.java | 6 +-- .../transaction/CertifiedMintTransaction.java | 4 +- .../CertifiedTransferTransaction.java | 4 +- .../sdk/transaction/MintTransaction.java | 4 +- .../unicitylabs/sdk/transaction/Token.java | 2 +- .../sdk/transaction/Transaction.java | 2 +- .../sdk/transaction/TransferTransaction.java | 24 +++++------ .../MintJustificationVerifierService.java | 8 ++-- .../SplitMintJustificationVerifierTest.java | 6 +-- 13 files changed, 53 insertions(+), 78 deletions(-) delete mode 100644 src/main/java/org/unicitylabs/sdk/payment/SplitPaymentData.java delete mode 100644 src/main/java/org/unicitylabs/sdk/payment/SplitPaymentDataDeserializer.java diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitAssetProof.java b/src/main/java/org/unicitylabs/sdk/payment/SplitAssetProof.java index e756b95..d891d25 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitAssetProof.java +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitAssetProof.java @@ -12,7 +12,7 @@ /** * Proof material for one split reason entry. */ -public class SplitAssetProof { +public final class SplitAssetProof { private final AssetId assetId; private final SparseMerkleTreePath aggregationPath; private final SparseMerkleSumTreePath assetTreePath; diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java b/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java index 96b6423..cc488b2 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java @@ -16,7 +16,12 @@ import org.unicitylabs.sdk.util.verification.VerificationStatus; import java.math.BigInteger; -import java.util.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; public class SplitMintJustificationVerifier implements MintJustificationVerifier { private final RootTrustBase trustBase; @@ -43,7 +48,7 @@ public VerificationResult verify(CertifiedMintTransaction tr Objects.requireNonNull(transaction, "transaction cannot be null"); Objects.requireNonNull(mintJustificationVerifier, "mintJustificationVerifierService cannot be null"); - var justificationBytes = transaction.getJustification().orElse(null); + byte[] justificationBytes = transaction.getJustification().orElse(null); if (justificationBytes == null) { return new VerificationResult<>( "SplitMintJustificationVerificationRule", @@ -52,9 +57,9 @@ public VerificationResult verify(CertifiedMintTransaction tr ); } - var justification = SplitMintJustification.fromCbor(justificationBytes); - var paymentDataBytes = transaction.getData().orElse(null); - var paymentData = paymentDataBytes != null ? this.decodePaymentData.decode(paymentDataBytes) : null; + SplitMintJustification justification = SplitMintJustification.fromCbor(justificationBytes); + byte[] paymentDataBytes = transaction.getData().orElse(null); + PaymentData paymentData = paymentDataBytes != null ? this.decodePaymentData.decode(paymentDataBytes) : null; if (paymentData == null || paymentData.getAssets() == null) { return new VerificationResult<>( @@ -75,14 +80,6 @@ public VerificationResult verify(CertifiedMintTransaction tr ); } - if (paymentData.getAssets().size() != justification.getProofs().size()) { - return new VerificationResult<>( - "SplitMintJustificationVerificationRule", - VerificationStatus.FAIL, - "Total amount of assets differ in token and proofs." - ); - } - Map assets = new HashMap<>(); for (Asset asset : paymentData.getAssets()) { if (asset == null) { @@ -103,7 +100,15 @@ public VerificationResult verify(CertifiedMintTransaction tr } } - var validatedAssets = new HashSet(); + if (assets.size() != justification.getProofs().size()) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Total amount of assets differ in token and proofs." + ); + } + + Set validatedAssets = new HashSet<>(); Transaction burnTokenLastTransaction = justification.getToken().getLatestTransaction(); DataHash root = justification.getProofs().get(0).getAggregationPath().getRootHash(); for (SplitAssetProof proof : justification.getProofs()) { @@ -166,10 +171,8 @@ public VerificationResult verify(CertifiedMintTransaction tr ); } - var recipient = burnTokenLastTransaction != null - ? EncodedPredicate.fromPredicate(burnTokenLastTransaction.getRecipient()) - : null; - var expectedRecipient = EncodedPredicate.fromPredicate( + EncodedPredicate recipient = EncodedPredicate.fromPredicate(burnTokenLastTransaction.getRecipient()); + EncodedPredicate expectedRecipient = EncodedPredicate.fromPredicate( BurnPredicate.create(proof.getAggregationPath().getRootHash().getImprint()) ); @@ -184,7 +187,7 @@ public VerificationResult verify(CertifiedMintTransaction tr validatedAssets.add(proof.getAssetId()); } - if (validatedAssets.size() != paymentData.getAssets().size()) { + if (validatedAssets.size() != assets.size()) { return new VerificationResult<>( "SplitMintJustificationVerificationRule", VerificationStatus.FAIL, diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentData.java b/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentData.java deleted file mode 100644 index 8170439..0000000 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentData.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.unicitylabs.sdk.payment; - -/** - * Payment data for already split payments. - */ -public interface SplitPaymentData extends PaymentData { - /** - * Returns the reason associated with the split. - * - * @return split reason - */ - SplitMintJustification getReason(); -} diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentDataDeserializer.java b/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentDataDeserializer.java deleted file mode 100644 index d1bc10d..0000000 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentDataDeserializer.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.unicitylabs.sdk.payment; - -/** - * Functional contract for decoding encoded split payment data. - */ -@FunctionalInterface -public interface SplitPaymentDataDeserializer { - /** - * Decodes split payment data bytes. - * - * @param data encoded split payment data bytes - * @return decoded split payment data - */ - SplitPaymentData decode(byte[] data); -} diff --git a/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java b/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java index 10d16bc..39cdda3 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java +++ b/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java @@ -113,13 +113,13 @@ public static SplitResult split( SparseMerkleTreeRootNode aggregationRoot = aggregationTree.calculateRoot(); BurnPredicate burnPredicate = BurnPredicate.create(aggregationRoot.getRootHash().getImprint()); - byte[] nonce = new byte[32]; - RANDOM.nextBytes(nonce); + byte[] stateMask = new byte[32]; + RANDOM.nextBytes(stateMask); TransferTransaction burnTransaction = TransferTransaction.create( token, burnPredicate, - nonce, + stateMask, CborSerializer.encodeNull() ); diff --git a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java index 4486032..7ea3b20 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java @@ -71,8 +71,8 @@ public Optional getJustification() { } @Override - public byte[] getNonce() { - return this.transaction.getNonce(); + public byte[] getStateMask() { + return this.transaction.getStateMask(); } /** diff --git a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java index 973713b..1b9bb79 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java @@ -52,8 +52,8 @@ public DataHash getSourceStateHash() { } @Override - public byte[] getNonce() { - return this.transaction.getNonce(); + public byte[] getStateMask() { + return this.transaction.getStateMask(); } /** diff --git a/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java index 2e8faa1..aafac72 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java @@ -111,7 +111,7 @@ public Optional getData() { } @Override - public byte[] getNonce() { + public byte[] getStateMask() { return this.tokenId.getBytes(); } @@ -188,7 +188,7 @@ public DataHash calculateStateHash() { .update( CborSerializer.encodeArray( CborSerializer.encodeByteString(this.sourceStateHash.getImprint()), - CborSerializer.encodeByteString(this.getNonce()) + CborSerializer.encodeByteString(this.getStateMask()) ) ) .digest(); diff --git a/src/main/java/org/unicitylabs/sdk/transaction/Token.java b/src/main/java/org/unicitylabs/sdk/transaction/Token.java index be45c07..93f4d95 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/Token.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/Token.java @@ -18,7 +18,7 @@ /** * Immutable token aggregate containing the certified genesis mint transaction and transfer history. */ -public class Token { +public final class Token { public static final long CBOR_TAG = 39040; private static final int VERSION = 1; diff --git a/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java b/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java index 19cd038..4731cd1 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java @@ -43,7 +43,7 @@ public interface Transaction { * * @return randomness bytes */ - byte[] getNonce(); + byte[] getStateMask(); /** * Calculates the resulting state hash. diff --git a/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java index 7a80e51..b22214d 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java @@ -27,20 +27,20 @@ public class TransferTransaction implements Transaction { private final DataHash sourceStateHash; private final Predicate lockScript; private final Predicate recipient; - private final byte[] nonce; + private final byte[] stateMask; private final byte[] data; private TransferTransaction( DataHash sourceStateHash, Predicate lockScript, Predicate recipient, - byte[] nonce, + byte[] stateMask, byte[] data ) { this.sourceStateHash = sourceStateHash; this.lockScript = lockScript; this.recipient = recipient; - this.nonce = nonce; + this.stateMask = stateMask; this.data = data; } @@ -70,8 +70,8 @@ public DataHash getSourceStateHash() { } @Override - public byte[] getNonce() { - return Arrays.copyOf(this.nonce, this.nonce.length); + public byte[] getStateMask() { + return Arrays.copyOf(this.stateMask, this.stateMask.length); } /** @@ -79,19 +79,19 @@ public byte[] getNonce() { * * @param token token whose latest transaction is used as the source * @param recipient recipient predicate - * @param nonce transaction randomness component + * @param stateMask transaction randomness component * @param data transfer payload * @return created transfer transaction */ public static TransferTransaction create(Token token, Predicate recipient, - byte[] nonce, byte[] data) { + byte[] stateMask, byte[] data) { Transaction transaction = token.getLatestTransaction(); return new TransferTransaction( transaction.calculateStateHash(), transaction.getRecipient(), recipient, - nonce, + stateMask, data ); } @@ -129,7 +129,7 @@ public DataHash calculateStateHash() { .update( CborSerializer.encodeArray( CborSerializer.encodeByteString(this.sourceStateHash.getImprint()), - CborSerializer.encodeByteString(this.nonce) + CborSerializer.encodeByteString(this.stateMask) ) ) .digest(); @@ -149,7 +149,7 @@ public byte[] toCbor() { CborSerializer.encodeArray( CborSerializer.encodeUnsignedInteger(TransferTransaction.VERSION), EncodedPredicate.fromPredicate(this.recipient).toCbor(), - CborSerializer.encodeByteString(this.nonce), + CborSerializer.encodeByteString(this.stateMask), CborSerializer.encodeNullable(this.data, CborSerializer::encodeByteString) ) ); @@ -179,8 +179,8 @@ public CertifiedTransferTransaction toCertifiedTransaction( @Override public String toString() { return String.format( - "TransferTransaction{sourceStateHash=%s, lockScript=%s, recipient=%s, nonce=%s, data=%s}", - this.sourceStateHash, this.lockScript, this.recipient, HexConverter.encode(this.nonce), + "TransferTransaction{sourceStateHash=%s, lockScript=%s, recipient=%s, stateMask=%s, data=%s}", + this.sourceStateHash, this.lockScript, this.recipient, HexConverter.encode(this.stateMask), HexConverter.encode(this.data)); } } diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/MintJustificationVerifierService.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/MintJustificationVerifierService.java index d5e06af..2e1f34a 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/verification/MintJustificationVerifierService.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/MintJustificationVerifierService.java @@ -45,13 +45,13 @@ public MintJustificationVerifierService register(MintJustificationVerifier verif * of the verifier registered for the justification's CBOR tag */ public VerificationResult verify(CertifiedMintTransaction transaction) { - var bytes = transaction.getJustification().orElse(null); + byte[] bytes = transaction.getJustification().orElse(null); if (bytes == null) { return new VerificationResult<>("MintJustificationVerification", VerificationStatus.OK); } - var tag = CborDeserializer.decodeTag(bytes); - var verifier = this.verifiers.get(tag.getTag()); + CborDeserializer.CborTag tag = CborDeserializer.decodeTag(bytes); + MintJustificationVerifier verifier = this.verifiers.get(tag.getTag()); if (verifier == null) { return new VerificationResult<>( "MintJustificationVerification", @@ -60,7 +60,7 @@ public VerificationResult verify(CertifiedMintTransaction tr ); } - var result = verifier.verify(transaction, this); + VerificationResult result = verifier.verify(transaction, this); if (result.getStatus() != VerificationStatus.OK) { return new VerificationResult<>("MintJustificationVerification", VerificationStatus.FAIL, String.format("Verification failed for tag %s", tag.getTag()), result); } diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/SplitMintJustificationVerifierTest.java b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitMintJustificationVerifierTest.java index 419c2fb..888fcd3 100644 --- a/src/test/java/org/unicitylabs/sdk/functional/payment/SplitMintJustificationVerifierTest.java +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitMintJustificationVerifierTest.java @@ -414,9 +414,9 @@ private byte[] corruptBurnTokenInJustification(byte[] justificationBytes) { CborDeserializer.CborTag transferTag = CborDeserializer.decodeTag(certifiedTransfer.get(0)); List transfer = CborDeserializer.decodeArray(transferTag.getData()); - byte[] differentNonce = new byte[32]; - differentNonce[0] = 1; - transfer.set(2, CborSerializer.encodeByteString(differentNonce)); + byte[] differentStateMask = new byte[32]; + differentStateMask[0] = 1; + transfer.set(2, CborSerializer.encodeByteString(differentStateMask)); certifiedTransfer.set(0, CborSerializer.encodeTag(transferTag.getTag(), encodeArray(transfer))); transactions.set(0, encodeArray(certifiedTransfer)); From 91c9d0efc8f17e1769a30c4d413d38eafa254899 Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Wed, 29 Apr 2026 14:57:47 +0000 Subject: [PATCH 03/11] #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 04/11] #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 08673c94acbbf2c81db3124d9433d5ce4533bd40 Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Wed, 29 Apr 2026 17:12:07 +0000 Subject: [PATCH 05/11] #50 Add initial unicity id support --- .../sdk/api/CertificationData.java | 3 - ...cityIdMintTransactionVerificationRule.java | 60 ++++ .../CertifiedUnicityIdMintTransaction.java | 172 +++++++++++ .../unicitylabs/sdk/unicityid/UnicityId.java | 127 ++++++++ .../unicityid/UnicityIdMintTransaction.java | 274 ++++++++++++++++++ .../sdk/unicityid/UnicityIdToken.java | 141 +++++++++ .../sdk/common/CommonTestFlow.java | 70 +++++ 7 files changed, 844 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedUnicityIdMintTransactionVerificationRule.java create mode 100644 src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java create mode 100644 src/main/java/org/unicitylabs/sdk/unicityid/UnicityId.java create mode 100644 src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java create mode 100644 src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java diff --git a/src/main/java/org/unicitylabs/sdk/api/CertificationData.java b/src/main/java/org/unicitylabs/sdk/api/CertificationData.java index 7ec2771..11a0e36 100644 --- a/src/main/java/org/unicitylabs/sdk/api/CertificationData.java +++ b/src/main/java/org/unicitylabs/sdk/api/CertificationData.java @@ -117,8 +117,6 @@ 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( @@ -137,7 +135,6 @@ 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()); diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedUnicityIdMintTransactionVerificationRule.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedUnicityIdMintTransactionVerificationRule.java new file mode 100644 index 0000000..5064636 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedUnicityIdMintTransactionVerificationRule.java @@ -0,0 +1,60 @@ +package org.unicitylabs.sdk.transaction.verification; + +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +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. + */ +public class CertifiedUnicityIdMintTransactionVerificationRule { + + private CertifiedUnicityIdMintTransactionVerificationRule() { + } + + /** + * Verify the certified unicity id mint transaction. + * + * @param trustBase root trust base + * @param predicateVerifier predicate verifier + * @param genesis certified unicity id mint transaction to verify + * + * @return verification result + */ + public static VerificationResult verify( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + CertifiedUnicityIdMintTransaction genesis + ) { + List> results = new ArrayList<>(); + + VerificationResult result = InclusionProofVerificationRule.verify( + trustBase, + predicateVerifier, + 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 new file mode 100644 index 0000000..3f3c2db --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java @@ -0,0 +1,172 @@ +package org.unicitylabs.sdk.unicityid; + +import org.unicitylabs.sdk.api.InclusionProof; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +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.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.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 Predicate getLockScript() { + return this.transaction.getLockScript(); + } + + @Override + public Predicate getRecipient() { + return this.transaction.getRecipient(); + } + + @Override + public DataHash getSourceStateHash() { + return this.transaction.getSourceStateHash(); + } + + @Override + public byte[] 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 PayToPublicKeyPredicate 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); + 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 + ) { + 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 new file mode 100644 index 0000000..af7ac76 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityId.java @@ -0,0 +1,127 @@ +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.TokenId; + +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 TokenId}. + */ +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); + 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 id from this unicity id by hashing the tagged ("NAMETAG_", domain, name) + * tuple with SHA-256. + * + * @return derived token id + */ + public TokenId toTokenId() { + DataHash hash = new DataHasher(HashAlgorithm.SHA256) + .update( + CborSerializer.encodeArray( + CborSerializer.encodeTextString("NAMETAG_"), + CborSerializer.encodeNullable(this.domain, CborSerializer::encodeTextString), + CborSerializer.encodeTextString(this.name) + ) + ) + .digest(); + return new TokenId(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 new file mode 100644 index 0000000..ece2806 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java @@ -0,0 +1,274 @@ +package org.unicitylabs.sdk.unicityid; + +import org.unicitylabs.sdk.api.InclusionProof; +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.PayToPublicKeyPredicate; +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.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 PayToPublicKeyPredicate lockScript; + private final Predicate recipient; + private final TokenId tokenId; + private final TokenType tokenType; + private final PayToPublicKeyPredicate targetPredicate; + private final UnicityId unicityId; + + private UnicityIdMintTransaction( + MintTransactionState sourceStateHash, + PayToPublicKeyPredicate lockScript, + Predicate recipient, + TokenId tokenId, + TokenType tokenType, + PayToPublicKeyPredicate targetPredicate, + UnicityId unicityId + ) { + this.sourceStateHash = sourceStateHash; + this.lockScript = lockScript; + 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 PayToPublicKeyPredicate getLockScript() { + return this.lockScript; + } + + @Override + public Predicate getRecipient() { + return this.recipient; + } + + /** + * 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 PayToPublicKeyPredicate 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 byte[] getStateMask() { + return 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 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( + PayToPublicKeyPredicate lockScript, + Predicate recipient, + UnicityId unicityId, + TokenType tokenType, + PayToPublicKeyPredicate targetPredicate + ) { + 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 = unicityId.toTokenId(); + + return new UnicityIdMintTransaction( + MintTransactionState.create(tokenId), + lockScript, + 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()); + + int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); + if (version != UnicityIdMintTransaction.VERSION) { + throw new CborSerializationException(String.format("Unsupported version: %s", version)); + } + + PayToPublicKeyPredicate lockScript = PayToPublicKeyPredicate.fromPredicate( + EncodedPredicate.fromCbor(data.get(1)) + ); + Predicate recipient = EncodedPredicate.fromCbor(data.get(2)); + TokenId tokenId = TokenId.fromCbor(data.get(3)); + TokenType tokenType = TokenType.fromCbor(data.get(4)); + UnicityId unicityId = UnicityId.fromCbor(data.get(5)); + PayToPublicKeyPredicate targetPredicate = PayToPublicKeyPredicate.fromPredicate( + EncodedPredicate.fromCbor(data.get(6)) + ); + + if (!tokenId.equals(unicityId.toTokenId())) { + throw new CborSerializationException("Token id does not match unicity id"); + } + + return new UnicityIdMintTransaction( + MintTransactionState.create(tokenId), + lockScript, + recipient, + tokenId, + tokenType, + targetPredicate, + unicityId + ); + } + + @Override + public DataHash calculateStateHash() { + return new DataHasher(HashAlgorithm.SHA256) + .update( + CborSerializer.encodeArray( + CborSerializer.encodeByteString(this.sourceStateHash.getImprint()), + CborSerializer.encodeByteString(this.getStateMask()) + ) + ) + .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), + EncodedPredicate.fromPredicate(this.lockScript).toCbor(), + EncodedPredicate.fromPredicate(this.recipient).toCbor(), + this.tokenId.toCbor(), + this.tokenType.toCbor(), + this.unicityId.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{lockScript=%s, recipient=%s, tokenId=%s, tokenType=%s, unicityId=%s, targetPredicate=%s}", + 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 new file mode 100644 index 0000000..180570b --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java @@ -0,0 +1,141 @@ +package org.unicitylabs.sdk.unicityid; + +import org.unicitylabs.sdk.api.bft.RootTrustBase; +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.TokenId; +import org.unicitylabs.sdk.transaction.TokenType; +import org.unicitylabs.sdk.transaction.verification.CertifiedUnicityIdMintTransactionVerificationRule; +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; + +/** + * 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); + return new UnicityIdToken(CertifiedUnicityIdMintTransaction.fromCbor(data.get(0))); + } + + /** + * Build a unicity id token from a certified genesis transaction and verify it. + * + * @param trustBase trust base used for certification verification + * @param predicateVerifier predicate verifier service + * @param genesis certified mint transaction + * + * @return verified token + * + * @throws VerificationException if genesis verification fails + */ + public static UnicityIdToken mint( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + CertifiedUnicityIdMintTransaction genesis + ) { + UnicityIdToken token = new UnicityIdToken(genesis); + VerificationResult result = token.verify(trustBase, predicateVerifier); + if (result.getStatus() != VerificationStatus.OK) { + throw new VerificationException("Invalid token genesis", result); + } + + return token; + } + + /** + * 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. + * + * @param trustBase trust base used for certification verification + * @param predicateVerifier predicate verifier service + * + * @return verification result + */ + public VerificationResult verify( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier + ) { + List> results = new ArrayList<>(); + VerificationResult result = CertifiedUnicityIdMintTransactionVerificationRule.verify( + trustBase, + predicateVerifier, + this.genesis + ); + 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/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java b/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java index 44b326d..528edb0 100644 --- a/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java +++ b/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java @@ -3,12 +3,21 @@ 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.api.bft.RootTrustBase; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.transaction.Token; +import org.unicitylabs.sdk.transaction.TokenType; import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifierService; +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,4 +71,65 @@ public void testTransferFlow() throws Exception { Assertions.assertEquals(VerificationStatus.OK, carolToken.verify(this.trustBase, this.predicateVerifier, this.mintJustificationVerifier).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(); + PayToPublicKeyPredicate targetPredicate = PayToPublicKeyPredicate.create( + ALICE_SIGNING_SERVICE.getPublicKey()); + + UnicityId unicityId = new UnicityId("testuser", "unicity-labs/test"); + UnicityIdMintTransaction unicityIdMintTransaction = UnicityIdMintTransaction.create( + PayToPublicKeyPredicate.fromSigningService(unicityIdSigningService), + targetPredicate, + unicityId, + TokenType.generate(), + targetPredicate + ); + + CertificationData unicityIdCertificationData = CertificationData.fromTransaction( + unicityIdMintTransaction, + PayToPublicKeyPredicateUnlockScript.create(unicityIdMintTransaction, unicityIdSigningService) + ); + + CertificationResponse unicityIdResponse = this.client + .submitCertificationRequest(unicityIdCertificationData).get(); + Assertions.assertEquals(CertificationStatus.SUCCESS, unicityIdResponse.getStatus()); + + UnicityIdToken aliceUnicityIdToken = UnicityIdToken.mint( + this.trustBase, + this.predicateVerifier, + unicityIdMintTransaction.toCertifiedTransaction( + this.trustBase, + this.predicateVerifier, + InclusionProofUtils.waitInclusionProof(this.client, this.trustBase, + this.predicateVerifier, unicityIdMintTransaction).get() + ) + ); + + Assertions.assertEquals(VerificationStatus.OK, + aliceUnicityIdToken.verify(this.trustBase, this.predicateVerifier).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.trustBase, this.predicateVerifier).getStatus()); + + Token aliceToken = TokenUtils.mintToken( + this.client, + this.trustBase, + this.predicateVerifier, + this.mintJustificationVerifier, + aliceUnicityIdToken.getGenesis().getTargetPredicate() + ); + + Assertions.assertEquals(VerificationStatus.OK, + aliceToken.verify(this.trustBase, this.predicateVerifier, this.mintJustificationVerifier) + .getStatus()); + } } \ No newline at end of file From ff9f377af92ff754b34a3ab3ced492a0dfb43383 Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Wed, 29 Apr 2026 17:51:54 +0000 Subject: [PATCH 06/11] #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()); + } +} From 237f279f96177d935b211756e68b8c3a46005978 Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Wed, 29 Apr 2026 18:43:25 +0000 Subject: [PATCH 07/11] #50 Enforce array length on cbor parsing. Add stateID header --- .../sdk/api/CertificationData.java | 4 +- .../unicitylabs/sdk/api/InclusionProof.java | 4 +- .../sdk/api/InclusionProofResponse.java | 2 +- .../sdk/api/JsonRpcAggregatorClient.java | 10 +++-- .../unicitylabs/sdk/api/bft/InputRecord.java | 4 +- .../sdk/api/bft/ShardTreeCertificate.java | 4 +- .../sdk/api/bft/UnicityCertificate.java | 4 +- .../unicitylabs/sdk/api/bft/UnicitySeal.java | 4 +- .../sdk/api/bft/UnicityTreeCertificate.java | 6 +-- .../sdk/payment/SplitAssetProof.java | 2 +- .../sdk/payment/SplitMintJustification.java | 2 +- .../unicitylabs/sdk/payment/asset/Asset.java | 2 +- .../sdk/predicate/EncodedPredicate.java | 2 +- .../sdk/serializer/cbor/CborDeserializer.java | 19 ++++++++++ .../sdk/smt/plain/SparseMerkleTreePath.java | 2 +- .../smt/plain/SparseMerkleTreePathStep.java | 2 +- .../sdk/smt/sum/SparseMerkleSumTreePath.java | 4 +- .../smt/sum/SparseMerkleSumTreePathStep.java | 2 +- .../transaction/CertifiedMintTransaction.java | 2 +- .../CertifiedTransferTransaction.java | 2 +- .../sdk/transaction/MintTransaction.java | 2 +- .../unicitylabs/sdk/transaction/Token.java | 2 +- .../sdk/transaction/TransferTransaction.java | 2 +- .../CertifiedUnicityIdMintTransaction.java | 2 +- .../unicitylabs/sdk/unicityid/UnicityId.java | 2 +- .../unicityid/UnicityIdMintTransaction.java | 38 ++++++------------- .../sdk/unicityid/UnicityIdToken.java | 2 +- 27 files changed, 72 insertions(+), 61 deletions(-) diff --git a/src/main/java/org/unicitylabs/sdk/api/CertificationData.java b/src/main/java/org/unicitylabs/sdk/api/CertificationData.java index 11a0e36..7f298bd 100644 --- a/src/main/java/org/unicitylabs/sdk/api/CertificationData.java +++ b/src/main/java/org/unicitylabs/sdk/api/CertificationData.java @@ -94,7 +94,7 @@ public static CertificationData fromCbor(byte[] bytes) { if (tag.getTag() != CertificationData.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 5); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != CertificationData.VERSION) { @@ -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( diff --git a/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java b/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java index bab61ac..8944587 100644 --- a/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java +++ b/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java @@ -74,7 +74,7 @@ public static InclusionProof fromCbor(byte[] bytes) { if (tag.getTag() != InclusionProof.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 4); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != InclusionProof.VERSION) { @@ -120,7 +120,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(InclusionProof.VERSION, this.inclusionCertificate, this.certificationData, this.unicityCertificate); + return Objects.hash(this.inclusionCertificate, this.certificationData, this.unicityCertificate); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/api/InclusionProofResponse.java b/src/main/java/org/unicitylabs/sdk/api/InclusionProofResponse.java index 6f0a775..d53e22e 100644 --- a/src/main/java/org/unicitylabs/sdk/api/InclusionProofResponse.java +++ b/src/main/java/org/unicitylabs/sdk/api/InclusionProofResponse.java @@ -42,7 +42,7 @@ public InclusionProof getInclusionProof() { * @return inclusion proof response */ public static InclusionProofResponse fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new InclusionProofResponse( CborDeserializer.decodeUnsignedInteger(data.get(0)).asLong(), InclusionProof.fromCbor(data.get(1)) diff --git a/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java b/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java index 1c47e0d..18d6198 100644 --- a/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java +++ b/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java @@ -3,6 +3,7 @@ import org.unicitylabs.sdk.api.jsonrpc.JsonRpcHttpTransport; import org.unicitylabs.sdk.util.HexConverter; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -14,6 +15,7 @@ * Default aggregator client. */ public class JsonRpcAggregatorClient implements AggregatorClient { + private static final String STATE_ID_HEADER = "X-State-Id"; private final JsonRpcHttpTransport transport; private final String apiKey; @@ -54,9 +56,11 @@ public CompletableFuture submitCertificationRequest( CertificationRequest request = CertificationRequest.create( Objects.requireNonNull(certificationData, "certificationData cannot be null")); - Map> headers = this.apiKey == null - ? Map.of() - : Map.of(AUTHORIZATION, List.of(String.format("Bearer %s", this.apiKey))); + Map> headers = new HashMap<>(); + headers.put(STATE_ID_HEADER, List.of(request.getStateId().toString())); + if (this.apiKey != null) { + headers.put(AUTHORIZATION, List.of(String.format("Bearer %s", this.apiKey))); + } return this.transport.request( "certification_request", 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 af1e455..d8a1a1e 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java @@ -150,7 +150,7 @@ public static InputRecord fromCbor(byte[] bytes) { if (tag.getTag() != InputRecord.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 10); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != InputRecord.VERSION) { @@ -212,7 +212,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(InputRecord.VERSION, this.roundNumber, this.epoch, + return Objects.hash(this.roundNumber, this.epoch, Arrays.hashCode(this.previousHash), Arrays.hashCode(this.hash), Arrays.hashCode(this.summaryValue), this.timestamp, Arrays.hashCode(this.blockHash), 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 babcc26..94227b3 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java @@ -64,7 +64,7 @@ public static ShardTreeCertificate fromCbor(byte[] bytes) { if (tag.getTag() != ShardTreeCertificate.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 3); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != ShardTreeCertificate.VERSION) { @@ -111,7 +111,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(ShardTreeCertificate.VERSION, this.shard, this.siblingHashList); + return Objects.hash(this.shard, this.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 09286d3..4900501 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java @@ -173,7 +173,7 @@ public static UnicityCertificate fromCbor(byte[] bytes) { if (tag.getTag() != UnicityCertificate.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 7); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != UnicityCertificate.VERSION) { @@ -226,7 +226,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(UnicityCertificate.VERSION, this.inputRecord, Arrays.hashCode(this.technicalRecordHash), + return Objects.hash(this.inputRecord, Arrays.hashCode(this.technicalRecordHash), Arrays.hashCode(this.shardConfigurationHash), this.shardTreeCertificate, this.unicityTreeCertificate, this.unicitySeal); } 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 c7ec830..3759ffe 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java @@ -144,7 +144,7 @@ public static UnicitySeal fromCbor(byte[] bytes) { if (tag.getTag() != UnicitySeal.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 8); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != UnicitySeal.VERSION) { @@ -235,7 +235,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(UnicitySeal.VERSION, this.networkId, this.rootChainRoundNumber, this.epoch, + return Objects.hash(this.networkId, this.rootChainRoundNumber, this.epoch, this.timestamp, Arrays.hashCode(this.previousHash), Arrays.hashCode(this.hash), this.signatures); } 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 a248f54..0f979a6 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/UnicityTreeCertificate.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityTreeCertificate.java @@ -68,7 +68,7 @@ public static UnicityTreeCertificate fromCbor(byte[] bytes) { if (tag.getTag() != UnicityTreeCertificate.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 3); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != UnicityTreeCertificate.VERSION) { @@ -112,7 +112,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(UnicityTreeCertificate.VERSION, this.partitionIdentifier, this.steps); + return Objects.hash(this.partitionIdentifier, this.steps); } @Override @@ -160,7 +160,7 @@ public byte[] getHash() { * @return hash step */ public static HashStep fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new HashStep( CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(), diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitAssetProof.java b/src/main/java/org/unicitylabs/sdk/payment/SplitAssetProof.java index d891d25..29654c2 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitAssetProof.java +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitAssetProof.java @@ -79,7 +79,7 @@ public static SplitAssetProof create( * @return split reason proof */ public static SplitAssetProof fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 3); return new SplitAssetProof( AssetId.fromCbor(data.get(0)), diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustification.java b/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustification.java index 7f64f21..6f68714 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustification.java +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustification.java @@ -77,7 +77,7 @@ public static SplitMintJustification fromCbor(byte[] bytes) { if (tag.getTag() != SplitMintJustification.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 2); return SplitMintJustification.create( Token.fromCbor(data.get(0)), CborDeserializer.decodeArray(data.get(1)).stream().map(SplitAssetProof::fromCbor).collect(Collectors.toSet()) diff --git a/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java b/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java index e9744cb..ee253dd 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java +++ b/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java @@ -56,7 +56,7 @@ public BigInteger getValue() { * @return asset */ public static Asset fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new Asset( AssetId.fromCbor(data.get(0)), diff --git a/src/main/java/org/unicitylabs/sdk/predicate/EncodedPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/EncodedPredicate.java index 2f6ee17..ef35844 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/EncodedPredicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/EncodedPredicate.java @@ -41,7 +41,7 @@ public static EncodedPredicate fromCbor(byte[] bytes) { if (tag.getTag() != EncodedPredicate.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 3); PredicateEngine engine = PredicateEngine.fromId( CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt()); diff --git a/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializer.java b/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializer.java index 7712ebd..275ee9d 100644 --- a/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializer.java +++ b/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializer.java @@ -85,6 +85,25 @@ public static List decodeArray(byte[] data) { return result; } + /** + * Read a fixed-size CBOR array from bytes. Throws when the encoded array length does not match + * the expected length. + * + * @param data bytes + * @param expectedLength expected number of array elements + * @return CBOR element array + * + * @throws CborSerializationException when the array length differs from {@code expectedLength} + */ + public static List decodeArray(byte[] data, long expectedLength) { + List result = decodeArray(data); + if (result.size() != expectedLength) { + throw new CborSerializationException( + String.format("Expected array of %d elements, got %d", expectedLength, result.size())); + } + return result; + } + /** * Read elements as raw CBOR element map from CBOR bytes. * diff --git a/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePath.java b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePath.java index a8ff834..1dcd88e 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePath.java +++ b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePath.java @@ -122,7 +122,7 @@ public MerkleTreePathVerificationResult verify(BigInteger stateId) { * @return path */ public static SparseMerkleTreePath fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new SparseMerkleTreePath( DataHash.fromCbor(data.get(0)), diff --git a/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePathStep.java b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePathStep.java index e0fefb4..931c850 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePathStep.java +++ b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePathStep.java @@ -63,7 +63,7 @@ public Optional getData() { * @return sparse Merkle tree path step */ public static SparseMerkleTreePathStep fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new SparseMerkleTreePathStep( BigIntegerConverter.decode(CborDeserializer.decodeByteString(data.get(0))), diff --git a/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePath.java b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePath.java index 907d2cb..bdc0763 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePath.java +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePath.java @@ -132,7 +132,7 @@ public MerkleTreePathVerificationResult verify(BigInteger stateId) { * @return path */ public static SparseMerkleSumTreePath fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new SparseMerkleSumTreePath( DataHash.fromCbor(data.get(0)), @@ -218,7 +218,7 @@ public BigInteger getCounter() { * @return root */ public static Root fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new Root( DataHash.fromCbor(data.get(0)), diff --git a/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePathStep.java b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePathStep.java index dc0168f..532fce8 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePathStep.java +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePathStep.java @@ -67,7 +67,7 @@ public BigInteger getValue() { * @return step */ public static SparseMerkleSumTreePathStep fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 3); return new SparseMerkleSumTreePathStep( BigIntegerConverter.decode(CborDeserializer.decodeByteString(data.get(0))), diff --git a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java index 7ea3b20..52fbb5f 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java @@ -91,7 +91,7 @@ public InclusionProof getInclusionProof() { * @return decoded certified mint transaction */ public static CertifiedMintTransaction fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new CertifiedMintTransaction(MintTransaction.fromCbor(data.get(0)), InclusionProof.fromCbor(data.get(1))); } diff --git a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java index 1b9bb79..fd5ff1d 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java @@ -74,7 +74,7 @@ public InclusionProof getInclusionProof() { * @return certified transfer transaction */ public static CertifiedTransferTransaction fromCbor(byte[] bytes, Token token) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new CertifiedTransferTransaction( TransferTransaction.fromCbor(data.get(0), token), diff --git a/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java index aafac72..ddeb177 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java @@ -161,7 +161,7 @@ public static MintTransaction fromCbor(byte[] bytes) { if (tag.getTag() != MintTransaction.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 6); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != MintTransaction.VERSION) { diff --git a/src/main/java/org/unicitylabs/sdk/transaction/Token.java b/src/main/java/org/unicitylabs/sdk/transaction/Token.java index 93f4d95..3f20f39 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/Token.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/Token.java @@ -98,7 +98,7 @@ public static Token fromCbor(byte[] bytes) { if (tag.getTag() != Token.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 3); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != Token.VERSION) { diff --git a/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java index b22214d..deadca1 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java @@ -108,7 +108,7 @@ public static TransferTransaction fromCbor(byte[] bytes, Token token) { if (tag.getTag() != TransferTransaction.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 4); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != TransferTransaction.VERSION) { diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java b/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java index 3f3c2db..67381ac 100644 --- a/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java @@ -111,7 +111,7 @@ public InclusionProof getInclusionProof() { * @return decoded certified mint transaction */ public static CertifiedUnicityIdMintTransaction fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new CertifiedUnicityIdMintTransaction( UnicityIdMintTransaction.fromCbor(data.get(0)), InclusionProof.fromCbor(data.get(1)) diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityId.java b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityId.java index af7ac76..48bc1db 100644 --- a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityId.java +++ b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityId.java @@ -65,7 +65,7 @@ public String getDomain() { * @return unicity id */ public static UnicityId fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new UnicityId( CborDeserializer.decodeTextString(data.get(0)), CborDeserializer.decodeNullable(data.get(1), CborDeserializer::decodeTextString) diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java index ece2806..a056191 100644 --- a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java @@ -179,36 +179,23 @@ public static UnicityIdMintTransaction fromCbor(byte[] bytes) { if (tag.getTag() != UnicityIdMintTransaction.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 6); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != UnicityIdMintTransaction.VERSION) { throw new CborSerializationException(String.format("Unsupported version: %s", version)); } - PayToPublicKeyPredicate lockScript = PayToPublicKeyPredicate.fromPredicate( - EncodedPredicate.fromCbor(data.get(1)) - ); - Predicate recipient = EncodedPredicate.fromCbor(data.get(2)); - TokenId tokenId = TokenId.fromCbor(data.get(3)); - TokenType tokenType = TokenType.fromCbor(data.get(4)); - UnicityId unicityId = UnicityId.fromCbor(data.get(5)); - PayToPublicKeyPredicate targetPredicate = PayToPublicKeyPredicate.fromPredicate( - EncodedPredicate.fromCbor(data.get(6)) - ); - - if (!tokenId.equals(unicityId.toTokenId())) { - throw new CborSerializationException("Token id does not match unicity id"); - } - - return new UnicityIdMintTransaction( - MintTransactionState.create(tokenId), - lockScript, - recipient, - tokenId, - tokenType, - targetPredicate, - unicityId + return UnicityIdMintTransaction.create( + PayToPublicKeyPredicate.fromPredicate( + EncodedPredicate.fromCbor(data.get(1)) + ), + EncodedPredicate.fromCbor(data.get(2)), + UnicityId.fromCbor(data.get(3)), + TokenType.fromCbor(data.get(4)), + PayToPublicKeyPredicate.fromPredicate( + EncodedPredicate.fromCbor(data.get(5)) + ) ); } @@ -237,9 +224,8 @@ public byte[] toCbor() { CborSerializer.encodeUnsignedInteger(UnicityIdMintTransaction.VERSION), EncodedPredicate.fromPredicate(this.lockScript).toCbor(), EncodedPredicate.fromPredicate(this.recipient).toCbor(), - this.tokenId.toCbor(), - this.tokenType.toCbor(), this.unicityId.toCbor(), + this.tokenType.toCbor(), EncodedPredicate.fromPredicate(this.targetPredicate).toCbor() ) ); diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java index 180570b..b2a0fe2 100644 --- a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java +++ b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java @@ -70,7 +70,7 @@ public UnicityId getUnicityId() { * @return decoded token */ public static UnicityIdToken fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 1); return new UnicityIdToken(CertifiedUnicityIdMintTransaction.fromCbor(data.get(0))); } From 57c94a27f67dbd59da97a06ba8ced33d96087976 Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Thu, 30 Apr 2026 10:45:52 +0000 Subject: [PATCH 08/11] #50 Rename state ID header --- .../java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java b/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java index 18d6198..5f9f0db 100644 --- a/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java +++ b/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java @@ -15,7 +15,7 @@ * Default aggregator client. */ public class JsonRpcAggregatorClient implements AggregatorClient { - private static final String STATE_ID_HEADER = "X-State-Id"; + private static final String STATE_ID_HEADER = "X-State-ID"; private final JsonRpcHttpTransport transport; private final String apiKey; From a78fff6168bdd2f44c985e0923cd41b44c325aa2 Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Mon, 4 May 2026 12:28:55 +0000 Subject: [PATCH 09/11] #50 Fix state id header for aggregator client --- .../sdk/api/JsonRpcAggregatorClient.java | 2 +- .../unicitylabs/sdk/predicate/Predicate.java | 1 - .../sdk/smt/sum/SparseMerkleSumTreePath.java | 67 ------------------- 3 files changed, 1 insertion(+), 69 deletions(-) diff --git a/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java b/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java index 5f9f0db..fd298fb 100644 --- a/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java +++ b/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java @@ -57,7 +57,7 @@ public CompletableFuture submitCertificationRequest( Objects.requireNonNull(certificationData, "certificationData cannot be null")); Map> headers = new HashMap<>(); - headers.put(STATE_ID_HEADER, List.of(request.getStateId().toString())); + headers.put(STATE_ID_HEADER, List.of(HexConverter.encode(request.getStateId().getData()))); if (this.apiKey != null) { headers.put(AUTHORIZATION, List.of(String.format("Bearer %s", this.apiKey))); } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java b/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java index 540caaf..c0bf17f 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java @@ -1,7 +1,6 @@ package org.unicitylabs.sdk.predicate; import java.util.Arrays; -import java.util.Objects; /** * Base contract for all predicate implementations. diff --git a/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePath.java b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePath.java index bdc0763..da2e867 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePath.java +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePath.java @@ -176,71 +176,4 @@ public int hashCode() { public String toString() { return String.format("MerkleTreePath{rootHash=%s, steps=%s}", this.rootHash, this.steps); } - - /** - * Root of the sparse merkle sum tree path. - */ - public static class Root { - - private final DataHash hash; - private final BigInteger counter; - - Root( - DataHash hash, - BigInteger counter - ) { - this.hash = Objects.requireNonNull(hash, "hash cannot be null"); - this.counter = Objects.requireNonNull(counter, "counter cannot be null"); - } - - /** - * Get hash of the root. - * - * @return hash - */ - public DataHash getHash() { - return this.hash; - } - - /** - * Get the counter of the root. - * - * @return counter - */ - public BigInteger getCounter() { - return this.counter; - } - - /** - * Create root from CBOR bytes. - * - * @param bytes CBOR bytes - * @return root - */ - public static Root fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes, 2); - - return new Root( - DataHash.fromCbor(data.get(0)), - BigIntegerConverter.decode(CborDeserializer.decodeByteString(data.get(1))) - ); - } - - /** - * Serialize root to CBOR bytes. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeArray( - this.hash.toCbor(), - CborSerializer.encodeByteString(BigIntegerConverter.encode(this.counter)) - ); - } - - @Override - public String toString() { - return String.format("Root{hash=%s, counter=%s}", this.hash, this.counter); - } - } } From 6542149e30c5fbca438a5233369d99bd8e799c2e Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Tue, 5 May 2026 13:19:26 +0000 Subject: [PATCH 10/11] #64 Make CBOR more strict, split proof dedup, add unicity id issuer check --- .../sdk/api/CertificationData.java | 17 ++-- .../java/org/unicitylabs/sdk/api/StateId.java | 5 +- .../SplitMintJustificationVerifier.java | 13 ++- .../unicitylabs/sdk/predicate/Predicate.java | 13 --- .../builtin/BuiltInPredicateType.java | 8 +- .../sdk/predicate/builtin/BurnPredicate.java | 4 +- .../DefaultBuiltInPredicateVerifier.java | 14 +-- ...Predicate.java => SignaturePredicate.java} | 26 +++--- ...va => SignaturePredicateUnlockScript.java} | 14 +-- .../BuiltInPredicateVerifier.java | 4 +- ...r.java => SignaturePredicateVerifier.java} | 20 ++-- .../verification/PredicateVerifier.java | 4 +- .../PredicateVerifierService.java | 16 ++-- .../sdk/serializer/cbor/CborDeserializer.java | 92 +++++++++++++++---- .../sdk/serializer/cbor/CborSerializer.java | 36 +++++--- .../transaction/CertifiedMintTransaction.java | 6 +- .../CertifiedTransferTransaction.java | 6 +- .../sdk/transaction/MintTransaction.java | 20 ++-- .../sdk/transaction/Transaction.java | 6 +- .../sdk/transaction/TransferTransaction.java | 14 +-- ...tifiedMintTransactionVerificationRule.java | 18 ++-- ...cityIdMintTransactionVerificationRule.java | 27 +++++- .../CertifiedUnicityIdMintTransaction.java | 10 +- .../unicityid/UnicityIdMintTransaction.java | 36 ++++---- .../sdk/unicityid/UnicityIdToken.java | 24 +++-- .../unicitylabs/sdk/TestAggregatorClient.java | 2 +- .../sdk/TestApiKeyIntegration.java | 4 +- .../sdk/api/InclusionProofTest.java | 10 +- .../sdk/common/CommonTestFlow.java | 22 +++-- .../org/unicitylabs/sdk/e2e/TokenE2ETest.java | 2 +- .../functional/FunctionalCommonFlowTest.java | 2 +- .../functional/payment/SplitBuilderTest.java | 10 +- .../SplitMintJustificationVerifierTest.java | 10 +- .../functional/payment/TokenSplitTest.java | 6 +- .../org/unicitylabs/sdk/utils/TokenUtils.java | 4 +- 35 files changed, 310 insertions(+), 215 deletions(-) rename src/main/java/org/unicitylabs/sdk/predicate/builtin/{PayToPublicKeyPredicate.java => SignaturePredicate.java} (69%) rename src/main/java/org/unicitylabs/sdk/predicate/builtin/{PayToPublicKeyPredicateUnlockScript.java => SignaturePredicateUnlockScript.java} (76%) rename src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/{PayToPublicKeyPredicateVerifier.java => SignaturePredicateVerifier.java} (67%) diff --git a/src/main/java/org/unicitylabs/sdk/api/CertificationData.java b/src/main/java/org/unicitylabs/sdk/api/CertificationData.java index 7f298bd..38d71c1 100644 --- a/src/main/java/org/unicitylabs/sdk/api/CertificationData.java +++ b/src/main/java/org/unicitylabs/sdk/api/CertificationData.java @@ -5,9 +5,8 @@ import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; import org.unicitylabs.sdk.predicate.EncodedPredicate; -import org.unicitylabs.sdk.predicate.Predicate; import org.unicitylabs.sdk.predicate.UnlockScript; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicateUnlockScript; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.serializer.cbor.CborSerializationException; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; @@ -26,13 +25,13 @@ public class CertificationData { public static final long CBOR_TAG = 39031; private static final int VERSION = 1; - private final Predicate lockScript; + private final EncodedPredicate lockScript; private final DataHash sourceStateHash; private final DataHash transactionHash; private final byte[] unlockScript; CertificationData( - Predicate lockScript, + EncodedPredicate lockScript, DataHash sourceStateHash, DataHash transactionHash, byte[] unlockScript @@ -52,7 +51,7 @@ public int getVersion() { * * @return lock script */ - public Predicate getLockScript() { + public EncodedPredicate getLockScript() { return this.lockScript; } @@ -123,7 +122,7 @@ public static CertificationData fromMintTransaction(MintTransaction transaction) return CertificationData.fromTransaction( transaction, - PayToPublicKeyPredicateUnlockScript.create(transaction, signingService).getSignature() + SignaturePredicateUnlockScript.create(transaction, signingService).getSignature() .encode() ); } @@ -172,7 +171,7 @@ public byte[] toCbor() { CertificationData.CBOR_TAG, CborSerializer.encodeArray( CborSerializer.encodeUnsignedInteger(CertificationData.VERSION), - EncodedPredicate.fromPredicate(this.getLockScript()).toCbor(), + this.lockScript.toCbor(), CborSerializer.encodeByteString(this.sourceStateHash.getData()), CborSerializer.encodeByteString(this.transactionHash.getData()), CborSerializer.encodeByteString(this.unlockScript) @@ -186,7 +185,7 @@ public boolean equals(Object o) { return false; } CertificationData that = (CertificationData) o; - return Predicate.areEqual(this.lockScript, that.lockScript) + return Objects.equals(this.lockScript, that.lockScript) && Objects.equals(this.sourceStateHash, that.sourceStateHash) && Objects.equals(this.transactionHash, that.transactionHash) && Arrays.equals(this.unlockScript, that.unlockScript); @@ -194,7 +193,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(EncodedPredicate.fromPredicate(this.lockScript), this.sourceStateHash, this.transactionHash, Arrays.hashCode(this.unlockScript)); + return Objects.hash(this.lockScript, this.sourceStateHash, this.transactionHash, Arrays.hashCode(this.unlockScript)); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/api/StateId.java b/src/main/java/org/unicitylabs/sdk/api/StateId.java index 3da77e9..f0567d1 100644 --- a/src/main/java/org/unicitylabs/sdk/api/StateId.java +++ b/src/main/java/org/unicitylabs/sdk/api/StateId.java @@ -4,7 +4,6 @@ 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.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.transaction.Transaction; @@ -70,11 +69,11 @@ public static StateId fromTransaction(Transaction transaction) { return StateId.create(transaction.getLockScript(), transaction.getSourceStateHash()); } - private static StateId create(Predicate predicate, DataHash stateHash) { + private static StateId create(EncodedPredicate predicate, DataHash stateHash) { DataHash hash = new DataHasher(HashAlgorithm.SHA256) .update( CborSerializer.encodeArray( - EncodedPredicate.fromPredicate(predicate).toCbor(), + predicate.toCbor(), CborSerializer.encodeByteString(stateHash.getData()) ) ) diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java b/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java index cc488b2..a78a9db 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java @@ -112,6 +112,14 @@ public VerificationResult verify(CertifiedMintTransaction tr Transaction burnTokenLastTransaction = justification.getToken().getLatestTransaction(); DataHash root = justification.getProofs().get(0).getAggregationPath().getRootHash(); for (SplitAssetProof proof : justification.getProofs()) { + if (!validatedAssets.add(proof.getAssetId())) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + String.format("Duplicate split proof for asset id %s.", proof.getAssetId()) + ); + } + MerkleTreePathVerificationResult aggregationPathResult = proof.getAggregationPath() .verify(proof.getAssetId().toBitString().toBigInteger()); if (!aggregationPathResult.isSuccessful()) { @@ -171,20 +179,17 @@ public VerificationResult verify(CertifiedMintTransaction tr ); } - EncodedPredicate recipient = EncodedPredicate.fromPredicate(burnTokenLastTransaction.getRecipient()); EncodedPredicate expectedRecipient = EncodedPredicate.fromPredicate( BurnPredicate.create(proof.getAggregationPath().getRootHash().getImprint()) ); - if (!expectedRecipient.equals(recipient)) { + if (!expectedRecipient.equals(burnTokenLastTransaction.getRecipient())) { return new VerificationResult<>( "SplitMintJustificationVerificationRule", VerificationStatus.FAIL, "Aggregation path root does not match burn predicate." ); } - - validatedAssets.add(proof.getAssetId()); } if (validatedAssets.size() != assets.size()) { diff --git a/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java b/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java index c0bf17f..1a6d8e0 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java @@ -1,7 +1,5 @@ package org.unicitylabs.sdk.predicate; -import java.util.Arrays; - /** * Base contract for all predicate implementations. */ @@ -27,15 +25,4 @@ public interface Predicate { * @return encoded predicate parameter bytes */ byte[] encodeParameters(); - - /** - * Checks if two predicates are equal. - * @param a first predicate - * @param b second predicate - * @return {@code true} if predicates are equal, {@code false} otherwise - */ - 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/BuiltInPredicateType.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java index 75be5fc..ffb0222 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java @@ -5,11 +5,11 @@ */ public enum BuiltInPredicateType { /** Predicate that locks state to a public key. */ - PAY_TO_PUBLIC_KEY(1), - /** Predicate that references a Unicity identifier. */ - UNICITY_ID(2), + SIGNATURE(0x01), /** Predicate that marks state as unspendable (burned). */ - BURN(3); + BURN(0x02), + /** Predicate that references a Unicity identifier. */ + UNICITY_ID(0x100); private final int id; diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java index ff27ee3..d0a7263 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java @@ -1,6 +1,6 @@ package org.unicitylabs.sdk.predicate.builtin; -import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.PredicateEngine; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; @@ -55,7 +55,7 @@ public static BurnPredicate create(byte[] reason) { * @return converted burn predicate * @throws IllegalArgumentException if the predicate engine is not built-in or predicate type is not burn */ - public static BurnPredicate fromPredicate(Predicate predicate) { + public static BurnPredicate fromPredicate(EncodedPredicate predicate) { PredicateEngine engine = predicate.getEngine(); if (engine != PredicateEngine.BUILT_IN) { throw new IllegalArgumentException("Predicate engine must be BUILT_IN."); diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/DefaultBuiltInPredicateVerifier.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/DefaultBuiltInPredicateVerifier.java index ce47c1b..cb08129 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/DefaultBuiltInPredicateVerifier.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/DefaultBuiltInPredicateVerifier.java @@ -1,13 +1,11 @@ package org.unicitylabs.sdk.predicate.builtin; -import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.crypto.hash.DataHash; -import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.PredicateEngine; import org.unicitylabs.sdk.predicate.builtin.verification.BuiltInPredicateVerifier; -import org.unicitylabs.sdk.predicate.builtin.verification.PayToPublicKeyPredicateVerifier; +import org.unicitylabs.sdk.predicate.builtin.verification.SignaturePredicateVerifier; import org.unicitylabs.sdk.predicate.verification.PredicateVerifier; -import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; @@ -52,20 +50,18 @@ public PredicateEngine getPredicateEngine() { /** * Creates the default built-in predicate verifier set. * - * @param service predicate verifier service - * @param trustBase root trust base * @return default built-in predicate verifier */ - public static DefaultBuiltInPredicateVerifier create(PredicateVerifierService service, RootTrustBase trustBase) { + public static DefaultBuiltInPredicateVerifier create() { return new DefaultBuiltInPredicateVerifier( List.of( - new PayToPublicKeyPredicateVerifier() + new SignaturePredicateVerifier() ) ); } @Override - public VerificationResult verify(Predicate predicate, + public VerificationResult verify(EncodedPredicate predicate, DataHash sourceStateHash, DataHash transactionHash, byte[] unlockScript) { BuiltInPredicateType type = BuiltInPredicateType.fromId( diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/SignaturePredicate.java similarity index 69% rename from src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java rename to src/main/java/org/unicitylabs/sdk/predicate/builtin/SignaturePredicate.java index 1cf6e72..e53e6e5 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/SignaturePredicate.java @@ -1,7 +1,7 @@ package org.unicitylabs.sdk.predicate.builtin; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; -import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.PredicateEngine; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.util.HexConverter; @@ -12,11 +12,11 @@ /** * Built-in predicate that locks an output to a secp256k1 public key. */ -public class PayToPublicKeyPredicate implements BuiltInPredicate { +public class SignaturePredicate implements BuiltInPredicate { private final byte[] publicKey; - private PayToPublicKeyPredicate(byte[] publicKey) { + private SignaturePredicate(byte[] publicKey) { this.publicKey = publicKey; } @@ -35,7 +35,7 @@ public byte[] getPublicKey() { * @return predicate type */ public BuiltInPredicateType getType() { - return BuiltInPredicateType.PAY_TO_PUBLIC_KEY; + return BuiltInPredicateType.SIGNATURE; } /** @@ -45,8 +45,8 @@ public BuiltInPredicateType getType() { * * @return pay-to-public-key predicate */ - public static PayToPublicKeyPredicate create(byte[] publicKey) { - return new PayToPublicKeyPredicate(Arrays.copyOf(publicKey, publicKey.length)); + public static SignaturePredicate create(byte[] publicKey) { + return new SignaturePredicate(Arrays.copyOf(publicKey, publicKey.length)); } /** @@ -56,7 +56,7 @@ public static PayToPublicKeyPredicate create(byte[] publicKey) { * * @return pay-to-public-key predicate */ - public static PayToPublicKeyPredicate fromPredicate(Predicate predicate) { + public static SignaturePredicate fromPredicate(EncodedPredicate predicate) { PredicateEngine engine = predicate.getEngine(); if (engine != PredicateEngine.BUILT_IN) { throw new IllegalArgumentException("Predicate engine must be BUILT_IN."); @@ -64,11 +64,11 @@ public static PayToPublicKeyPredicate fromPredicate(Predicate predicate) { BuiltInPredicateType type = BuiltInPredicateType.fromId( CborDeserializer.decodeUnsignedInteger(predicate.encodeCode()).asInt()); - if (type != BuiltInPredicateType.PAY_TO_PUBLIC_KEY) { - throw new IllegalArgumentException("Predicate type must be PAY_TO_PUBLIC_KEY."); + if (type != BuiltInPredicateType.SIGNATURE) { + throw new IllegalArgumentException("Predicate type must be SIGNATURE."); } - return new PayToPublicKeyPredicate(predicate.encodeParameters()); + return new SignaturePredicate(predicate.encodeParameters()); } /** @@ -78,9 +78,9 @@ public static PayToPublicKeyPredicate fromPredicate(Predicate predicate) { * * @return pay-to-public-key predicate */ - public static PayToPublicKeyPredicate fromSigningService(SigningService signingService) { + public static SignaturePredicate fromSigningService(SigningService signingService) { Objects.requireNonNull(signingService, "Signing service cannot be null"); - return new PayToPublicKeyPredicate(signingService.getPublicKey()); + return new SignaturePredicate(signingService.getPublicKey()); } /** @@ -95,7 +95,7 @@ public byte[] encodeParameters() { @Override public String toString() { - return String.format("PayToPublicKeyPredicate{publicKey=%s}", HexConverter.encode(this.publicKey)); + return String.format("SignaturePredicate{publicKey=%s}", HexConverter.encode(this.publicKey)); } } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicateUnlockScript.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/SignaturePredicateUnlockScript.java similarity index 76% rename from src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicateUnlockScript.java rename to src/main/java/org/unicitylabs/sdk/predicate/builtin/SignaturePredicateUnlockScript.java index e7f22fa..01fb55b 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicateUnlockScript.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/SignaturePredicateUnlockScript.java @@ -10,13 +10,13 @@ import org.unicitylabs.sdk.transaction.Transaction; /** - * Unlock script for {@link PayToPublicKeyPredicate} containing a transaction signature. + * Unlock script for {@link SignaturePredicate} containing a transaction signature. */ -public class PayToPublicKeyPredicateUnlockScript implements UnlockScript { +public class SignaturePredicateUnlockScript implements UnlockScript { private final Signature signature; - private PayToPublicKeyPredicateUnlockScript(Signature signature) { + private SignaturePredicateUnlockScript(Signature signature) { this.signature = signature; } @@ -36,7 +36,7 @@ public Signature getSignature() { * @param signingService signing service used to produce the signature * @return created unlock script */ - public static PayToPublicKeyPredicateUnlockScript create( + public static SignaturePredicateUnlockScript create( Transaction transaction, SigningService signingService ) { @@ -49,7 +49,7 @@ public static PayToPublicKeyPredicateUnlockScript create( ) .digest(); - return new PayToPublicKeyPredicateUnlockScript(signingService.sign(hash)); + return new SignaturePredicateUnlockScript(signingService.sign(hash)); } /** @@ -58,8 +58,8 @@ public static PayToPublicKeyPredicateUnlockScript create( * @param bytes encoded signature bytes * @return decoded unlock script */ - public static PayToPublicKeyPredicateUnlockScript decode(byte[] bytes) { - return new PayToPublicKeyPredicateUnlockScript(Signature.decode(bytes)); + public static SignaturePredicateUnlockScript decode(byte[] bytes) { + return new SignaturePredicateUnlockScript(Signature.decode(bytes)); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/BuiltInPredicateVerifier.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/BuiltInPredicateVerifier.java index f481fb8..f618275 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/BuiltInPredicateVerifier.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/BuiltInPredicateVerifier.java @@ -1,7 +1,7 @@ package org.unicitylabs.sdk.predicate.builtin.verification; import org.unicitylabs.sdk.crypto.hash.DataHash; -import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.builtin.BuiltInPredicateType; import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; @@ -27,6 +27,6 @@ public interface BuiltInPredicateVerifier { * @param unlockScript unlock script bytes provided for the predicate * @return verification result with status and optional diagnostics */ - VerificationResult verify(Predicate predicate, DataHash sourceStateHash, + VerificationResult verify(EncodedPredicate predicate, DataHash sourceStateHash, DataHash transactionHash, byte[] unlockScript); } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/PayToPublicKeyPredicateVerifier.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/SignaturePredicateVerifier.java similarity index 67% rename from src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/PayToPublicKeyPredicateVerifier.java rename to src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/SignaturePredicateVerifier.java index d13a7eb..f3c0c54 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/PayToPublicKeyPredicateVerifier.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/SignaturePredicateVerifier.java @@ -5,35 +5,35 @@ 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.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.builtin.BuiltInPredicateType; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; /** - * Verifies {@link PayToPublicKeyPredicate} unlock scripts using secp256k1 signatures. + * Verifies {@link SignaturePredicate} unlock scripts using secp256k1 signatures. */ -public class PayToPublicKeyPredicateVerifier implements BuiltInPredicateVerifier { +public class SignaturePredicateVerifier implements BuiltInPredicateVerifier { /** * Creates a verifier instance for pay-to-public-key predicates. */ - public PayToPublicKeyPredicateVerifier() { + public SignaturePredicateVerifier() { } @Override public BuiltInPredicateType getType() { - return BuiltInPredicateType.PAY_TO_PUBLIC_KEY; + return BuiltInPredicateType.SIGNATURE; } @Override - public VerificationResult verify(Predicate encodedPredicate, + public VerificationResult verify(EncodedPredicate encodedPredicate, DataHash sourceStateHash, DataHash transactionHash, byte[] unlockScript) { - PayToPublicKeyPredicate predicate = PayToPublicKeyPredicate.fromPredicate(encodedPredicate); + SignaturePredicate predicate = SignaturePredicate.fromPredicate(encodedPredicate); boolean result = SigningService.verifyWithPublicKey( new DataHasher(HashAlgorithm.SHA256) @@ -49,10 +49,10 @@ public VerificationResult verify(Predicate encodedPredicate, ); if (!result) { - return new VerificationResult<>("PayToPublicKeyPredicateVerifier", VerificationStatus.FAIL, + return new VerificationResult<>("SignaturePredicateVerifier", VerificationStatus.FAIL, "Signature verification failed."); } - return new VerificationResult<>("PayToPublicKeyPredicateVerifier", VerificationStatus.OK); + return new VerificationResult<>("SignaturePredicateVerifier", VerificationStatus.OK); } } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifier.java b/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifier.java index b9f506f..915eb09 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifier.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifier.java @@ -1,7 +1,7 @@ package org.unicitylabs.sdk.predicate.verification; import org.unicitylabs.sdk.crypto.hash.DataHash; -import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.PredicateEngine; import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; @@ -27,6 +27,6 @@ public interface PredicateVerifier { * @param unlockScript unlock script bytes * @return verification result with status and diagnostics */ - VerificationResult verify(Predicate predicate, DataHash sourceStateHash, + VerificationResult verify(EncodedPredicate predicate, DataHash sourceStateHash, DataHash transactionHash, byte[] unlockScript); } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifierService.java b/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifierService.java index 5695437..42b5416 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifierService.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifierService.java @@ -1,8 +1,7 @@ package org.unicitylabs.sdk.predicate.verification; -import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.crypto.hash.DataHash; -import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.PredicateEngine; import org.unicitylabs.sdk.predicate.builtin.DefaultBuiltInPredicateVerifier; import org.unicitylabs.sdk.util.verification.VerificationResult; @@ -25,12 +24,11 @@ private PredicateVerifierService() { /** * Creates a predicate verifier service with default verifier registrations. * - * @param trustBase root trust base used by verifiers that require trust context * @return initialized predicate verifier service */ - public static PredicateVerifierService create(RootTrustBase trustBase) { + public static PredicateVerifierService create() { PredicateVerifierService verifier = new PredicateVerifierService(); - verifier.addVerifier(DefaultBuiltInPredicateVerifier.create(verifier, trustBase)); + verifier.addVerifier(DefaultBuiltInPredicateVerifier.create()); return verifier; } @@ -63,8 +61,12 @@ public PredicateVerifierService addVerifier(PredicateVerifier verifier) { * @return verification result from the engine-specific verifier * @throws IllegalArgumentException if no verifier is registered for the predicate engine */ - public VerificationResult verify(Predicate predicate, - DataHash sourceStateHash, DataHash transactionHash, byte[] unlockScript) { + public VerificationResult verify( + EncodedPredicate predicate, + DataHash sourceStateHash, + DataHash transactionHash, + byte[] unlockScript + ) { PredicateVerifier verifier = this.verifiers.get(predicate.getEngine()); if (verifier == null) { throw new IllegalArgumentException( diff --git a/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializer.java b/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializer.java index 275ee9d..1d38d5a 100644 --- a/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializer.java +++ b/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializer.java @@ -21,16 +21,20 @@ private CborDeserializer() { * Read optional value from CBOR bytes. * * @param data bytes - * @param reader parse method + * @param decoder parse method * @param parsed value type * @return parsed value */ - public static T decodeNullable(byte[] data, Function reader) { - if (Byte.compareUnsigned(new CborReader(data).readByte(), (byte) 0xf6) == 0) { + public static T decodeNullable(byte[] data, Function decoder) { + CborReader reader = new CborReader(data); + byte[] cbor = reader.readRawCbor(); + reader.assertExhausted(); + + if (cbor.length == 1 && cbor[0] == (byte) 0xf6) { return null; } - return reader.apply(data); + return decoder.apply(cbor); } /** @@ -41,7 +45,10 @@ public static T decodeNullable(byte[] data, Function reader) { */ public static CborNumber decodeUnsignedInteger(byte[] data) { CborReader reader = new CborReader(data); - return new CborNumber(reader.readLength(CborMajorType.UNSIGNED_INTEGER)); + long value = reader.readLength(CborMajorType.UNSIGNED_INTEGER); + reader.assertExhausted(); + + return new CborNumber(value); } /** @@ -52,7 +59,10 @@ public static CborNumber decodeUnsignedInteger(byte[] data) { */ public static byte[] decodeByteString(byte[] data) { CborReader reader = new CborReader(data); - return reader.read((int) reader.readLength(CborMajorType.BYTE_STRING)); + byte[] result = reader.read((int) reader.readLength(CborMajorType.BYTE_STRING)); + reader.assertExhausted(); + + return result; } /** @@ -63,8 +73,10 @@ public static byte[] decodeByteString(byte[] data) { */ public static String decodeTextString(byte[] data) { CborReader reader = new CborReader(data); - return new String( - reader.read((int) reader.readLength(CborMajorType.TEXT_STRING))); + byte[] bytes = reader.read((int) reader.readLength(CborMajorType.TEXT_STRING)); + reader.assertExhausted(); + + return new String(bytes); } /** @@ -81,6 +93,7 @@ public static List decodeArray(byte[] data) { for (int i = 0; i < length; i++) { result.add(reader.readRawCbor()); } + reader.assertExhausted(); return result; } @@ -115,11 +128,23 @@ public static Set decodeMap(byte[] data) { long length = (int) reader.readLength(CborMajorType.MAP); Set result = new LinkedHashSet<>(); + Entry previous = null; for (int i = 0; i < length; i++) { - byte[] key = reader.readRawCbor(); - byte[] value = reader.readRawCbor(); - result.add(new CborMap.Entry(key, value)); + Entry entry = new CborMap.Entry(reader.readRawCbor(), reader.readRawCbor()); + + if (previous != null) { + int comparison = CborMap.compareEntries(previous, entry); + if (comparison == 0) { + throw new CborSerializationException("Duplicate map key found."); + } + if (comparison > 0) { + throw new CborSerializationException("Map keys are not in canonical order."); + } + } + result.add(entry); + previous = entry; } + reader.assertExhausted(); return result; } @@ -133,7 +158,10 @@ public static Set decodeMap(byte[] data) { public static CborTag decodeTag(byte[] data) { CborReader reader = new CborReader(data); long tag = (int) reader.readLength(CborMajorType.TAG); - return new CborTag(tag, reader.readRawCbor()); + byte[] inner = reader.readRawCbor(); + reader.assertExhausted(); + + return new CborTag(tag, inner); } /** @@ -143,11 +171,14 @@ public static CborTag decodeTag(byte[] data) { * @return boolean */ public static boolean decodeBoolean(byte[] data) { - byte byteValue = new CborReader(data).readByte(); - if (byteValue == (byte) 0xf5) { + CborReader reader = new CborReader(data); + byte[] cbor = reader.readRawCbor(); + reader.assertExhausted(); + + if (cbor.length == 1 && cbor[0] == (byte) 0xf5) { return true; } - if (byteValue == (byte) 0xf4) { + if (cbor.length == 1 && cbor[0] == (byte) 0xf4) { return false; } throw new CborSerializationException("Type mismatch, expected boolean."); @@ -164,6 +195,14 @@ private static class CborReader { this.data = data; } + public void assertExhausted() { + if (this.position != this.data.length) { + throw new CborSerializationException(String.format( + "Expected end of data: %d byte(s) remaining at position %d.", + this.data.length - this.position, this.position)); + } + } + public byte readByte() { if (this.position >= this.data.length) { throw new CborSerializationException("Premature end of data."); @@ -187,8 +226,11 @@ public byte[] read(int length) { public long readLength(CborMajorType majorType) { byte initialByte = this.readByte(); - if (CborMajorType.fromType(initialByte & CborDeserializer.MAJOR_TYPE_MASK) != majorType) { - throw new CborSerializationException("Major type mismatch."); + CborMajorType parsedMajorType = CborMajorType.fromType( + initialByte & CborDeserializer.MAJOR_TYPE_MASK); + if (parsedMajorType != majorType) { + throw new CborSerializationException(String.format( + "Major type mismatch: expected %s, got %s.", majorType, parsedMajorType)); } byte additionalInformation = (byte) (initialByte @@ -198,18 +240,23 @@ public long readLength(CborMajorType majorType) { } switch (majorType) { + case MAP: case ARRAY: case BYTE_STRING: case TEXT_STRING: if (Byte.compareUnsigned(additionalInformation, (byte) 31) == 0) { - throw new CborSerializationException("Indefinite length array not supported."); + throw new CborSerializationException(String.format( + "Indefinite-length encoding not allowed in canonical CBOR (major type %s).", + majorType)); } break; default: } if (Byte.compareUnsigned(additionalInformation, (byte) 27) > 0) { - throw new CborSerializationException("Encoded item is not well-formed."); + throw new CborSerializationException(String.format( + "Reserved additional information %d for major type %s.", + additionalInformation, majorType)); } long t = 0; @@ -218,6 +265,13 @@ public long readLength(CborMajorType majorType) { t = (t << 8) | this.readByte() & 0xFF; } + long threshold = length == 1 ? 24L : 1L << (length * 4); + if (Long.compareUnsigned(t, threshold) < 0) { + throw new CborSerializationException(String.format( + "Byte length %d is not canonical for value %s.", + length, Long.toUnsignedString(t))); + } + return t; } diff --git a/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborSerializer.java b/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborSerializer.java index db5e3b6..9cb62f5 100644 --- a/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborSerializer.java +++ b/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborSerializer.java @@ -236,19 +236,13 @@ public static final class CborMap { */ public CborMap(Set entries) { this.entries = new ArrayList<>(entries); - this.entries.sort((a, b) -> { - if (a.key.length != b.key.length) { - return a.key.length - b.key.length; - } + this.entries.sort(CborMap::compareEntries); - for (int i = 0; i < a.key.length; i++) { - if (a.key[i] != b.key[i]) { - return a.key[i] - b.key[i]; - } + for (int i = 1; i < this.entries.size(); i++) { + if (CborMap.compareEntries(this.entries.get(i - 1), this.entries.get(i)) == 0) { + throw new CborSerializationException("Duplicate map key in CborMap."); } - - return 0; - }); + } } /** @@ -260,6 +254,26 @@ public List getEntries() { return List.copyOf(this.entries); } + /** + * Compare two map entries by their CBOR-encoded keys using canonical bytewise lexicographic + * order (compare bytes byte-by-byte, then break ties by length). + * + * @param a first entry + * @param b second entry + * @return negative, zero, or positive per {@link Comparable} + */ + public static int compareEntries(Entry a, Entry b) { + int length = Math.min(a.key.length, b.key.length); + for (int i = 0; i < length; i++) { + int diff = Byte.toUnsignedInt(a.key[i]) - Byte.toUnsignedInt(b.key[i]); + if (diff != 0) { + return diff; + } + } + + return a.key.length - b.key.length; + } + /** * CBOR entry for map. */ diff --git a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java index 52fbb5f..17bc829 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java @@ -3,7 +3,7 @@ import org.unicitylabs.sdk.api.InclusionProof; import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.crypto.hash.DataHash; -import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; @@ -34,12 +34,12 @@ public Optional getData() { } @Override - public Predicate getLockScript() { + public EncodedPredicate getLockScript() { return this.transaction.getLockScript(); } @Override - public Predicate getRecipient() { + public EncodedPredicate getRecipient() { return this.transaction.getRecipient(); } diff --git a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java index fd5ff1d..53dc84d 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java @@ -3,7 +3,7 @@ import org.unicitylabs.sdk.api.InclusionProof; import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.crypto.hash.DataHash; -import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; @@ -37,12 +37,12 @@ public Optional getData() { } @Override - public Predicate getLockScript() { + public EncodedPredicate getLockScript() { return this.transaction.getLockScript(); } @Override - public Predicate getRecipient() { + public EncodedPredicate getRecipient() { return this.transaction.getRecipient(); } diff --git a/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java index ddeb177..80eaf3a 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java @@ -9,7 +9,7 @@ import org.unicitylabs.sdk.crypto.secp256k1.SigningService; import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.Predicate; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +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; @@ -33,8 +33,8 @@ public class MintTransaction implements Transaction { private static final int VERSION = 1; private final MintTransactionState sourceStateHash; - private final Predicate lockScript; - private final Predicate recipient; + private final EncodedPredicate lockScript; + private final EncodedPredicate recipient; private final TokenId tokenId; private final TokenType tokenType; private final byte[] justification; @@ -42,8 +42,8 @@ public class MintTransaction implements Transaction { private MintTransaction( MintTransactionState sourceStateHash, - Predicate lockScript, - Predicate recipient, + EncodedPredicate lockScript, + EncodedPredicate recipient, TokenId tokenId, TokenType tokenType, byte[] justification, @@ -69,12 +69,12 @@ public MintTransactionState getSourceStateHash() { } @Override - public Predicate getLockScript() { + public EncodedPredicate getLockScript() { return this.lockScript; } @Override - public Predicate getRecipient() { + public EncodedPredicate getRecipient() { return this.recipient; } @@ -140,8 +140,8 @@ public static MintTransaction create( SigningService signingService = MintSigningService.create(tokenId); return new MintTransaction( MintTransactionState.create(tokenId), - PayToPublicKeyPredicate.fromSigningService(signingService), - recipient, + EncodedPredicate.fromPredicate(SignaturePredicate.fromSigningService(signingService)), + EncodedPredicate.fromPredicate(recipient), tokenId, tokenType, justification != null ? Arrays.copyOf(justification, justification.length) : null, @@ -215,7 +215,7 @@ public byte[] toCbor() { MintTransaction.CBOR_TAG, CborSerializer.encodeArray( CborSerializer.encodeUnsignedInteger(MintTransaction.VERSION), - EncodedPredicate.fromPredicate(this.recipient).toCbor(), + this.recipient.toCbor(), this.tokenId.toCbor(), this.tokenType.toCbor(), CborSerializer.encodeNullable(this.justification, CborSerializer::encodeByteString), diff --git a/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java b/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java index 4731cd1..38791ea 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java @@ -1,7 +1,7 @@ package org.unicitylabs.sdk.transaction; import org.unicitylabs.sdk.crypto.hash.DataHash; -import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import java.util.Optional; @@ -22,14 +22,14 @@ public interface Transaction { * * @return lock script predicate */ - Predicate getLockScript(); + EncodedPredicate getLockScript(); /** * Gets the transaction recipient. * * @return recipient predicate */ - Predicate getRecipient(); + EncodedPredicate getRecipient(); /** * Gets the source state hash. diff --git a/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java index deadca1..9390b38 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java @@ -25,15 +25,15 @@ public class TransferTransaction implements Transaction { private static final int VERSION = 1; private final DataHash sourceStateHash; - private final Predicate lockScript; - private final Predicate recipient; + private final EncodedPredicate lockScript; + private final EncodedPredicate recipient; private final byte[] stateMask; private final byte[] data; private TransferTransaction( DataHash sourceStateHash, - Predicate lockScript, - Predicate recipient, + EncodedPredicate lockScript, + EncodedPredicate recipient, byte[] stateMask, byte[] data ) { @@ -55,12 +55,12 @@ public Optional getData() { } @Override - public Predicate getLockScript() { + public EncodedPredicate getLockScript() { return this.lockScript; } @Override - public Predicate getRecipient() { + public EncodedPredicate getRecipient() { return this.recipient; } @@ -90,7 +90,7 @@ public static TransferTransaction create(Token token, Predicate recipient, return new TransferTransaction( transaction.calculateStateHash(), transaction.getRecipient(), - recipient, + EncodedPredicate.fromPredicate(recipient), stateMask, data ); diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedMintTransactionVerificationRule.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedMintTransactionVerificationRule.java index c4a4b72..4c041dc 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedMintTransactionVerificationRule.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedMintTransactionVerificationRule.java @@ -1,10 +1,11 @@ package org.unicitylabs.sdk.transaction.verification; +import org.unicitylabs.sdk.api.CertificationData; import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.crypto.MintSigningService; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; import org.unicitylabs.sdk.predicate.EncodedPredicate; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.transaction.CertifiedMintTransaction; import org.unicitylabs.sdk.util.verification.VerificationResult; @@ -44,13 +45,14 @@ public static VerificationResult verify( List> results = new ArrayList<>(); SigningService signingService = MintSigningService.create(transaction.getTokenId()); - VerificationResult result = Arrays.equals( - EncodedPredicate.fromPredicate(PayToPublicKeyPredicate.fromSigningService(signingService)) - .toCbor(), - transaction.getInclusionProof() - .getCertificationData() - .map(c -> EncodedPredicate.fromPredicate(c.getLockScript()).toCbor()) - .orElse(null)) + EncodedPredicate expectedLockScript = EncodedPredicate.fromPredicate(SignaturePredicate.fromSigningService(signingService)); + VerificationResult result = expectedLockScript + .equals( + transaction.getInclusionProof() + .getCertificationData() + .map(CertificationData::getLockScript) + .orElse(null) + ) ? new VerificationResult<>("IsLockScriptValidVerificationRule", VerificationStatus.OK) : new VerificationResult<>("IsLockScriptValidVerificationRule", VerificationStatus.FAIL); diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedUnicityIdMintTransactionVerificationRule.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedUnicityIdMintTransactionVerificationRule.java index 5064636..bf74896 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedUnicityIdMintTransactionVerificationRule.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedUnicityIdMintTransactionVerificationRule.java @@ -1,6 +1,8 @@ package org.unicitylabs.sdk.transaction.verification; import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.predicate.EncodedPredicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.unicityid.CertifiedUnicityIdMintTransaction; import org.unicitylabs.sdk.util.verification.VerificationResult; @@ -11,7 +13,8 @@ /** * Verification rule for the genesis (mint) of a unicity id token. Validates the inclusion proof of - * the certified mint transaction. + * the certified mint transaction, and optionally checks that the genesis lock script matches an + * expected issuer public key. */ public class CertifiedUnicityIdMintTransactionVerificationRule { @@ -24,16 +27,36 @@ private CertifiedUnicityIdMintTransactionVerificationRule() { * @param trustBase root trust base * @param predicateVerifier predicate verifier * @param genesis certified unicity id mint transaction to verify + * @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( RootTrustBase trustBase, PredicateVerifierService predicateVerifier, - CertifiedUnicityIdMintTransaction genesis + CertifiedUnicityIdMintTransaction genesis, + byte[] issuerPublicKey ) { List> results = new ArrayList<>(); + 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( trustBase, predicateVerifier, diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java b/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java index 67381ac..da07655 100644 --- a/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java @@ -3,8 +3,8 @@ import org.unicitylabs.sdk.api.InclusionProof; import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.crypto.hash.DataHash; -import org.unicitylabs.sdk.predicate.Predicate; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +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; @@ -39,12 +39,12 @@ public Optional getData() { } @Override - public Predicate getLockScript() { + public EncodedPredicate getLockScript() { return this.transaction.getLockScript(); } @Override - public Predicate getRecipient() { + public EncodedPredicate getRecipient() { return this.transaction.getRecipient(); } @@ -81,7 +81,7 @@ public TokenType getTokenType() { * * @return target predicate */ - public PayToPublicKeyPredicate getTargetPredicate() { + public SignaturePredicate getTargetPredicate() { return this.transaction.getTargetPredicate(); } diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java index a056191..b7ffb1f 100644 --- a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java @@ -7,7 +7,7 @@ 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.PayToPublicKeyPredicate; +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; @@ -30,20 +30,20 @@ public final class UnicityIdMintTransaction implements Transaction { private static final int VERSION = 1; private final MintTransactionState sourceStateHash; - private final PayToPublicKeyPredicate lockScript; - private final Predicate recipient; + private final EncodedPredicate lockScript; + private final EncodedPredicate recipient; private final TokenId tokenId; private final TokenType tokenType; - private final PayToPublicKeyPredicate targetPredicate; + private final SignaturePredicate targetPredicate; private final UnicityId unicityId; private UnicityIdMintTransaction( MintTransactionState sourceStateHash, - PayToPublicKeyPredicate lockScript, - Predicate recipient, + EncodedPredicate lockScript, + EncodedPredicate recipient, TokenId tokenId, TokenType tokenType, - PayToPublicKeyPredicate targetPredicate, + SignaturePredicate targetPredicate, UnicityId unicityId ) { this.sourceStateHash = sourceStateHash; @@ -70,12 +70,12 @@ public MintTransactionState getSourceStateHash() { } @Override - public PayToPublicKeyPredicate getLockScript() { + public EncodedPredicate getLockScript() { return this.lockScript; } @Override - public Predicate getRecipient() { + public EncodedPredicate getRecipient() { return this.recipient; } @@ -102,7 +102,7 @@ public TokenType getTokenType() { * * @return target predicate */ - public PayToPublicKeyPredicate getTargetPredicate() { + public SignaturePredicate getTargetPredicate() { return this.targetPredicate; } @@ -139,11 +139,11 @@ public byte[] getStateMask() { * @return mint transaction */ public static UnicityIdMintTransaction create( - PayToPublicKeyPredicate lockScript, + SignaturePredicate lockScript, Predicate recipient, UnicityId unicityId, TokenType tokenType, - PayToPublicKeyPredicate targetPredicate + SignaturePredicate targetPredicate ) { Objects.requireNonNull(lockScript, "lockScript cannot be null"); Objects.requireNonNull(recipient, "recipient cannot be null"); @@ -155,8 +155,8 @@ public static UnicityIdMintTransaction create( return new UnicityIdMintTransaction( MintTransactionState.create(tokenId), - lockScript, - recipient, + EncodedPredicate.fromPredicate(lockScript), + EncodedPredicate.fromPredicate(recipient), tokenId, tokenType, targetPredicate, @@ -187,13 +187,13 @@ public static UnicityIdMintTransaction fromCbor(byte[] bytes) { } return UnicityIdMintTransaction.create( - PayToPublicKeyPredicate.fromPredicate( + SignaturePredicate.fromPredicate( EncodedPredicate.fromCbor(data.get(1)) ), EncodedPredicate.fromCbor(data.get(2)), UnicityId.fromCbor(data.get(3)), TokenType.fromCbor(data.get(4)), - PayToPublicKeyPredicate.fromPredicate( + SignaturePredicate.fromPredicate( EncodedPredicate.fromCbor(data.get(5)) ) ); @@ -222,8 +222,8 @@ public byte[] toCbor() { UnicityIdMintTransaction.CBOR_TAG, CborSerializer.encodeArray( CborSerializer.encodeUnsignedInteger(UnicityIdMintTransaction.VERSION), - EncodedPredicate.fromPredicate(this.lockScript).toCbor(), - EncodedPredicate.fromPredicate(this.recipient).toCbor(), + this.lockScript.toCbor(), + this.recipient.toCbor(), this.unicityId.toCbor(), this.tokenType.toCbor(), EncodedPredicate.fromPredicate(this.targetPredicate).toCbor() diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java index b2a0fe2..068b13e 100644 --- a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java +++ b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java @@ -13,6 +13,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * Token whose genesis is a {@link CertifiedUnicityIdMintTransaction}. The token's identifier is @@ -90,13 +91,18 @@ public static UnicityIdToken mint( PredicateVerifierService predicateVerifier, CertifiedUnicityIdMintTransaction genesis ) { - UnicityIdToken token = new UnicityIdToken(genesis); - VerificationResult result = token.verify(trustBase, predicateVerifier); + VerificationResult result = + CertifiedUnicityIdMintTransactionVerificationRule.verify( + trustBase, + predicateVerifier, + genesis, + null + ); if (result.getStatus() != VerificationStatus.OK) { throw new VerificationException("Invalid token genesis", result); } - return token; + return new UnicityIdToken(genesis); } /** @@ -109,22 +115,28 @@ public byte[] toCbor() { } /** - * Verify the token by validating its certified mint transaction. + * Verify the token by validating its certified mint transaction against an expected issuer. * * @param trustBase trust base used for certification verification * @param predicateVerifier predicate verifier service + * @param issuerPublicKey expected issuer public key (required) * * @return verification result + * @throws NullPointerException if {@code issuerPublicKey} is {@code null} */ public VerificationResult verify( RootTrustBase trustBase, - PredicateVerifierService predicateVerifier + PredicateVerifierService predicateVerifier, + byte[] issuerPublicKey ) { + Objects.requireNonNull(issuerPublicKey, "issuerPublicKey cannot be null"); + List> results = new ArrayList<>(); VerificationResult result = CertifiedUnicityIdMintTransactionVerificationRule.verify( trustBase, predicateVerifier, - this.genesis + this.genesis, + issuerPublicKey ); results.add(result); if (result.getStatus() != VerificationStatus.OK) { diff --git a/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java b/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java index d5bd967..93be061 100644 --- a/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java +++ b/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java @@ -26,7 +26,7 @@ private TestAggregatorClient(SparseMerkleTree smt, SigningService signingService this.sparseMerkleTree = smt; this.signingService = signingService; this.trustBase = RootTrustBaseUtils.generateRootTrustBase(this.signingService.getPublicKey()); - this.predicateVerifier = PredicateVerifierService.create(this.trustBase); + this.predicateVerifier = PredicateVerifierService.create(); } public RootTrustBase getTrustBase() { diff --git a/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java b/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java index 2cbd995..c3bc5c2 100644 --- a/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java +++ b/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java @@ -7,7 +7,7 @@ import org.unicitylabs.sdk.api.*; import org.unicitylabs.sdk.api.jsonrpc.JsonRpcNetworkException; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; import org.unicitylabs.sdk.transaction.MintTransaction; import org.unicitylabs.sdk.transaction.TokenId; import org.unicitylabs.sdk.transaction.TokenType; @@ -43,7 +43,7 @@ void setUp() throws Exception { HexConverter.decode("0000000000000000000000000000000000000000000000000000000000000001")); MintTransaction transaction = MintTransaction.create( - PayToPublicKeyPredicate.fromSigningService(signingService), + SignaturePredicate.fromSigningService(signingService), TokenId.generate(), TokenType.generate(), null, diff --git a/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java b/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java index 938d443..faa0064 100644 --- a/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java +++ b/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java @@ -12,8 +12,8 @@ import org.unicitylabs.sdk.crypto.hash.DataHash; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; +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; @@ -42,7 +42,7 @@ public void createMerkleTreePath() throws Exception { transaction = MintTransaction.create( - PayToPublicKeyPredicate.fromSigningService(signingService), + SignaturePredicate.fromSigningService(signingService), TokenId.generate(), TokenType.generate(), null, @@ -60,7 +60,7 @@ public void createMerkleTreePath() throws Exception { // Reuse user signing service as unicity certificate signing service. trustBase = RootTrustBaseUtils.generateRootTrustBase(signingService.getPublicKey()); unicityCertificate = UnicityCertificateUtils.generateCertificate(signingService, root.getHash()); - predicateVerifier = PredicateVerifierService.create(trustBase); + predicateVerifier = PredicateVerifierService.create(); } @Test @@ -147,7 +147,7 @@ public void testItNotAuthenticated() { this.certificationData.getLockScript(), this.certificationData.getSourceStateHash(), this.certificationData.getTransactionHash(), - PayToPublicKeyPredicateUnlockScript.create( + SignaturePredicateUnlockScript.create( this.transaction, new SigningService(SigningService.generatePrivateKey()) ).encode() diff --git a/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java b/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java index 528edb0..5fce87e 100644 --- a/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java +++ b/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java @@ -8,8 +8,8 @@ import org.unicitylabs.sdk.api.CertificationStatus; import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; +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.transaction.Token; import org.unicitylabs.sdk.transaction.TokenType; @@ -45,7 +45,7 @@ public void testTransferFlow() throws Exception { this.trustBase, this.predicateVerifier, this.mintJustificationVerifier, - PayToPublicKeyPredicate.create(ALICE_SIGNING_SERVICE.getPublicKey()) + SignaturePredicate.create(ALICE_SIGNING_SERVICE.getPublicKey()) ); Token bobToken = TokenUtils.transferToken( @@ -54,7 +54,7 @@ public void testTransferFlow() throws Exception { this.predicateVerifier, this.mintJustificationVerifier, aliceToken.toCbor(), - PayToPublicKeyPredicate.create(BOB_SIGNING_SERVICE.getPublicKey()), + SignaturePredicate.create(BOB_SIGNING_SERVICE.getPublicKey()), ALICE_SIGNING_SERVICE ); @@ -64,7 +64,7 @@ public void testTransferFlow() throws Exception { this.predicateVerifier, this.mintJustificationVerifier, bobToken.toCbor(), - PayToPublicKeyPredicate.create(CAROL_SIGNING_SERVICE.getPublicKey()), + SignaturePredicate.create(CAROL_SIGNING_SERVICE.getPublicKey()), BOB_SIGNING_SERVICE ); @@ -79,12 +79,12 @@ public void testTransferFlow() throws Exception { @Test public void testUnicityIdMintFlow() throws Exception { SigningService unicityIdSigningService = SigningService.generate(); - PayToPublicKeyPredicate targetPredicate = PayToPublicKeyPredicate.create( + SignaturePredicate targetPredicate = SignaturePredicate.create( ALICE_SIGNING_SERVICE.getPublicKey()); UnicityId unicityId = new UnicityId("testuser", "unicity-labs/test"); UnicityIdMintTransaction unicityIdMintTransaction = UnicityIdMintTransaction.create( - PayToPublicKeyPredicate.fromSigningService(unicityIdSigningService), + SignaturePredicate.fromSigningService(unicityIdSigningService), targetPredicate, unicityId, TokenType.generate(), @@ -93,7 +93,7 @@ public void testUnicityIdMintFlow() throws Exception { CertificationData unicityIdCertificationData = CertificationData.fromTransaction( unicityIdMintTransaction, - PayToPublicKeyPredicateUnlockScript.create(unicityIdMintTransaction, unicityIdSigningService) + SignaturePredicateUnlockScript.create(unicityIdMintTransaction, unicityIdSigningService) ); CertificationResponse unicityIdResponse = this.client @@ -112,13 +112,15 @@ public void testUnicityIdMintFlow() throws Exception { ); Assertions.assertEquals(VerificationStatus.OK, - aliceUnicityIdToken.verify(this.trustBase, this.predicateVerifier).getStatus()); + aliceUnicityIdToken.verify(this.trustBase, this.predicateVerifier, + 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.trustBase, this.predicateVerifier).getStatus()); + decodedUnicityIdToken.verify(this.trustBase, this.predicateVerifier, + unicityIdSigningService.getPublicKey()).getStatus()); Token aliceToken = TokenUtils.mintToken( this.client, diff --git a/src/test/java/org/unicitylabs/sdk/e2e/TokenE2ETest.java b/src/test/java/org/unicitylabs/sdk/e2e/TokenE2ETest.java index 032950b..d829eb0 100644 --- a/src/test/java/org/unicitylabs/sdk/e2e/TokenE2ETest.java +++ b/src/test/java/org/unicitylabs/sdk/e2e/TokenE2ETest.java @@ -38,7 +38,7 @@ void setUp() throws IOException { try (InputStream stream = getClass().getResourceAsStream("/trust-base.json")) { assertNotNull(stream, "trust-base.json not found"); this.trustBase = RootTrustBase.fromJson(new String(stream.readAllBytes())); - this.predicateVerifier = PredicateVerifierService.create(this.trustBase); + this.predicateVerifier = PredicateVerifierService.create(); this.mintJustificationVerifier = new MintJustificationVerifierService(); } } diff --git a/src/test/java/org/unicitylabs/sdk/functional/FunctionalCommonFlowTest.java b/src/test/java/org/unicitylabs/sdk/functional/FunctionalCommonFlowTest.java index 3d0b676..89b3df9 100644 --- a/src/test/java/org/unicitylabs/sdk/functional/FunctionalCommonFlowTest.java +++ b/src/test/java/org/unicitylabs/sdk/functional/FunctionalCommonFlowTest.java @@ -14,7 +14,7 @@ void setUp() { TestAggregatorClient aggregatorClient = TestAggregatorClient.create(); this.client = new StateTransitionClient(aggregatorClient); this.trustBase = aggregatorClient.getTrustBase(); - this.predicateVerifier = PredicateVerifierService.create(this.trustBase); + this.predicateVerifier = PredicateVerifierService.create(); this.mintJustificationVerifier = new MintJustificationVerifierService(); } } \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java index 0e2a369..bad56fb 100644 --- a/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java @@ -12,8 +12,8 @@ import org.unicitylabs.sdk.payment.TokenSplit; import org.unicitylabs.sdk.payment.asset.Asset; import org.unicitylabs.sdk.payment.asset.AssetId; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; +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.transaction.Token; import org.unicitylabs.sdk.transaction.TokenId; @@ -40,14 +40,14 @@ public void buildAndVerifySplitToken() throws Exception { TestAggregatorClient aggregatorClient = TestAggregatorClient.create(); RootTrustBase trustBase = aggregatorClient.getTrustBase(); StateTransitionClient client = new StateTransitionClient(aggregatorClient); - PredicateVerifierService predicateVerifier = PredicateVerifierService.create(trustBase); + PredicateVerifierService predicateVerifier = PredicateVerifierService.create(); MintJustificationVerifierService mintJustificationVerifier = new MintJustificationVerifierService(); mintJustificationVerifier.register(new SplitMintJustificationVerifier( trustBase, predicateVerifier, TestPaymentData::decode)); SigningService signingService = SigningService.generate(); - PayToPublicKeyPredicate ownerPredicate = PayToPublicKeyPredicate.fromSigningService(signingService); + SignaturePredicate ownerPredicate = SignaturePredicate.fromSigningService(signingService); Set assets = Set.of( new Asset(new AssetId("ASSET_1".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)), @@ -77,7 +77,7 @@ public void buildAndVerifySplitToken() throws Exception { predicateVerifier, sourceToken, split.getBurnTransaction(), - PayToPublicKeyPredicateUnlockScript.create(split.getBurnTransaction(), signingService) + SignaturePredicateUnlockScript.create(split.getBurnTransaction(), signingService) ); SplitMintJustification justification = SplitMintJustification.create( diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/SplitMintJustificationVerifierTest.java b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitMintJustificationVerifierTest.java index 888fcd3..1cae00d 100644 --- a/src/test/java/org/unicitylabs/sdk/functional/payment/SplitMintJustificationVerifierTest.java +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitMintJustificationVerifierTest.java @@ -17,8 +17,8 @@ import org.unicitylabs.sdk.payment.TokenSplit; import org.unicitylabs.sdk.payment.asset.Asset; import org.unicitylabs.sdk.payment.asset.AssetId; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; +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.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; @@ -70,7 +70,7 @@ public void setupFixture() throws Exception { this.trustBase = aggregatorClient.getTrustBase(); StateTransitionClient client = new StateTransitionClient(aggregatorClient); - this.predicateVerifier = PredicateVerifierService.create(this.trustBase); + this.predicateVerifier = PredicateVerifierService.create(); this.splitMintJustificationVerifier = new SplitMintJustificationVerifier( this.trustBase, this.predicateVerifier, TestPaymentData::decode); @@ -78,7 +78,7 @@ public void setupFixture() throws Exception { this.mintJustificationVerifier.register(this.splitMintJustificationVerifier); SigningService signingService = SigningService.generate(); - PayToPublicKeyPredicate ownerPredicate = PayToPublicKeyPredicate.fromSigningService(signingService); + SignaturePredicate ownerPredicate = SignaturePredicate.fromSigningService(signingService); this.asset1 = new Asset(new AssetId("ASSET_1".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); this.asset2 = new Asset(new AssetId("ASSET_2".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); @@ -108,7 +108,7 @@ public void setupFixture() throws Exception { this.predicateVerifier, sourceToken, split.getBurnTransaction(), - PayToPublicKeyPredicateUnlockScript.create(split.getBurnTransaction(), signingService) + SignaturePredicateUnlockScript.create(split.getBurnTransaction(), signingService) ); this.splitJustification = SplitMintJustification.create( diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/TokenSplitTest.java b/src/test/java/org/unicitylabs/sdk/functional/payment/TokenSplitTest.java index 78194a4..cf56c85 100644 --- a/src/test/java/org/unicitylabs/sdk/functional/payment/TokenSplitTest.java +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/TokenSplitTest.java @@ -12,7 +12,7 @@ import org.unicitylabs.sdk.payment.TokenSplit; import org.unicitylabs.sdk.payment.asset.Asset; import org.unicitylabs.sdk.payment.asset.AssetId; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.transaction.Token; import org.unicitylabs.sdk.transaction.TokenId; @@ -39,14 +39,14 @@ public void setupFixture() throws Exception { TestAggregatorClient aggregatorClient = TestAggregatorClient.create(); RootTrustBase trustBase = aggregatorClient.getTrustBase(); StateTransitionClient client = new StateTransitionClient(aggregatorClient); - PredicateVerifierService predicateVerifier = PredicateVerifierService.create(trustBase); + PredicateVerifierService predicateVerifier = PredicateVerifierService.create(); MintJustificationVerifierService mintJustificationVerifier = new MintJustificationVerifierService(); mintJustificationVerifier.register(new SplitMintJustificationVerifier( trustBase, predicateVerifier, TestPaymentData::decode)); SigningService signingService = SigningService.generate(); - PayToPublicKeyPredicate ownerPredicate = PayToPublicKeyPredicate.fromSigningService(signingService); + SignaturePredicate ownerPredicate = SignaturePredicate.fromSigningService(signingService); this.asset1 = new Asset(new AssetId("ASSET_1".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); this.asset2 = new Asset(new AssetId("ASSET_2".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); diff --git a/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java b/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java index f5c7c10..1fa7845 100644 --- a/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java +++ b/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java @@ -9,7 +9,7 @@ import org.unicitylabs.sdk.crypto.secp256k1.SigningService; import org.unicitylabs.sdk.predicate.Predicate; import org.unicitylabs.sdk.predicate.UnlockScript; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicateUnlockScript; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.transaction.*; @@ -148,7 +148,7 @@ public static Token transferToken( predicateVerifier, token, transaction, - PayToPublicKeyPredicateUnlockScript.create(transaction, signingService) + SignaturePredicateUnlockScript.create(transaction, signingService) ); } From d44472c64d6188955e6575edbcee6fd9189d3bcc Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Tue, 5 May 2026 13:40:53 +0000 Subject: [PATCH 11/11] #64 Add null checks for token creation --- .../transaction/CertifiedMintTransaction.java | 15 ++++++++++--- .../CertifiedTransferTransaction.java | 15 ++++++++++--- .../unicitylabs/sdk/transaction/Token.java | 21 +++++++++++++++++-- .../CertifiedUnicityIdMintTransaction.java | 6 ++++++ .../sdk/unicityid/UnicityIdToken.java | 8 ++++++- .../sdk/common/CommonTestFlow.java | 1 + 6 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java index 17bc829..9c9e777 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java @@ -13,6 +13,7 @@ import org.unicitylabs.sdk.util.verification.VerificationResult; import java.util.List; +import java.util.Objects; import java.util.Optional; /** @@ -106,9 +107,17 @@ public static CertifiedMintTransaction fromCbor(byte[] bytes) { * @return certified mint transaction * @throws VerificationException if inclusion proof verification fails */ - public static CertifiedMintTransaction fromTransaction(RootTrustBase trustBase, - PredicateVerifierService predicateVerifier, MintTransaction transaction, - InclusionProof inclusionProof) { + public static CertifiedMintTransaction fromTransaction( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + MintTransaction 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, diff --git a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java index 53dc84d..16beb0e 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java @@ -13,6 +13,7 @@ import org.unicitylabs.sdk.util.verification.VerificationResult; import java.util.List; +import java.util.Objects; import java.util.Optional; /** @@ -97,9 +98,17 @@ public static CertifiedTransferTransaction fromCbor(byte[] bytes, Token token) { * * @throws VerificationException if inclusion proof verification fails */ - public static CertifiedTransferTransaction fromTransaction(RootTrustBase trustBase, - PredicateVerifierService predicateVerifier, TransferTransaction transaction, - InclusionProof inclusionProof) { + public static CertifiedTransferTransaction fromTransaction( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + TransferTransaction 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, diff --git a/src/main/java/org/unicitylabs/sdk/transaction/Token.java b/src/main/java/org/unicitylabs/sdk/transaction/Token.java index 3f20f39..23b4cc0 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/Token.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/Token.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * Immutable token aggregate containing the certified genesis mint transaction and transfer history. @@ -132,6 +133,11 @@ public static Token mint( MintJustificationVerifierService mintJustificationVerifier, CertifiedMintTransaction genesis ) { + Objects.requireNonNull(trustBase, "trustBase cannot be null"); + Objects.requireNonNull(predicateVerifier, "predicateVerifier cannot be null"); + Objects.requireNonNull(mintJustificationVerifier, "mintJustificationVerifier cannot be null"); + Objects.requireNonNull(genesis, "genesis cannot be null"); + Token token = new Token(genesis); VerificationResult result = token.verify(trustBase, predicateVerifier, mintJustificationVerifier); if (result.getStatus() != VerificationStatus.OK) { @@ -150,8 +156,15 @@ public static Token mint( * @return new token instance with appended transfer * @throws VerificationException if transfer verification fails */ - public Token transfer(RootTrustBase trustBase, PredicateVerifierService predicateVerifier, - CertifiedTransferTransaction transaction) { + public Token transfer( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + CertifiedTransferTransaction transaction + ) { + Objects.requireNonNull(trustBase, "trustBase cannot be null"); + Objects.requireNonNull(predicateVerifier, "predicateVerifier cannot be null"); + Objects.requireNonNull(transaction, "transaction cannot be null"); + VerificationResult result = CertifiedTransferTransactionVerificationRule.verify( trustBase, predicateVerifier, @@ -179,6 +192,10 @@ public VerificationResult verify( PredicateVerifierService predicateVerifier, MintJustificationVerifierService mintJustificationVerifier ) { + Objects.requireNonNull(trustBase, "trustBase cannot be null"); + Objects.requireNonNull(predicateVerifier, "predicateVerifier cannot be null"); + Objects.requireNonNull(mintJustificationVerifier, "mintJustificationVerifier cannot be null"); + List> results = new ArrayList<>(); VerificationResult result = CertifiedMintTransactionVerificationRule.verify( trustBase, diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java b/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java index da07655..3691253 100644 --- a/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java @@ -17,6 +17,7 @@ import org.unicitylabs.sdk.util.verification.VerificationResult; import java.util.List; +import java.util.Objects; import java.util.Optional; /** @@ -136,6 +137,11 @@ public static CertifiedUnicityIdMintTransaction fromTransaction( 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, diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java index 068b13e..d74a4c1 100644 --- a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java +++ b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java @@ -91,6 +91,10 @@ public static UnicityIdToken mint( PredicateVerifierService predicateVerifier, CertifiedUnicityIdMintTransaction genesis ) { + Objects.requireNonNull(trustBase, "trustBase cannot be null"); + Objects.requireNonNull(predicateVerifier, "predicateVerifier cannot be null"); + Objects.requireNonNull(genesis, "genesis cannot be null"); + VerificationResult result = CertifiedUnicityIdMintTransactionVerificationRule.verify( trustBase, @@ -119,7 +123,7 @@ public byte[] toCbor() { * * @param trustBase trust base used for certification verification * @param predicateVerifier predicate verifier service - * @param issuerPublicKey expected issuer public key (required) + * @param issuerPublicKey expected issuer public key * * @return verification result * @throws NullPointerException if {@code issuerPublicKey} is {@code null} @@ -129,6 +133,8 @@ public VerificationResult verify( PredicateVerifierService predicateVerifier, byte[] issuerPublicKey ) { + Objects.requireNonNull(trustBase, "trustBase cannot be null"); + Objects.requireNonNull(predicateVerifier, "predicateVerifier cannot be null"); Objects.requireNonNull(issuerPublicKey, "issuerPublicKey cannot be null"); List> results = new ArrayList<>(); diff --git a/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java b/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java index 5fce87e..25859d7 100644 --- a/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java +++ b/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java @@ -17,6 +17,7 @@ import org.unicitylabs.sdk.unicityid.UnicityId; import org.unicitylabs.sdk.unicityid.UnicityIdMintTransaction; import org.unicitylabs.sdk.unicityid.UnicityIdToken; +import org.unicitylabs.sdk.util.HexConverter; import org.unicitylabs.sdk.util.InclusionProofUtils; import org.unicitylabs.sdk.util.verification.VerificationStatus; import org.unicitylabs.sdk.utils.TokenUtils;