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..d891d25 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 final 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..cc488b2 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java @@ -0,0 +1,203 @@ +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.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; + 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"); + + byte[] justificationBytes = transaction.getJustification().orElse(null); + if (justificationBytes == null) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Transaction has no justification." + ); + } + + 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<>( + "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 + ); + } + + 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) + ); + } + } + + 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()) { + 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()) + ); + } + + EncodedPredicate recipient = EncodedPredicate.fromPredicate(burnTokenLastTransaction.getRecipient()); + EncodedPredicate 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() != assets.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 deleted file mode 100644 index a3b2804..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 - */ - SplitReason 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/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..39cdda3 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[] stateMask = new byte[32]; + RANDOM.nextBytes(stateMask); TransferTransaction burnTransaction = TransferTransaction.create( token, - ownerPredicate, - Address.fromPredicate(burnPredicate), - x, + burnPredicate, + stateMask, 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..7ea3b20 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,9 +66,13 @@ public TokenType getTokenType() { return this.transaction.getTokenType(); } + public Optional getJustification() { + return this.transaction.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 a74d318..1b9bb79 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,54 +31,29 @@ 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(); + public byte[] getStateMask() { + return this.transaction.getStateMask(); } /** @@ -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..aafac72 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,36 +96,46 @@ 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 - public byte[] getNonce() { + public byte[] getStateMask() { return this.tokenId.getBytes(); } /** * 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) ); } @@ -184,7 +188,7 @@ public DataHash calculateStateHash() { .update( CborSerializer.encodeArray( CborSerializer.encodeByteString(this.sourceStateHash.getImprint()), - CborSerializer.encodeByteString(this.getNonce()) + CborSerializer.encodeByteString(this.getStateMask()) ) ) .digest(); @@ -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..93f4d95 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/Token.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/Token.java @@ -7,18 +7,18 @@ 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. */ -public class Token { +public final class Token { public static final long CBOR_TAG = 39040; private static final int VERSION = 1; @@ -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..4731cd1 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. @@ -41,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 2f798c7..b22214d 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,21 +26,21 @@ public class TransferTransaction implements Transaction { private final DataHash sourceStateHash; private final Predicate lockScript; - private final Address recipient; - private final byte[] nonce; + private final Predicate recipient; + private final byte[] stateMask; private final byte[] data; private TransferTransaction( DataHash sourceStateHash, Predicate lockScript, - Address recipient, - byte[] nonce, + Predicate recipient, + byte[] stateMask, byte[] data ) { this.sourceStateHash = sourceStateHash; this.lockScript = lockScript; this.recipient = recipient; - this.nonce = nonce; + this.stateMask = stateMask; this.data = 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; } @@ -69,33 +70,28 @@ 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); } /** * 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 stateMask 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[] stateMask, 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, + stateMask, 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) ); } @@ -133,7 +129,7 @@ public DataHash calculateStateHash() { .update( CborSerializer.encodeArray( CborSerializer.encodeByteString(this.sourceStateHash.getImprint()), - CborSerializer.encodeByteString(this.nonce) + CborSerializer.encodeByteString(this.stateMask) ) ) .digest(); @@ -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(), - CborSerializer.encodeByteString(this.nonce), - CborSerializer.encodeByteString(this.data) + EncodedPredicate.fromPredicate(this.recipient).toCbor(), + CborSerializer.encodeByteString(this.stateMask), + CborSerializer.encodeNullable(this.data, CborSerializer::encodeByteString) ) ); } @@ -180,15 +168,19 @@ public CertifiedTransferTransaction toCertifiedTransaction( PredicateVerifierService predicateVerifier, InclusionProof inclusionProof ) { - return CertifiedTransferTransaction.fromTransaction(trustBase, predicateVerifier, this, - inclusionProof); + return CertifiedTransferTransaction.fromTransaction( + trustBase, + predicateVerifier, + this, + inclusionProof + ); } @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/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..2e1f34a --- /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) { + byte[] bytes = transaction.getJustification().orElse(null); + if (bytes == null) { + return new VerificationResult<>("MintJustificationVerification", VerificationStatus.OK); + } + + CborDeserializer.CborTag tag = CborDeserializer.decodeTag(bytes); + MintJustificationVerifier verifier = this.verifiers.get(tag.getTag()); + if (verifier == null) { + return new VerificationResult<>( + "MintJustificationVerification", + VerificationStatus.FAIL, + String.format("Unsupported mint justification tag: %s", tag.getTag()) + ); + } + + 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); + } + + 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..888fcd3 --- /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[] 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)); + 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()