From cff52e9f820967de6c1ff74688bc9670d2085dcd Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Mon, 15 Jun 2026 17:43:52 +0000 Subject: [PATCH 1/2] #71 Add network id to unicity id minting --- .../sdk/api/CertificationStatus.java | 87 +++++++++++++++---- ...cityIdMintTransactionVerificationRule.java | 13 +++ .../CertifiedUnicityIdMintTransaction.java | 10 +++ .../unicitylabs/sdk/unicityid/UnicityId.java | 12 +-- .../unicityid/UnicityIdMintTransaction.java | 39 ++++++--- .../sdk/common/CommonTestFlow.java | 1 + 6 files changed, 129 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/unicitylabs/sdk/api/CertificationStatus.java b/src/main/java/org/unicitylabs/sdk/api/CertificationStatus.java index d062de4..911202b 100644 --- a/src/main/java/org/unicitylabs/sdk/api/CertificationStatus.java +++ b/src/main/java/org/unicitylabs/sdk/api/CertificationStatus.java @@ -1,50 +1,75 @@ package org.unicitylabs.sdk.api; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + /** - * Status codes for certification. + * Status codes for certification. Known statuses are exposed as singleton + * constants; any other value returned by the aggregator is accepted and + * preserved as a custom status instead of being rejected. */ -public enum CertificationStatus { +public final class CertificationStatus { + /** * The certification request was accepted and stored. */ - SUCCESS("SUCCESS"), + public static final CertificationStatus SUCCESS = new CertificationStatus("SUCCESS"); /** * The certification request failed because the state ID does not match the expected format. */ - STATE_ID_MISMATCH("STATE_ID_MISMATCH"), + public static final CertificationStatus STATE_ID_MISMATCH = + new CertificationStatus("STATE_ID_MISMATCH"); /** * The certification request failed because the signature verification failed. */ - SIGNATURE_VERIFICATION_FAILED("SIGNATURE_VERIFICATION_FAILED"), + public static final CertificationStatus SIGNATURE_VERIFICATION_FAILED = + new CertificationStatus("SIGNATURE_VERIFICATION_FAILED"); /** * The certification request failed because signature has invalid format. */ - INVALID_SIGNATURE_FORMAT("INVALID_SIGNATURE_FORMAT"), + public static final CertificationStatus INVALID_SIGNATURE_FORMAT = + new CertificationStatus("INVALID_SIGNATURE_FORMAT"); /** * The certification request failed because the public key has invalid format. */ - INVALID_PUBLIC_KEY_FORMAT("INVALID_PUBLIC_KEY_FORMAT"), + public static final CertificationStatus INVALID_PUBLIC_KEY_FORMAT = + new CertificationStatus("INVALID_PUBLIC_KEY_FORMAT"); /** * The certification request failed because the source state hash has invalid format. */ - INVALID_SOURCE_STATE_HASH_FORMAT("INVALID_SOURCE_STATE_HASH_FORMAT"), + public static final CertificationStatus INVALID_SOURCE_STATE_HASH_FORMAT = + new CertificationStatus("INVALID_SOURCE_STATE_HASH_FORMAT"); /** * The certification request failed because the transaction hash has invalid format. */ - INVALID_TRANSACTION_HASH_FORMAT("INVALID_TRANSACTION_HASH_FORMAT"), + public static final CertificationStatus INVALID_TRANSACTION_HASH_FORMAT = + new CertificationStatus("INVALID_TRANSACTION_HASH_FORMAT"); /** * The certification request failed because the algorithm is not supported. */ - UNSUPPORTED_ALGORITHM("UNSUPPORTED_ALGORITHM"), + public static final CertificationStatus UNSUPPORTED_ALGORITHM = + new CertificationStatus("UNSUPPORTED_ALGORITHM"); /** * The certification request failed because request was sent to invalid shard. */ - INVALID_SHARD("INVALID_SHARD"); + public static final CertificationStatus INVALID_SHARD = new CertificationStatus("INVALID_SHARD"); + + private static final CertificationStatus[] VALUES = { + SUCCESS, + STATE_ID_MISMATCH, + SIGNATURE_VERIFICATION_FAILED, + INVALID_SIGNATURE_FORMAT, + INVALID_PUBLIC_KEY_FORMAT, + INVALID_SOURCE_STATE_HASH_FORMAT, + INVALID_TRANSACTION_HASH_FORMAT, + UNSUPPORTED_ALGORITHM, + INVALID_SHARD + }; private final String value; - CertificationStatus(String value) { + private CertificationStatus(String value) { this.value = value; } @@ -53,22 +78,50 @@ public enum CertificationStatus { * * @return string value */ + @JsonValue public String getValue() { - return value; + return this.value; } /** - * Create status from string value. + * Resolve a status from its string value. Returns the registered singleton + * for known statuses; constructs a new instance for any other (custom) value + * returned by the aggregator. * * @param value string value * @return status */ + @JsonCreator public static CertificationStatus fromString(String value) { - for (CertificationStatus status : CertificationStatus.values()) { + if (value == null) { + throw new IllegalArgumentException("Status value must not be null"); + } + for (CertificationStatus status : CertificationStatus.VALUES) { if (status.value.equalsIgnoreCase(value)) { return status; } } - throw new IllegalArgumentException("Unknown status: " + value); + return new CertificationStatus(value); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CertificationStatus)) { + return false; + } + return this.value.equalsIgnoreCase(((CertificationStatus) o).value); + } + + @Override + public int hashCode() { + return this.value.toUpperCase().hashCode(); + } + + @Override + public String toString() { + return this.value; } -} \ No newline at end of file +} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedUnicityIdMintTransactionVerificationRule.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedUnicityIdMintTransactionVerificationRule.java index bf74896..a01a0e8 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedUnicityIdMintTransactionVerificationRule.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedUnicityIdMintTransactionVerificationRule.java @@ -40,6 +40,19 @@ public static VerificationResult verify( ) { List> results = new ArrayList<>(); + if (!genesis.getNetworkId().equals(trustBase.getNetworkId())) { + results.add(new VerificationResult<>("MintNetworkMatchesTrustBaseRule", + VerificationStatus.FAIL)); + return new VerificationResult<>( + "CertifiedUnicityIdMintTransactionVerificationRule", + VerificationStatus.FAIL, + "Mint network does not match trust base.", + results + ); + } + results.add(new VerificationResult<>("MintNetworkMatchesTrustBaseRule", + VerificationStatus.OK)); + if (issuerPublicKey != null) { EncodedPredicate expectedLockScript = EncodedPredicate.fromPredicate( SignaturePredicate.create(issuerPublicKey)); diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java b/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java index 3691253..e502406 100644 --- a/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/unicityid/CertifiedUnicityIdMintTransaction.java @@ -1,6 +1,7 @@ package org.unicitylabs.sdk.unicityid; import org.unicitylabs.sdk.api.InclusionProof; +import org.unicitylabs.sdk.api.NetworkId; import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.crypto.hash.DataHash; import org.unicitylabs.sdk.predicate.EncodedPredicate; @@ -49,6 +50,15 @@ public EncodedPredicate getRecipient() { return this.transaction.getRecipient(); } + /** + * Returns the network identifier. + * + * @return network identifier + */ + public NetworkId getNetworkId() { + return this.transaction.getNetworkId(); + } + @Override public DataHash getSourceStateHash() { return this.transaction.getSourceStateHash(); diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityId.java b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityId.java index 48bc1db..d783e9b 100644 --- a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityId.java +++ b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityId.java @@ -5,14 +5,14 @@ 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 org.unicitylabs.sdk.transaction.TokenSalt; 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}. + * to derive the corresponding {@link TokenSalt}. */ public final class UnicityId { @@ -85,12 +85,12 @@ public byte[] toCbor() { } /** - * Derive the token id from this unicity id by hashing the tagged ("NAMETAG_", domain, name) + * Derive the token salt from this unicity id by hashing the tagged ("NAMETAG_", domain, name) * tuple with SHA-256. * - * @return derived token id + * @return derived token salt */ - public TokenId toTokenId() { + public TokenSalt toTokenSalt() { DataHash hash = new DataHasher(HashAlgorithm.SHA256) .update( CborSerializer.encodeArray( @@ -100,7 +100,7 @@ public TokenId toTokenId() { ) ) .digest(); - return new TokenId(hash.getData()); + return TokenSalt.fromBytes(hash.getData()); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java index b7ffb1f..06d0f73 100644 --- a/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/unicityid/UnicityIdMintTransaction.java @@ -1,6 +1,7 @@ package org.unicitylabs.sdk.unicityid; import org.unicitylabs.sdk.api.InclusionProof; +import org.unicitylabs.sdk.api.NetworkId; import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.crypto.hash.DataHash; import org.unicitylabs.sdk.crypto.hash.DataHasher; @@ -31,6 +32,7 @@ public final class UnicityIdMintTransaction implements Transaction { private final MintTransactionState sourceStateHash; private final EncodedPredicate lockScript; + private final NetworkId networkId; private final EncodedPredicate recipient; private final TokenId tokenId; private final TokenType tokenType; @@ -40,6 +42,7 @@ public final class UnicityIdMintTransaction implements Transaction { private UnicityIdMintTransaction( MintTransactionState sourceStateHash, EncodedPredicate lockScript, + NetworkId networkId, EncodedPredicate recipient, TokenId tokenId, TokenType tokenType, @@ -48,6 +51,7 @@ private UnicityIdMintTransaction( ) { this.sourceStateHash = sourceStateHash; this.lockScript = lockScript; + this.networkId = networkId; this.recipient = recipient; this.tokenId = tokenId; this.tokenType = tokenType; @@ -79,6 +83,15 @@ public EncodedPredicate getRecipient() { return this.recipient; } + /** + * Get the network identifier. + * + * @return network identifier + */ + public NetworkId getNetworkId() { + return this.networkId; + } + /** * Get the token id derived from the unicity id. * @@ -129,6 +142,7 @@ public byte[] getStateMask() { * Create a unicity id mint transaction. The token id is derived from the unicity id; the lock * script is supplied by the caller. * + * @param networkId network identifier * @param lockScript lock script predicate (the predicate that must be unlocked to spend this * transaction) * @param recipient recipient predicate @@ -139,23 +153,26 @@ public byte[] getStateMask() { * @return mint transaction */ public static UnicityIdMintTransaction create( + NetworkId networkId, SignaturePredicate lockScript, Predicate recipient, UnicityId unicityId, TokenType tokenType, SignaturePredicate targetPredicate ) { + Objects.requireNonNull(networkId, "Network ID must not be null"); 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(); + TokenId tokenId = TokenId.fromSalt(networkId, unicityId.toTokenSalt()); return new UnicityIdMintTransaction( MintTransactionState.create(tokenId), EncodedPredicate.fromPredicate(lockScript), + networkId, EncodedPredicate.fromPredicate(recipient), tokenId, tokenType, @@ -179,7 +196,7 @@ public static UnicityIdMintTransaction fromCbor(byte[] bytes) { if (tag.getTag() != UnicityIdMintTransaction.CBOR_TAG) { throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } - List data = CborDeserializer.decodeArray(tag.getData(), 6); + List data = CborDeserializer.decodeArray(tag.getData(), 7); int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); if (version != UnicityIdMintTransaction.VERSION) { @@ -187,14 +204,15 @@ public static UnicityIdMintTransaction fromCbor(byte[] bytes) { } return UnicityIdMintTransaction.create( + NetworkId.fromId(CborDeserializer.decodeUnsignedInteger(data.get(1)).asShort()), SignaturePredicate.fromPredicate( - EncodedPredicate.fromCbor(data.get(1)) + EncodedPredicate.fromCbor(data.get(2)) ), - EncodedPredicate.fromCbor(data.get(2)), - UnicityId.fromCbor(data.get(3)), - TokenType.fromCbor(data.get(4)), + EncodedPredicate.fromCbor(data.get(3)), + UnicityId.fromCbor(data.get(4)), + TokenType.fromCbor(data.get(5)), SignaturePredicate.fromPredicate( - EncodedPredicate.fromCbor(data.get(5)) + EncodedPredicate.fromCbor(data.get(6)) ) ); } @@ -222,6 +240,7 @@ public byte[] toCbor() { UnicityIdMintTransaction.CBOR_TAG, CborSerializer.encodeArray( CborSerializer.encodeUnsignedInteger(UnicityIdMintTransaction.VERSION), + CborSerializer.encodeUnsignedInteger(this.networkId.getId()), this.lockScript.toCbor(), this.recipient.toCbor(), this.unicityId.toCbor(), @@ -252,9 +271,9 @@ public CertifiedUnicityIdMintTransaction toCertifiedTransaction( @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 + "UnicityIdMintTransaction{networkId=%s, lockScript=%s, recipient=%s, tokenId=%s, tokenType=%s, unicityId=%s, targetPredicate=%s}", + this.networkId, this.lockScript, this.recipient, this.tokenId, this.tokenType, + this.unicityId, this.targetPredicate ); } } diff --git a/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java b/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java index 25859d7..f2861fb 100644 --- a/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java +++ b/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java @@ -85,6 +85,7 @@ public void testUnicityIdMintFlow() throws Exception { UnicityId unicityId = new UnicityId("testuser", "unicity-labs/test"); UnicityIdMintTransaction unicityIdMintTransaction = UnicityIdMintTransaction.create( + this.trustBase.getNetworkId(), SignaturePredicate.fromSigningService(unicityIdSigningService), targetPredicate, unicityId, From d9c12e25bdc9143382a6fd954a20473c81fe0ecf Mon Sep 17 00:00:00 2001 From: Martti Marran Date: Mon, 15 Jun 2026 17:58:48 +0000 Subject: [PATCH 2/2] #71 Update CertificationStatus equals and hashcode method --- .../java/org/unicitylabs/sdk/api/CertificationStatus.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/unicitylabs/sdk/api/CertificationStatus.java b/src/main/java/org/unicitylabs/sdk/api/CertificationStatus.java index 911202b..74d28b7 100644 --- a/src/main/java/org/unicitylabs/sdk/api/CertificationStatus.java +++ b/src/main/java/org/unicitylabs/sdk/api/CertificationStatus.java @@ -112,12 +112,12 @@ public boolean equals(Object o) { if (!(o instanceof CertificationStatus)) { return false; } - return this.value.equalsIgnoreCase(((CertificationStatus) o).value); + return this.value.equals(((CertificationStatus) o).value); } @Override public int hashCode() { - return this.value.toUpperCase().hashCode(); + return this.value.hashCode(); } @Override