From 4c66ac03b5bf67606e963e0c4e6c953b5de6dbd9 Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Tue, 24 Mar 2026 13:34:07 +0000 Subject: [PATCH 1/3] #51 Add payment functionality --- .../sdk/api/CertificationData.java | 5 + .../unicitylabs/sdk/payment/PaymentData.java | 10 + .../sdk/payment/PaymentDataDeserializer.java | 6 + .../sdk/payment/SplitPaymentData.java | 5 + .../payment/SplitPaymentDataDeserializer.java | 6 + .../unicitylabs/sdk/payment/SplitReason.java | 57 ++++ .../sdk/payment/SplitReasonProof.java | 58 +++++ .../unicitylabs/sdk/payment/SplitResult.java | 28 ++ .../unicitylabs/sdk/payment/TokenSplit.java | 244 ++++++++++++++++++ .../unicitylabs/sdk/payment/asset/Asset.java | 61 +++++ .../sdk/payment/asset/AssetId.java | 52 ++++ .../sdk/predicate/PredicateEngine.java | 2 +- .../sdk/predicate/UnlockScript.java | 5 + .../predicate/builtin/BuiltInPredicate.java | 10 + .../builtin/BuiltInPredicateType.java | 3 +- .../sdk/predicate/builtin/BurnPredicate.java | 49 ++++ .../builtin/PayToPublicKeyPredicate.java | 10 - .../PayToPublicKeyPredicateUnlockScript.java | 3 +- .../unicitylabs/sdk/transaction/Token.java | 4 + .../functional/payment/SplitBuilderTest.java | 135 ++++++++++ .../functional/payment/TestPaymentData.java | 39 +++ .../payment/TestSplitPaymentData.java | 51 ++++ .../org/unicitylabs/sdk/utils/TokenUtils.java | 71 ++++- 23 files changed, 898 insertions(+), 16 deletions(-) create mode 100644 src/main/java/org/unicitylabs/sdk/payment/PaymentData.java create mode 100644 src/main/java/org/unicitylabs/sdk/payment/PaymentDataDeserializer.java create mode 100644 src/main/java/org/unicitylabs/sdk/payment/SplitPaymentData.java create mode 100644 src/main/java/org/unicitylabs/sdk/payment/SplitPaymentDataDeserializer.java create mode 100644 src/main/java/org/unicitylabs/sdk/payment/SplitReason.java create mode 100644 src/main/java/org/unicitylabs/sdk/payment/SplitReasonProof.java create mode 100644 src/main/java/org/unicitylabs/sdk/payment/SplitResult.java create mode 100644 src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java create mode 100644 src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java create mode 100644 src/main/java/org/unicitylabs/sdk/payment/asset/AssetId.java create mode 100644 src/main/java/org/unicitylabs/sdk/predicate/UnlockScript.java create mode 100644 src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java create mode 100644 src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java create mode 100644 src/test/java/org/unicitylabs/sdk/functional/payment/TestPaymentData.java create mode 100644 src/test/java/org/unicitylabs/sdk/functional/payment/TestSplitPaymentData.java diff --git a/src/main/java/org/unicitylabs/sdk/api/CertificationData.java b/src/main/java/org/unicitylabs/sdk/api/CertificationData.java index 7d035b3..e8b02ee 100644 --- a/src/main/java/org/unicitylabs/sdk/api/CertificationData.java +++ b/src/main/java/org/unicitylabs/sdk/api/CertificationData.java @@ -10,6 +10,7 @@ import org.unicitylabs.sdk.crypto.secp256k1.SigningService; import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.UnlockScript; import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; @@ -92,6 +93,10 @@ public static CertificationData fromMintTransaction(MintTransaction transaction) ); } + public static CertificationData fromTransaction(Transaction transaction, UnlockScript unlockScript) { + return CertificationData.fromTransaction(transaction, unlockScript.encode()); + } + public static CertificationData fromTransaction(Transaction transaction, byte[] unlockScript) { return new CertificationData( transaction.getLockScript(), diff --git a/src/main/java/org/unicitylabs/sdk/payment/PaymentData.java b/src/main/java/org/unicitylabs/sdk/payment/PaymentData.java new file mode 100644 index 0000000..b8424b9 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/PaymentData.java @@ -0,0 +1,10 @@ +package org.unicitylabs.sdk.payment; + +import java.util.Set; +import org.unicitylabs.sdk.payment.asset.Asset; + +public interface PaymentData { + Set getAssets(); + + byte[] encode(); +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/PaymentDataDeserializer.java b/src/main/java/org/unicitylabs/sdk/payment/PaymentDataDeserializer.java new file mode 100644 index 0000000..642c0bf --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/PaymentDataDeserializer.java @@ -0,0 +1,6 @@ +package org.unicitylabs.sdk.payment; + +@FunctionalInterface +public interface PaymentDataDeserializer { + PaymentData decode(byte[] data); +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentData.java b/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentData.java new file mode 100644 index 0000000..f8a443e --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentData.java @@ -0,0 +1,5 @@ +package org.unicitylabs.sdk.payment; + +public interface SplitPaymentData extends PaymentData { + SplitReason getReason(); +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentDataDeserializer.java b/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentDataDeserializer.java new file mode 100644 index 0000000..2d810d4 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentDataDeserializer.java @@ -0,0 +1,6 @@ +package org.unicitylabs.sdk.payment; + +@FunctionalInterface +public interface SplitPaymentDataDeserializer { + 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 new file mode 100644 index 0000000..4970e31 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitReason.java @@ -0,0 +1,57 @@ +package org.unicitylabs.sdk.payment; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.transaction.Token; + +public class SplitReason { + + private final Token token; + private final List proofs; + + private SplitReason( + Token token, + List proofs + ) { + this.token = token; + this.proofs = List.copyOf(proofs); + } + + public Token getToken() { + return this.token; + } + + public List getProofs() { + return this.proofs; + } + + public static SplitReason create(Token token, List proofs) { + Objects.requireNonNull(token, "token cannot be null"); + Objects.requireNonNull(proofs, "proofs cannot be null"); + + if (proofs.size() == 0) { + throw new IllegalArgumentException("proofs cannot be empty"); + } + + return new SplitReason(token, proofs); + } + + 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()) + ); + } + + 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/SplitReasonProof.java b/src/main/java/org/unicitylabs/sdk/payment/SplitReasonProof.java new file mode 100644 index 0000000..ee6fb01 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitReasonProof.java @@ -0,0 +1,58 @@ +package org.unicitylabs.sdk.payment; + +import java.util.List; +import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreePath; +import org.unicitylabs.sdk.mtree.sum.SparseMerkleSumTreePath; +import org.unicitylabs.sdk.payment.asset.AssetId; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; + +public class SplitReasonProof { + private final AssetId assetId; + private final SparseMerkleTreePath aggregationPath; + private final SparseMerkleSumTreePath assetTreePath; + + private SplitReasonProof( + AssetId assetId, + SparseMerkleTreePath aggregationPath, + SparseMerkleSumTreePath assetTreePath + ) { + this.assetId = assetId; + this.aggregationPath = aggregationPath; + this.assetTreePath = assetTreePath; + } + + public AssetId getAssetId() { + return this.assetId; + } + + public SparseMerkleTreePath getAggregationPath() { + return this.aggregationPath; + } + + public SparseMerkleSumTreePath getAssetTreePath() { + return this.assetTreePath; + } + + public static SplitReasonProof create(AssetId assetId, SparseMerkleTreePath aggregationPath, SparseMerkleSumTreePath assetTreePath) { + return new SplitReasonProof(assetId, aggregationPath, assetTreePath); + } + + public static SplitReasonProof fromCbor(byte[] bytes) { + List data = CborDeserializer.decodeArray(bytes); + + return new SplitReasonProof( + AssetId.fromCbor(data.get(0)), + SparseMerkleTreePath.fromCbor(data.get(1)), + SparseMerkleSumTreePath.fromCbor(data.get(2)) + ); + } + + public byte[] toCbor() { + return CborSerializer.encodeArray( + this.assetId.toCbor(), + this.aggregationPath.toCbor(), + this.assetTreePath.toCbor() + ); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitResult.java b/src/main/java/org/unicitylabs/sdk/payment/SplitResult.java new file mode 100644 index 0000000..fa422ed --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitResult.java @@ -0,0 +1,28 @@ +package org.unicitylabs.sdk.payment; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.transaction.TransferTransaction; + +public class SplitResult { + private final TransferTransaction burnTransaction; + private final Map> proofs; + + SplitResult(TransferTransaction burnTransaction, Map> proofs) { + this.burnTransaction = burnTransaction; + this.proofs = Map.copyOf( + proofs.entrySet().stream().collect(Collectors.toMap(Entry::getKey, value -> List.copyOf(value.getValue()))) + ); + } + + public TransferTransaction getBurnTransaction() { + return this.burnTransaction; + } + + 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 new file mode 100644 index 0000000..60cdd00 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java @@ -0,0 +1,244 @@ +package org.unicitylabs.sdk.payment; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.mtree.BranchExistsException; +import org.unicitylabs.sdk.mtree.LeafOutOfBoundsException; +import org.unicitylabs.sdk.mtree.MerkleTreePathVerificationResult; +import org.unicitylabs.sdk.mtree.plain.SparseMerkleTree; +import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreeRootNode; +import org.unicitylabs.sdk.mtree.sum.SparseMerkleSumTree; +import org.unicitylabs.sdk.mtree.sum.SparseMerkleSumTreeRootNode; +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.transaction.Address; +import org.unicitylabs.sdk.transaction.Token; +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.transaction.Transaction; +import org.unicitylabs.sdk.transaction.TransferTransaction; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +public class TokenSplit { + private static final SecureRandom RANDOM = new SecureRandom(); + + + 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"); + + HashMap trees = new HashMap(); + for (Entry> entry : splitTokens.entrySet()) { + Objects.requireNonNull(entry.getKey(), "Split token id cannot be null"); + for (Asset asset : entry.getValue()) { + Objects.requireNonNull(asset, "Split token asset cannot be null"); + + SparseMerkleSumTree tree = trees.computeIfAbsent(asset.getId(), + v -> new SparseMerkleSumTree(HashAlgorithm.SHA256)); + tree.addLeaf( + entry.getKey().toBitString().toBigInteger(), + new SparseMerkleSumTree.LeafValue(asset.getId().getBytes(), asset.getValue()) + ); + } + } + + PaymentData paymentData = paymentDataDeserializer.decode(token.getGenesis().getData()); + Map assets = paymentData.getAssets().stream().collect(Collectors.toMap(Asset::getId, asset -> asset)); + + if (trees.size() != assets.size()) { + throw new IllegalArgumentException("Token and split tokens asset counts differ."); + } + + SparseMerkleTree aggregationTree = new SparseMerkleTree(HashAlgorithm.SHA256); + HashMap assetTreeRoots = new HashMap(); + for (Entry entry : trees.entrySet()) { + Asset tokenAsset = assets.get(entry.getKey()); + if (tokenAsset == null) { + throw new IllegalArgumentException(String.format("Token did not contain asset %s.", entry.getKey())); + } + + SparseMerkleSumTreeRootNode root = entry.getValue().calculateRoot(); + if (!root.getValue().equals(tokenAsset.getValue())) { + throw new IllegalArgumentException( + String.format( + "Token contained %s %s assets, but tree has %s", + tokenAsset.getValue(), + tokenAsset.getId(), + root.getValue() + ) + ); + } + + assetTreeRoots.put(tokenAsset.getId(), root); + aggregationTree.addLeaf(tokenAsset.getId().toBitString().toBigInteger(), root.getRootHash().getImprint()); + } + + SparseMerkleTreeRootNode aggregationRoot = aggregationTree.calculateRoot(); + BurnPredicate burnPredicate = BurnPredicate.create(aggregationRoot.getRootHash().getImprint()); + byte[] x = new byte[32]; + RANDOM.nextBytes(x); + + TransferTransaction burnTransaction = TransferTransaction.create( + token, + ownerPredicate, + Address.fromPredicate(burnPredicate), + x, + CborSerializer.encodeNull() + ); + + HashMap> proofs = new HashMap>(); + for (Entry> entry : splitTokens.entrySet()) { + proofs.put( + entry.getKey(), + List.copyOf( + entry.getValue().stream().map(asset -> SplitReasonProof.create( + asset.getId(), + aggregationRoot.getPath(asset.getId().toBitString().toBigInteger()), + assetTreeRoots.get(asset.getId()).getPath(entry.getKey().toBitString().toBigInteger()) + ) + ).collect(Collectors.toList()) + ) + ); + } + + return new SplitResult(burnTransaction, proofs); + } + + 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 = data.getAssets().stream().collect(Collectors.toMap(Asset::getId, asset -> asset)); + Transaction burnTokenLastTransaction = data.getReason().getToken().getLatestTransaction(); + 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 (!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/Asset.java b/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java new file mode 100644 index 0000000..a8f00ac --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java @@ -0,0 +1,61 @@ +package org.unicitylabs.sdk.payment.asset; + +import java.math.BigInteger; +import java.util.List; +import java.util.Objects; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.util.BigIntegerConverter; + +public class Asset { + + private final BigInteger value; + private final AssetId id; + + public Asset(AssetId id, BigInteger value) { + this.id = id; + this.value = value; + } + + public AssetId getId() { + return this.id; + } + + public BigInteger getValue() { + return this.value; + } + + public static Asset fromCbor(byte[] bytes) { + List data = CborDeserializer.decodeArray(bytes); + + return new Asset( + AssetId.fromCbor(data.get(0)), + BigIntegerConverter.decode(CborDeserializer.decodeByteString(data.get(1))) + ); + } + + public byte[] toCbor() { + return CborSerializer.encodeArray( + this.id.toCbor(), + CborSerializer.encodeByteString(BigIntegerConverter.encode(this.value)) + ); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Asset)) { + return false; + } + Asset asset = (Asset) o; + return Objects.equals(this.value, asset.value) && Objects.equals(this.id, asset.id); + } + + @Override + public int hashCode() { + return Objects.hash(this.value, this.id); + } + + public String toString() { + return String.format("Asset{value=%s, id=%s}", this.value, this.id); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/asset/AssetId.java b/src/main/java/org/unicitylabs/sdk/payment/asset/AssetId.java new file mode 100644 index 0000000..94e6498 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/asset/AssetId.java @@ -0,0 +1,52 @@ +package org.unicitylabs.sdk.payment.asset; + +import java.util.Arrays; +import java.util.Objects; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.util.BitString; +import org.unicitylabs.sdk.util.HexConverter; + +public class AssetId { + private final byte[] bytes; + + public AssetId(byte[] bytes) { + Objects.requireNonNull(bytes, "Asset id cannot be null"); + + this.bytes = Arrays.copyOf(bytes, bytes.length); + } + + public byte[] getBytes() { + return Arrays.copyOf(this.bytes, this.bytes.length); + } + + public static AssetId fromCbor(byte[] bytes) { + return new AssetId(CborDeserializer.decodeByteString(bytes)); + } + + public BitString toBitString() { + return new BitString(this.bytes); + } + + public byte[] toCbor() { + return CborSerializer.encodeByteString(this.bytes); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AssetId)) { + return false; + } + AssetId assetId = (AssetId) o; + return Arrays.equals(this.bytes, assetId.bytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(bytes); + } + + public String toString() { + return String.format("AssetId{bytes=%s}", HexConverter.encode(this.bytes)); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/PredicateEngine.java b/src/main/java/org/unicitylabs/sdk/predicate/PredicateEngine.java index 2a5d499..67c9d7a 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/PredicateEngine.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/PredicateEngine.java @@ -3,7 +3,7 @@ public enum PredicateEngine { BUILT_IN(1); - public final int id; + private final int id; PredicateEngine(int id) { this.id = id; diff --git a/src/main/java/org/unicitylabs/sdk/predicate/UnlockScript.java b/src/main/java/org/unicitylabs/sdk/predicate/UnlockScript.java new file mode 100644 index 0000000..fbb6bc4 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/predicate/UnlockScript.java @@ -0,0 +1,5 @@ +package org.unicitylabs.sdk.predicate; + +public interface UnlockScript { + byte[] encode(); +} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicate.java index 18328e9..3e593cb 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicate.java @@ -1,8 +1,18 @@ package org.unicitylabs.sdk.predicate.builtin; import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.PredicateEngine; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; public interface BuiltInPredicate extends Predicate { BuiltInPredicateType getType(); + + default PredicateEngine getEngine() { + return PredicateEngine.BUILT_IN; + } + + default byte[] encodeCode() { + return CborSerializer.encodeUnsignedInteger(this.getType().getId()); + } } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java index 9cf1b2b..19c65d9 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java @@ -2,7 +2,8 @@ public enum BuiltInPredicateType { PAY_TO_PUBLIC_KEY(1), - UNICITY_ID(2); + UNICITY_ID(2), + BURN(3); private final int id; diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java new file mode 100644 index 0000000..f38ad27 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java @@ -0,0 +1,49 @@ +package org.unicitylabs.sdk.predicate.builtin; + +import java.util.Arrays; +import java.util.Objects; +import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.PredicateEngine; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; + +public class BurnPredicate implements BuiltInPredicate { + private final byte[] reason; + + private BurnPredicate(byte[] reason) { + this.reason = Arrays.copyOf(reason, reason.length); + } + + public BuiltInPredicateType getType() { + return BuiltInPredicateType.BURN; + } + + public byte[] getReason() { + return Arrays.copyOf(this.reason, this.reason.length); + } + + public static BurnPredicate create(byte[] reason) { + Objects.requireNonNull(reason, "Reason cannot be null"); + + return new BurnPredicate(reason); + } + + public static BurnPredicate fromPredicate(Predicate predicate) { + PredicateEngine engine = predicate.getEngine(); + if (engine != PredicateEngine.BUILT_IN) { + throw new IllegalArgumentException("Predicate engine must be BUILT_IN."); + } + + BuiltInPredicateType type = BuiltInPredicateType.fromId( + CborDeserializer.decodeUnsignedInteger(predicate.encodeCode()).asInt()); + if (type != BuiltInPredicateType.BURN) { + throw new IllegalArgumentException("Predicate type must be BURN."); + } + + return new BurnPredicate(predicate.encodeParameters()); + } + + @Override + public byte[] encodeParameters() { + return this.getReason(); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java index 2110add..4a55407 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java @@ -16,11 +16,6 @@ private PayToPublicKeyPredicate(byte[] publicKey) { this.publicKey = publicKey; } - @Override - public PredicateEngine getEngine() { - return PredicateEngine.BUILT_IN; - } - public byte[] getPublicKey() { return Arrays.copyOf(this.publicKey, this.publicKey.length); } @@ -53,11 +48,6 @@ public static PayToPublicKeyPredicate fromSigningService(SigningService signingS return new PayToPublicKeyPredicate(signingService.getPublicKey()); } - @Override - public byte[] encodeCode() { - return CborSerializer.encodeUnsignedInteger(this.getType().getId()); - } - @Override public byte[] encodeParameters() { return this.getPublicKey(); diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicateUnlockScript.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicateUnlockScript.java index 80281b6..27e4db1 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicateUnlockScript.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicateUnlockScript.java @@ -5,10 +5,11 @@ import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.unicitylabs.sdk.crypto.secp256k1.Signature; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.predicate.UnlockScript; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.transaction.Transaction; -public class PayToPublicKeyPredicateUnlockScript { +public class PayToPublicKeyPredicateUnlockScript implements UnlockScript { private final Signature signature; diff --git a/src/main/java/org/unicitylabs/sdk/transaction/Token.java b/src/main/java/org/unicitylabs/sdk/transaction/Token.java index 71a3808..79c9631 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/Token.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/Token.java @@ -35,6 +35,10 @@ public TokenType getType() { return this.genesis.getTokenType(); } + public CertifiedMintTransaction getGenesis() { + return this.genesis; + } + public Transaction getLatestTransaction() { if (this.transactions.isEmpty()) { return this.genesis; diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java new file mode 100644 index 0000000..59777a8 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java @@ -0,0 +1,135 @@ +package org.unicitylabs.sdk.functional.payment; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +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.SplitReason; +import org.unicitylabs.sdk.payment.SplitReasonProof; +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.verification.PredicateVerifierService; +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.util.verification.VerificationStatus; +import org.unicitylabs.sdk.utils.TokenUtils; + +public class SplitBuilderTest { + + @Test + public void testMintAndSplitToken() throws Exception { + TestAggregatorClient aggregatorClient = TestAggregatorClient.create(); + RootTrustBase trustBase = aggregatorClient.getTrustBase(); + StateTransitionClient client = new StateTransitionClient(aggregatorClient); + PredicateVerifierService predicateVerifier = PredicateVerifierService.create(trustBase); + + SigningService signingService = SigningService.generate(); + PayToPublicKeyPredicate predicate = PayToPublicKeyPredicate.fromSigningService(signingService); + + Asset asset1 = new Asset(new AssetId("ASSET_1".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); + Asset asset2 = new Asset(new AssetId("ASSET_2".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); + + Set assets = Set.of(asset1, asset2); + TestPaymentData paymentData = new TestPaymentData(assets); + + Token token = TokenUtils.mintToken( + client, + trustBase, + predicateVerifier, + Address.fromPredicate(predicate), + paymentData.encode() + ); + + IllegalArgumentException exception = Assertions.assertThrows( + IllegalArgumentException.class, + () -> TokenSplit.split( + token, + predicate, + TestPaymentData::decode, + Map.of(TokenId.generate(), Set.of(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(asset1, new Asset(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(asset1, new Asset(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(asset1), + TokenId.generate(), Set.of(asset2) + ); + + SplitResult result = TokenSplit.split(token, predicate, TestPaymentData::decode, splitTokens); + + Token burnToken = TokenUtils.transferToken( + client, trustBase, predicateVerifier, token, result.getBurnTransaction(), signingService + ); + + for (Entry> entry : splitTokens.entrySet()) { + List proofs = result.getProofs().get(entry.getKey()); + Assertions.assertNotNull(proofs); + + Token splitToken = TokenUtils.mintToken( + client, + trustBase, + predicateVerifier, + entry.getKey(), + Address.fromPredicate(predicate), + new TestSplitPaymentData( + entry.getValue(), + SplitReason.create( + burnToken, + proofs + ) + ).encode() + ); + + Assertions.assertEquals(VerificationStatus.OK, splitToken.verify(trustBase, predicateVerifier).getStatus()); + Assertions.assertEquals(VerificationStatus.OK, + TokenSplit.verify( + Token.fromCbor(splitToken.toCbor()), + TestSplitPaymentData::decode, + trustBase, + predicateVerifier + ).getStatus()); + } + } + +} diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/TestPaymentData.java b/src/test/java/org/unicitylabs/sdk/functional/payment/TestPaymentData.java new file mode 100644 index 0000000..73e7167 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/TestPaymentData.java @@ -0,0 +1,39 @@ +package org.unicitylabs.sdk.functional.payment; + +import java.util.Set; +import java.util.stream.Collectors; +import org.unicitylabs.sdk.payment.PaymentData; +import org.unicitylabs.sdk.payment.asset.Asset; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; + +public class TestPaymentData implements PaymentData { + + private final Set assets; + + public TestPaymentData(Set assets) { + this.assets = Set.copyOf(assets); + } + + @Override + public Set getAssets() { + return this.assets; + } + + public static TestPaymentData decode(byte[] bytes) { + Set assets = CborDeserializer.decodeArray(bytes).stream() + .map(Asset::fromCbor) + .collect(Collectors.toSet()); + + return new TestPaymentData(assets); + } + + @Override + public byte[] encode() { + return CborSerializer.encodeArray( + this.assets.stream() + .map(Asset::toCbor) + .toArray(byte[][]::new) + ); + } +} \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/TestSplitPaymentData.java b/src/test/java/org/unicitylabs/sdk/functional/payment/TestSplitPaymentData.java new file mode 100644 index 0000000..cbcac58 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/TestSplitPaymentData.java @@ -0,0 +1,51 @@ +package org.unicitylabs.sdk.functional.payment; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +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; + +public class TestSplitPaymentData implements SplitPaymentData { + private final Set assets; + private final SplitReason reason; + + public TestSplitPaymentData(Set assets, SplitReason reason) { + this.assets = Set.copyOf(assets); + this.reason = reason; + } + + public Set getAssets() { + return this.assets; + } + + @Override + public SplitReason getReason() { + return this.reason; + } + + public static TestSplitPaymentData decode(byte[] bytes) { + List data = CborDeserializer.decodeArray(bytes); + + Set assets = CborDeserializer.decodeArray(data.get(0)).stream() + .map(Asset::fromCbor) + .collect(Collectors.toSet()); + + SplitReason reason = SplitReason.fromCbor(data.get(1)); + + return new TestSplitPaymentData(assets, reason); + } + + @Override + public byte[] encode() { + return CborSerializer.encodeArray( + CborSerializer.encodeArray( + this.assets.stream().map(Asset::toCbor).toArray(byte[][]::new) + ), + this.reason.toCbor() + ); + } +} \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java b/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java index 09d0ea3..9a2fe13 100644 --- a/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java +++ b/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java @@ -29,11 +29,65 @@ public static Token mintToken( PredicateVerifierService predicateVerifier, Address recipient ) throws Exception { - MintTransaction transaction = MintTransaction.create( + return TokenUtils.mintToken( + client, + trustBase, + predicateVerifier, recipient, + CborSerializer.encodeArray() + ); + } + + public static Token mintToken( + StateTransitionClient client, + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + Address recipient, + byte[] data + ) throws Exception { + return TokenUtils.mintToken( + client, + trustBase, + predicateVerifier, TokenId.generate(), + recipient, + data + ); + } + + public static Token mintToken( + StateTransitionClient client, + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + TokenId tokenId, + Address recipient, + byte[] data + ) throws Exception { + return TokenUtils.mintToken( + client, + trustBase, + predicateVerifier, + tokenId, TokenType.generate(), - CborSerializer.encodeArray() + recipient, + data + ); + } + + public static Token mintToken( + StateTransitionClient client, + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + TokenId tokenId, + TokenType tokenType, + Address recipient, + byte[] data + ) throws Exception { + MintTransaction transaction = MintTransaction.create( + recipient, + tokenId, + tokenType, + data ); CertificationData certificationData = CertificationData.fromMintTransaction(transaction); @@ -78,10 +132,21 @@ public static Token transferToken( CborSerializer.encodeArray() ); + return TokenUtils.transferToken(client, trustBase, predicateVerifier, token, transaction, signingService); + } + + public static Token transferToken( + StateTransitionClient client, + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + Token token, + TransferTransaction transaction, + SigningService signingService + ) throws Exception { CertificationResponse response = client.submitCertificationRequest( CertificationData.fromTransaction( transaction, - PayToPublicKeyPredicateUnlockScript.create(transaction, signingService).encode() + PayToPublicKeyPredicateUnlockScript.create(transaction, signingService) ) ).get(); From 3456668500ab28d641e78effefe2ef5762632e2f Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Mon, 6 Apr 2026 14:52:39 +0000 Subject: [PATCH 2/3] #51 Fix merge issues, update javadoc --- .../sdk/StateTransitionClient.java | 6 ++ .../sdk/api/CertificationData.java | 37 ++++++- .../sdk/api/CertificationRequest.java | 4 +- .../unicitylabs/sdk/api/InclusionProof.java | 11 +- .../sdk/api/InclusionProofRequest.java | 5 + .../sdk/api/InclusionProofResponse.java | 7 +- .../sdk/api/JsonRpcAggregatorClient.java | 28 +++-- .../java/org/unicitylabs/sdk/api/StateId.java | 53 +++++++-- .../unicitylabs/sdk/api/bft/InputRecord.java | 4 +- .../sdk/api/bft/ShardTreeCertificate.java | 4 +- .../sdk/api/bft/UnicityCertificate.java | 4 +- .../unicitylabs/sdk/api/bft/UnicitySeal.java | 4 +- .../sdk/api/bft/UnicityTreeCertificate.java | 8 +- .../UnicityCertificateVerification.java | 12 +++ .../UnicityCertificateVerificationResult.java | 15 +++ ...nputRecordCurrentHashVerificationRule.java | 10 ++ ...nicitySealHashMatchesWithRootHashRule.java | 8 ++ ...ySealQuorumSignaturesVerificationRule.java | 9 ++ .../sdk/crypto/MintSigningService.java | 12 +++ .../unicitylabs/sdk/crypto/hash/DataHash.java | 4 +- .../sdk/crypto/hash/HashAlgorithm.java | 5 + .../sdk/crypto/secp256k1/Signature.java | 2 +- .../sdk/crypto/secp256k1/SigningService.java | 5 + .../sdk/mtree/plain/SparseMerkleTreePath.java | 4 +- .../mtree/plain/SparseMerkleTreePathStep.java | 4 +- .../mtree/sum/SparseMerkleSumTreePath.java | 4 +- .../sum/SparseMerkleSumTreePathStep.java | 4 +- .../unicitylabs/sdk/payment/PaymentData.java | 13 +++ .../sdk/payment/PaymentDataDeserializer.java | 9 ++ .../sdk/payment/SplitPaymentData.java | 8 ++ .../payment/SplitPaymentDataDeserializer.java | 9 ++ .../unicitylabs/sdk/payment/SplitReason.java | 33 ++++++ .../sdk/payment/SplitReasonProof.java | 45 +++++++- .../unicitylabs/sdk/payment/SplitResult.java | 19 +++- .../unicitylabs/sdk/payment/TokenSplit.java | 89 +++++++++++++-- .../unicitylabs/sdk/payment/asset/Asset.java | 45 +++++++- .../sdk/payment/asset/AssetId.java | 31 ++++++ .../sdk/predicate/EncodedPredicate.java | 38 +++++-- .../unicitylabs/sdk/predicate/Predicate.java | 24 +++++ .../sdk/predicate/PredicateEngine.java | 16 +++ .../sdk/predicate/UnlockScript.java | 8 ++ .../predicate/builtin/BuiltInPredicate.java | 18 ++++ .../builtin/BuiltInPredicateType.java | 18 ++++ .../sdk/predicate/builtin/BurnPredicate.java | 32 ++++++ .../DefaultBuiltInPredicateVerifier.java | 16 +++ .../builtin/PayToPublicKeyPredicate.java | 40 ++++++- .../PayToPublicKeyPredicateUnlockScript.java | 24 ++++- .../BuiltInPredicateVerifier.java | 17 +++ .../PayToPublicKeyPredicateVerifier.java | 8 ++ .../verification/PredicateVerifier.java | 17 +++ .../PredicateVerifierService.java | 26 +++++ .../unicitylabs/sdk/transaction/Address.java | 36 ++++++- .../transaction/CertifiedMintTransaction.java | 46 +++++++- .../CertifiedTransferTransaction.java | 74 ++++++++++++- .../sdk/transaction/MintTransaction.java | 78 +++++++++++++- .../sdk/transaction/MintTransactionState.java | 9 ++ .../unicitylabs/sdk/transaction/Token.java | 67 +++++++++++- .../unicitylabs/sdk/transaction/TokenId.java | 37 ++++++- .../sdk/transaction/TokenType.java | 37 ++++++- .../sdk/transaction/Transaction.java | 45 +++++++- .../sdk/transaction/TransferTransaction.java | 52 +++++++-- ...tifiedMintTransactionVerificationRule.java | 18 ++++ ...edTransferTransactionVerificationRule.java | 19 ++++ .../InclusionProofVerificationRule.java | 37 ++++++- .../InclusionProofVerificationStatus.java | 11 ++ .../org/unicitylabs/sdk/util/BitString.java | 4 - .../verification/VerificationContext.java | 5 - .../verification/VerificationException.java | 18 +++- .../util/verification/VerificationResult.java | 67 ++++++++++-- .../util/verification/VerificationStatus.java | 8 +- .../functional/payment/SplitBuilderTest.java | 18 +++- .../payment/TestSplitPaymentData.java | 61 +++++++++-- .../org/unicitylabs/sdk/utils/TokenUtils.java | 102 ++++++++++++++++-- 73 files changed, 1581 insertions(+), 144 deletions(-) delete mode 100644 src/main/java/org/unicitylabs/sdk/util/verification/VerificationContext.java diff --git a/src/main/java/org/unicitylabs/sdk/StateTransitionClient.java b/src/main/java/org/unicitylabs/sdk/StateTransitionClient.java index 06513c2..3ddd20d 100644 --- a/src/main/java/org/unicitylabs/sdk/StateTransitionClient.java +++ b/src/main/java/org/unicitylabs/sdk/StateTransitionClient.java @@ -37,6 +37,12 @@ public CompletableFuture getInclusionProof(StateId state return this.client.getInclusionProof(stateId); } + /** + * Submits a certification request to the aggregator. + * + * @param certificationData The certification data to submit. + * @return certification response from the aggregator. + */ public CompletableFuture submitCertificationRequest(CertificationData certificationData) { return this.client.submitCertificationRequest(certificationData); } diff --git a/src/main/java/org/unicitylabs/sdk/api/CertificationData.java b/src/main/java/org/unicitylabs/sdk/api/CertificationData.java index e8b02ee..a011017 100644 --- a/src/main/java/org/unicitylabs/sdk/api/CertificationData.java +++ b/src/main/java/org/unicitylabs/sdk/api/CertificationData.java @@ -40,6 +40,11 @@ public class CertificationData { this.unlockScript = Arrays.copyOf(unlockScript, unlockScript.length); } + /** + * Get lock script of certified transaction output. + * + * @return lock script + */ public Predicate getLockScript() { return this.lockScript; } @@ -62,12 +67,17 @@ public DataHash getTransactionHash() { return this.transactionHash; } + /** + * Get unlock script used for certification. + * + * @return unlock script bytes + */ public byte[] getUnlockScript() { return Arrays.copyOf(this.unlockScript, this.unlockScript.length); } /** - * Create CertificationData from CBOR bytes. + * Deserialize CertificationData from CBOR bytes. * * @param bytes CBOR bytes * @return CertificationData @@ -83,6 +93,13 @@ public static CertificationData fromCbor(byte[] bytes) { ); } + /** + * Build certification data for a mint transaction using the deterministic mint signing service. + * + * @param transaction mint transaction + * + * @return certification data + */ public static CertificationData fromMintTransaction(MintTransaction transaction) { SigningService signingService = MintSigningService.create(transaction.getTokenId()); @@ -93,10 +110,26 @@ public static CertificationData fromMintTransaction(MintTransaction transaction) ); } + /** + * Build certification data from a transaction and unlock script object. + * + * @param transaction transaction to certify + * @param unlockScript unlock script + * + * @return certification data + */ public static CertificationData fromTransaction(Transaction transaction, UnlockScript unlockScript) { return CertificationData.fromTransaction(transaction, unlockScript.encode()); } + /** + * Build certification data from a transaction and encoded unlock script bytes. + * + * @param transaction transaction to certify + * @param unlockScript encoded unlock script bytes + * + * @return certification data + */ public static CertificationData fromTransaction(Transaction transaction, byte[] unlockScript) { return new CertificationData( transaction.getLockScript(), @@ -118,7 +151,7 @@ public DataHash calculateLeafValue() { } /** - * Convert the certification data to CBOR bytes. + * Serialize certification data to CBOR bytes. * * @return CBOR bytes */ diff --git a/src/main/java/org/unicitylabs/sdk/api/CertificationRequest.java b/src/main/java/org/unicitylabs/sdk/api/CertificationRequest.java index 90281d5..54cd212 100644 --- a/src/main/java/org/unicitylabs/sdk/api/CertificationRequest.java +++ b/src/main/java/org/unicitylabs/sdk/api/CertificationRequest.java @@ -54,11 +54,11 @@ public static CertificationRequest create(CertificationData certificationData) { } /** - * Convert the request to a CBOR bytes. + * Serialize request to a CBOR bytes. * * @return CBOR bytes */ - public byte[] toCBOR() { + public byte[] toCbor() { return CborSerializer.encodeArray( this.stateId.toCbor(), this.certificationData.toCbor(), diff --git a/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java b/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java index 28d4e08..1df8ea2 100644 --- a/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java +++ b/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java @@ -22,12 +22,9 @@ public class InclusionProof { CertificationData certificationData, UnicityCertificate unicityCertificate ) { - Objects.requireNonNull(merkleTreePath, "Merkle tree path cannot be null."); - Objects.requireNonNull(unicityCertificate, "Unicity certificate cannot be null."); - - this.merkleTreePath = merkleTreePath; + this.merkleTreePath = Objects.requireNonNull(merkleTreePath, "Merkle tree path cannot be null.");; this.certificationData = certificationData; - this.unicityCertificate = unicityCertificate; + this.unicityCertificate = Objects.requireNonNull(unicityCertificate, "Unicity certificate cannot be null.");; } /** @@ -58,7 +55,7 @@ public Optional getCertificationData() { } /** - * Create inclusion proof from CBOR bytes. + * Deserialize inclusion proof from CBOR bytes. * * @param bytes CBOR bytes * @return inclusion proof @@ -74,7 +71,7 @@ public static InclusionProof fromCbor(byte[] bytes) { } /** - * Convert inclusion proof to CBOR bytes. + * Serialize inclusion proof to CBOR bytes. * * @return CBOR bytes */ diff --git a/src/main/java/org/unicitylabs/sdk/api/InclusionProofRequest.java b/src/main/java/org/unicitylabs/sdk/api/InclusionProofRequest.java index 49a51dd..7abed45 100644 --- a/src/main/java/org/unicitylabs/sdk/api/InclusionProofRequest.java +++ b/src/main/java/org/unicitylabs/sdk/api/InclusionProofRequest.java @@ -26,6 +26,11 @@ public InclusionProofRequest( this.stateId = stateId.getData(); } + /** + * Get state id. + * + * @return state id + */ public byte[] getStateId() { return Arrays.copyOf(this.stateId, this.stateId.length); } diff --git a/src/main/java/org/unicitylabs/sdk/api/InclusionProofResponse.java b/src/main/java/org/unicitylabs/sdk/api/InclusionProofResponse.java index 7c4f3e1..5a44091 100644 --- a/src/main/java/org/unicitylabs/sdk/api/InclusionProofResponse.java +++ b/src/main/java/org/unicitylabs/sdk/api/InclusionProofResponse.java @@ -35,7 +35,7 @@ public InclusionProof getInclusionProof() { } /** - * Create response from CBOR bytes. + * Deserialize response from CBOR bytes. * * @param bytes CBOR bytes * @return inclusion proof response @@ -48,6 +48,11 @@ public static InclusionProofResponse fromCbor(byte[] bytes) { ); } + /** + * Serialize inclusion proof response to CBOR bytes. + * + * @return CBOR bytes + */ public byte[] toCbor() { return CborSerializer.encodeArray( CborSerializer.encodeUnsignedInteger(this.blockNumber), diff --git a/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java b/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java index c08db12..4471f2e 100644 --- a/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java +++ b/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java @@ -35,18 +35,23 @@ public JsonRpcAggregatorClient(String url) { * */ public JsonRpcAggregatorClient(String url, String apiKey) { - Objects.requireNonNull(url, "url cannot be null"); - - this.transport = new JsonRpcHttpTransport(url); + this.transport = new JsonRpcHttpTransport(Objects.requireNonNull(url, "url cannot be null")); this.apiKey = apiKey; } + /** + * Submit a certification request for a transaction state transition. + * + * @param certificationData certification payload + * + * @return asynchronous certification response + */ + @Override public CompletableFuture submitCertificationRequest( CertificationData certificationData ) { - Objects.requireNonNull(certificationData, "certificationData cannot be null"); - - CertificationRequest request = CertificationRequest.create(certificationData); + CertificationRequest request = CertificationRequest.create( + Objects.requireNonNull(certificationData, "certificationData cannot be null")); Map> headers = this.apiKey == null ? Map.of() @@ -54,7 +59,7 @@ public CompletableFuture submitCertificationRequest( return this.transport.request( "certification_request", - HexConverter.encode(request.toCBOR()), + HexConverter.encode(request.toCbor()), CertificationResponse.class, headers ); @@ -66,10 +71,10 @@ public CompletableFuture submitCertificationRequest( * @param stateId state id * @return inclusion / non inclusion proof */ + @Override public CompletableFuture getInclusionProof(StateId stateId) { - Objects.requireNonNull(stateId, "stateId cannot be null"); - - InclusionProofRequest request = new InclusionProofRequest(stateId); + InclusionProofRequest request = new InclusionProofRequest( + Objects.requireNonNull(stateId, "stateId cannot be null")); return this.transport .request("get_inclusion_proof.v2", request, String.class) @@ -81,8 +86,9 @@ public CompletableFuture getInclusionProof(StateId state * * @return block height */ + @Override public CompletableFuture getBlockHeight() { return this.transport.request("get_block_height", Map.of(), BlockHeightResponse.class) .thenApply(BlockHeightResponse::getBlockNumber); } -} \ No newline at end of file +} diff --git a/src/main/java/org/unicitylabs/sdk/api/StateId.java b/src/main/java/org/unicitylabs/sdk/api/StateId.java index ec99162..4cb910c 100644 --- a/src/main/java/org/unicitylabs/sdk/api/StateId.java +++ b/src/main/java/org/unicitylabs/sdk/api/StateId.java @@ -12,7 +12,10 @@ import org.unicitylabs.sdk.util.BitString; import org.unicitylabs.sdk.util.HexConverter; -public class StateId { +/** + * Represents a state identifier for requests. + */ +public final class StateId { private final DataHash hash; @@ -20,19 +23,42 @@ private StateId(DataHash hash) { this.hash = hash; } + /** + * Returns the raw hash bytes of this state id. + * + * @return state id hash bytes + */ public byte[] getData() { return this.hash.getData(); } + /** + * Returns the hash imprint bytes. + * + * @return state id imprint bytes + */ public byte[] getImprint() { return this.hash.getImprint(); } + /** + * Deserializes a state id from CBOR. + * + * @param bytes CBOR byte string containing SHA-256 hash bytes + * @return decoded state id + */ public static StateId fromCbor(byte[] bytes) { return new StateId( new DataHash(HashAlgorithm.SHA256, CborDeserializer.decodeByteString(bytes))); } + /** + * Creates a state id from certification data. + * + * @param certificationData certification data carrying lock script and source state hash + * @return created state id + * @throws NullPointerException if {@code certificationData} is {@code null} + */ public static StateId fromCertificationData(CertificationData certificationData) { Objects.requireNonNull(certificationData, "Certification data cannot be null"); @@ -40,6 +66,13 @@ public static StateId fromCertificationData(CertificationData certificationData) certificationData.getSourceStateHash()); } + /** + * Creates a state id from transaction data. + * + * @param transaction transaction carrying lock script and source state hash + * @return created state id + * @throws NullPointerException if {@code transaction} is {@code null} + */ public static StateId fromTransaction(Transaction transaction) { Objects.requireNonNull(transaction, "Transaction cannot be null"); @@ -59,17 +92,22 @@ private static StateId create(Predicate predicate, DataHash stateHash) { return new StateId(hash); } + /** + * Serializes this state id as a CBOR bytes. + * + * @return CBOR-encoded state id + */ public byte[] toCbor() { return CborSerializer.encodeByteString(this.getData()); } /** - * Converts the StateId to a BitString. + * Converts this state id to a {@link BitString}. * - * @return The BitString representation of the StateId. + * @return bit string representation of this state id */ public BitString toBitString() { - return BitString.fromStateId(this); + return new BitString(this.getImprint()); } @Override @@ -86,13 +124,8 @@ public int hashCode() { return Objects.hashCode(this.hash); } - /** - * Returns a string representation of the StateId. - * - * @return The string representation. - */ @Override public String toString() { return String.format("StateId[%s]", HexConverter.encode(this.getImprint())); } -} \ No newline at end of file +} 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 bdb898b..89be419 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java @@ -144,7 +144,7 @@ public byte[] getExecutedTransactionsHash() { } /** - * Create InputRecord from CBOR bytes. + * Deserialize InputRecord from CBOR bytes. * * @param bytes CBOR bytes * @return input record @@ -168,7 +168,7 @@ public static InputRecord fromCbor(byte[] bytes) { } /** - * Convert InputRecord to CBOR bytes. + * Serialize InputRecord to CBOR bytes. * * @return CBOR bytes */ diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java b/src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java index e34e5f3..d1aedc8 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java @@ -47,7 +47,7 @@ public List getSiblingHashList() { } /** - * Create shard tree certificate from CBOR bytes. + * Deserialize shard tree certificate from CBOR bytes. * * @param bytes CBOR bytes * @return shard tree certificate @@ -64,7 +64,7 @@ public static ShardTreeCertificate fromCbor(byte[] bytes) { } /** - * Convert shard tree certificate to CBOR bytes. + * Serialize shard tree certificate to CBOR bytes. * * @return CBOR bytes */ 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 8d5b6cb..a60f9cb 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java @@ -163,7 +163,7 @@ public static DataHash calculateShardTreeCertificateRootHash( } /** - * Create unicity certificate from CBOR bytes. + * Deserialize unicity certificate from CBOR bytes. * * @param bytes CBOR bytes * @return unicity certificate @@ -184,7 +184,7 @@ public static UnicityCertificate fromCbor(byte[] bytes) { } /** - * Convert unicity certificate to CBOR bytes. + * Serialize unicity certificate to CBOR bytes. * * @return CBOR bytes */ 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 15e0a8b..3ebce9b 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java @@ -171,7 +171,7 @@ public Map getSignatures() { } /** - * Create unicity seal from CBOR bytes. + * Deserialize unicity seal from CBOR bytes. * * @param bytes CBOR bytes * @return unicity seal @@ -200,7 +200,7 @@ public static UnicitySeal fromCbor(byte[] bytes) { } /** - * Convert unicity seal to CBOR bytes. + * Serialize unicity seal to CBOR bytes. * * @return CBOR bytes */ diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/UnicityTreeCertificate.java b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityTreeCertificate.java index 77d44ea..5143c2c 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/UnicityTreeCertificate.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityTreeCertificate.java @@ -58,7 +58,7 @@ public List getSteps() { } /** - * Create certificate from CBOR bytes. + * Deserialize certificate from CBOR bytes. * * @param bytes CBOR bytes * @return certificate @@ -77,7 +77,7 @@ public static UnicityTreeCertificate fromCbor(byte[] bytes) { } /** - * Convert certificate to CBOR bytes. + * Serialize certificate to CBOR bytes. * * @return CBOR bytes */ @@ -149,7 +149,7 @@ public byte[] getHash() { } /** - * Create hash step from CBOR bytes. + * Deserialize hash step from CBOR bytes. * * @param bytes CBOR bytes * @return hash step @@ -164,7 +164,7 @@ public static HashStep fromCbor(byte[] bytes) { } /** - * Convert hash step to CBOR bytes. + * Serialize hash step to CBOR bytes. * * @return CBOR bytes */ diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/verification/UnicityCertificateVerification.java b/src/main/java/org/unicitylabs/sdk/api/bft/verification/UnicityCertificateVerification.java index 2ec2f4e..26d3018 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/verification/UnicityCertificateVerification.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/verification/UnicityCertificateVerification.java @@ -9,8 +9,20 @@ import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; +/** + * Verifies unicity certificate within an inclusion proof. + */ public class UnicityCertificateVerification { + private UnicityCertificateVerification() {} + + /** + * Runs unicity certificate verification rules against the provided inclusion proof. + * + * @param trustBase trust base used for quorum signature verification + * @param inclusionProof inclusion proof containing the certificate and seal + * @return verification result aggregating rule outcomes + */ public static UnicityCertificateVerificationResult verify(RootTrustBase trustBase, InclusionProof inclusionProof) { ArrayList> results = new ArrayList>(); diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/verification/UnicityCertificateVerificationResult.java b/src/main/java/org/unicitylabs/sdk/api/bft/verification/UnicityCertificateVerificationResult.java index 2956877..918c5ea 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/verification/UnicityCertificateVerificationResult.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/verification/UnicityCertificateVerificationResult.java @@ -4,6 +4,9 @@ import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; +/** + * Verification result type for unicity certificate verification. + */ public class UnicityCertificateVerificationResult extends VerificationResult { private UnicityCertificateVerificationResult(VerificationStatus status, @@ -11,10 +14,22 @@ private UnicityCertificateVerificationResult(VerificationStatus status, super("UnicityCertificateVerification", status, "", results); } + /** + * Creates a failed unicity certificate verification result. + * + * @param results detailed rule verification results + * @return failed verification result + */ public static UnicityCertificateVerificationResult fail(List> results) { return new UnicityCertificateVerificationResult(VerificationStatus.FAIL, results); } + /** + * Creates a successful unicity certificate verification result. + * + * @param results detailed rule verification results + * @return successful verification result + */ public static UnicityCertificateVerificationResult ok(List> results) { return new UnicityCertificateVerificationResult(VerificationStatus.OK, results); } diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/InputRecordCurrentHashVerificationRule.java b/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/InputRecordCurrentHashVerificationRule.java index 7335c07..1cec9b3 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/InputRecordCurrentHashVerificationRule.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/InputRecordCurrentHashVerificationRule.java @@ -10,6 +10,16 @@ */ public class InputRecordCurrentHashVerificationRule { + private InputRecordCurrentHashVerificationRule() { + } + + /** + * Verify that inclusion proof merkle root matches current hash in input record. + * + * @param inclusionProof inclusion proof to verify + * + * @return verification result + */ public static VerificationResult verify(InclusionProof inclusionProof) { if (inclusionProof.getMerkleTreePath().getRootHash().equals( DataHash.fromImprint(inclusionProof.getUnicityCertificate().getInputRecord().getHash()))) { diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealHashMatchesWithRootHashRule.java b/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealHashMatchesWithRootHashRule.java index 417bc7c..d2639c4 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealHashMatchesWithRootHashRule.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealHashMatchesWithRootHashRule.java @@ -18,6 +18,14 @@ */ public class UnicitySealHashMatchesWithRootHashRule { + private UnicitySealHashMatchesWithRootHashRule() {} + + /** + * Verifies that the unicity seal hash matches the recomputed root hash of the unicity tree. + * + * @param unicityCertificate unicity certificate containing tree and seal data + * @return verification result with {@link VerificationStatus#OK} on match, otherwise fail + */ public static VerificationResult verify( UnicityCertificate unicityCertificate) { DataHash shardTreeCertificateRootHash = UnicityCertificate diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.java b/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.java index ad58a1c..ba7947f 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.java @@ -19,6 +19,15 @@ */ public class UnicitySealQuorumSignaturesVerificationRule { + private UnicitySealQuorumSignaturesVerificationRule() {} + + /** + * Verifies unicity seal signatures and checks that the quorum threshold is reached. + * + * @param trustBase trust base containing root nodes and quorum threshold + * @param unicitySeal unicity seal with node signatures + * @return verification result with per-signature details + */ public static VerificationResult verify(RootTrustBase trustBase, UnicitySeal unicitySeal) { List> results = new ArrayList<>(); diff --git a/src/main/java/org/unicitylabs/sdk/crypto/MintSigningService.java b/src/main/java/org/unicitylabs/sdk/crypto/MintSigningService.java index af0317c..f498f2b 100644 --- a/src/main/java/org/unicitylabs/sdk/crypto/MintSigningService.java +++ b/src/main/java/org/unicitylabs/sdk/crypto/MintSigningService.java @@ -8,11 +8,23 @@ import org.unicitylabs.sdk.transaction.TokenId; import org.unicitylabs.sdk.util.HexConverter; +/** + * Factory for the deterministic signing key used by mint transactions. + */ public class MintSigningService { private static final byte[] MINTER_SECRET = HexConverter.decode( "495f414d5f554e4956455253414c5f4d494e5445525f464f525f"); + private MintSigningService() {} + + /** + * Create a signing service for the provided token id. + * + * @param tokenId token id + * + * @return signing service + */ public static SigningService create(TokenId tokenId) { Objects.requireNonNull(tokenId, "Token ID cannot be null"); diff --git a/src/main/java/org/unicitylabs/sdk/crypto/hash/DataHash.java b/src/main/java/org/unicitylabs/sdk/crypto/hash/DataHash.java index 79d0a2d..fbe1492 100644 --- a/src/main/java/org/unicitylabs/sdk/crypto/hash/DataHash.java +++ b/src/main/java/org/unicitylabs/sdk/crypto/hash/DataHash.java @@ -91,7 +91,7 @@ public byte[] getImprint() { } /** - * Create data hash from CBOR bytes. + * Deserialize data hash from CBOR bytes. * * @param bytes CBOR bytes * @return data hash @@ -101,7 +101,7 @@ public static DataHash fromCbor(byte[] bytes) { } /** - * Convert data hash to CBOR bytes. + * Serialize data hash to CBOR bytes. * * @return CBOR bytes */ diff --git a/src/main/java/org/unicitylabs/sdk/crypto/hash/HashAlgorithm.java b/src/main/java/org/unicitylabs/sdk/crypto/hash/HashAlgorithm.java index 304924d..30d57de 100644 --- a/src/main/java/org/unicitylabs/sdk/crypto/hash/HashAlgorithm.java +++ b/src/main/java/org/unicitylabs/sdk/crypto/hash/HashAlgorithm.java @@ -53,6 +53,11 @@ public String getAlgorithm() { return this.algorithm; } + /** + * Hash algorithm length in bytes. + * + * @return length + */ public int getLength() { return this.length; } diff --git a/src/main/java/org/unicitylabs/sdk/crypto/secp256k1/Signature.java b/src/main/java/org/unicitylabs/sdk/crypto/secp256k1/Signature.java index 651408d..eaf61d5 100644 --- a/src/main/java/org/unicitylabs/sdk/crypto/secp256k1/Signature.java +++ b/src/main/java/org/unicitylabs/sdk/crypto/secp256k1/Signature.java @@ -75,7 +75,7 @@ public static Signature decode(byte[] input) { } /** - * Create Signature from CBOR bytes. + * Deserialize Signature from CBOR bytes. * * @param bytes CBOR bytes * @return signature diff --git a/src/main/java/org/unicitylabs/sdk/crypto/secp256k1/SigningService.java b/src/main/java/org/unicitylabs/sdk/crypto/secp256k1/SigningService.java index 0c0e236..fce64ab 100644 --- a/src/main/java/org/unicitylabs/sdk/crypto/secp256k1/SigningService.java +++ b/src/main/java/org/unicitylabs/sdk/crypto/secp256k1/SigningService.java @@ -87,6 +87,11 @@ public static byte[] generatePrivateKey() { return privateKey; } + /** + * Generate a signing service instance with a randomly generated private key. + * + * @return signing service instance + */ public static SigningService generate() { return new SigningService(SigningService.generatePrivateKey()); } diff --git a/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePath.java b/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePath.java index c8ad150..a3df4cb 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePath.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePath.java @@ -115,7 +115,7 @@ public MerkleTreePathVerificationResult verify(BigInteger stateId) { } /** - * Create sparse merkle tree path from CBOR bytes. + * Deserialize sparse merkle tree path from CBOR bytes. * * @param bytes CBOR bytes * @return path @@ -132,7 +132,7 @@ public static SparseMerkleTreePath fromCbor(byte[] bytes) { } /** - * Convert sparse merkle tree path to CBOR bytes. + * Serialize sparse merkle tree path to CBOR bytes. * * @return CBOR bytes */ diff --git a/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePathStep.java b/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePathStep.java index 9531d30..fcccc81 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePathStep.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePathStep.java @@ -56,7 +56,7 @@ public Optional getData() { } /** - * Create sparse Merkle tree path step from CBOR bytes. + * Deserialize sparse Merkle tree path step from CBOR bytes. * * @param bytes CBOR bytes * @return sparse Merkle tree path step @@ -71,7 +71,7 @@ public static SparseMerkleTreePathStep fromCbor(byte[] bytes) { } /** - * Convert sparse Merkle tree path step to CBOR bytes. + * Serialize sparse Merkle tree path step to CBOR bytes. * * @return CBOR bytes */ diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePath.java b/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePath.java index e4cfa2c..79a1347 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePath.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePath.java @@ -142,7 +142,7 @@ public static SparseMerkleSumTreePath fromCbor(byte[] bytes) { } /** - * Convert path to CBOR bytes. + * Serialize path to CBOR bytes. * * @return CBOR bytes */ @@ -226,7 +226,7 @@ public static Root fromCbor(byte[] bytes) { } /** - * Convert root to CBOR bytes. + * Serialize root to CBOR bytes. * * @return CBOR bytes */ diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePathStep.java b/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePathStep.java index 52e095a..4e94ffb 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePathStep.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePathStep.java @@ -60,7 +60,7 @@ public BigInteger getValue() { } /** - * Create a step from CBOR bytes. + * Deserialize a step from CBOR bytes. * * @param bytes CBOR bytes * @return step @@ -76,7 +76,7 @@ public static SparseMerkleSumTreePathStep fromCbor(byte[] bytes) { } /** - * Convert step to CBOR bytes. + * Serialize step to CBOR bytes. * * @return CBOR bytes */ diff --git a/src/main/java/org/unicitylabs/sdk/payment/PaymentData.java b/src/main/java/org/unicitylabs/sdk/payment/PaymentData.java index b8424b9..5012d8b 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/PaymentData.java +++ b/src/main/java/org/unicitylabs/sdk/payment/PaymentData.java @@ -3,8 +3,21 @@ import java.util.Set; import org.unicitylabs.sdk.payment.asset.Asset; +/** + * Represents payment payload data. + */ public interface PaymentData { + /** + * Returns the assets included in this payment payload. + * + * @return set of assets + */ Set getAssets(); + /** + * Encodes this payment payload into bytes. + * + * @return encoded payment data + */ byte[] encode(); } diff --git a/src/main/java/org/unicitylabs/sdk/payment/PaymentDataDeserializer.java b/src/main/java/org/unicitylabs/sdk/payment/PaymentDataDeserializer.java index 642c0bf..25f2ec0 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/PaymentDataDeserializer.java +++ b/src/main/java/org/unicitylabs/sdk/payment/PaymentDataDeserializer.java @@ -1,6 +1,15 @@ package org.unicitylabs.sdk.payment; +/** + * Functional contract for decoding encoded payment data. + */ @FunctionalInterface public interface PaymentDataDeserializer { + /** + * Decodes payment data bytes into a {@link PaymentData} instance. + * + * @param data encoded payment data bytes + * @return decoded payment data + */ PaymentData decode(byte[] data); } diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentData.java b/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentData.java index f8a443e..a3b2804 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentData.java +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentData.java @@ -1,5 +1,13 @@ 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 index 2d810d4..d1bc10d 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentDataDeserializer.java +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentDataDeserializer.java @@ -1,6 +1,15 @@ 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 index 4970e31..279a32b 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitReason.java +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitReason.java @@ -7,6 +7,9 @@ import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.transaction.Token; +/** + * The reason for token splitting represented by an input token and inclusion proofs. + */ public class SplitReason { private final Token token; @@ -20,14 +23,32 @@ private SplitReason( 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"); @@ -39,6 +60,13 @@ public static SplitReason create(Token token, List proofs) { 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); @@ -48,6 +76,11 @@ public static SplitReason fromCbor(byte[] bytes) { ); } + /** + * Serialize split reason to CBOR bytes. + * + * @return CBOR bytes + */ public byte[] toCbor() { return CborSerializer.encodeArray( this.token.toCbor(), diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitReasonProof.java b/src/main/java/org/unicitylabs/sdk/payment/SplitReasonProof.java index ee6fb01..72ce197 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitReasonProof.java +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitReasonProof.java @@ -7,6 +7,9 @@ import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +/** + * Proof material for one split reason entry. + */ public class SplitReasonProof { private final AssetId assetId; private final SparseMerkleTreePath aggregationPath; @@ -22,22 +25,57 @@ private SplitReasonProof( this.assetTreePath = assetTreePath; } + /** + * Get asset id referenced by this proof. + * + * @return asset id + */ public AssetId getAssetId() { return this.assetId; } + /** + * Get sparse merkle path in the aggregation tree. + * + * @return aggregation path + */ public SparseMerkleTreePath getAggregationPath() { return this.aggregationPath; } + /** + * Get sparse merkle sum tree path for the asset tree. + * + * @return asset tree path + */ public SparseMerkleSumTreePath getAssetTreePath() { return this.assetTreePath; } - public static SplitReasonProof create(AssetId assetId, SparseMerkleTreePath aggregationPath, SparseMerkleSumTreePath assetTreePath) { + /** + * Create split reason proof. + * + * @param assetId asset id + * @param aggregationPath aggregation path + * @param assetTreePath asset tree path + * + * @return split reason proof + */ + public static SplitReasonProof create( + AssetId assetId, + SparseMerkleTreePath aggregationPath, + SparseMerkleSumTreePath assetTreePath + ) { return new SplitReasonProof(assetId, aggregationPath, assetTreePath); } + /** + * Deserialize split reason proof from CBOR bytes. + * + * @param bytes CBOR bytes + * + * @return split reason proof + */ public static SplitReasonProof fromCbor(byte[] bytes) { List data = CborDeserializer.decodeArray(bytes); @@ -48,6 +86,11 @@ public static SplitReasonProof fromCbor(byte[] bytes) { ); } + /** + * Serialize split reason proof to CBOR bytes. + * + * @return CBOR bytes + */ public byte[] toCbor() { return CborSerializer.encodeArray( this.assetId.toCbor(), diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitResult.java b/src/main/java/org/unicitylabs/sdk/payment/SplitResult.java index fa422ed..77c73e7 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitResult.java +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitResult.java @@ -7,21 +7,38 @@ import org.unicitylabs.sdk.transaction.TokenId; import org.unicitylabs.sdk.transaction.TransferTransaction; +/** + * Result of token split generation containing burn transaction and per-token proofs. + */ public class SplitResult { + private final TransferTransaction burnTransaction; private final Map> proofs; SplitResult(TransferTransaction burnTransaction, Map> proofs) { this.burnTransaction = burnTransaction; this.proofs = Map.copyOf( - proofs.entrySet().stream().collect(Collectors.toMap(Entry::getKey, value -> List.copyOf(value.getValue()))) + proofs.entrySet().stream() + .collect( + Collectors.toMap(Entry::getKey, value -> List.copyOf(value.getValue())) + ) ); } + /** + * Get the burn transaction that anchors split proofs. + * + * @return burn transaction + */ public TransferTransaction getBurnTransaction() { return this.burnTransaction; } + /** + * Get proofs grouped by resulting token id. + * + * @return split proofs map + */ 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 60cdd00..a97d12d 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java +++ b/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java @@ -11,6 +11,7 @@ import java.util.Set; import java.util.stream.Collectors; 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.mtree.BranchExistsException; import org.unicitylabs.sdk.mtree.LeafOutOfBoundsException; @@ -33,10 +34,28 @@ import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; +/** + * Utilities for creating and verifying token split proofs. + */ public class TokenSplit { - private static final SecureRandom RANDOM = new SecureRandom(); + private static final SecureRandom RANDOM = new SecureRandom(); + 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 + * + * @return split result containing burn transaction and proof map + * + * @throws LeafOutOfBoundsException if a leaf path is invalid for merkle tree insertion + * @throws BranchExistsException if duplicate branches are inserted into a merkle tree + */ public static SplitResult split( Token token, Predicate ownerPredicate, @@ -50,6 +69,7 @@ public static SplitResult split( HashMap trees = new HashMap(); for (Entry> entry : splitTokens.entrySet()) { + Objects.requireNonNull(entry, "Split token entry cannot be null"); Objects.requireNonNull(entry.getKey(), "Split token id cannot be null"); for (Asset asset : entry.getValue()) { Objects.requireNonNull(asset, "Split token asset cannot be null"); @@ -64,7 +84,16 @@ public static SplitResult split( } PaymentData paymentData = paymentDataDeserializer.decode(token.getGenesis().getData()); - Map assets = paymentData.getAssets().stream().collect(Collectors.toMap(Asset::getId, asset -> asset)); + Map assets = paymentData.getAssets().stream() + .collect(Collectors.toMap( + Asset::getId, + asset -> asset, + (a, b) -> { + throw new IllegalArgumentException( + "Payment data contains multiple assets with the same id: " + a.getId()); + } + ) + ); if (trees.size() != assets.size()) { throw new IllegalArgumentException("Token and split tokens asset counts differ."); @@ -125,6 +154,16 @@ 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, @@ -154,7 +193,8 @@ public static VerificationResult verify( ); } - VerificationResult verificationResult = data.getReason().getToken().verify(trustBase, predicateVerifier); + VerificationResult verificationResult = data.getReason().getToken() + .verify(trustBase, predicateVerifier); if (verificationResult.getStatus() != VerificationStatus.OK) { return new VerificationResult<>( "TokenSplitReasonVerificationRule", @@ -172,10 +212,31 @@ public static VerificationResult verify( ); } - Map assets = data.getAssets().stream().collect(Collectors.toMap(Asset::getId, asset -> asset)); + 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).getAssetTreePath().getRootHash(); for (SplitReasonProof proof : data.getReason().getProofs()) { - MerkleTreePathVerificationResult aggregationPathResult = proof.getAggregationPath().verify(proof.getAssetId().toBitString().toBigInteger()); + MerkleTreePathVerificationResult aggregationPathResult = proof.getAggregationPath() + .verify(proof.getAssetId().toBitString().toBigInteger()); if (!aggregationPathResult.isSuccessful()) { return new VerificationResult<>( "TokenSplitReasonVerificationRule", @@ -184,7 +245,8 @@ public static VerificationResult verify( ); } - MerkleTreePathVerificationResult assetTreePathResult = proof.getAssetTreePath().verify(token.getId().toBitString().toBigInteger()); + MerkleTreePathVerificationResult assetTreePathResult = proof.getAssetTreePath() + .verify(token.getId().toBitString().toBigInteger()); if (!assetTreePathResult.isSuccessful()) { return new VerificationResult<>( "TokenSplitReasonVerificationRule", @@ -193,8 +255,16 @@ public static VerificationResult verify( ); } + if (!proof.getAssetTreePath().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(), + root.getImprint(), proof.getAggregationPath().getSteps().get(0).getData().orElse(null) )) { return new VerificationResult<>( @@ -216,8 +286,6 @@ public static VerificationResult verify( BigInteger amount = asset.getValue(); - - if (!proof.getAssetTreePath().getSteps().get(0).getValue().equals(amount)) { return new VerificationResult<>( "TokenSplitReasonVerificationRule", @@ -226,7 +294,8 @@ public static VerificationResult verify( ); } - if (!burnTokenLastTransaction.getRecipient().equals(Address.fromPredicate(BurnPredicate.create(proof.getAggregationPath().getRootHash().getImprint())))) { + if (!burnTokenLastTransaction.getRecipient() + .equals(Address.fromPredicate(BurnPredicate.create(proof.getAggregationPath().getRootHash().getImprint())))) { return new VerificationResult<>( "TokenSplitReasonVerificationRule", VerificationStatus.FAIL, diff --git a/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java b/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java index a8f00ac..b20bce6 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java +++ b/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java @@ -7,24 +7,53 @@ import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.util.BigIntegerConverter; -public class Asset { +/** + * Represents an asset with an ID and a value. + */ +public final class Asset { private final BigInteger value; private final AssetId id; + /** + * Create a new asset with the given ID and value. + * + * @param id asset ID + * @param value asset value + */ public Asset(AssetId id, BigInteger value) { - this.id = id; - this.value = value; + this.id = Objects.requireNonNull(id, "Asset ID cannot be null"); + this.value = Objects.requireNonNull(value, "Asset value cannot be null"); + + if (this.value.compareTo(BigInteger.ZERO) < 0) { + throw new IllegalArgumentException("Asset value cannot be negative"); + } } + /** + * Get asset ID. + * + * @return asset ID + */ public AssetId getId() { return this.id; } + /** + * Get asset value. + * + * @return asset value + */ public BigInteger getValue() { return this.value; } + /** + * Deserialize asset from CBOR bytes. + * + * @param bytes CBOR bytes + * @return asset + */ public static Asset fromCbor(byte[] bytes) { List data = CborDeserializer.decodeArray(bytes); @@ -34,6 +63,11 @@ public static Asset fromCbor(byte[] bytes) { ); } + /** + * Serialize asset to CBOR bytes. + * + * @return CBOR bytes + */ public byte[] toCbor() { return CborSerializer.encodeArray( this.id.toCbor(), @@ -47,14 +81,15 @@ public boolean equals(Object o) { return false; } Asset asset = (Asset) o; - return Objects.equals(this.value, asset.value) && Objects.equals(this.id, asset.id); + return Objects.equals(this.id, asset.id); } @Override public int hashCode() { - return Objects.hash(this.value, this.id); + return Objects.hash(this.id); } + @Override public String toString() { return String.format("Asset{value=%s, id=%s}", this.value, this.id); } 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 94e6498..4625845 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/asset/AssetId.java +++ b/src/main/java/org/unicitylabs/sdk/payment/asset/AssetId.java @@ -7,27 +7,57 @@ import org.unicitylabs.sdk.util.BitString; import org.unicitylabs.sdk.util.HexConverter; +/** + * Unique identifier of an asset. + */ public class AssetId { private final byte[] bytes; + /** + * Create asset id from bytes. + * + * @param bytes asset id bytes + */ public AssetId(byte[] bytes) { Objects.requireNonNull(bytes, "Asset id cannot be null"); this.bytes = Arrays.copyOf(bytes, bytes.length); } + /** + * Get asset id bytes. + * + * @return asset id bytes + */ public byte[] getBytes() { return Arrays.copyOf(this.bytes, this.bytes.length); } + /** + * Deserialize asset id from CBOR bytes. + * + * @param bytes CBOR bytes + * + * @return asset id + */ public static AssetId fromCbor(byte[] bytes) { return new AssetId(CborDeserializer.decodeByteString(bytes)); } + /** + * Convert asset id to bit string form used by sparse merkle trees. + * + * @return bit string + */ public BitString toBitString() { return new BitString(this.bytes); } + /** + * Serialize asset id to CBOR bytes. + * + * @return CBOR bytes + */ public byte[] toCbor() { return CborSerializer.encodeByteString(this.bytes); } @@ -46,6 +76,7 @@ public int hashCode() { return Arrays.hashCode(bytes); } + @Override public String toString() { return String.format("AssetId{bytes=%s}", HexConverter.encode(this.bytes)); } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/EncodedPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/EncodedPredicate.java index 1cb8b02..1807ca3 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/EncodedPredicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/EncodedPredicate.java @@ -7,6 +7,9 @@ import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.util.HexConverter; +/** + * Generic predicate representation that stores engine, code, and parameters as encoded bytes. + */ public class EncodedPredicate implements Predicate { private final PredicateEngine engine; @@ -24,6 +27,12 @@ public PredicateEngine getEngine() { return this.engine; } + /** + * Deserializes an encoded predicate from CBOR. + * + * @param bytes CBOR-encoded predicate bytes + * @return decoded encoded predicate + */ public static EncodedPredicate fromCbor(byte[] bytes) { List data = CborDeserializer.decodeArray(bytes); PredicateEngine engine = PredicateEngine.fromId( @@ -36,9 +45,18 @@ public static EncodedPredicate fromCbor(byte[] bytes) { ); } + /** + * Creates an encoded predicate snapshot from any predicate implementation. + * + * @param predicate source predicate + * @return encoded predicate containing engine, code, and parameters + */ public static EncodedPredicate fromPredicate(Predicate predicate) { - return new EncodedPredicate(predicate.getEngine(), predicate.encodeCode(), - predicate.encodeParameters()); + return new EncodedPredicate( + predicate.getEngine(), + predicate.encodeCode(), + predicate.encodeParameters() + ); } @Override @@ -51,6 +69,11 @@ public byte[] encodeParameters() { return Arrays.copyOf(this.parameters, this.parameters.length); } + /** + * Serializes this predicate into CBOR. + * + * @return CBOR-encoded predicate bytes + */ public byte[] toCbor() { return CborSerializer.encodeArray( CborSerializer.encodeUnsignedInteger(this.engine.getId()), @@ -76,10 +99,11 @@ public int hashCode() { @Override public String toString() { - return "EncodedPredicate{" + - "engine=" + this.engine + - ", code=" + HexConverter.encode(this.code) + - ", parameters=" + HexConverter.encode(this.parameters) + - '}'; + return String.format( + "EncodedPredicate{engine=%s, code=%s, parameters=%s}", + this.engine, + HexConverter.encode(this.code), + HexConverter.encode(this.parameters) + ); } } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java b/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java index 2e7612c..91facb2 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java @@ -2,14 +2,38 @@ import java.util.Arrays; +/** + * Base contract for all predicate implementations. + */ public interface Predicate { + /** + * Returns the predicate engine used by this predicate. + * + * @return the predicate engine + */ PredicateEngine getEngine(); + /** + * Encodes the predicate type/code portion. + * + * @return encoded predicate code bytes + */ byte[] encodeCode(); + /** + * Encodes the predicate parameter payload. + * + * @return encoded predicate parameter bytes + */ byte[] encodeParameters(); + /** + * Compares this predicate with another predicate using encoded representation. + * + * @param other the predicate to compare against + * @return {@code true} when engine, code, and parameters are equal; otherwise {@code false} + */ default boolean isEqualTo(Predicate other) { if (other == null) { return false; diff --git a/src/main/java/org/unicitylabs/sdk/predicate/PredicateEngine.java b/src/main/java/org/unicitylabs/sdk/predicate/PredicateEngine.java index c0dffca..0954c24 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/PredicateEngine.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/PredicateEngine.java @@ -1,6 +1,10 @@ package org.unicitylabs.sdk.predicate; +/** + * Enumerates supported predicate engines and their numeric identifiers. + */ public enum PredicateEngine { + /** Engine for built-in predicate implementations. */ BUILT_IN(1); private final int id; @@ -9,10 +13,22 @@ public enum PredicateEngine { this.id = id; } + /** + * Returns the numeric identifier of this predicate engine. + * + * @return predicate engine id + */ public int getId() { return this.id; } + /** + * Resolves a predicate engine from its numeric identifier. + * + * @param id predicate engine id + * @return matching predicate engine + * @throws IllegalArgumentException if the id is not mapped to a predicate engine + */ public static PredicateEngine fromId(int id) { for (PredicateEngine engine : PredicateEngine.values()) { if (engine.id == id) { diff --git a/src/main/java/org/unicitylabs/sdk/predicate/UnlockScript.java b/src/main/java/org/unicitylabs/sdk/predicate/UnlockScript.java index fbb6bc4..bbd95ae 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/UnlockScript.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/UnlockScript.java @@ -1,5 +1,13 @@ package org.unicitylabs.sdk.predicate; +/** + * Contract for predicate unlock script payloads. + */ public interface UnlockScript { + /** + * Encodes this unlock script into bytes. + * + * @return encoded unlock script + */ byte[] encode(); } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicate.java index 3e593cb..e69ed5f 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicate.java @@ -4,14 +4,32 @@ import org.unicitylabs.sdk.predicate.PredicateEngine; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +/** + * Base contract for predicates represented by a built-in predicate type. + */ public interface BuiltInPredicate extends Predicate { + /** + * Returns the built-in type identifier for this predicate. + * + * @return the built-in predicate type + */ BuiltInPredicateType getType(); + /** + * Returns the predicate engine used by all built-in predicates. + * + * @return {@link PredicateEngine#BUILT_IN} + */ default PredicateEngine getEngine() { return PredicateEngine.BUILT_IN; } + /** + * Encodes this predicate type id as an unsigned CBOR integer. + * + * @return the encoded predicate type id + */ default byte[] encodeCode() { return CborSerializer.encodeUnsignedInteger(this.getType().getId()); } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java index c2fb72c..75be5fc 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java @@ -1,8 +1,14 @@ package org.unicitylabs.sdk.predicate.builtin; +/** + * Enumerates supported built-in predicate types and their numeric identifiers. + */ public enum BuiltInPredicateType { + /** Predicate that locks state to a public key. */ PAY_TO_PUBLIC_KEY(1), + /** Predicate that references a Unicity identifier. */ UNICITY_ID(2), + /** Predicate that marks state as unspendable (burned). */ BURN(3); private final int id; @@ -11,10 +17,22 @@ public enum BuiltInPredicateType { this.id = id; } + /** + * Returns the numeric identifier of this predicate type. + * + * @return predicate type id + */ public int getId() { return this.id; } + /** + * Resolves a predicate type from its numeric identifier. + * + * @param id the predicate type id + * @return the matching {@link BuiltInPredicateType} + * @throws IllegalArgumentException if the id is not mapped to a built-in type + */ public static BuiltInPredicateType fromId(int id) { for (BuiltInPredicateType type : BuiltInPredicateType.values()) { if (type.id == id) { diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java index f38ad27..b2c88d3 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java @@ -6,6 +6,9 @@ import org.unicitylabs.sdk.predicate.PredicateEngine; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +/** + * Built-in predicate representing a burn operation. + */ public class BurnPredicate implements BuiltInPredicate { private final byte[] reason; @@ -13,20 +16,44 @@ private BurnPredicate(byte[] reason) { this.reason = Arrays.copyOf(reason, reason.length); } + /** + * Returns the built-in predicate type. + * + * @return {@link BuiltInPredicateType#BURN} + */ public BuiltInPredicateType getType() { return BuiltInPredicateType.BURN; } + /** + * Returns the burn reason bytes. + * + * @return a defensive copy of the burn reason + */ public byte[] getReason() { return Arrays.copyOf(this.reason, this.reason.length); } + /** + * Creates a burn predicate from the provided reason bytes. + * + * @param reason burn reason bytes + * @return created burn predicate + * @throws NullPointerException if {@code reason} is {@code null} + */ public static BurnPredicate create(byte[] reason) { Objects.requireNonNull(reason, "Reason cannot be null"); return new BurnPredicate(reason); } + /** + * Converts a generic predicate into a {@link BurnPredicate}. + * + * @param predicate predicate to convert + * @return converted burn predicate + * @throws IllegalArgumentException if the predicate engine is not built-in or predicate type is not burn + */ public static BurnPredicate fromPredicate(Predicate predicate) { PredicateEngine engine = predicate.getEngine(); if (engine != PredicateEngine.BUILT_IN) { @@ -42,6 +69,11 @@ public static BurnPredicate fromPredicate(Predicate predicate) { return new BurnPredicate(predicate.encodeParameters()); } + /** + * Encodes burn predicate parameters. + * + * @return burn reason bytes + */ @Override public byte[] encodeParameters() { return this.getReason(); diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/DefaultBuiltInPredicateVerifier.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/DefaultBuiltInPredicateVerifier.java index 479c471..07e0bce 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/DefaultBuiltInPredicateVerifier.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/DefaultBuiltInPredicateVerifier.java @@ -15,11 +15,20 @@ import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; +/** + * Default {@link PredicateVerifier} implementation for built-in predicates. + */ public class DefaultBuiltInPredicateVerifier implements PredicateVerifier { private final Map verifiers; + /** + * Creates a verifier registry from built-in predicate verifiers. + * + * @param verifiers verifiers to register, one per predicate type + * @throws IllegalArgumentException if multiple verifiers are provided for the same type + */ public DefaultBuiltInPredicateVerifier( List verifiers) { Map result = new HashMap<>(); @@ -39,6 +48,13 @@ public PredicateEngine getPredicateEngine() { return PredicateEngine.BUILT_IN; } + /** + * Creates the default built-in predicate verifier set. + * + * @param service predicate verifier service + * @param trustBase root trust base + * @return default built-in predicate verifier + */ public static DefaultBuiltInPredicateVerifier create(PredicateVerifierService service, RootTrustBase trustBase) { return new DefaultBuiltInPredicateVerifier( List.of( diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java index 4a55407..df43f36 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java @@ -6,8 +6,10 @@ import org.unicitylabs.sdk.predicate.Predicate; import org.unicitylabs.sdk.predicate.PredicateEngine; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +/** + * Built-in predicate that locks an output to a secp256k1 public key. + */ public class PayToPublicKeyPredicate implements BuiltInPredicate { private final byte[] publicKey; @@ -16,18 +18,42 @@ private PayToPublicKeyPredicate(byte[] publicKey) { this.publicKey = publicKey; } + /** + * Get public key bytes. + * + * @return public key bytes + */ public byte[] getPublicKey() { return Arrays.copyOf(this.publicKey, this.publicKey.length); } + /** + * Get built-in predicate type. + * + * @return predicate type + */ public BuiltInPredicateType getType() { return BuiltInPredicateType.PAY_TO_PUBLIC_KEY; } + /** + * Create predicate from public key bytes. + * + * @param publicKey public key bytes + * + * @return pay-to-public-key predicate + */ public static PayToPublicKeyPredicate create(byte[] publicKey) { return new PayToPublicKeyPredicate(Arrays.copyOf(publicKey, publicKey.length)); } + /** + * Parse pay-to-public-key predicate from generic predicate. + * + * @param predicate generic predicate + * + * @return pay-to-public-key predicate + */ public static PayToPublicKeyPredicate fromPredicate(Predicate predicate) { PredicateEngine engine = predicate.getEngine(); if (engine != PredicateEngine.BUILT_IN) { @@ -43,11 +69,23 @@ public static PayToPublicKeyPredicate fromPredicate(Predicate predicate) { return new PayToPublicKeyPredicate(predicate.encodeParameters()); } + /** + * Create predicate from signing service public key. + * + * @param signingService signing service + * + * @return pay-to-public-key predicate + */ public static PayToPublicKeyPredicate fromSigningService(SigningService signingService) { Objects.requireNonNull(signingService, "Signing service cannot be null"); return new PayToPublicKeyPredicate(signingService.getPublicKey()); } + /** + * Encode predicate parameters. + * + * @return encoded parameter bytes + */ @Override public byte[] encodeParameters() { return this.getPublicKey(); diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicateUnlockScript.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicateUnlockScript.java index 27e4db1..e50843b 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicateUnlockScript.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicateUnlockScript.java @@ -9,6 +9,9 @@ import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.transaction.Transaction; +/** + * Unlock script for {@link PayToPublicKeyPredicate} containing a transaction signature. + */ public class PayToPublicKeyPredicateUnlockScript implements UnlockScript { private final Signature signature; @@ -17,10 +20,22 @@ private PayToPublicKeyPredicateUnlockScript(Signature signature) { this.signature = signature; } + /** + * Returns the unlock signature. + * + * @return signature used to unlock the predicate + */ public Signature getSignature() { return this.signature; } + /** + * Creates an unlock script by signing the source-state and transaction-hash payload. + * + * @param transaction transaction being authorized + * @param signingService signing service used to produce the signature + * @return created unlock script + */ public static PayToPublicKeyPredicateUnlockScript create( Transaction transaction, SigningService signingService @@ -37,13 +52,18 @@ public static PayToPublicKeyPredicateUnlockScript create( return new PayToPublicKeyPredicateUnlockScript(signingService.sign(hash)); } + /** + * Decodes an unlock script from encoded signature bytes. + * + * @param bytes encoded signature bytes + * @return decoded unlock script + */ public static PayToPublicKeyPredicateUnlockScript decode(byte[] bytes) { return new PayToPublicKeyPredicateUnlockScript(Signature.decode(bytes)); } + @Override public byte[] encode() { return this.signature.encode(); } - - } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/BuiltInPredicateVerifier.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/BuiltInPredicateVerifier.java index 4da29cd..82e595d 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/BuiltInPredicateVerifier.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/BuiltInPredicateVerifier.java @@ -6,10 +6,27 @@ import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; +/** + * Verifier contract for a specific built-in predicate type. + */ public interface BuiltInPredicateVerifier { + /** + * Returns the built-in predicate type handled by this verifier. + * + * @return supported built-in predicate type + */ BuiltInPredicateType getType(); + /** + * Verifies that the provided unlock script satisfies the predicate in the current context. + * + * @param predicate the predicate to verify + * @param sourceStateHash hash of the source state + * @param transactionHash hash of the transaction being validated + * @param unlockScript unlock script bytes provided for the predicate + * @return verification result with status and optional diagnostics + */ VerificationResult verify(Predicate predicate, DataHash sourceStateHash, DataHash transactionHash, byte[] unlockScript); } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/PayToPublicKeyPredicateVerifier.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/PayToPublicKeyPredicateVerifier.java index d317399..bca8e6e 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/PayToPublicKeyPredicateVerifier.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/PayToPublicKeyPredicateVerifier.java @@ -12,8 +12,16 @@ import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; +/** + * Verifies {@link PayToPublicKeyPredicate} unlock scripts using secp256k1 signatures. + */ public class PayToPublicKeyPredicateVerifier implements BuiltInPredicateVerifier { + /** + * Creates a verifier instance for pay-to-public-key predicates. + */ + public PayToPublicKeyPredicateVerifier() {} + @Override public BuiltInPredicateType getType() { return BuiltInPredicateType.PAY_TO_PUBLIC_KEY; diff --git a/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifier.java b/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifier.java index a9ddded..9998bee 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifier.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifier.java @@ -6,10 +6,27 @@ import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; +/** + * Verifier contract for predicates handled by a specific predicate engine. + */ public interface PredicateVerifier { + /** + * Returns the predicate engine supported by this verifier. + * + * @return supported predicate engine + */ PredicateEngine getPredicateEngine(); + /** + * Verifies a predicate in the context of a source state, transaction, and unlock script. + * + * @param predicate predicate to verify + * @param sourceStateHash hash of the source state + * @param transactionHash hash of the transaction being validated + * @param unlockScript unlock script bytes + * @return verification result with status and diagnostics + */ VerificationResult verify(Predicate predicate, DataHash sourceStateHash, DataHash transactionHash, byte[] unlockScript); } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifierService.java b/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifierService.java index 7d173f1..ac03726 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifierService.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifierService.java @@ -10,6 +10,9 @@ import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; +/** + * Service registry that routes predicate verification to engine-specific verifiers. + */ public class PredicateVerifierService { private final Map verifiers = new HashMap<>(); @@ -18,6 +21,12 @@ private PredicateVerifierService() { } + /** + * Creates a predicate verifier service with default verifier registrations. + * + * @param trustBase root trust base used by verifiers that require trust context + * @return initialized predicate verifier service + */ public static PredicateVerifierService create(RootTrustBase trustBase) { PredicateVerifierService verifier = new PredicateVerifierService(); verifier.addVerifier(DefaultBuiltInPredicateVerifier.create(verifier, trustBase)); @@ -25,6 +34,13 @@ public static PredicateVerifierService create(RootTrustBase trustBase) { return verifier; } + /** + * Registers a predicate verifier for its predicate engine. + * + * @param verifier verifier to register + * @return this service instance + * @throws RuntimeException if a verifier is already registered for the same predicate engine + */ public PredicateVerifierService addVerifier(PredicateVerifier verifier) { if (this.verifiers.containsKey(verifier.getPredicateEngine())) { throw new RuntimeException("Predicate verifier already registered for predicate engine: " @@ -36,6 +52,16 @@ public PredicateVerifierService addVerifier(PredicateVerifier verifier) { return this; } + /** + * Verifies a predicate by dispatching to a verifier registered for its engine. + * + * @param predicate predicate to verify + * @param sourceStateHash hash of the source state + * @param transactionHash hash of the transaction being verified + * @param unlockScript unlock script bytes + * @return verification result from the engine-specific verifier + * @throws IllegalArgumentException if no verifier is registered for the predicate engine + */ public VerificationResult verify(Predicate predicate, DataHash sourceStateHash, DataHash transactionHash, byte[] unlockScript) { PredicateVerifier verifier = this.verifiers.get(predicate.getEngine()); diff --git a/src/main/java/org/unicitylabs/sdk/transaction/Address.java b/src/main/java/org/unicitylabs/sdk/transaction/Address.java index e66d726..b344895 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/Address.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/Address.java @@ -1,7 +1,6 @@ package org.unicitylabs.sdk.transaction; import java.util.Arrays; -import java.util.Objects; import org.unicitylabs.sdk.crypto.hash.DataHash; import org.unicitylabs.sdk.crypto.hash.DataHasher; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; @@ -11,6 +10,9 @@ import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.util.HexConverter; +/** + * Transaction address. + */ public class Address { private final byte[] bytes; @@ -19,10 +21,22 @@ 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"); @@ -31,16 +45,35 @@ public static Address fromBytes(byte[] bytes) { 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); } @@ -59,6 +92,7 @@ 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 cd39663..a8546e9 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java @@ -13,6 +13,9 @@ import org.unicitylabs.sdk.util.verification.VerificationException; import org.unicitylabs.sdk.util.verification.VerificationResult; +/** + * Mint transaction bundled with an inclusion proof. + */ public class CertifiedMintTransaction implements Transaction { private final MintTransaction transaction; @@ -43,34 +46,69 @@ public DataHash getSourceStateHash() { return this.transaction.getSourceStateHash(); } + /** + * Returns the token identifier. + * + * @return token id + */ public TokenId getTokenId() { return this.transaction.getTokenId(); } + /** + * Returns the token type. + * + * @return token type + */ public TokenType getTokenType() { return this.transaction.getTokenType(); } @Override - public byte[] getX() { - return this.transaction.getX(); + public byte[] getNonce() { + return this.transaction.getNonce(); } + /** + * Returns the inclusion proof certifying this transaction. + * + * @return inclusion proof + */ public InclusionProof getInclusionProof() { return this.inclusionProof; } + /** + * Deserializes a certified mint transaction from CBOR. + * + * @param bytes CBOR-encoded certified mint transaction + * @return decoded certified mint transaction + */ public static CertifiedMintTransaction fromCbor(byte[] bytes) { List data = CborDeserializer.decodeArray(bytes); return new CertifiedMintTransaction(MintTransaction.fromCbor(data.get(0)), InclusionProof.fromCbor(data.get(1))); } + /** + * Creates a certified mint transaction after verifying the inclusion proof. + * + * @param trustBase trust base used to verify inclusion proof signatures + * @param predicateVerifier service used for predicate verification during proof validation + * @param transaction mint transaction to certify + * @param inclusionProof inclusion proof for the transaction + * @return certified mint transaction + * @throws VerificationException if inclusion proof verification fails + */ public static CertifiedMintTransaction fromTransaction(RootTrustBase trustBase, PredicateVerifierService predicateVerifier, MintTransaction transaction, InclusionProof inclusionProof) { - VerificationResult result = InclusionProofVerificationRule.verify(trustBase, predicateVerifier, inclusionProof, - transaction); + VerificationResult result = InclusionProofVerificationRule.verify( + trustBase, + predicateVerifier, + inclusionProof, + transaction + ); if (result.getStatus() != InclusionProofVerificationStatus.OK) { throw new VerificationException("Inclusion proof verification failed", result); } diff --git a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java index 3fb95a2..c386ac3 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java @@ -13,6 +13,9 @@ import org.unicitylabs.sdk.util.verification.VerificationException; import org.unicitylabs.sdk.util.verification.VerificationResult; +/** + * Transfer transaction with a verified inclusion proof. + */ public class CertifiedTransferTransaction implements Transaction { private final TransferTransaction transaction; @@ -26,35 +29,72 @@ private CertifiedTransferTransaction( this.inclusionProof = inclusionProof; } + /** + * Get transaction payload data. + * + * @return payload data bytes + */ @Override public byte[] 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() { 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[] getX() { - return this.transaction.getX(); + public byte[] getNonce() { + return this.transaction.getNonce(); } + /** + * Get inclusion proof for this transaction. + * + * @return inclusion proof + */ public InclusionProof getInclusionProof() { return this.inclusionProof; } + /** + * Deserialize a certified transfer transaction from CBOR bytes. + * + * @param bytes CBOR encoded certified transfer transaction + * + * @return certified transfer transaction + */ public static CertifiedTransferTransaction fromCbor(byte[] bytes) { List data = CborDeserializer.decodeArray(bytes); @@ -62,6 +102,21 @@ public static CertifiedTransferTransaction fromCbor(byte[] bytes) { InclusionProof.fromCbor(data.get(1))); } + /** + * Create a certified transfer transaction from a transfer transaction and inclusion proof. + * + *

The inclusion proof is verified against the transaction before creating the certified + * instance. + * + * @param trustBase trust base used for proof verification + * @param predicateVerifier predicate verifier used by verification rules + * @param transaction transfer transaction + * @param inclusionProof inclusion proof + * + * @return certified transfer transaction + * + * @throws VerificationException if inclusion proof verification fails + */ public static CertifiedTransferTransaction fromTransaction(RootTrustBase trustBase, PredicateVerifierService predicateVerifier, TransferTransaction transaction, InclusionProof inclusionProof) { @@ -78,16 +133,31 @@ public static CertifiedTransferTransaction fromTransaction(RootTrustBase trustBa return new CertifiedTransferTransaction(transaction, inclusionProof); } + /** + * Calculate state hash of the transfer transaction. + * + * @return state hash + */ @Override public DataHash calculateStateHash() { return this.transaction.calculateStateHash(); } + /** + * Calculate hash of the transfer transaction. + * + * @return transaction hash + */ @Override public DataHash calculateTransactionHash() { return this.transaction.calculateTransactionHash(); } + /** + * Serialize this certified transfer transaction to CBOR bytes. + * + * @return CBOR bytes + */ @Override public byte[] toCbor() { return CborSerializer.encodeArray(this.transaction.toCbor(), this.inclusionProof.toCbor()); diff --git a/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java index 0fbcd6b..2c50b34 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java @@ -17,6 +17,13 @@ import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.util.HexConverter; + +/** + * Represents a Mint Transaction. + * + *

This transaction is responsible for minting new tokens with specific attributes and assigns + * it to an initial owner. + */ public class MintTransaction implements Transaction { private final MintTransactionState sourceStateHash; @@ -42,22 +49,48 @@ private MintTransaction( this.data = data; } + + /** + * Retrieves the state hash of the source state. + * + * @return the source state hash represented as a {@code MintTransactionState}. + */ public MintTransactionState getSourceStateHash() { return this.sourceStateHash; } + /** + * Retrieves the lock script. + * + * @return a {@code Predicate} representing the lock script. + */ public Predicate getLockScript() { return this.lockScript; } + /** + * Retrieves the initial owner address. + * + * @return the recipient address as an {@code Address}. + */ public Address getRecipient() { return this.recipient; } + /** + * Retrieves the unique token identifier. + * + * @return the token identifier as a {@code TokenId}. + */ public TokenId getTokenId() { return this.tokenId; } + /** + * Retrieves the type identifier of the token. + * + * @return the token type as a {@code TokenType}. + */ public TokenType getTokenType() { return this.tokenType; } @@ -68,10 +101,20 @@ public byte[] getData() { } @Override - public byte[] getX() { + public byte[] getNonce() { return this.tokenId.getBytes(); } + /** + * Create a mint transaction. + * + * @param recipient recipient address + * @param tokenId token identifier + * @param tokenType token type identifier + * @param data payload bytes + * + * @return mint transaction + */ public static MintTransaction create( Address recipient, TokenId tokenId, @@ -94,6 +137,13 @@ public static MintTransaction create( ); } + /** + * Deserialize mint transaction from CBOR bytes. + * + * @param bytes CBOR bytes + * + * @return mint transaction + */ public static MintTransaction fromCbor(byte[] bytes) { List data = CborDeserializer.decodeArray(bytes); List aux = CborDeserializer.decodeArray(data.get(2)); @@ -106,23 +156,38 @@ public static MintTransaction fromCbor(byte[] bytes) { ); } + /** + * Calculate mint transaction state hash. + * + * @return state hash + */ @Override public DataHash calculateStateHash() { return new DataHasher(HashAlgorithm.SHA256) .update( CborSerializer.encodeArray( CborSerializer.encodeByteString(this.sourceStateHash.getImprint()), - CborSerializer.encodeByteString(this.getX()) + CborSerializer.encodeByteString(this.getNonce()) ) ) .digest(); } + /** + * Calculate hash of serialized mint transaction. + * + * @return transaction hash + */ @Override public DataHash calculateTransactionHash() { return new DataHasher(HashAlgorithm.SHA256).update(this.toCbor()).digest(); } + /** + * Serialize mint transaction to CBOR bytes. + * + * @return CBOR bytes + */ @Override public byte[] toCbor() { return CborSerializer.encodeArray( @@ -133,6 +198,15 @@ public byte[] toCbor() { ); } + /** + * Build certified mint transaction by attaching and verifying inclusion proof. + * + * @param trustBase root trust base + * @param predicateVerifier predicate verifier + * @param inclusionProof inclusion proof + * + * @return certified mint transaction + */ public CertifiedMintTransaction toCertifiedTransaction( RootTrustBase trustBase, PredicateVerifierService predicateVerifier, diff --git a/src/main/java/org/unicitylabs/sdk/transaction/MintTransactionState.java b/src/main/java/org/unicitylabs/sdk/transaction/MintTransactionState.java index 28a016c..b2147b5 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/MintTransactionState.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/MintTransactionState.java @@ -7,6 +7,9 @@ import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.util.HexConverter; +/** + * Represents the state of a mint transaction. + */ public class MintTransactionState extends DataHash { private static final byte[] MINT_SUFFIX = HexConverter.decode( @@ -16,6 +19,12 @@ private MintTransactionState(DataHash hash) { super(hash.getAlgorithm(), hash.getData()); } + /** + * Create a mint transaction state from token id. + * + * @param tokenId token id + * @return mint transaction state + */ public static MintTransactionState create(TokenId tokenId) { Objects.requireNonNull(tokenId, "Token ID cannot be null"); diff --git a/src/main/java/org/unicitylabs/sdk/transaction/Token.java b/src/main/java/org/unicitylabs/sdk/transaction/Token.java index 79c9631..ee1d8ee 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/Token.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/Token.java @@ -13,7 +13,10 @@ import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; -public class Token { +/** + * Immutable token aggregate containing the certified genesis mint transaction and transfer history. + */ +public final class Token { private final CertifiedMintTransaction genesis; private final List transactions; @@ -27,18 +30,38 @@ private Token(CertifiedMintTransaction genesis) { this(genesis, List.of()); } + /** + * Returns the token identifier. + * + * @return token id + */ public TokenId getId() { return this.genesis.getTokenId(); } + /** + * Returns the token type. + * + * @return token type + */ public TokenType getType() { return this.genesis.getTokenType(); } + /** + * Returns the certified genesis mint transaction. + * + * @return genesis transaction + */ public CertifiedMintTransaction getGenesis() { return this.genesis; } + /** + * Returns the most recent transaction in the token history. + * + * @return latest transfer transaction, or genesis transaction when no transfers exist + */ public Transaction getLatestTransaction() { if (this.transactions.isEmpty()) { return this.genesis; @@ -47,10 +70,21 @@ public Transaction getLatestTransaction() { return this.transactions.get(this.transactions.size() - 1); } + /** + * Returns the certified transfer transactions. + * + * @return immutable list of transfer transactions + */ public List getTransactions() { return this.transactions; } + /** + * Deserializes a token from CBOR. + * + * @param bytes CBOR-encoded token bytes + * @return decoded token + */ public static Token fromCbor(byte[] bytes) { List data = CborDeserializer.decodeArray(bytes); List transactions = CborDeserializer.decodeArray(data.get(1)); @@ -62,6 +96,15 @@ public static Token fromCbor(byte[] bytes) { ); } + /** + * Creates a token from a certified genesis transaction and verifies it. + * + * @param trustBase trust base used for certification checks + * @param predicateVerifier predicate 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) { Token token = new Token(genesis); @@ -73,6 +116,15 @@ public static Token mint(RootTrustBase trustBase, PredicateVerifierService predi return token; } + /** + * Returns a new token instance with an additional verified transfer transaction. + * + * @param trustBase trust base used for certification checks + * @param predicateVerifier predicate verifier service + * @param transaction certified transfer transaction to append + * @return new token instance with appended transfer + * @throws VerificationException if transfer verification fails + */ public Token transfer(RootTrustBase trustBase, PredicateVerifierService predicateVerifier, CertifiedTransferTransaction transaction) { VerificationResult result = CertifiedTransferTransactionVerificationRule.verify( @@ -90,6 +142,13 @@ public Token transfer(RootTrustBase trustBase, PredicateVerifierService predicat return new Token(this.genesis, transactions); } + /** + * Verifies genesis and transfer transaction chain integrity. + * + * @param trustBase trust base used for certification checks + * @param predicateVerifier predicate verifier service + * @return verification result with nested per-step verification details + */ public VerificationResult verify(RootTrustBase trustBase, PredicateVerifierService predicateVerifier) { List> results = new ArrayList<>(); @@ -123,6 +182,11 @@ public VerificationResult verify(RootTrustBase trustBase, return new VerificationResult<>("TokenVerification", VerificationStatus.OK, "", results); } + /** + * Serializes this token to CBOR bytes. + * + * @return CBOR-encoded token bytes + */ public byte[] toCbor() { return CborSerializer.encodeArray( this.genesis.toCbor(), @@ -131,6 +195,7 @@ public byte[] toCbor() { ); } + @Override public String toString() { return String.format("Token{genesis=%s, transactions=%s}", this.genesis, this.transactions); } diff --git a/src/main/java/org/unicitylabs/sdk/transaction/TokenId.java b/src/main/java/org/unicitylabs/sdk/transaction/TokenId.java index d131b3d..e64f434 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/TokenId.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/TokenId.java @@ -8,35 +8,70 @@ import org.unicitylabs.sdk.util.BitString; import org.unicitylabs.sdk.util.HexConverter; -public class TokenId { +/** + * Globally unique identifier of a token. + */ +public final class TokenId { private static final SecureRandom RANDOM = new SecureRandom(); private final byte[] bytes; + /** + * Create a token id from byte array. + * + * @param bytes token id bytes + */ public TokenId(byte[] bytes) { Objects.requireNonNull(bytes, "Token id cannot be null"); this.bytes = Arrays.copyOf(bytes, bytes.length); } + /** + * Generate a random token id. + * + * @return token id + */ public static TokenId generate() { byte[] bytes = new byte[32]; RANDOM.nextBytes(bytes); return new TokenId(bytes); } + /** + * Get token id bytes. + * + * @return token id bytes + */ public byte[] getBytes() { return Arrays.copyOf(this.bytes, this.bytes.length); } + /** + * Deserialize an token id from CBOR bytes. + * + * @param bytes CBOR encoded token id bytes + * + * @return token id + */ public static TokenId fromCbor(byte[] bytes) { return new TokenId(CborDeserializer.decodeByteString(bytes)); } + /** + * Serialize token id to CBOR bytes. + * + * @return CBOR bytes + */ public byte[] toCbor() { return CborSerializer.encodeByteString(this.bytes); } + /** + * Convert token id to bit string. + * + * @return bit string + */ public BitString toBitString() { return new BitString(this.bytes); } diff --git a/src/main/java/org/unicitylabs/sdk/transaction/TokenType.java b/src/main/java/org/unicitylabs/sdk/transaction/TokenType.java index ab7ff88..cafdd9e 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/TokenType.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/TokenType.java @@ -8,35 +8,70 @@ import org.unicitylabs.sdk.util.BitString; import org.unicitylabs.sdk.util.HexConverter; -public class TokenType { +/** + * Type identifier of a token. + */ +public final class TokenType { private static final SecureRandom RANDOM = new SecureRandom(); private final byte[] bytes; + /** + * Create a token type from byte array. + * + * @param bytes token type bytes + */ public TokenType(byte[] bytes) { Objects.requireNonNull(bytes, "Token type cannot be null"); this.bytes = Arrays.copyOf(bytes, bytes.length); } + /** + * Get token type bytes. + * + * @return token type bytes + */ public byte[] getBytes() { return Arrays.copyOf(this.bytes, this.bytes.length); } + /** + * Generate a random token type. + * + * @return token type + */ public static TokenType generate() { byte[] bytes = new byte[32]; RANDOM.nextBytes(bytes); return new TokenType(bytes); } + /** + * Deserialize a token type from CBOR bytes. + * + * @param bytes CBOR encoded token type bytes + * + * @return token type + */ public static TokenType fromCbor(byte[] bytes) { return new TokenType(CborDeserializer.decodeByteString(bytes)); } + /** + * Serialize token type to CBOR bytes. + * + * @return CBOR bytes + */ public byte[] toCbor() { return CborSerializer.encodeByteString(this.bytes); } + /** + * Convert token type to bit string. + * + * @return bit string + */ public BitString toBitString() { return new BitString(this.bytes); } diff --git a/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java b/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java index 99e93e4..3cdea0e 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java @@ -3,21 +3,64 @@ import org.unicitylabs.sdk.crypto.hash.DataHash; import org.unicitylabs.sdk.predicate.Predicate; +/** + * Common interface for token transactions. + */ public interface Transaction { + /** + * Get transaction payload bytes. + * + * @return payload bytes + */ byte[] getData(); + /** + * Gets the predicate that locks this transaction. + * + * @return lock script predicate + */ Predicate getLockScript(); + /** + * Gets the transaction recipient address. + * + * @return recipient address + */ Address getRecipient(); + /** + * Gets the source state hash. + * + * @return source state hash + */ DataHash getSourceStateHash(); - byte[] getX(); + /** + * Get transaction randomness component. + * + * @return randomness bytes + */ + byte[] getNonce(); + /** + * Calculates the resulting state hash. + * + * @return state hash + */ DataHash calculateStateHash(); + /** + * Calculates the transaction hash. + * + * @return transaction hash + */ DataHash calculateTransactionHash(); + /** + * Serializes this transaction as CBOR. + * + * @return CBOR bytes + */ byte[] toCbor(); } diff --git a/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java index 06e6074..83754ee 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java @@ -14,21 +14,28 @@ import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.util.HexConverter; +/** + * Transfer transaction that moves token ownership from a source state to a recipient. + */ public class TransferTransaction implements Transaction { private final DataHash sourceStateHash; private final Predicate lockScript; private final Address recipient; - private final byte[] x; + private final byte[] nonce; private final byte[] data; - private TransferTransaction(DataHash sourceStateHash, Predicate lockScript, Address recipient, + private TransferTransaction( + DataHash sourceStateHash, + Predicate lockScript, + Address recipient, byte[] x, - byte[] data) { + byte[] data + ) { this.sourceStateHash = sourceStateHash; this.lockScript = lockScript; this.recipient = recipient; - this.x = x; + this.nonce = x; this.data = data; } @@ -54,10 +61,21 @@ public DataHash getSourceStateHash() { } @Override - public byte[] getX() { - return Arrays.copyOf(this.x, this.x.length); + public byte[] getNonce() { + return Arrays.copyOf(this.nonce, this.nonce.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 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) { Transaction transaction = token.getLatestTransaction(); @@ -74,6 +92,12 @@ public static TransferTransaction create(Token token, Predicate owner, Address r ); } + /** + * Deserializes a transfer transaction from CBOR bytes. + * + * @param bytes CBOR-encoded transfer transaction + * @return decoded transfer transaction + */ public static TransferTransaction fromCbor(byte[] bytes) { List data = CborDeserializer.decodeArray(bytes); @@ -92,7 +116,7 @@ public DataHash calculateStateHash() { .update( CborSerializer.encodeArray( CborSerializer.encodeByteString(this.sourceStateHash.getImprint()), - CborSerializer.encodeByteString(this.x) + CborSerializer.encodeByteString(this.nonce) ) ) .digest(); @@ -104,7 +128,7 @@ public DataHash calculateTransactionHash() { .update( CborSerializer.encodeArray( this.recipient.toCbor(), - CborSerializer.encodeByteString(this.x), + CborSerializer.encodeByteString(this.nonce), CborSerializer.encodeByteString(this.data) ) ) @@ -117,11 +141,19 @@ public byte[] toCbor() { CborSerializer.encodeByteString(this.sourceStateHash.getData()), EncodedPredicate.fromPredicate(this.lockScript).toCbor(), this.recipient.toCbor(), - CborSerializer.encodeByteString(this.x), + CborSerializer.encodeByteString(this.nonce), CborSerializer.encodeByteString(this.data) ); } + /** + * Converts this transfer transaction to a certified transfer transaction. + * + * @param trustBase trust base used for proof verification + * @param predicateVerifier predicate verifier service + * @param inclusionProof inclusion proof for this transaction + * @return certified transfer transaction + */ public CertifiedTransferTransaction toCertifiedTransaction( RootTrustBase trustBase, PredicateVerifierService predicateVerifier, @@ -135,7 +167,7 @@ public CertifiedTransferTransaction toCertifiedTransaction( public String toString() { return String.format( "TransferTransaction{sourceStateHash=%s, lockScript=%s, recipient=%s, x=%s, data=%s}", - this.sourceStateHash, this.lockScript, this.recipient, HexConverter.encode(this.x), + this.sourceStateHash, this.lockScript, this.recipient, HexConverter.encode(this.nonce), 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 9cca457..f85a454 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedMintTransactionVerificationRule.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedMintTransactionVerificationRule.java @@ -12,8 +12,26 @@ import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; +/** + * Verification rule set for certified mint transactions. + * + *

The verification checks that the lock script in certification data matches the expected + * mint lock script derived from the token id, and that the inclusion proof is valid. + */ public class CertifiedMintTransactionVerificationRule { + 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 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>(); 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 4e99694..9a107fa 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedTransferTransactionVerificationRule.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedTransferTransactionVerificationRule.java @@ -9,8 +9,27 @@ import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; +/** + * 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. + */ public class CertifiedTransferTransactionVerificationRule { + private CertifiedTransferTransactionVerificationRule() { + } + + /** + * Verify a certified transfer transaction against the previous transaction. + * + * @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 + */ public static VerificationResult verify( RootTrustBase trustBase, PredicateVerifierService predicateVerifier, diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationRule.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationRule.java index 9d56233..a8ef5e2 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationRule.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationRule.java @@ -14,11 +14,42 @@ import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; + +/** + * This class provides the functionality to verify an inclusion proof against a given trust base + * and transaction. It ensures that the inclusion proof is valid, authentic, and corresponds to + * the specified transaction. + * + *

The verification process involves several checks, including: + * - Validating the trust base against the inclusion proof. + * - Ensuring the Merkle tree path is valid and included in the committed tree. + * - Verifying the certification data referenced by the inclusion proof. + * - Checking that the transaction hash matches the reference in the proof. + * - Confirming the proof's leaf value aligns with the expected hash. + * - Verifies given predicate against certification data + */ public class InclusionProofVerificationRule { - public static VerificationResult verify(RootTrustBase trustBase, - PredicateVerifierService predicateVerifier, InclusionProof inclusionProof, - Transaction transaction) { + private InclusionProofVerificationRule() { + } + + /** + * Verifies the provided inclusion proof against the specified trust base and transaction. + * + * @param trustBase the root trust base used to validate the inclusion proof + * @param predicateVerifier the service responsible for evaluating transaction predicates + * @param inclusionProof the inclusion proof containing certification data and merkle tree path + * @param transaction the transaction that is being verified against the proof + * + * @return a {@code VerificationResult} object containing the {@code InclusionProofVerificationStatus} + * and additional details about the verification outcome + */ + public static VerificationResult verify( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + InclusionProof inclusionProof, + Transaction transaction + ) { VerificationResult result = UnicityCertificateVerification.verify(trustBase, inclusionProof); if (result.getStatus() != VerificationStatus.OK) { return new VerificationResult<>("InclusionProofVerificationRule", diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationStatus.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationStatus.java index 57724fa..20178bd 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationStatus.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationStatus.java @@ -1,12 +1,23 @@ package org.unicitylabs.sdk.transaction.verification; +/** + * Status codes returned by inclusion proof verification. + */ public enum InclusionProofVerificationStatus { + /** The provided trust base is invalid or cannot be used for verification. */ INVALID_TRUSTBASE, + /** Leaf value in the proof does not match the expected transaction. */ LEAF_VALUE_MISMATCH, + /** Certification data required for verification is missing. */ MISSING_CERTIFICATION_DATA, + /** Transaction hash does not match the value referenced by the proof. */ TRANSACTION_HASH_MISMATCH, + /** Proof authentication failed. */ NOT_AUTHENTICATED, + /** Proof path is not included in the committed tree state. */ PATH_NOT_INCLUDED, + /** Proof path structure or hashes are invalid. */ PATH_INVALID, + /** Inclusion proof verification succeeded. */ OK } diff --git a/src/main/java/org/unicitylabs/sdk/util/BitString.java b/src/main/java/org/unicitylabs/sdk/util/BitString.java index c996534..eb286fa 100644 --- a/src/main/java/org/unicitylabs/sdk/util/BitString.java +++ b/src/main/java/org/unicitylabs/sdk/util/BitString.java @@ -23,10 +23,6 @@ public BitString(byte[] data) { this.value = new BigInteger(1, dataWithPrefix); } - public static BitString fromStateId(StateId stateId) { - return new BitString(stateId.getImprint()); - } - /** * Converts BitString to BigInteger by adding a leading byte 1 to input byte array. This is to ensure that the * BigInteger will retain the leading zero bits. diff --git a/src/main/java/org/unicitylabs/sdk/util/verification/VerificationContext.java b/src/main/java/org/unicitylabs/sdk/util/verification/VerificationContext.java deleted file mode 100644 index a4d52f8..0000000 --- a/src/main/java/org/unicitylabs/sdk/util/verification/VerificationContext.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.unicitylabs.sdk.util.verification; - -public interface VerificationContext { - -} diff --git a/src/main/java/org/unicitylabs/sdk/util/verification/VerificationException.java b/src/main/java/org/unicitylabs/sdk/util/verification/VerificationException.java index ef6714f..8990742 100644 --- a/src/main/java/org/unicitylabs/sdk/util/verification/VerificationException.java +++ b/src/main/java/org/unicitylabs/sdk/util/verification/VerificationException.java @@ -1,15 +1,31 @@ package org.unicitylabs.sdk.util.verification; +/** + * Exception thrown when a verification flow returns a failing result. + */ public class VerificationException extends RuntimeException { - + /** + * Verification result associated with this exception. + */ private final VerificationResult result; + /** + * Creates a verification exception with message and failing verification result. + * + * @param message verification failure message + * @param result verification result associated with the failure + */ public VerificationException(String message, VerificationResult result) { super(String.format("Verification exception { message: '%s', result: %s", message, result.toString())); this.result = result; } + /** + * Returns the verification result associated with this exception. + * + * @return verification result + */ public VerificationResult getVerificationResult() { return this.result; } diff --git a/src/main/java/org/unicitylabs/sdk/util/verification/VerificationResult.java b/src/main/java/org/unicitylabs/sdk/util/verification/VerificationResult.java index 5a81ece..9801716 100644 --- a/src/main/java/org/unicitylabs/sdk/util/verification/VerificationResult.java +++ b/src/main/java/org/unicitylabs/sdk/util/verification/VerificationResult.java @@ -3,6 +3,11 @@ import java.util.List; import java.util.Objects; +/** + * Generic verification result containing status, message and optional nested rule results. + * + * @param status enum/type used by the verification rule + */ public class VerificationResult { private final String rule; @@ -10,6 +15,13 @@ public class VerificationResult { private final String message; private final List> results; + /** + * Create verification result with no nested results. + * + * @param rule verification rule name + * @param status verification status + * @param message descriptive message + */ public VerificationResult( String rule, S status, @@ -18,6 +30,12 @@ public VerificationResult( this(rule, status, message, List.of()); } + /** + * Create verification result with empty message and no nested results. + * + * @param rule verification rule name + * @param status verification status + */ public VerificationResult( String rule, S status @@ -25,6 +43,14 @@ public VerificationResult( this(rule, status, "", List.of()); } + /** + * Create verification result with nested results as varargs. + * + * @param rule verification rule name + * @param status verification status + * @param message descriptive message + * @param results nested verification results + */ public VerificationResult( String rule, S status, @@ -34,6 +60,14 @@ public VerificationResult( this(rule, status, message, List.of(results)); } + /** + * Create verification result. + * + * @param rule verification rule name + * @param status verification status + * @param message descriptive message + * @param results nested verification results + */ public VerificationResult( String rule, S status, @@ -51,18 +85,38 @@ public VerificationResult( this.results = List.copyOf(results); } + /** + * Get verification rule name. + * + * @return rule name + */ public String getRule() { return this.rule; } + /** + * Get verification status. + * + * @return verification status + */ public S getStatus() { return this.status; } + /** + * Get verification message. + * + * @return verification message + */ public String getMessage() { return this.message; } + /** + * Get nested verification results. + * + * @return nested results + */ public List> getResults() { return this.results; } @@ -70,11 +124,12 @@ public List> getResults() { @Override public String toString() { - return "VerificationResult{" + - "rule='" + this.rule + '\'' + - ", status=" + this.status + - ", message='" + this.message + '\'' + - ", results=" + this.results + - '}'; + return String.format( + "VerificationResult{rule=%s, status=%s, message=%s, results=%s}", + this.rule, + this.status, + this.message, + this.results + ); } } diff --git a/src/main/java/org/unicitylabs/sdk/util/verification/VerificationStatus.java b/src/main/java/org/unicitylabs/sdk/util/verification/VerificationStatus.java index f400102..1ce6656 100644 --- a/src/main/java/org/unicitylabs/sdk/util/verification/VerificationStatus.java +++ b/src/main/java/org/unicitylabs/sdk/util/verification/VerificationStatus.java @@ -1,5 +1,11 @@ package org.unicitylabs.sdk.util.verification; +/** + * Outcome status of a verification step. + */ public enum VerificationStatus { - OK, FAIL + /** Verification succeeded. */ + OK, + /** Verification failed. */ + FAIL } 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 59777a8..cef59de 100644 --- a/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java @@ -19,16 +19,24 @@ 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.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.util.verification.VerificationStatus; import org.unicitylabs.sdk.utils.TokenUtils; +/** + * Functional tests for minting and splitting tokens with proof verification. + */ public class SplitBuilderTest { + /** + * Verifies end-to-end mint, split, burn and validation flow. + * + * @throws Exception when async client interactions fail + */ @Test public void testMintAndSplitToken() throws Exception { TestAggregatorClient aggregatorClient = TestAggregatorClient.create(); @@ -99,7 +107,12 @@ public void testMintAndSplitToken() throws Exception { SplitResult result = TokenSplit.split(token, predicate, TestPaymentData::decode, splitTokens); Token burnToken = TokenUtils.transferToken( - client, trustBase, predicateVerifier, token, result.getBurnTransaction(), signingService + client, + trustBase, + predicateVerifier, + token, + result.getBurnTransaction(), + PayToPublicKeyPredicateUnlockScript.create(result.getBurnTransaction(), signingService) ); for (Entry> entry : splitTokens.entrySet()) { @@ -131,5 +144,4 @@ public void testMintAndSplitToken() throws Exception { ).getStatus()); } } - } diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/TestSplitPaymentData.java b/src/test/java/org/unicitylabs/sdk/functional/payment/TestSplitPaymentData.java index cbcac58..bbf5062 100644 --- a/src/test/java/org/unicitylabs/sdk/functional/payment/TestSplitPaymentData.java +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/TestSplitPaymentData.java @@ -9,43 +9,86 @@ import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +/** + * 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 = Set.copyOf(assets); + 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.decodeArray(data.get(0)).stream() - .map(Asset::fromCbor) - .collect(Collectors.toSet()); + Set assets = CborDeserializer.decodeNullable( + data.get(0), + result -> CborDeserializer.decodeArray(result).stream() + .map(asset -> CborDeserializer.decodeNullable(asset, Asset::fromCbor)) + .collect(Collectors.toSet()) + ); - SplitReason reason = SplitReason.fromCbor(data.get(1)); + 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.encodeArray( - this.assets.stream().map(Asset::toCbor).toArray(byte[][]::new) + CborSerializer.encodeOptional( + this.assets, + assets -> CborSerializer.encodeArray( + assets.stream().map(asset -> CborSerializer.encodeOptional(asset, Asset::toCbor)).toArray(byte[][]::new) + ) ), - this.reason.toCbor() + CborSerializer.encodeOptional(this.reason, SplitReason::toCbor) ); } -} \ No newline at end of file + + @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/utils/TokenUtils.java b/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java index 9a2fe13..559e7cc 100644 --- a/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java +++ b/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java @@ -8,6 +8,7 @@ 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.UnlockScript; import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; @@ -21,8 +22,23 @@ import org.unicitylabs.sdk.util.InclusionProofUtils; import org.unicitylabs.sdk.util.verification.VerificationStatus; +/** + * Test helpers for minting and transferring certified tokens. + */ 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, @@ -38,6 +54,19 @@ public static Token mintToken( ); } + /** + * 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, @@ -55,6 +84,20 @@ public static Token mintToken( ); } + /** + * 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, @@ -74,6 +117,21 @@ public static Token mintToken( ); } + /** + * 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, @@ -110,6 +168,20 @@ public static Token mintToken( } + /** + * Deserialize token, build transfer transaction and submit certified transfer. + * + * @param client state transition client + * @param trustBase trust base + * @param predicateVerifier predicate verifier + * @param tokenBytes serialized token bytes + * @param recipient recipient address + * @param signingService sender signing service + * + * @return transferred token + * + * @throws Exception when request or verification fails + */ public static Token transferToken( StateTransitionClient client, RootTrustBase trustBase, @@ -132,22 +204,40 @@ public static Token transferToken( CborSerializer.encodeArray() ); - return TokenUtils.transferToken(client, trustBase, predicateVerifier, token, transaction, signingService); + return TokenUtils.transferToken( + client, + trustBase, + predicateVerifier, + token, + transaction, + PayToPublicKeyPredicateUnlockScript.create(transaction, signingService) + ); } + /** + * Submit a prepared transfer transaction and return resulting transferred token. + * + * @param client state transition client + * @param trustBase trust base + * @param predicateVerifier predicate verifier + * @param token source token + * @param transaction transfer transaction + * @param unlockScript unlock script for transaction + * + * @return transferred token + * + * @throws Exception when request or verification fails + */ public static Token transferToken( StateTransitionClient client, RootTrustBase trustBase, PredicateVerifierService predicateVerifier, Token token, TransferTransaction transaction, - SigningService signingService + UnlockScript unlockScript ) throws Exception { CertificationResponse response = client.submitCertificationRequest( - CertificationData.fromTransaction( - transaction, - PayToPublicKeyPredicateUnlockScript.create(transaction, signingService) - ) + CertificationData.fromTransaction(transaction, unlockScript) ).get(); if (response.getStatus() != CertificationStatus.SUCCESS) { From 798ad986db0c2cdf3ac247dda69d559c1c3fda7b Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Mon, 6 Apr 2026 22:47:42 +0300 Subject: [PATCH 3/3] #51 Fix issue in split payload verification and add tests for it --- .../unicitylabs/sdk/payment/SplitReason.java | 4 +- .../unicitylabs/sdk/payment/TokenSplit.java | 6 +- .../functional/payment/SplitBuilderTest.java | 505 +++++++++++++++++- 3 files changed, 483 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitReason.java b/src/main/java/org/unicitylabs/sdk/payment/SplitReason.java index 279a32b..de5ea84 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitReason.java +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitReason.java @@ -10,7 +10,7 @@ /** * The reason for token splitting represented by an input token and inclusion proofs. */ -public class SplitReason { +public final class SplitReason { private final Token token; private final List proofs; @@ -53,7 +53,7 @@ public static SplitReason create(Token token, List proofs) { Objects.requireNonNull(token, "token cannot be null"); Objects.requireNonNull(proofs, "proofs cannot be null"); - if (proofs.size() == 0) { + if (proofs.isEmpty()) { throw new IllegalArgumentException("proofs cannot be empty"); } diff --git a/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java b/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java index a97d12d..fe2fa41 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java +++ b/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java @@ -233,7 +233,7 @@ public static VerificationResult verify( } Transaction burnTokenLastTransaction = data.getReason().getToken().getLatestTransaction(); - DataHash root = data.getReason().getProofs().get(0).getAssetTreePath().getRootHash(); + DataHash root = data.getReason().getProofs().get(0).getAggregationPath().getRootHash(); for (SplitReasonProof proof : data.getReason().getProofs()) { MerkleTreePathVerificationResult aggregationPathResult = proof.getAggregationPath() .verify(proof.getAssetId().toBitString().toBigInteger()); @@ -255,7 +255,7 @@ public static VerificationResult verify( ); } - if (!proof.getAssetTreePath().getRootHash().equals(root)) { + if (!proof.getAggregationPath().getRootHash().equals(root)) { return new VerificationResult<>( "TokenSplitReasonVerificationRule", VerificationStatus.FAIL, @@ -264,7 +264,7 @@ public static VerificationResult verify( } if (!Arrays.equals( - root.getImprint(), + proof.getAssetTreePath().getRootHash().getImprint(), proof.getAggregationPath().getSteps().get(0).getData().orElse(null) )) { return new VerificationResult<>( 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 cef59de..bf4cfed 100644 --- a/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java @@ -2,16 +2,29 @@ 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.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.stream.Collectors; 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.mtree.plain.SparseMerkleTree; +import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreeRootNode; +import org.unicitylabs.sdk.mtree.sum.SparseMerkleSumTree; +import org.unicitylabs.sdk.mtree.sum.SparseMerkleSumTreeRootNode; +import org.unicitylabs.sdk.payment.SplitPaymentData; import org.unicitylabs.sdk.payment.SplitReason; import org.unicitylabs.sdk.payment.SplitReasonProof; import org.unicitylabs.sdk.payment.SplitResult; @@ -21,42 +34,68 @@ 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.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.util.verification.VerificationStatus; import org.unicitylabs.sdk.utils.TokenUtils; /** * Functional tests for minting and splitting tokens with proof verification. */ +@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 { + TestAggregatorClient aggregatorClient = TestAggregatorClient.create(); + this.trustBase = aggregatorClient.getTrustBase(); + + this.client = new StateTransitionClient(aggregatorClient); + this.predicateVerifier = PredicateVerifierService.create(this.trustBase); + + 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 testMintAndSplitToken() throws Exception { - TestAggregatorClient aggregatorClient = TestAggregatorClient.create(); - RootTrustBase trustBase = aggregatorClient.getTrustBase(); - StateTransitionClient client = new StateTransitionClient(aggregatorClient); - PredicateVerifierService predicateVerifier = PredicateVerifierService.create(trustBase); - + public void verifyTokenSplitIsSuccessful() throws Exception { SigningService signingService = SigningService.generate(); PayToPublicKeyPredicate predicate = PayToPublicKeyPredicate.fromSigningService(signingService); - Asset asset1 = new Asset(new AssetId("ASSET_1".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); - Asset asset2 = new Asset(new AssetId("ASSET_2".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); - - Set assets = Set.of(asset1, asset2); + Set assets = Set.of(this.asset1, this.asset2); TestPaymentData paymentData = new TestPaymentData(assets); Token token = TokenUtils.mintToken( - client, - trustBase, - predicateVerifier, + this.client, + this.trustBase, + this.predicateVerifier, Address.fromPredicate(predicate), paymentData.encode() ); @@ -67,7 +106,7 @@ public void testMintAndSplitToken() throws Exception { token, predicate, TestPaymentData::decode, - Map.of(TokenId.generate(), Set.of(asset1)) + Map.of(TokenId.generate(), Set.of(this.asset1)) ) ); @@ -79,7 +118,10 @@ public void testMintAndSplitToken() throws Exception { token, predicate, TestPaymentData::decode, - Map.of(TokenId.generate(), Set.of(asset1, new Asset(asset2.getId(), BigInteger.valueOf(400)))) + Map.of( + TokenId.generate(), + Set.of(this.asset1, new Asset(this.asset2.getId(), BigInteger.valueOf(400))) + ) ) ); @@ -92,7 +134,10 @@ public void testMintAndSplitToken() throws Exception { token, predicate, TestPaymentData::decode, - Map.of(TokenId.generate(), Set.of(asset1, new Asset(asset2.getId(), BigInteger.valueOf(1500)))) + Map.of( + TokenId.generate(), + Set.of(this.asset1, new Asset(this.asset2.getId(), BigInteger.valueOf(1500))) + ) ) ); @@ -100,16 +145,16 @@ public void testMintAndSplitToken() throws Exception { exception.getMessage()); Map> splitTokens = Map.of( - TokenId.generate(), Set.of(asset1), - TokenId.generate(), Set.of(asset2) + 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( - client, - trustBase, - predicateVerifier, + this.client, + this.trustBase, + this.predicateVerifier, token, result.getBurnTransaction(), PayToPublicKeyPredicateUnlockScript.create(result.getBurnTransaction(), signingService) @@ -120,9 +165,9 @@ public void testMintAndSplitToken() throws Exception { Assertions.assertNotNull(proofs); Token splitToken = TokenUtils.mintToken( - client, - trustBase, - predicateVerifier, + this.client, + this.trustBase, + this.predicateVerifier, entry.getKey(), Address.fromPredicate(predicate), new TestSplitPaymentData( @@ -134,14 +179,420 @@ public void testMintAndSplitToken() throws Exception { ).encode() ); - Assertions.assertEquals(VerificationStatus.OK, splitToken.verify(trustBase, predicateVerifier).getStatus()); + Assertions.assertEquals( + VerificationStatus.OK, + splitToken.verify(this.trustBase, this.predicateVerifier).getStatus() + ); Assertions.assertEquals(VerificationStatus.OK, TokenSplit.verify( Token.fromCbor(splitToken.toCbor()), TestSplitPaymentData::decode, - trustBase, - predicateVerifier + 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)); + List reasonTokenData = CborDeserializer.decodeArray(reasonData.get(0)); + List transactions = CborDeserializer.decodeArray(reasonTokenData.get(1)); + List certifiedTransfer = CborDeserializer.decodeArray(transactions.get(0)); + List transfer = CborDeserializer.decodeArray(certifiedTransfer.get(0)); + + // Corrupt burn transaction recipient address so burn token verification fails. + byte[] invalidRecipient = new byte[32]; + invalidRecipient[0] = 1; + transfer.set(2, Address.fromBytes(invalidRecipient).toCbor()); + + certifiedTransfer.set(0, encodeArray(transfer)); + transactions.set(0, encodeArray(certifiedTransfer)); + reasonTokenData.set(1, encodeArray(transactions)); + reasonData.set(0, 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() + ); + 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() + ); + + TokenId outputTokenId = TokenId.generate(); + SplitResult split = TokenSplit.split( + sourceToken, + ownerPredicate, + TestPaymentData::decode, + Map.of(outputTokenId, outputAssets) + ); + + Token burnToken = TokenUtils.transferToken( + client, + this.trustBase, + this.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() + ); + } + + private VerificationResult verify(Token token) { + return TokenSplit.verify( + Token.fromCbor(token.toCbor()), + TestSplitPaymentData::decode, + this.trustBase, + this.predicateVerifier + ); + } + + private VerificationResult verifyWithData(Token token, SplitPaymentData paymentData) { + return TokenSplit.verify( + Token.fromCbor(token.toCbor()), + ignored -> paymentData, + this.trustBase, + this.predicateVerifier + ); + } + + private VerificationResult verifyWithPayload(Token token, byte[] payload) { + return this.verify(withPayload(token, payload)); + } + + private Token withPayload(Token token, byte[] payload) { + List tokenData = CborDeserializer.decodeArray(token.toCbor()); + List genesis = CborDeserializer.decodeArray(tokenData.get(0)); + List mint = CborDeserializer.decodeArray(genesis.get(0)); + List aux = CborDeserializer.decodeArray(mint.get(2)); + + aux.set(1, CborSerializer.encodeByteString(payload)); + mint.set(2, encodeArray(aux)); + genesis.set(0, encodeArray(mint)); + tokenData.set(0, encodeArray(genesis)); + + return Token.fromCbor(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(); + } + } }