diff --git a/src/main/java/org/unicitylabs/sdk/api/CertificationData.java b/src/main/java/org/unicitylabs/sdk/api/CertificationData.java index 7ec2771..38d71c1 100644 --- a/src/main/java/org/unicitylabs/sdk/api/CertificationData.java +++ b/src/main/java/org/unicitylabs/sdk/api/CertificationData.java @@ -5,9 +5,8 @@ import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; import org.unicitylabs.sdk.predicate.EncodedPredicate; -import org.unicitylabs.sdk.predicate.Predicate; import org.unicitylabs.sdk.predicate.UnlockScript; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicateUnlockScript; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.serializer.cbor.CborSerializationException; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; @@ -26,13 +25,13 @@ public class CertificationData { public static final long CBOR_TAG = 39031; private static final int VERSION = 1; - private final Predicate lockScript; + private final EncodedPredicate lockScript; private final DataHash sourceStateHash; private final DataHash transactionHash; private final byte[] unlockScript; CertificationData( - Predicate lockScript, + EncodedPredicate lockScript, DataHash sourceStateHash, DataHash transactionHash, byte[] unlockScript @@ -52,7 +51,7 @@ public int getVersion() { * * @return lock script */ - public Predicate getLockScript() { + public EncodedPredicate getLockScript() { return this.lockScript; } @@ -94,7 +93,7 @@ public static CertificationData fromCbor(byte[] bytes) { if (tag.getTag() != CertificationData.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 5); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != CertificationData.VERSION) { @@ -123,7 +122,7 @@ public static CertificationData fromMintTransaction(MintTransaction transaction) return CertificationData.fromTransaction( transaction, - PayToPublicKeyPredicateUnlockScript.create(transaction, signingService).getSignature() + SignaturePredicateUnlockScript.create(transaction, signingService).getSignature() .encode() ); } @@ -137,7 +136,6 @@ public static CertificationData fromMintTransaction(MintTransaction transaction) * @return certification data */ public static CertificationData fromTransaction(Transaction transaction, UnlockScript unlockScript) { - Objects.requireNonNull(transaction, "transaction cannot be null"); Objects.requireNonNull(unlockScript, "unlockScript cannot be null"); return CertificationData.fromTransaction(transaction, unlockScript.encode()); @@ -173,7 +171,7 @@ public byte[] toCbor() { CertificationData.CBOR_TAG, CborSerializer.encodeArray( CborSerializer.encodeUnsignedInteger(CertificationData.VERSION), - EncodedPredicate.fromPredicate(this.getLockScript()).toCbor(), + this.lockScript.toCbor(), CborSerializer.encodeByteString(this.sourceStateHash.getData()), CborSerializer.encodeByteString(this.transactionHash.getData()), CborSerializer.encodeByteString(this.unlockScript) @@ -187,7 +185,7 @@ public boolean equals(Object o) { return false; } CertificationData that = (CertificationData) o; - return Predicate.areEqual(this.lockScript, that.lockScript) + return Objects.equals(this.lockScript, that.lockScript) && Objects.equals(this.sourceStateHash, that.sourceStateHash) && Objects.equals(this.transactionHash, that.transactionHash) && Arrays.equals(this.unlockScript, that.unlockScript); @@ -195,7 +193,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(EncodedPredicate.fromPredicate(this.lockScript), this.sourceStateHash, this.transactionHash, Arrays.hashCode(this.unlockScript)); + return Objects.hash(this.lockScript, this.sourceStateHash, this.transactionHash, Arrays.hashCode(this.unlockScript)); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java b/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java index bab61ac..8944587 100644 --- a/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java +++ b/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java @@ -74,7 +74,7 @@ public static InclusionProof fromCbor(byte[] bytes) { if (tag.getTag() != InclusionProof.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 4); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != InclusionProof.VERSION) { @@ -120,7 +120,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(InclusionProof.VERSION, this.inclusionCertificate, this.certificationData, this.unicityCertificate); + return Objects.hash(this.inclusionCertificate, this.certificationData, this.unicityCertificate); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/api/InclusionProofResponse.java b/src/main/java/org/unicitylabs/sdk/api/InclusionProofResponse.java index 6f0a775..d53e22e 100644 --- a/src/main/java/org/unicitylabs/sdk/api/InclusionProofResponse.java +++ b/src/main/java/org/unicitylabs/sdk/api/InclusionProofResponse.java @@ -42,7 +42,7 @@ public InclusionProof getInclusionProof() { * @return inclusion proof response */ public static InclusionProofResponse fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new InclusionProofResponse( CborDeserializer.decodeUnsignedInteger(data.get(0)).asLong(), InclusionProof.fromCbor(data.get(1)) diff --git a/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java b/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java index 1c47e0d..fd298fb 100644 --- a/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java +++ b/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java @@ -3,6 +3,7 @@ import org.unicitylabs.sdk.api.jsonrpc.JsonRpcHttpTransport; import org.unicitylabs.sdk.util.HexConverter; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -14,6 +15,7 @@ * Default aggregator client. */ public class JsonRpcAggregatorClient implements AggregatorClient { + private static final String STATE_ID_HEADER = "X-State-ID"; private final JsonRpcHttpTransport transport; private final String apiKey; @@ -54,9 +56,11 @@ public CompletableFuture submitCertificationRequest( CertificationRequest request = CertificationRequest.create( Objects.requireNonNull(certificationData, "certificationData cannot be null")); - Map> headers = this.apiKey == null - ? Map.of() - : Map.of(AUTHORIZATION, List.of(String.format("Bearer %s", this.apiKey))); + Map> headers = new HashMap<>(); + headers.put(STATE_ID_HEADER, List.of(HexConverter.encode(request.getStateId().getData()))); + if (this.apiKey != null) { + headers.put(AUTHORIZATION, List.of(String.format("Bearer %s", this.apiKey))); + } return this.transport.request( "certification_request", diff --git a/src/main/java/org/unicitylabs/sdk/api/StateId.java b/src/main/java/org/unicitylabs/sdk/api/StateId.java index 3da77e9..f0567d1 100644 --- a/src/main/java/org/unicitylabs/sdk/api/StateId.java +++ b/src/main/java/org/unicitylabs/sdk/api/StateId.java @@ -4,7 +4,6 @@ import org.unicitylabs.sdk.crypto.hash.DataHasher; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.unicitylabs.sdk.predicate.EncodedPredicate; -import org.unicitylabs.sdk.predicate.Predicate; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.transaction.Transaction; @@ -70,11 +69,11 @@ public static StateId fromTransaction(Transaction transaction) { return StateId.create(transaction.getLockScript(), transaction.getSourceStateHash()); } - private static StateId create(Predicate predicate, DataHash stateHash) { + private static StateId create(EncodedPredicate predicate, DataHash stateHash) { DataHash hash = new DataHasher(HashAlgorithm.SHA256) .update( CborSerializer.encodeArray( - EncodedPredicate.fromPredicate(predicate).toCbor(), + predicate.toCbor(), CborSerializer.encodeByteString(stateHash.getData()) ) ) diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java b/src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java index af1e455..d8a1a1e 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java @@ -150,7 +150,7 @@ public static InputRecord fromCbor(byte[] bytes) { if (tag.getTag() != InputRecord.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 10); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != InputRecord.VERSION) { @@ -212,7 +212,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(InputRecord.VERSION, this.roundNumber, this.epoch, + return Objects.hash(this.roundNumber, this.epoch, Arrays.hashCode(this.previousHash), Arrays.hashCode(this.hash), Arrays.hashCode(this.summaryValue), this.timestamp, Arrays.hashCode(this.blockHash), diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java b/src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java index babcc26..94227b3 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java @@ -64,7 +64,7 @@ public static ShardTreeCertificate fromCbor(byte[] bytes) { if (tag.getTag() != ShardTreeCertificate.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 3); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != ShardTreeCertificate.VERSION) { @@ -111,7 +111,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(ShardTreeCertificate.VERSION, this.shard, this.siblingHashList); + return Objects.hash(this.shard, this.siblingHashList); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java index 09286d3..4900501 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java @@ -173,7 +173,7 @@ public static UnicityCertificate fromCbor(byte[] bytes) { if (tag.getTag() != UnicityCertificate.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 7); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != UnicityCertificate.VERSION) { @@ -226,7 +226,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(UnicityCertificate.VERSION, this.inputRecord, Arrays.hashCode(this.technicalRecordHash), + return Objects.hash(this.inputRecord, Arrays.hashCode(this.technicalRecordHash), Arrays.hashCode(this.shardConfigurationHash), this.shardTreeCertificate, this.unicityTreeCertificate, this.unicitySeal); } diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java b/src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java index c7ec830..3759ffe 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java @@ -144,7 +144,7 @@ public static UnicitySeal fromCbor(byte[] bytes) { if (tag.getTag() != UnicitySeal.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 8); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != UnicitySeal.VERSION) { @@ -235,7 +235,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(UnicitySeal.VERSION, this.networkId, this.rootChainRoundNumber, this.epoch, + return Objects.hash(this.networkId, this.rootChainRoundNumber, this.epoch, this.timestamp, Arrays.hashCode(this.previousHash), Arrays.hashCode(this.hash), this.signatures); } diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/UnicityTreeCertificate.java b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityTreeCertificate.java index a248f54..0f979a6 100644 --- a/src/main/java/org/unicitylabs/sdk/api/bft/UnicityTreeCertificate.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityTreeCertificate.java @@ -68,7 +68,7 @@ public static UnicityTreeCertificate fromCbor(byte[] bytes) { if (tag.getTag() != UnicityTreeCertificate.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 3); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != UnicityTreeCertificate.VERSION) { @@ -112,7 +112,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(UnicityTreeCertificate.VERSION, this.partitionIdentifier, this.steps); + return Objects.hash(this.partitionIdentifier, this.steps); } @Override @@ -160,7 +160,7 @@ public byte[] getHash() { * @return hash step */ public static HashStep fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new HashStep( CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(), diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitAssetProof.java b/src/main/java/org/unicitylabs/sdk/payment/SplitAssetProof.java index d891d25..29654c2 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitAssetProof.java +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitAssetProof.java @@ -79,7 +79,7 @@ public static SplitAssetProof create( * @return split reason proof */ public static SplitAssetProof fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 3); return new SplitAssetProof( AssetId.fromCbor(data.get(0)), diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustification.java b/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustification.java index 7f64f21..6f68714 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustification.java +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustification.java @@ -77,7 +77,7 @@ public static SplitMintJustification fromCbor(byte[] bytes) { if (tag.getTag() != SplitMintJustification.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 2); return SplitMintJustification.create( Token.fromCbor(data.get(0)), CborDeserializer.decodeArray(data.get(1)).stream().map(SplitAssetProof::fromCbor).collect(Collectors.toSet()) diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java b/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java index cc488b2..a78a9db 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java @@ -112,6 +112,14 @@ public VerificationResult verify(CertifiedMintTransaction tr Transaction burnTokenLastTransaction = justification.getToken().getLatestTransaction(); DataHash root = justification.getProofs().get(0).getAggregationPath().getRootHash(); for (SplitAssetProof proof : justification.getProofs()) { + if (!validatedAssets.add(proof.getAssetId())) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + String.format("Duplicate split proof for asset id %s.", proof.getAssetId()) + ); + } + MerkleTreePathVerificationResult aggregationPathResult = proof.getAggregationPath() .verify(proof.getAssetId().toBitString().toBigInteger()); if (!aggregationPathResult.isSuccessful()) { @@ -171,20 +179,17 @@ public VerificationResult verify(CertifiedMintTransaction tr ); } - EncodedPredicate recipient = EncodedPredicate.fromPredicate(burnTokenLastTransaction.getRecipient()); EncodedPredicate expectedRecipient = EncodedPredicate.fromPredicate( BurnPredicate.create(proof.getAggregationPath().getRootHash().getImprint()) ); - if (!expectedRecipient.equals(recipient)) { + if (!expectedRecipient.equals(burnTokenLastTransaction.getRecipient())) { return new VerificationResult<>( "SplitMintJustificationVerificationRule", VerificationStatus.FAIL, "Aggregation path root does not match burn predicate." ); } - - validatedAssets.add(proof.getAssetId()); } if (validatedAssets.size() != assets.size()) { diff --git a/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java b/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java index e9744cb..ee253dd 100644 --- a/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java +++ b/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java @@ -56,7 +56,7 @@ public BigInteger getValue() { * @return asset */ public static Asset fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new Asset( AssetId.fromCbor(data.get(0)), diff --git a/src/main/java/org/unicitylabs/sdk/predicate/EncodedPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/EncodedPredicate.java index 2f6ee17..ef35844 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/EncodedPredicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/EncodedPredicate.java @@ -41,7 +41,7 @@ public static EncodedPredicate fromCbor(byte[] bytes) { if (tag.getTag() != EncodedPredicate.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 3); PredicateEngine engine = PredicateEngine.fromId( CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt()); diff --git a/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java b/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java index 540caaf..1a6d8e0 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java @@ -1,8 +1,5 @@ package org.unicitylabs.sdk.predicate; -import java.util.Arrays; -import java.util.Objects; - /** * Base contract for all predicate implementations. */ @@ -28,15 +25,4 @@ public interface Predicate { * @return encoded predicate parameter bytes */ byte[] encodeParameters(); - - /** - * Checks if two predicates are equal. - * @param a first predicate - * @param b second predicate - * @return {@code true} if predicates are equal, {@code false} otherwise - */ - static boolean areEqual(Predicate a, Predicate b) { - return a.getEngine() == b.getEngine() && Arrays.equals(a.encodeCode(), b.encodeCode()) && Arrays.equals( - a.encodeParameters(), b.encodeParameters()); - } } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java index 75be5fc..ffb0222 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java @@ -5,11 +5,11 @@ */ public enum BuiltInPredicateType { /** Predicate that locks state to a public key. */ - PAY_TO_PUBLIC_KEY(1), - /** Predicate that references a Unicity identifier. */ - UNICITY_ID(2), + SIGNATURE(0x01), /** Predicate that marks state as unspendable (burned). */ - BURN(3); + BURN(0x02), + /** Predicate that references a Unicity identifier. */ + UNICITY_ID(0x100); private final int id; diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java index ff27ee3..d0a7263 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java @@ -1,6 +1,6 @@ package org.unicitylabs.sdk.predicate.builtin; -import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.PredicateEngine; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; @@ -55,7 +55,7 @@ public static BurnPredicate create(byte[] reason) { * @return converted burn predicate * @throws IllegalArgumentException if the predicate engine is not built-in or predicate type is not burn */ - public static BurnPredicate fromPredicate(Predicate predicate) { + public static BurnPredicate fromPredicate(EncodedPredicate predicate) { PredicateEngine engine = predicate.getEngine(); if (engine != PredicateEngine.BUILT_IN) { throw new IllegalArgumentException("Predicate engine must be BUILT_IN."); diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/DefaultBuiltInPredicateVerifier.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/DefaultBuiltInPredicateVerifier.java index ce47c1b..cb08129 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/DefaultBuiltInPredicateVerifier.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/DefaultBuiltInPredicateVerifier.java @@ -1,13 +1,11 @@ package org.unicitylabs.sdk.predicate.builtin; -import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.crypto.hash.DataHash; -import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.PredicateEngine; import org.unicitylabs.sdk.predicate.builtin.verification.BuiltInPredicateVerifier; -import org.unicitylabs.sdk.predicate.builtin.verification.PayToPublicKeyPredicateVerifier; +import org.unicitylabs.sdk.predicate.builtin.verification.SignaturePredicateVerifier; import org.unicitylabs.sdk.predicate.verification.PredicateVerifier; -import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; @@ -52,20 +50,18 @@ public PredicateEngine getPredicateEngine() { /** * Creates the default built-in predicate verifier set. * - * @param service predicate verifier service - * @param trustBase root trust base * @return default built-in predicate verifier */ - public static DefaultBuiltInPredicateVerifier create(PredicateVerifierService service, RootTrustBase trustBase) { + public static DefaultBuiltInPredicateVerifier create() { return new DefaultBuiltInPredicateVerifier( List.of( - new PayToPublicKeyPredicateVerifier() + new SignaturePredicateVerifier() ) ); } @Override - public VerificationResult verify(Predicate predicate, + public VerificationResult verify(EncodedPredicate predicate, DataHash sourceStateHash, DataHash transactionHash, byte[] unlockScript) { BuiltInPredicateType type = BuiltInPredicateType.fromId( diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/SignaturePredicate.java similarity index 69% rename from src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java rename to src/main/java/org/unicitylabs/sdk/predicate/builtin/SignaturePredicate.java index 1cf6e72..e53e6e5 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/SignaturePredicate.java @@ -1,7 +1,7 @@ package org.unicitylabs.sdk.predicate.builtin; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; -import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.PredicateEngine; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.util.HexConverter; @@ -12,11 +12,11 @@ /** * Built-in predicate that locks an output to a secp256k1 public key. */ -public class PayToPublicKeyPredicate implements BuiltInPredicate { +public class SignaturePredicate implements BuiltInPredicate { private final byte[] publicKey; - private PayToPublicKeyPredicate(byte[] publicKey) { + private SignaturePredicate(byte[] publicKey) { this.publicKey = publicKey; } @@ -35,7 +35,7 @@ public byte[] getPublicKey() { * @return predicate type */ public BuiltInPredicateType getType() { - return BuiltInPredicateType.PAY_TO_PUBLIC_KEY; + return BuiltInPredicateType.SIGNATURE; } /** @@ -45,8 +45,8 @@ public BuiltInPredicateType getType() { * * @return pay-to-public-key predicate */ - public static PayToPublicKeyPredicate create(byte[] publicKey) { - return new PayToPublicKeyPredicate(Arrays.copyOf(publicKey, publicKey.length)); + public static SignaturePredicate create(byte[] publicKey) { + return new SignaturePredicate(Arrays.copyOf(publicKey, publicKey.length)); } /** @@ -56,7 +56,7 @@ public static PayToPublicKeyPredicate create(byte[] publicKey) { * * @return pay-to-public-key predicate */ - public static PayToPublicKeyPredicate fromPredicate(Predicate predicate) { + public static SignaturePredicate fromPredicate(EncodedPredicate predicate) { PredicateEngine engine = predicate.getEngine(); if (engine != PredicateEngine.BUILT_IN) { throw new IllegalArgumentException("Predicate engine must be BUILT_IN."); @@ -64,11 +64,11 @@ public static PayToPublicKeyPredicate fromPredicate(Predicate predicate) { BuiltInPredicateType type = BuiltInPredicateType.fromId( CborDeserializer.decodeUnsignedInteger(predicate.encodeCode()).asInt()); - if (type != BuiltInPredicateType.PAY_TO_PUBLIC_KEY) { - throw new IllegalArgumentException("Predicate type must be PAY_TO_PUBLIC_KEY."); + if (type != BuiltInPredicateType.SIGNATURE) { + throw new IllegalArgumentException("Predicate type must be SIGNATURE."); } - return new PayToPublicKeyPredicate(predicate.encodeParameters()); + return new SignaturePredicate(predicate.encodeParameters()); } /** @@ -78,9 +78,9 @@ public static PayToPublicKeyPredicate fromPredicate(Predicate predicate) { * * @return pay-to-public-key predicate */ - public static PayToPublicKeyPredicate fromSigningService(SigningService signingService) { + public static SignaturePredicate fromSigningService(SigningService signingService) { Objects.requireNonNull(signingService, "Signing service cannot be null"); - return new PayToPublicKeyPredicate(signingService.getPublicKey()); + return new SignaturePredicate(signingService.getPublicKey()); } /** @@ -95,7 +95,7 @@ public byte[] encodeParameters() { @Override public String toString() { - return String.format("PayToPublicKeyPredicate{publicKey=%s}", HexConverter.encode(this.publicKey)); + return String.format("SignaturePredicate{publicKey=%s}", HexConverter.encode(this.publicKey)); } } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicateUnlockScript.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/SignaturePredicateUnlockScript.java similarity index 76% rename from src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicateUnlockScript.java rename to src/main/java/org/unicitylabs/sdk/predicate/builtin/SignaturePredicateUnlockScript.java index e7f22fa..01fb55b 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicateUnlockScript.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/SignaturePredicateUnlockScript.java @@ -10,13 +10,13 @@ import org.unicitylabs.sdk.transaction.Transaction; /** - * Unlock script for {@link PayToPublicKeyPredicate} containing a transaction signature. + * Unlock script for {@link SignaturePredicate} containing a transaction signature. */ -public class PayToPublicKeyPredicateUnlockScript implements UnlockScript { +public class SignaturePredicateUnlockScript implements UnlockScript { private final Signature signature; - private PayToPublicKeyPredicateUnlockScript(Signature signature) { + private SignaturePredicateUnlockScript(Signature signature) { this.signature = signature; } @@ -36,7 +36,7 @@ public Signature getSignature() { * @param signingService signing service used to produce the signature * @return created unlock script */ - public static PayToPublicKeyPredicateUnlockScript create( + public static SignaturePredicateUnlockScript create( Transaction transaction, SigningService signingService ) { @@ -49,7 +49,7 @@ public static PayToPublicKeyPredicateUnlockScript create( ) .digest(); - return new PayToPublicKeyPredicateUnlockScript(signingService.sign(hash)); + return new SignaturePredicateUnlockScript(signingService.sign(hash)); } /** @@ -58,8 +58,8 @@ public static PayToPublicKeyPredicateUnlockScript create( * @param bytes encoded signature bytes * @return decoded unlock script */ - public static PayToPublicKeyPredicateUnlockScript decode(byte[] bytes) { - return new PayToPublicKeyPredicateUnlockScript(Signature.decode(bytes)); + public static SignaturePredicateUnlockScript decode(byte[] bytes) { + return new SignaturePredicateUnlockScript(Signature.decode(bytes)); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/BuiltInPredicateVerifier.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/BuiltInPredicateVerifier.java index f481fb8..f618275 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/BuiltInPredicateVerifier.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/BuiltInPredicateVerifier.java @@ -1,7 +1,7 @@ package org.unicitylabs.sdk.predicate.builtin.verification; import org.unicitylabs.sdk.crypto.hash.DataHash; -import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.builtin.BuiltInPredicateType; import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; @@ -27,6 +27,6 @@ public interface BuiltInPredicateVerifier { * @param unlockScript unlock script bytes provided for the predicate * @return verification result with status and optional diagnostics */ - VerificationResult verify(Predicate predicate, DataHash sourceStateHash, + VerificationResult verify(EncodedPredicate predicate, DataHash sourceStateHash, DataHash transactionHash, byte[] unlockScript); } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/PayToPublicKeyPredicateVerifier.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/SignaturePredicateVerifier.java similarity index 67% rename from src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/PayToPublicKeyPredicateVerifier.java rename to src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/SignaturePredicateVerifier.java index d13a7eb..f3c0c54 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/PayToPublicKeyPredicateVerifier.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/SignaturePredicateVerifier.java @@ -5,35 +5,35 @@ import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.unicitylabs.sdk.crypto.secp256k1.Signature; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; -import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.builtin.BuiltInPredicateType; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; /** - * Verifies {@link PayToPublicKeyPredicate} unlock scripts using secp256k1 signatures. + * Verifies {@link SignaturePredicate} unlock scripts using secp256k1 signatures. */ -public class PayToPublicKeyPredicateVerifier implements BuiltInPredicateVerifier { +public class SignaturePredicateVerifier implements BuiltInPredicateVerifier { /** * Creates a verifier instance for pay-to-public-key predicates. */ - public PayToPublicKeyPredicateVerifier() { + public SignaturePredicateVerifier() { } @Override public BuiltInPredicateType getType() { - return BuiltInPredicateType.PAY_TO_PUBLIC_KEY; + return BuiltInPredicateType.SIGNATURE; } @Override - public VerificationResult verify(Predicate encodedPredicate, + public VerificationResult verify(EncodedPredicate encodedPredicate, DataHash sourceStateHash, DataHash transactionHash, byte[] unlockScript) { - PayToPublicKeyPredicate predicate = PayToPublicKeyPredicate.fromPredicate(encodedPredicate); + SignaturePredicate predicate = SignaturePredicate.fromPredicate(encodedPredicate); boolean result = SigningService.verifyWithPublicKey( new DataHasher(HashAlgorithm.SHA256) @@ -49,10 +49,10 @@ public VerificationResult verify(Predicate encodedPredicate, ); if (!result) { - return new VerificationResult<>("PayToPublicKeyPredicateVerifier", VerificationStatus.FAIL, + return new VerificationResult<>("SignaturePredicateVerifier", VerificationStatus.FAIL, "Signature verification failed."); } - return new VerificationResult<>("PayToPublicKeyPredicateVerifier", VerificationStatus.OK); + return new VerificationResult<>("SignaturePredicateVerifier", VerificationStatus.OK); } } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifier.java b/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifier.java index b9f506f..915eb09 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifier.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifier.java @@ -1,7 +1,7 @@ package org.unicitylabs.sdk.predicate.verification; import org.unicitylabs.sdk.crypto.hash.DataHash; -import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.PredicateEngine; import org.unicitylabs.sdk.util.verification.VerificationResult; import org.unicitylabs.sdk.util.verification.VerificationStatus; @@ -27,6 +27,6 @@ public interface PredicateVerifier { * @param unlockScript unlock script bytes * @return verification result with status and diagnostics */ - VerificationResult verify(Predicate predicate, DataHash sourceStateHash, + VerificationResult verify(EncodedPredicate predicate, DataHash sourceStateHash, DataHash transactionHash, byte[] unlockScript); } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifierService.java b/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifierService.java index 5695437..42b5416 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifierService.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifierService.java @@ -1,8 +1,7 @@ package org.unicitylabs.sdk.predicate.verification; -import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.crypto.hash.DataHash; -import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.PredicateEngine; import org.unicitylabs.sdk.predicate.builtin.DefaultBuiltInPredicateVerifier; import org.unicitylabs.sdk.util.verification.VerificationResult; @@ -25,12 +24,11 @@ private PredicateVerifierService() { /** * Creates a predicate verifier service with default verifier registrations. * - * @param trustBase root trust base used by verifiers that require trust context * @return initialized predicate verifier service */ - public static PredicateVerifierService create(RootTrustBase trustBase) { + public static PredicateVerifierService create() { PredicateVerifierService verifier = new PredicateVerifierService(); - verifier.addVerifier(DefaultBuiltInPredicateVerifier.create(verifier, trustBase)); + verifier.addVerifier(DefaultBuiltInPredicateVerifier.create()); return verifier; } @@ -63,8 +61,12 @@ public PredicateVerifierService addVerifier(PredicateVerifier verifier) { * @return verification result from the engine-specific verifier * @throws IllegalArgumentException if no verifier is registered for the predicate engine */ - public VerificationResult verify(Predicate predicate, - DataHash sourceStateHash, DataHash transactionHash, byte[] unlockScript) { + public VerificationResult verify( + EncodedPredicate predicate, + DataHash sourceStateHash, + DataHash transactionHash, + byte[] unlockScript + ) { PredicateVerifier verifier = this.verifiers.get(predicate.getEngine()); if (verifier == null) { throw new IllegalArgumentException( diff --git a/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializer.java b/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializer.java index 7712ebd..1d38d5a 100644 --- a/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializer.java +++ b/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializer.java @@ -21,16 +21,20 @@ private CborDeserializer() { * Read optional value from CBOR bytes. * * @param data bytes - * @param reader parse method + * @param decoder parse method * @param parsed value type * @return parsed value */ - public static T decodeNullable(byte[] data, Function reader) { - if (Byte.compareUnsigned(new CborReader(data).readByte(), (byte) 0xf6) == 0) { + public static T decodeNullable(byte[] data, Function decoder) { + CborReader reader = new CborReader(data); + byte[] cbor = reader.readRawCbor(); + reader.assertExhausted(); + + if (cbor.length == 1 && cbor[0] == (byte) 0xf6) { return null; } - return reader.apply(data); + return decoder.apply(cbor); } /** @@ -41,7 +45,10 @@ public static T decodeNullable(byte[] data, Function reader) { */ public static CborNumber decodeUnsignedInteger(byte[] data) { CborReader reader = new CborReader(data); - return new CborNumber(reader.readLength(CborMajorType.UNSIGNED_INTEGER)); + long value = reader.readLength(CborMajorType.UNSIGNED_INTEGER); + reader.assertExhausted(); + + return new CborNumber(value); } /** @@ -52,7 +59,10 @@ public static CborNumber decodeUnsignedInteger(byte[] data) { */ public static byte[] decodeByteString(byte[] data) { CborReader reader = new CborReader(data); - return reader.read((int) reader.readLength(CborMajorType.BYTE_STRING)); + byte[] result = reader.read((int) reader.readLength(CborMajorType.BYTE_STRING)); + reader.assertExhausted(); + + return result; } /** @@ -63,8 +73,10 @@ public static byte[] decodeByteString(byte[] data) { */ public static String decodeTextString(byte[] data) { CborReader reader = new CborReader(data); - return new String( - reader.read((int) reader.readLength(CborMajorType.TEXT_STRING))); + byte[] bytes = reader.read((int) reader.readLength(CborMajorType.TEXT_STRING)); + reader.assertExhausted(); + + return new String(bytes); } /** @@ -81,10 +93,30 @@ public static List decodeArray(byte[] data) { for (int i = 0; i < length; i++) { result.add(reader.readRawCbor()); } + reader.assertExhausted(); return result; } + /** + * Read a fixed-size CBOR array from bytes. Throws when the encoded array length does not match + * the expected length. + * + * @param data bytes + * @param expectedLength expected number of array elements + * @return CBOR element array + * + * @throws CborSerializationException when the array length differs from {@code expectedLength} + */ + public static List decodeArray(byte[] data, long expectedLength) { + List result = decodeArray(data); + if (result.size() != expectedLength) { + throw new CborSerializationException( + String.format("Expected array of %d elements, got %d", expectedLength, result.size())); + } + return result; + } + /** * Read elements as raw CBOR element map from CBOR bytes. * @@ -96,11 +128,23 @@ public static Set decodeMap(byte[] data) { long length = (int) reader.readLength(CborMajorType.MAP); Set result = new LinkedHashSet<>(); + Entry previous = null; for (int i = 0; i < length; i++) { - byte[] key = reader.readRawCbor(); - byte[] value = reader.readRawCbor(); - result.add(new CborMap.Entry(key, value)); + Entry entry = new CborMap.Entry(reader.readRawCbor(), reader.readRawCbor()); + + if (previous != null) { + int comparison = CborMap.compareEntries(previous, entry); + if (comparison == 0) { + throw new CborSerializationException("Duplicate map key found."); + } + if (comparison > 0) { + throw new CborSerializationException("Map keys are not in canonical order."); + } + } + result.add(entry); + previous = entry; } + reader.assertExhausted(); return result; } @@ -114,7 +158,10 @@ public static Set decodeMap(byte[] data) { public static CborTag decodeTag(byte[] data) { CborReader reader = new CborReader(data); long tag = (int) reader.readLength(CborMajorType.TAG); - return new CborTag(tag, reader.readRawCbor()); + byte[] inner = reader.readRawCbor(); + reader.assertExhausted(); + + return new CborTag(tag, inner); } /** @@ -124,11 +171,14 @@ public static CborTag decodeTag(byte[] data) { * @return boolean */ public static boolean decodeBoolean(byte[] data) { - byte byteValue = new CborReader(data).readByte(); - if (byteValue == (byte) 0xf5) { + CborReader reader = new CborReader(data); + byte[] cbor = reader.readRawCbor(); + reader.assertExhausted(); + + if (cbor.length == 1 && cbor[0] == (byte) 0xf5) { return true; } - if (byteValue == (byte) 0xf4) { + if (cbor.length == 1 && cbor[0] == (byte) 0xf4) { return false; } throw new CborSerializationException("Type mismatch, expected boolean."); @@ -145,6 +195,14 @@ private static class CborReader { this.data = data; } + public void assertExhausted() { + if (this.position != this.data.length) { + throw new CborSerializationException(String.format( + "Expected end of data: %d byte(s) remaining at position %d.", + this.data.length - this.position, this.position)); + } + } + public byte readByte() { if (this.position >= this.data.length) { throw new CborSerializationException("Premature end of data."); @@ -168,8 +226,11 @@ public byte[] read(int length) { public long readLength(CborMajorType majorType) { byte initialByte = this.readByte(); - if (CborMajorType.fromType(initialByte & CborDeserializer.MAJOR_TYPE_MASK) != majorType) { - throw new CborSerializationException("Major type mismatch."); + CborMajorType parsedMajorType = CborMajorType.fromType( + initialByte & CborDeserializer.MAJOR_TYPE_MASK); + if (parsedMajorType != majorType) { + throw new CborSerializationException(String.format( + "Major type mismatch: expected %s, got %s.", majorType, parsedMajorType)); } byte additionalInformation = (byte) (initialByte @@ -179,18 +240,23 @@ public long readLength(CborMajorType majorType) { } switch (majorType) { + case MAP: case ARRAY: case BYTE_STRING: case TEXT_STRING: if (Byte.compareUnsigned(additionalInformation, (byte) 31) == 0) { - throw new CborSerializationException("Indefinite length array not supported."); + throw new CborSerializationException(String.format( + "Indefinite-length encoding not allowed in canonical CBOR (major type %s).", + majorType)); } break; default: } if (Byte.compareUnsigned(additionalInformation, (byte) 27) > 0) { - throw new CborSerializationException("Encoded item is not well-formed."); + throw new CborSerializationException(String.format( + "Reserved additional information %d for major type %s.", + additionalInformation, majorType)); } long t = 0; @@ -199,6 +265,13 @@ public long readLength(CborMajorType majorType) { t = (t << 8) | this.readByte() & 0xFF; } + long threshold = length == 1 ? 24L : 1L << (length * 4); + if (Long.compareUnsigned(t, threshold) < 0) { + throw new CborSerializationException(String.format( + "Byte length %d is not canonical for value %s.", + length, Long.toUnsignedString(t))); + } + return t; } diff --git a/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborSerializer.java b/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborSerializer.java index db5e3b6..9cb62f5 100644 --- a/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborSerializer.java +++ b/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborSerializer.java @@ -236,19 +236,13 @@ public static final class CborMap { */ public CborMap(Set entries) { this.entries = new ArrayList<>(entries); - this.entries.sort((a, b) -> { - if (a.key.length != b.key.length) { - return a.key.length - b.key.length; - } + this.entries.sort(CborMap::compareEntries); - for (int i = 0; i < a.key.length; i++) { - if (a.key[i] != b.key[i]) { - return a.key[i] - b.key[i]; - } + for (int i = 1; i < this.entries.size(); i++) { + if (CborMap.compareEntries(this.entries.get(i - 1), this.entries.get(i)) == 0) { + throw new CborSerializationException("Duplicate map key in CborMap."); } - - return 0; - }); + } } /** @@ -260,6 +254,26 @@ public List getEntries() { return List.copyOf(this.entries); } + /** + * Compare two map entries by their CBOR-encoded keys using canonical bytewise lexicographic + * order (compare bytes byte-by-byte, then break ties by length). + * + * @param a first entry + * @param b second entry + * @return negative, zero, or positive per {@link Comparable} + */ + public static int compareEntries(Entry a, Entry b) { + int length = Math.min(a.key.length, b.key.length); + for (int i = 0; i < length; i++) { + int diff = Byte.toUnsignedInt(a.key[i]) - Byte.toUnsignedInt(b.key[i]); + if (diff != 0) { + return diff; + } + } + + return a.key.length - b.key.length; + } + /** * CBOR entry for map. */ diff --git a/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePath.java b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePath.java index a8ff834..1dcd88e 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePath.java +++ b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePath.java @@ -122,7 +122,7 @@ public MerkleTreePathVerificationResult verify(BigInteger stateId) { * @return path */ public static SparseMerkleTreePath fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new SparseMerkleTreePath( DataHash.fromCbor(data.get(0)), diff --git a/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePathStep.java b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePathStep.java index e0fefb4..931c850 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePathStep.java +++ b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePathStep.java @@ -63,7 +63,7 @@ public Optional getData() { * @return sparse Merkle tree path step */ public static SparseMerkleTreePathStep fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new SparseMerkleTreePathStep( BigIntegerConverter.decode(CborDeserializer.decodeByteString(data.get(0))), diff --git a/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePath.java b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePath.java index 907d2cb..da2e867 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePath.java +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePath.java @@ -132,7 +132,7 @@ public MerkleTreePathVerificationResult verify(BigInteger stateId) { * @return path */ public static SparseMerkleSumTreePath fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new SparseMerkleSumTreePath( DataHash.fromCbor(data.get(0)), @@ -176,71 +176,4 @@ public int hashCode() { public String toString() { return String.format("MerkleTreePath{rootHash=%s, steps=%s}", this.rootHash, this.steps); } - - /** - * Root of the sparse merkle sum tree path. - */ - public static class Root { - - private final DataHash hash; - private final BigInteger counter; - - Root( - DataHash hash, - BigInteger counter - ) { - this.hash = Objects.requireNonNull(hash, "hash cannot be null"); - this.counter = Objects.requireNonNull(counter, "counter cannot be null"); - } - - /** - * Get hash of the root. - * - * @return hash - */ - public DataHash getHash() { - return this.hash; - } - - /** - * Get the counter of the root. - * - * @return counter - */ - public BigInteger getCounter() { - return this.counter; - } - - /** - * Create root from CBOR bytes. - * - * @param bytes CBOR bytes - * @return root - */ - public static Root fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); - - return new Root( - DataHash.fromCbor(data.get(0)), - BigIntegerConverter.decode(CborDeserializer.decodeByteString(data.get(1))) - ); - } - - /** - * Serialize root to CBOR bytes. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeArray( - this.hash.toCbor(), - CborSerializer.encodeByteString(BigIntegerConverter.encode(this.counter)) - ); - } - - @Override - public String toString() { - return String.format("Root{hash=%s, counter=%s}", this.hash, this.counter); - } - } } diff --git a/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePathStep.java b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePathStep.java index dc0168f..532fce8 100644 --- a/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePathStep.java +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePathStep.java @@ -67,7 +67,7 @@ public BigInteger getValue() { * @return step */ public static SparseMerkleSumTreePathStep fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 3); return new SparseMerkleSumTreePathStep( BigIntegerConverter.decode(CborDeserializer.decodeByteString(data.get(0))), diff --git a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java index 7ea3b20..9c9e777 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java @@ -3,7 +3,7 @@ import org.unicitylabs.sdk.api.InclusionProof; import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.crypto.hash.DataHash; -import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; @@ -13,6 +13,7 @@ import org.unicitylabs.sdk.util.verification.VerificationResult; import java.util.List; +import java.util.Objects; import java.util.Optional; /** @@ -34,12 +35,12 @@ public Optional getData() { } @Override - public Predicate getLockScript() { + public EncodedPredicate getLockScript() { return this.transaction.getLockScript(); } @Override - public Predicate getRecipient() { + public EncodedPredicate getRecipient() { return this.transaction.getRecipient(); } @@ -91,7 +92,7 @@ public InclusionProof getInclusionProof() { * @return decoded certified mint transaction */ public static CertifiedMintTransaction fromCbor(byte[] bytes) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new CertifiedMintTransaction(MintTransaction.fromCbor(data.get(0)), InclusionProof.fromCbor(data.get(1))); } @@ -106,9 +107,17 @@ public static CertifiedMintTransaction fromCbor(byte[] bytes) { * @return certified mint transaction * @throws VerificationException if inclusion proof verification fails */ - public static CertifiedMintTransaction fromTransaction(RootTrustBase trustBase, - PredicateVerifierService predicateVerifier, MintTransaction transaction, - InclusionProof inclusionProof) { + public static CertifiedMintTransaction fromTransaction( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + MintTransaction transaction, + InclusionProof inclusionProof + ) { + Objects.requireNonNull(trustBase, "trustBase cannot be null"); + Objects.requireNonNull(predicateVerifier, "predicateVerifier cannot be null"); + Objects.requireNonNull(transaction, "transaction cannot be null"); + Objects.requireNonNull(inclusionProof, "inclusionProof cannot be null"); + VerificationResult result = InclusionProofVerificationRule.verify( trustBase, predicateVerifier, diff --git a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java index 1b9bb79..16beb0e 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java @@ -3,7 +3,7 @@ import org.unicitylabs.sdk.api.InclusionProof; import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.crypto.hash.DataHash; -import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; @@ -13,6 +13,7 @@ import org.unicitylabs.sdk.util.verification.VerificationResult; import java.util.List; +import java.util.Objects; import java.util.Optional; /** @@ -37,12 +38,12 @@ public Optional getData() { } @Override - public Predicate getLockScript() { + public EncodedPredicate getLockScript() { return this.transaction.getLockScript(); } @Override - public Predicate getRecipient() { + public EncodedPredicate getRecipient() { return this.transaction.getRecipient(); } @@ -74,7 +75,7 @@ public InclusionProof getInclusionProof() { * @return certified transfer transaction */ public static CertifiedTransferTransaction fromCbor(byte[] bytes, Token token) { - List data = CborDeserializer.decodeArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new CertifiedTransferTransaction( TransferTransaction.fromCbor(data.get(0), token), @@ -97,9 +98,17 @@ public static CertifiedTransferTransaction fromCbor(byte[] bytes, Token token) { * * @throws VerificationException if inclusion proof verification fails */ - public static CertifiedTransferTransaction fromTransaction(RootTrustBase trustBase, - PredicateVerifierService predicateVerifier, TransferTransaction transaction, - InclusionProof inclusionProof) { + public static CertifiedTransferTransaction fromTransaction( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + TransferTransaction transaction, + InclusionProof inclusionProof + ) { + Objects.requireNonNull(trustBase, "trustBase cannot be null"); + Objects.requireNonNull(predicateVerifier, "predicateVerifier cannot be null"); + Objects.requireNonNull(transaction, "transaction cannot be null"); + Objects.requireNonNull(inclusionProof, "inclusionProof cannot be null"); + VerificationResult result = InclusionProofVerificationRule.verify( trustBase, predicateVerifier, diff --git a/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java index aafac72..80eaf3a 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java @@ -9,7 +9,7 @@ import org.unicitylabs.sdk.crypto.secp256k1.SigningService; import org.unicitylabs.sdk.predicate.EncodedPredicate; import org.unicitylabs.sdk.predicate.Predicate; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.serializer.cbor.CborSerializationException; @@ -33,8 +33,8 @@ public class MintTransaction implements Transaction { private static final int VERSION = 1; private final MintTransactionState sourceStateHash; - private final Predicate lockScript; - private final Predicate recipient; + private final EncodedPredicate lockScript; + private final EncodedPredicate recipient; private final TokenId tokenId; private final TokenType tokenType; private final byte[] justification; @@ -42,8 +42,8 @@ public class MintTransaction implements Transaction { private MintTransaction( MintTransactionState sourceStateHash, - Predicate lockScript, - Predicate recipient, + EncodedPredicate lockScript, + EncodedPredicate recipient, TokenId tokenId, TokenType tokenType, byte[] justification, @@ -69,12 +69,12 @@ public MintTransactionState getSourceStateHash() { } @Override - public Predicate getLockScript() { + public EncodedPredicate getLockScript() { return this.lockScript; } @Override - public Predicate getRecipient() { + public EncodedPredicate getRecipient() { return this.recipient; } @@ -140,8 +140,8 @@ public static MintTransaction create( SigningService signingService = MintSigningService.create(tokenId); return new MintTransaction( MintTransactionState.create(tokenId), - PayToPublicKeyPredicate.fromSigningService(signingService), - recipient, + EncodedPredicate.fromPredicate(SignaturePredicate.fromSigningService(signingService)), + EncodedPredicate.fromPredicate(recipient), tokenId, tokenType, justification != null ? Arrays.copyOf(justification, justification.length) : null, @@ -161,7 +161,7 @@ public static MintTransaction fromCbor(byte[] bytes) { if (tag.getTag() != MintTransaction.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 6); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != MintTransaction.VERSION) { @@ -215,7 +215,7 @@ public byte[] toCbor() { MintTransaction.CBOR_TAG, CborSerializer.encodeArray( CborSerializer.encodeUnsignedInteger(MintTransaction.VERSION), - EncodedPredicate.fromPredicate(this.recipient).toCbor(), + this.recipient.toCbor(), this.tokenId.toCbor(), this.tokenType.toCbor(), CborSerializer.encodeNullable(this.justification, CborSerializer::encodeByteString), diff --git a/src/main/java/org/unicitylabs/sdk/transaction/Token.java b/src/main/java/org/unicitylabs/sdk/transaction/Token.java index 93f4d95..23b4cc0 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/Token.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/Token.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * Immutable token aggregate containing the certified genesis mint transaction and transfer history. @@ -98,7 +99,7 @@ public static Token fromCbor(byte[] bytes) { if (tag.getTag() != Token.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 3); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != Token.VERSION) { @@ -132,6 +133,11 @@ public static Token mint( MintJustificationVerifierService mintJustificationVerifier, CertifiedMintTransaction genesis ) { + Objects.requireNonNull(trustBase, "trustBase cannot be null"); + Objects.requireNonNull(predicateVerifier, "predicateVerifier cannot be null"); + Objects.requireNonNull(mintJustificationVerifier, "mintJustificationVerifier cannot be null"); + Objects.requireNonNull(genesis, "genesis cannot be null"); + Token token = new Token(genesis); VerificationResult result = token.verify(trustBase, predicateVerifier, mintJustificationVerifier); if (result.getStatus() != VerificationStatus.OK) { @@ -150,8 +156,15 @@ public static Token mint( * @return new token instance with appended transfer * @throws VerificationException if transfer verification fails */ - public Token transfer(RootTrustBase trustBase, PredicateVerifierService predicateVerifier, - CertifiedTransferTransaction transaction) { + public Token transfer( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + CertifiedTransferTransaction transaction + ) { + Objects.requireNonNull(trustBase, "trustBase cannot be null"); + Objects.requireNonNull(predicateVerifier, "predicateVerifier cannot be null"); + Objects.requireNonNull(transaction, "transaction cannot be null"); + VerificationResult result = CertifiedTransferTransactionVerificationRule.verify( trustBase, predicateVerifier, @@ -179,6 +192,10 @@ public VerificationResult verify( PredicateVerifierService predicateVerifier, MintJustificationVerifierService mintJustificationVerifier ) { + Objects.requireNonNull(trustBase, "trustBase cannot be null"); + Objects.requireNonNull(predicateVerifier, "predicateVerifier cannot be null"); + Objects.requireNonNull(mintJustificationVerifier, "mintJustificationVerifier cannot be null"); + List> results = new ArrayList<>(); VerificationResult result = CertifiedMintTransactionVerificationRule.verify( trustBase, diff --git a/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java b/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java index 4731cd1..38791ea 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java @@ -1,7 +1,7 @@ package org.unicitylabs.sdk.transaction; import org.unicitylabs.sdk.crypto.hash.DataHash; -import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.EncodedPredicate; import java.util.Optional; @@ -22,14 +22,14 @@ public interface Transaction { * * @return lock script predicate */ - Predicate getLockScript(); + EncodedPredicate getLockScript(); /** * Gets the transaction recipient. * * @return recipient predicate */ - Predicate getRecipient(); + EncodedPredicate getRecipient(); /** * Gets the source state hash. diff --git a/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java index b22214d..9390b38 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java @@ -25,15 +25,15 @@ public class TransferTransaction implements Transaction { private static final int VERSION = 1; private final DataHash sourceStateHash; - private final Predicate lockScript; - private final Predicate recipient; + private final EncodedPredicate lockScript; + private final EncodedPredicate recipient; private final byte[] stateMask; private final byte[] data; private TransferTransaction( DataHash sourceStateHash, - Predicate lockScript, - Predicate recipient, + EncodedPredicate lockScript, + EncodedPredicate recipient, byte[] stateMask, byte[] data ) { @@ -55,12 +55,12 @@ public Optional getData() { } @Override - public Predicate getLockScript() { + public EncodedPredicate getLockScript() { return this.lockScript; } @Override - public Predicate getRecipient() { + public EncodedPredicate getRecipient() { return this.recipient; } @@ -90,7 +90,7 @@ public static TransferTransaction create(Token token, Predicate recipient, return new TransferTransaction( transaction.calculateStateHash(), transaction.getRecipient(), - recipient, + EncodedPredicate.fromPredicate(recipient), stateMask, data ); @@ -108,7 +108,7 @@ public static TransferTransaction fromCbor(byte[] bytes, Token token) { if (tag.getTag() != TransferTransaction.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData()); + List data = CborDeserializer.decodeArray(tag.getData(), 4); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != TransferTransaction.VERSION) { diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedMintTransactionVerificationRule.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedMintTransactionVerificationRule.java index c4a4b72..4c041dc 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedMintTransactionVerificationRule.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedMintTransactionVerificationRule.java @@ -1,10 +1,11 @@ package org.unicitylabs.sdk.transaction.verification; +import org.unicitylabs.sdk.api.CertificationData; import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.crypto.MintSigningService; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; import org.unicitylabs.sdk.predicate.EncodedPredicate; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.transaction.CertifiedMintTransaction; import org.unicitylabs.sdk.util.verification.VerificationResult; @@ -44,13 +45,14 @@ public static VerificationResult verify( List> results = new ArrayList<>(); SigningService signingService = MintSigningService.create(transaction.getTokenId()); - VerificationResult result = Arrays.equals( - EncodedPredicate.fromPredicate(PayToPublicKeyPredicate.fromSigningService(signingService)) - .toCbor(), - transaction.getInclusionProof() - .getCertificationData() - .map(c -> EncodedPredicate.fromPredicate(c.getLockScript()).toCbor()) - .orElse(null)) + EncodedPredicate expectedLockScript = EncodedPredicate.fromPredicate(SignaturePredicate.fromSigningService(signingService)); + VerificationResult result = expectedLockScript + .equals( + transaction.getInclusionProof() + .getCertificationData() + .map(CertificationData::getLockScript) + .orElse(null) + ) ? new VerificationResult<>("IsLockScriptValidVerificationRule", VerificationStatus.OK) : new VerificationResult<>("IsLockScriptValidVerificationRule", VerificationStatus.FAIL); diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedUnicityIdMintTransactionVerificationRule.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedUnicityIdMintTransactionVerificationRule.java new file mode 100644 index 0000000..bf74896 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedUnicityIdMintTransactionVerificationRule.java @@ -0,0 +1,83 @@ +package org.unicitylabs.sdk.transaction.verification; + +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.predicate.EncodedPredicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.unicityid.CertifiedUnicityIdMintTransaction; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +import java.util.ArrayList; +import java.util.List; + +/** + * Verification rule for the genesis (mint) of a unicity id token. Validates the inclusion proof of + * the certified mint transaction, and optionally checks that the genesis lock script matches an + * expected issuer public key. + */ +public class CertifiedUnicityIdMintTransactionVerificationRule { + + private CertifiedUnicityIdMintTransactionVerificationRule() { + } + + /** + * Verify the certified unicity id mint transaction. + * + * @param trustBase root trust base + * @param predicateVerifier predicate verifier + * @param genesis certified unicity id mint transaction to verify + * @param issuerPublicKey expected issuer public key, or {@code null} to skip the lock-script + * issuer check (e.g., when minting a fresh token where no external issuer is being asserted) + * + * @return verification result + */ + public static VerificationResult verify( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + CertifiedUnicityIdMintTransaction genesis, + byte[] issuerPublicKey + ) { + List> results = new ArrayList<>(); + + if (issuerPublicKey != null) { + EncodedPredicate expectedLockScript = EncodedPredicate.fromPredicate( + SignaturePredicate.create(issuerPublicKey)); + if (!expectedLockScript.equals(genesis.getLockScript())) { + results.add(new VerificationResult<>("IsLockScriptValidVerificationRule", + VerificationStatus.FAIL)); + return new VerificationResult<>( + "CertifiedUnicityIdMintTransactionVerificationRule", + VerificationStatus.FAIL, + "Lock script does not match expected unicity-id issuer.", + results + ); + } + results.add(new VerificationResult<>("IsLockScriptValidVerificationRule", + VerificationStatus.OK)); + } + + VerificationResult result = InclusionProofVerificationRule.verify( + trustBase, + predicateVerifier, + genesis.getInclusionProof(), + genesis + ); + results.add(result); + if (result.getStatus() != InclusionProofVerificationStatus.OK) { + return new VerificationResult<>( + "CertifiedUnicityIdMintTransactionVerificationRule", + VerificationStatus.FAIL, + String.format("Inclusion proof verification failed: %s", result.getStatus()), + results + ); + } + + return new VerificationResult<>( + "CertifiedUnicityIdMintTransactionVerificationRule", + VerificationStatus.OK, + "", + results + ); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java b/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java new file mode 100644 index 0000000..3691253 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java @@ -0,0 +1,178 @@ +package org.unicitylabs.sdk.unicityid; + +import org.unicitylabs.sdk.api.InclusionProof; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.predicate.EncodedPredicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.transaction.TokenType; +import org.unicitylabs.sdk.transaction.Transaction; +import org.unicitylabs.sdk.transaction.verification.InclusionProofVerificationRule; +import org.unicitylabs.sdk.transaction.verification.InclusionProofVerificationStatus; +import org.unicitylabs.sdk.util.verification.VerificationException; +import org.unicitylabs.sdk.util.verification.VerificationResult; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Unicity id mint transaction bundled with a verified inclusion proof. + */ +public final class CertifiedUnicityIdMintTransaction implements Transaction { + + private final UnicityIdMintTransaction transaction; + private final InclusionProof inclusionProof; + + private CertifiedUnicityIdMintTransaction(UnicityIdMintTransaction transaction, + InclusionProof inclusionProof) { + this.transaction = transaction; + this.inclusionProof = inclusionProof; + } + + @Override + public Optional getData() { + return this.transaction.getData(); + } + + @Override + public EncodedPredicate getLockScript() { + return this.transaction.getLockScript(); + } + + @Override + public EncodedPredicate getRecipient() { + return this.transaction.getRecipient(); + } + + @Override + public DataHash getSourceStateHash() { + return this.transaction.getSourceStateHash(); + } + + @Override + public byte[] getStateMask() { + return this.transaction.getStateMask(); + } + + /** + * Returns the token id derived from the unicity id. + * + * @return token id + */ + public TokenId getTokenId() { + return this.transaction.getTokenId(); + } + + /** + * Returns the token type. + * + * @return token type + */ + public TokenType getTokenType() { + return this.transaction.getTokenType(); + } + + /** + * Returns the target predicate. + * + * @return target predicate + */ + public SignaturePredicate getTargetPredicate() { + return this.transaction.getTargetPredicate(); + } + + /** + * Returns the unicity id. + * + * @return unicity id + */ + public UnicityId getUnicityId() { + return this.transaction.getUnicityId(); + } + + /** + * Returns the inclusion proof certifying this transaction. + * + * @return inclusion proof + */ + public InclusionProof getInclusionProof() { + return this.inclusionProof; + } + + /** + * Deserializes a certified unicity id mint transaction from CBOR. + * + * @param bytes CBOR-encoded certified mint transaction + * + * @return decoded certified mint transaction + */ + public static CertifiedUnicityIdMintTransaction fromCbor(byte[] bytes) { + List data = CborDeserializer.decodeArray(bytes, 2); + return new CertifiedUnicityIdMintTransaction( + UnicityIdMintTransaction.fromCbor(data.get(0)), + InclusionProof.fromCbor(data.get(1)) + ); + } + + /** + * Creates a certified unicity id mint transaction after verifying its inclusion proof. + * + * @param trustBase trust base used to verify inclusion proof signatures + * @param predicateVerifier predicate verifier service + * @param transaction unicity id mint transaction to certify + * @param inclusionProof inclusion proof for the transaction + * + * @return certified mint transaction + * + * @throws VerificationException if inclusion proof verification fails + */ + public static CertifiedUnicityIdMintTransaction fromTransaction( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + UnicityIdMintTransaction transaction, + InclusionProof inclusionProof + ) { + Objects.requireNonNull(trustBase, "trustBase cannot be null"); + Objects.requireNonNull(predicateVerifier, "predicateVerifier cannot be null"); + Objects.requireNonNull(transaction, "transaction cannot be null"); + Objects.requireNonNull(inclusionProof, "inclusionProof cannot be null"); + + VerificationResult result = InclusionProofVerificationRule.verify( + trustBase, + predicateVerifier, + inclusionProof, + transaction + ); + if (result.getStatus() != InclusionProofVerificationStatus.OK) { + throw new VerificationException("Inclusion proof verification failed", result); + } + + return new CertifiedUnicityIdMintTransaction(transaction, inclusionProof); + } + + @Override + public DataHash calculateStateHash() { + return this.transaction.calculateStateHash(); + } + + @Override + public DataHash calculateTransactionHash() { + return this.transaction.calculateTransactionHash(); + } + + @Override + public byte[] toCbor() { + return CborSerializer.encodeArray(this.transaction.toCbor(), this.inclusionProof.toCbor()); + } + + @Override + public String toString() { + return String.format("CertifiedUnicityIdMintTransaction{transaction=%s, inclusionProof=%s}", + this.transaction, this.inclusionProof); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityId.java b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityId.java new file mode 100644 index 0000000..48bc1db --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityId.java @@ -0,0 +1,127 @@ +package org.unicitylabs.sdk.unicityid; + +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.DataHasher; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.transaction.TokenId; + +import java.util.List; +import java.util.Objects; + +/** + * Human-readable identifier for a unicity token. The pair (domain, name) is hashed deterministically + * to derive the corresponding {@link TokenId}. + */ +public final class UnicityId { + + private final String name; + private final String domain; + + /** + * Create a unicity id with name only (no domain). + * + * @param name token name + */ + public UnicityId(String name) { + this(name, null); + } + + /** + * Create a unicity id. + * + * @param name token name + * @param domain optional domain; may be null + */ + public UnicityId(String name, String domain) { + this.name = Objects.requireNonNull(name, "name cannot be null"); + this.domain = domain; + } + + /** + * Get the token name. + * + * @return name + */ + public String getName() { + return this.name; + } + + /** + * Get the optional domain. + * + * @return domain, or null if not set + */ + public String getDomain() { + return this.domain; + } + + /** + * Deserialize a unicity id from CBOR bytes. + * + * @param bytes CBOR bytes + * + * @return unicity id + */ + public static UnicityId fromCbor(byte[] bytes) { + List data = CborDeserializer.decodeArray(bytes, 2); + return new UnicityId( + CborDeserializer.decodeTextString(data.get(0)), + CborDeserializer.decodeNullable(data.get(1), CborDeserializer::decodeTextString) + ); + } + + /** + * Serialize the unicity id to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeArray( + CborSerializer.encodeTextString(this.name), + CborSerializer.encodeNullable(this.domain, CborSerializer::encodeTextString) + ); + } + + /** + * Derive the token id from this unicity id by hashing the tagged ("NAMETAG_", domain, name) + * tuple with SHA-256. + * + * @return derived token id + */ + public TokenId toTokenId() { + DataHash hash = new DataHasher(HashAlgorithm.SHA256) + .update( + CborSerializer.encodeArray( + CborSerializer.encodeTextString("NAMETAG_"), + CborSerializer.encodeNullable(this.domain, CborSerializer::encodeTextString), + CborSerializer.encodeTextString(this.name) + ) + ) + .digest(); + return new TokenId(hash.getData()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof UnicityId)) { + return false; + } + UnicityId that = (UnicityId) o; + return this.name.equals(that.name) && Objects.equals(this.domain, that.domain); + } + + @Override + public int hashCode() { + return Objects.hash(this.name, this.domain); + } + + @Override + public String toString() { + return "@" + (this.domain != null ? this.domain + "/" : "") + this.name; + } +} diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java new file mode 100644 index 0000000..b7ffb1f --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java @@ -0,0 +1,260 @@ +package org.unicitylabs.sdk.unicityid; + +import org.unicitylabs.sdk.api.InclusionProof; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.DataHasher; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.predicate.EncodedPredicate; +import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializationException; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.transaction.MintTransactionState; +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.transaction.TokenType; +import org.unicitylabs.sdk.transaction.Transaction; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Mint transaction that derives its token id from a {@link UnicityId}. The token's data field is + * the encoded target predicate. + */ +public final class UnicityIdMintTransaction implements Transaction { + public static final long CBOR_TAG = 39041; + private static final int VERSION = 1; + + private final MintTransactionState sourceStateHash; + private final EncodedPredicate lockScript; + private final EncodedPredicate recipient; + private final TokenId tokenId; + private final TokenType tokenType; + private final SignaturePredicate targetPredicate; + private final UnicityId unicityId; + + private UnicityIdMintTransaction( + MintTransactionState sourceStateHash, + EncodedPredicate lockScript, + EncodedPredicate recipient, + TokenId tokenId, + TokenType tokenType, + SignaturePredicate targetPredicate, + UnicityId unicityId + ) { + this.sourceStateHash = sourceStateHash; + this.lockScript = lockScript; + this.recipient = recipient; + this.tokenId = tokenId; + this.tokenType = tokenType; + this.targetPredicate = targetPredicate; + this.unicityId = unicityId; + } + + /** + * Get the version number. + * + * @return version + */ + public int getVersion() { + return UnicityIdMintTransaction.VERSION; + } + + @Override + public MintTransactionState getSourceStateHash() { + return this.sourceStateHash; + } + + @Override + public EncodedPredicate getLockScript() { + return this.lockScript; + } + + @Override + public EncodedPredicate getRecipient() { + return this.recipient; + } + + /** + * Get the token id derived from the unicity id. + * + * @return token id + */ + public TokenId getTokenId() { + return this.tokenId; + } + + /** + * Get the token type. + * + * @return token type + */ + public TokenType getTokenType() { + return this.tokenType; + } + + /** + * Get the target predicate (the predicate the minted token is locked to). + * + * @return target predicate + */ + public SignaturePredicate getTargetPredicate() { + return this.targetPredicate; + } + + /** + * Get the unicity id. + * + * @return unicity id + */ + public UnicityId getUnicityId() { + return this.unicityId; + } + + @Override + public Optional getData() { + return Optional.of(EncodedPredicate.fromPredicate(this.targetPredicate).toCbor()); + } + + @Override + public byte[] getStateMask() { + return this.tokenId.getBytes(); + } + + /** + * Create a unicity id mint transaction. The token id is derived from the unicity id; the lock + * script is supplied by the caller. + * + * @param lockScript lock script predicate (the predicate that must be unlocked to spend this + * transaction) + * @param recipient recipient predicate + * @param unicityId unicity id producing the token id + * @param tokenType token type identifier + * @param targetPredicate target predicate the minted token will be locked to + * + * @return mint transaction + */ + public static UnicityIdMintTransaction create( + SignaturePredicate lockScript, + Predicate recipient, + UnicityId unicityId, + TokenType tokenType, + SignaturePredicate targetPredicate + ) { + Objects.requireNonNull(lockScript, "lockScript cannot be null"); + Objects.requireNonNull(recipient, "recipient cannot be null"); + Objects.requireNonNull(unicityId, "unicityId cannot be null"); + Objects.requireNonNull(tokenType, "tokenType cannot be null"); + Objects.requireNonNull(targetPredicate, "targetPredicate cannot be null"); + + TokenId tokenId = unicityId.toTokenId(); + + return new UnicityIdMintTransaction( + MintTransactionState.create(tokenId), + EncodedPredicate.fromPredicate(lockScript), + EncodedPredicate.fromPredicate(recipient), + tokenId, + tokenType, + targetPredicate, + unicityId + ); + } + + /** + * Deserialize a unicity id mint transaction from CBOR bytes. + * + * @param bytes CBOR bytes + * + * @return mint transaction + * + * @throws CborSerializationException if the bytes do not carry the expected tag, version, or if + * the encoded token id does not match the unicity id + */ + public static UnicityIdMintTransaction fromCbor(byte[] bytes) { + CborDeserializer.CborTag tag = CborDeserializer.decodeTag(bytes); + if (tag.getTag() != UnicityIdMintTransaction.CBOR_TAG) { + throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); + } + List data = CborDeserializer.decodeArray(tag.getData(), 6); + + int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); + if (version != UnicityIdMintTransaction.VERSION) { + throw new CborSerializationException(String.format("Unsupported version: %s", version)); + } + + return UnicityIdMintTransaction.create( + SignaturePredicate.fromPredicate( + EncodedPredicate.fromCbor(data.get(1)) + ), + EncodedPredicate.fromCbor(data.get(2)), + UnicityId.fromCbor(data.get(3)), + TokenType.fromCbor(data.get(4)), + SignaturePredicate.fromPredicate( + EncodedPredicate.fromCbor(data.get(5)) + ) + ); + } + + @Override + public DataHash calculateStateHash() { + return new DataHasher(HashAlgorithm.SHA256) + .update( + CborSerializer.encodeArray( + CborSerializer.encodeByteString(this.sourceStateHash.getImprint()), + CborSerializer.encodeByteString(this.getStateMask()) + ) + ) + .digest(); + } + + @Override + public DataHash calculateTransactionHash() { + return new DataHasher(HashAlgorithm.SHA256).update(this.toCbor()).digest(); + } + + @Override + public byte[] toCbor() { + return CborSerializer.encodeTag( + UnicityIdMintTransaction.CBOR_TAG, + CborSerializer.encodeArray( + CborSerializer.encodeUnsignedInteger(UnicityIdMintTransaction.VERSION), + this.lockScript.toCbor(), + this.recipient.toCbor(), + this.unicityId.toCbor(), + this.tokenType.toCbor(), + EncodedPredicate.fromPredicate(this.targetPredicate).toCbor() + ) + ); + } + + /** + * Build the certified version by attaching and verifying an inclusion proof. + * + * @param trustBase root trust base + * @param predicateVerifier predicate verifier + * @param inclusionProof inclusion proof + * + * @return certified mint transaction + */ + public CertifiedUnicityIdMintTransaction toCertifiedTransaction( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + InclusionProof inclusionProof + ) { + return CertifiedUnicityIdMintTransaction.fromTransaction(trustBase, predicateVerifier, this, + inclusionProof); + } + + @Override + public String toString() { + return String.format( + "UnicityIdMintTransaction{lockScript=%s, recipient=%s, tokenId=%s, tokenType=%s, unicityId=%s, targetPredicate=%s}", + this.lockScript, this.recipient, this.tokenId, this.tokenType, this.unicityId, + this.targetPredicate + ); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java new file mode 100644 index 0000000..d74a4c1 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdToken.java @@ -0,0 +1,159 @@ +package org.unicitylabs.sdk.unicityid; + +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.transaction.TokenType; +import org.unicitylabs.sdk.transaction.verification.CertifiedUnicityIdMintTransactionVerificationRule; +import org.unicitylabs.sdk.util.verification.VerificationException; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Token whose genesis is a {@link CertifiedUnicityIdMintTransaction}. The token's identifier is + * deterministically derived from a {@link UnicityId}. + */ +public final class UnicityIdToken { + + private final CertifiedUnicityIdMintTransaction genesis; + + private UnicityIdToken(CertifiedUnicityIdMintTransaction genesis) { + this.genesis = genesis; + } + + /** + * Returns the certified genesis mint transaction. + * + * @return genesis transaction + */ + public CertifiedUnicityIdMintTransaction getGenesis() { + return this.genesis; + } + + /** + * Returns the token id. + * + * @return token id + */ + public TokenId getId() { + return this.genesis.getTokenId(); + } + + /** + * Returns the token type. + * + * @return token type + */ + public TokenType getType() { + return this.genesis.getTokenType(); + } + + /** + * Returns the unicity id used to derive this token's identifier. + * + * @return unicity id + */ + public UnicityId getUnicityId() { + return this.genesis.getUnicityId(); + } + + /** + * Deserialize a unicity id token from CBOR bytes. + * + * @param bytes CBOR bytes + * + * @return decoded token + */ + public static UnicityIdToken fromCbor(byte[] bytes) { + List data = CborDeserializer.decodeArray(bytes, 1); + return new UnicityIdToken(CertifiedUnicityIdMintTransaction.fromCbor(data.get(0))); + } + + /** + * Build a unicity id token from a certified genesis transaction and verify it. + * + * @param trustBase trust base used for certification verification + * @param predicateVerifier predicate verifier service + * @param genesis certified mint transaction + * + * @return verified token + * + * @throws VerificationException if genesis verification fails + */ + public static UnicityIdToken mint( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + CertifiedUnicityIdMintTransaction genesis + ) { + Objects.requireNonNull(trustBase, "trustBase cannot be null"); + Objects.requireNonNull(predicateVerifier, "predicateVerifier cannot be null"); + Objects.requireNonNull(genesis, "genesis cannot be null"); + + VerificationResult result = + CertifiedUnicityIdMintTransactionVerificationRule.verify( + trustBase, + predicateVerifier, + genesis, + null + ); + if (result.getStatus() != VerificationStatus.OK) { + throw new VerificationException("Invalid token genesis", result); + } + + return new UnicityIdToken(genesis); + } + + /** + * Serialize this token to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeArray(this.genesis.toCbor()); + } + + /** + * Verify the token by validating its certified mint transaction against an expected issuer. + * + * @param trustBase trust base used for certification verification + * @param predicateVerifier predicate verifier service + * @param issuerPublicKey expected issuer public key + * + * @return verification result + * @throws NullPointerException if {@code issuerPublicKey} is {@code null} + */ + public VerificationResult verify( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + byte[] issuerPublicKey + ) { + Objects.requireNonNull(trustBase, "trustBase cannot be null"); + Objects.requireNonNull(predicateVerifier, "predicateVerifier cannot be null"); + Objects.requireNonNull(issuerPublicKey, "issuerPublicKey cannot be null"); + + List> results = new ArrayList<>(); + VerificationResult result = CertifiedUnicityIdMintTransactionVerificationRule.verify( + trustBase, + predicateVerifier, + this.genesis, + issuerPublicKey + ); + results.add(result); + if (result.getStatus() != VerificationStatus.OK) { + return new VerificationResult<>("TokenVerification", VerificationStatus.FAIL, "", results); + } + + return new VerificationResult<>("TokenVerification", VerificationStatus.OK, "", results); + } + + @Override + public String toString() { + return String.format("UnicityIdToken{genesis=%s}", this.genesis); + } +} diff --git a/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java b/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java index d5bd967..93be061 100644 --- a/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java +++ b/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java @@ -26,7 +26,7 @@ private TestAggregatorClient(SparseMerkleTree smt, SigningService signingService this.sparseMerkleTree = smt; this.signingService = signingService; this.trustBase = RootTrustBaseUtils.generateRootTrustBase(this.signingService.getPublicKey()); - this.predicateVerifier = PredicateVerifierService.create(this.trustBase); + this.predicateVerifier = PredicateVerifierService.create(); } public RootTrustBase getTrustBase() { diff --git a/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java b/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java index 2cbd995..c3bc5c2 100644 --- a/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java +++ b/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java @@ -7,7 +7,7 @@ import org.unicitylabs.sdk.api.*; import org.unicitylabs.sdk.api.jsonrpc.JsonRpcNetworkException; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; import org.unicitylabs.sdk.transaction.MintTransaction; import org.unicitylabs.sdk.transaction.TokenId; import org.unicitylabs.sdk.transaction.TokenType; @@ -43,7 +43,7 @@ void setUp() throws Exception { HexConverter.decode("0000000000000000000000000000000000000000000000000000000000000001")); MintTransaction transaction = MintTransaction.create( - PayToPublicKeyPredicate.fromSigningService(signingService), + SignaturePredicate.fromSigningService(signingService), TokenId.generate(), TokenType.generate(), null, diff --git a/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java b/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java index 938d443..faa0064 100644 --- a/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java +++ b/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java @@ -12,8 +12,8 @@ import org.unicitylabs.sdk.crypto.hash.DataHash; import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicateUnlockScript; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.smt.radix.FinalizedNodeBranch; import org.unicitylabs.sdk.smt.radix.SparseMerkleTree; @@ -42,7 +42,7 @@ public void createMerkleTreePath() throws Exception { transaction = MintTransaction.create( - PayToPublicKeyPredicate.fromSigningService(signingService), + SignaturePredicate.fromSigningService(signingService), TokenId.generate(), TokenType.generate(), null, @@ -60,7 +60,7 @@ public void createMerkleTreePath() throws Exception { // Reuse user signing service as unicity certificate signing service. trustBase = RootTrustBaseUtils.generateRootTrustBase(signingService.getPublicKey()); unicityCertificate = UnicityCertificateUtils.generateCertificate(signingService, root.getHash()); - predicateVerifier = PredicateVerifierService.create(trustBase); + predicateVerifier = PredicateVerifierService.create(); } @Test @@ -147,7 +147,7 @@ public void testItNotAuthenticated() { this.certificationData.getLockScript(), this.certificationData.getSourceStateHash(), this.certificationData.getTransactionHash(), - PayToPublicKeyPredicateUnlockScript.create( + SignaturePredicateUnlockScript.create( this.transaction, new SigningService(SigningService.generatePrivateKey()) ).encode() diff --git a/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java b/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java index 44b326d..25859d7 100644 --- a/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java +++ b/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java @@ -3,12 +3,22 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.unicitylabs.sdk.StateTransitionClient; +import org.unicitylabs.sdk.api.CertificationData; +import org.unicitylabs.sdk.api.CertificationResponse; +import org.unicitylabs.sdk.api.CertificationStatus; import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.crypto.secp256k1.SigningService; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicateUnlockScript; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.transaction.Token; +import org.unicitylabs.sdk.transaction.TokenType; import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifierService; +import org.unicitylabs.sdk.unicityid.UnicityId; +import org.unicitylabs.sdk.unicityid.UnicityIdMintTransaction; +import org.unicitylabs.sdk.unicityid.UnicityIdToken; +import org.unicitylabs.sdk.util.HexConverter; +import org.unicitylabs.sdk.util.InclusionProofUtils; import org.unicitylabs.sdk.util.verification.VerificationStatus; import org.unicitylabs.sdk.utils.TokenUtils; @@ -36,7 +46,7 @@ public void testTransferFlow() throws Exception { this.trustBase, this.predicateVerifier, this.mintJustificationVerifier, - PayToPublicKeyPredicate.create(ALICE_SIGNING_SERVICE.getPublicKey()) + SignaturePredicate.create(ALICE_SIGNING_SERVICE.getPublicKey()) ); Token bobToken = TokenUtils.transferToken( @@ -45,7 +55,7 @@ public void testTransferFlow() throws Exception { this.predicateVerifier, this.mintJustificationVerifier, aliceToken.toCbor(), - PayToPublicKeyPredicate.create(BOB_SIGNING_SERVICE.getPublicKey()), + SignaturePredicate.create(BOB_SIGNING_SERVICE.getPublicKey()), ALICE_SIGNING_SERVICE ); @@ -55,11 +65,74 @@ public void testTransferFlow() throws Exception { this.predicateVerifier, this.mintJustificationVerifier, bobToken.toCbor(), - PayToPublicKeyPredicate.create(CAROL_SIGNING_SERVICE.getPublicKey()), + SignaturePredicate.create(CAROL_SIGNING_SERVICE.getPublicKey()), BOB_SIGNING_SERVICE ); Assertions.assertEquals(VerificationStatus.OK, carolToken.verify(this.trustBase, this.predicateVerifier, this.mintJustificationVerifier).getStatus()); } + + /** + * Default successful flow: mint a unicity-id token and then mint a regular token whose recipient + * is the unicity-id token's target predicate. + */ + @Test + public void testUnicityIdMintFlow() throws Exception { + SigningService unicityIdSigningService = SigningService.generate(); + SignaturePredicate targetPredicate = SignaturePredicate.create( + ALICE_SIGNING_SERVICE.getPublicKey()); + + UnicityId unicityId = new UnicityId("testuser", "unicity-labs/test"); + UnicityIdMintTransaction unicityIdMintTransaction = UnicityIdMintTransaction.create( + SignaturePredicate.fromSigningService(unicityIdSigningService), + targetPredicate, + unicityId, + TokenType.generate(), + targetPredicate + ); + + CertificationData unicityIdCertificationData = CertificationData.fromTransaction( + unicityIdMintTransaction, + SignaturePredicateUnlockScript.create(unicityIdMintTransaction, unicityIdSigningService) + ); + + CertificationResponse unicityIdResponse = this.client + .submitCertificationRequest(unicityIdCertificationData).get(); + Assertions.assertEquals(CertificationStatus.SUCCESS, unicityIdResponse.getStatus()); + + UnicityIdToken aliceUnicityIdToken = UnicityIdToken.mint( + this.trustBase, + this.predicateVerifier, + unicityIdMintTransaction.toCertifiedTransaction( + this.trustBase, + this.predicateVerifier, + InclusionProofUtils.waitInclusionProof(this.client, this.trustBase, + this.predicateVerifier, unicityIdMintTransaction).get() + ) + ); + + Assertions.assertEquals(VerificationStatus.OK, + aliceUnicityIdToken.verify(this.trustBase, this.predicateVerifier, + unicityIdSigningService.getPublicKey()).getStatus()); + + UnicityIdToken decodedUnicityIdToken = UnicityIdToken.fromCbor(aliceUnicityIdToken.toCbor()); + Assertions.assertArrayEquals(aliceUnicityIdToken.toCbor(), decodedUnicityIdToken.toCbor()); + Assertions.assertEquals(aliceUnicityIdToken.getId(), decodedUnicityIdToken.getId()); + Assertions.assertEquals(VerificationStatus.OK, + decodedUnicityIdToken.verify(this.trustBase, this.predicateVerifier, + unicityIdSigningService.getPublicKey()).getStatus()); + + Token aliceToken = TokenUtils.mintToken( + this.client, + this.trustBase, + this.predicateVerifier, + this.mintJustificationVerifier, + aliceUnicityIdToken.getGenesis().getTargetPredicate() + ); + + Assertions.assertEquals(VerificationStatus.OK, + aliceToken.verify(this.trustBase, this.predicateVerifier, this.mintJustificationVerifier) + .getStatus()); + } } \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/e2e/TokenE2ETest.java b/src/test/java/org/unicitylabs/sdk/e2e/TokenE2ETest.java index 032950b..d829eb0 100644 --- a/src/test/java/org/unicitylabs/sdk/e2e/TokenE2ETest.java +++ b/src/test/java/org/unicitylabs/sdk/e2e/TokenE2ETest.java @@ -38,7 +38,7 @@ void setUp() throws IOException { try (InputStream stream = getClass().getResourceAsStream("/trust-base.json")) { assertNotNull(stream, "trust-base.json not found"); this.trustBase = RootTrustBase.fromJson(new String(stream.readAllBytes())); - this.predicateVerifier = PredicateVerifierService.create(this.trustBase); + this.predicateVerifier = PredicateVerifierService.create(); this.mintJustificationVerifier = new MintJustificationVerifierService(); } } diff --git a/src/test/java/org/unicitylabs/sdk/functional/FunctionalCommonFlowTest.java b/src/test/java/org/unicitylabs/sdk/functional/FunctionalCommonFlowTest.java index 3d0b676..89b3df9 100644 --- a/src/test/java/org/unicitylabs/sdk/functional/FunctionalCommonFlowTest.java +++ b/src/test/java/org/unicitylabs/sdk/functional/FunctionalCommonFlowTest.java @@ -14,7 +14,7 @@ void setUp() { TestAggregatorClient aggregatorClient = TestAggregatorClient.create(); this.client = new StateTransitionClient(aggregatorClient); this.trustBase = aggregatorClient.getTrustBase(); - this.predicateVerifier = PredicateVerifierService.create(this.trustBase); + this.predicateVerifier = PredicateVerifierService.create(); this.mintJustificationVerifier = new MintJustificationVerifierService(); } } \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java index 0e2a369..bad56fb 100644 --- a/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java @@ -12,8 +12,8 @@ import org.unicitylabs.sdk.payment.TokenSplit; import org.unicitylabs.sdk.payment.asset.Asset; import org.unicitylabs.sdk.payment.asset.AssetId; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicateUnlockScript; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.transaction.Token; import org.unicitylabs.sdk.transaction.TokenId; @@ -40,14 +40,14 @@ public void buildAndVerifySplitToken() throws Exception { TestAggregatorClient aggregatorClient = TestAggregatorClient.create(); RootTrustBase trustBase = aggregatorClient.getTrustBase(); StateTransitionClient client = new StateTransitionClient(aggregatorClient); - PredicateVerifierService predicateVerifier = PredicateVerifierService.create(trustBase); + PredicateVerifierService predicateVerifier = PredicateVerifierService.create(); MintJustificationVerifierService mintJustificationVerifier = new MintJustificationVerifierService(); mintJustificationVerifier.register(new SplitMintJustificationVerifier( trustBase, predicateVerifier, TestPaymentData::decode)); SigningService signingService = SigningService.generate(); - PayToPublicKeyPredicate ownerPredicate = PayToPublicKeyPredicate.fromSigningService(signingService); + SignaturePredicate ownerPredicate = SignaturePredicate.fromSigningService(signingService); Set assets = Set.of( new Asset(new AssetId("ASSET_1".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)), @@ -77,7 +77,7 @@ public void buildAndVerifySplitToken() throws Exception { predicateVerifier, sourceToken, split.getBurnTransaction(), - PayToPublicKeyPredicateUnlockScript.create(split.getBurnTransaction(), signingService) + SignaturePredicateUnlockScript.create(split.getBurnTransaction(), signingService) ); SplitMintJustification justification = SplitMintJustification.create( diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/SplitMintJustificationVerifierTest.java b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitMintJustificationVerifierTest.java index 888fcd3..1cae00d 100644 --- a/src/test/java/org/unicitylabs/sdk/functional/payment/SplitMintJustificationVerifierTest.java +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitMintJustificationVerifierTest.java @@ -17,8 +17,8 @@ import org.unicitylabs.sdk.payment.TokenSplit; import org.unicitylabs.sdk.payment.asset.Asset; import org.unicitylabs.sdk.payment.asset.AssetId; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicateUnlockScript; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; @@ -70,7 +70,7 @@ public void setupFixture() throws Exception { this.trustBase = aggregatorClient.getTrustBase(); StateTransitionClient client = new StateTransitionClient(aggregatorClient); - this.predicateVerifier = PredicateVerifierService.create(this.trustBase); + this.predicateVerifier = PredicateVerifierService.create(); this.splitMintJustificationVerifier = new SplitMintJustificationVerifier( this.trustBase, this.predicateVerifier, TestPaymentData::decode); @@ -78,7 +78,7 @@ public void setupFixture() throws Exception { this.mintJustificationVerifier.register(this.splitMintJustificationVerifier); SigningService signingService = SigningService.generate(); - PayToPublicKeyPredicate ownerPredicate = PayToPublicKeyPredicate.fromSigningService(signingService); + SignaturePredicate ownerPredicate = SignaturePredicate.fromSigningService(signingService); this.asset1 = new Asset(new AssetId("ASSET_1".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); this.asset2 = new Asset(new AssetId("ASSET_2".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); @@ -108,7 +108,7 @@ public void setupFixture() throws Exception { this.predicateVerifier, sourceToken, split.getBurnTransaction(), - PayToPublicKeyPredicateUnlockScript.create(split.getBurnTransaction(), signingService) + SignaturePredicateUnlockScript.create(split.getBurnTransaction(), signingService) ); this.splitJustification = SplitMintJustification.create( diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/TokenSplitTest.java b/src/test/java/org/unicitylabs/sdk/functional/payment/TokenSplitTest.java index 78194a4..cf56c85 100644 --- a/src/test/java/org/unicitylabs/sdk/functional/payment/TokenSplitTest.java +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/TokenSplitTest.java @@ -12,7 +12,7 @@ import org.unicitylabs.sdk.payment.TokenSplit; import org.unicitylabs.sdk.payment.asset.Asset; import org.unicitylabs.sdk.payment.asset.AssetId; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.transaction.Token; import org.unicitylabs.sdk.transaction.TokenId; @@ -39,14 +39,14 @@ public void setupFixture() throws Exception { TestAggregatorClient aggregatorClient = TestAggregatorClient.create(); RootTrustBase trustBase = aggregatorClient.getTrustBase(); StateTransitionClient client = new StateTransitionClient(aggregatorClient); - PredicateVerifierService predicateVerifier = PredicateVerifierService.create(trustBase); + PredicateVerifierService predicateVerifier = PredicateVerifierService.create(); MintJustificationVerifierService mintJustificationVerifier = new MintJustificationVerifierService(); mintJustificationVerifier.register(new SplitMintJustificationVerifier( trustBase, predicateVerifier, TestPaymentData::decode)); SigningService signingService = SigningService.generate(); - PayToPublicKeyPredicate ownerPredicate = PayToPublicKeyPredicate.fromSigningService(signingService); + SignaturePredicate ownerPredicate = SignaturePredicate.fromSigningService(signingService); this.asset1 = new Asset(new AssetId("ASSET_1".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); this.asset2 = new Asset(new AssetId("ASSET_2".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); diff --git a/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java b/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java index f5c7c10..1fa7845 100644 --- a/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java +++ b/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java @@ -9,7 +9,7 @@ import org.unicitylabs.sdk.crypto.secp256k1.SigningService; import org.unicitylabs.sdk.predicate.Predicate; import org.unicitylabs.sdk.predicate.UnlockScript; -import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicateUnlockScript; import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.transaction.*; @@ -148,7 +148,7 @@ public static Token transferToken( predicateVerifier, token, transaction, - PayToPublicKeyPredicateUnlockScript.create(transaction, signingService) + SignaturePredicateUnlockScript.create(transaction, signingService) ); }