diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..d89e120 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,31 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/java +{ + "name": "State Transition Java SDK", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/java:21-bullseye", + + "features": { + "ghcr.io/devcontainers/features/java:1": { + "version": "none", + "installMaven": "false", + "installGradle": "true" + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "java -version", + + // Configure tool-specific properties. + "customizations" : { + "jetbrains" : { + "backend" : "IntelliJ" + } + }, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/README.md b/README.md index e40e019..4691c4b 100644 --- a/README.md +++ b/README.md @@ -56,247 +56,6 @@ dependencies { ## Quick Start -### Util methods - -```java -private static final SecureRandom RANDOM = new SecureRandom(); - -/** - * Generate random bytes of specified length. - */ -public static byte[] randomBytes(int length) { - byte[] bytes = new byte[length]; - RANDOM.nextBytes(bytes); - return bytes; -} -``` - -### Initialize the Client - -```java -// Connect to the Unicity test network -String aggregatorUrl = "https://gateway-test.unicity.network"; -DefaultAggregatorClient aggregatorClient = new DefaultAggregatorClient(aggregatorUrl); -StateTransitionClient client = new StateTransitionClient(aggregatorClient); - -// Create root trust base from classpath -RootTrustBase trustbase = RootTrustBase.fromJson( - new String(getClass().getResourceAsStream("/trust-base.json").readAllBytes()) -); -``` - -### Mint a Token - -```java -byte[] secret = "minter_secret".getBytes(StandardCharsets.UTF_8); -// Generate data for token -TokenId tokenId = new TokenId(randomBytes(32)); -TokenType tokenType = new TokenType(randomBytes(32)); -byte[] tokenData = "token immutable data".getBytes(StandardCharsets.UTF_8); -TokenCoinData coinData = new TokenCoinData( - Map.of( - new CoinId("coin".getBytes()), BigInteger.valueOf(100), - new CoinId("second coin".getBytes()), BigInteger.valueOf(5) - ) -); - -// Create predicate for initial state and use its reference as address -byte[] nonce = randomBytes(32); -// Create key pair from nonce and secret -SigningService signingService = SigningService.createFromMaskedSecret(secret, nonce); -MaskedPredicate predicate = MaskedPredicate.create( - tokenId, - tokenType, - signingService, - HashAlgorithm.SHA256, - nonce -); - -byte[] salt = randomBytes(32); -MintCommitment commitment = MintCommitment.create( - new MintTransaction.Data<>( - tokenId, - tokenType, - tokenData, - coinData, - predicate.getReference().toAddress(), - salt, - null, - null - ) -); - -// Submit mint transaction using StateTransitionClient -SubmitCommitmentResponse response = client - .submitCommitment(commitment) - .get(); - -if (response.getStatus() != SubmitCommitmentStatus.SUCCESS) { - throw new Exception( - String.format( - "Failed to submit mint commitment: %s", - response.getStatus() - ) - ); -} - -// Wait for inclusion proof -InclusionProof inclusionProof = InclusionProofUtils.waitInclusionProof( - client, - trustBase, - commitment -).get(); - -// Create mint transaction -Token token = Token.create( - trustBase, - // Create initial state with transaction data - new TokenState(predicate, null), - commitment.toTransaction(inclusionProof) -); -``` - -### Get Block Height - -```java -Long blockHeight = client.getAggregatorClient().getBlockHeight().get(); -System.out.println("Current block height: "+blockHeight); -``` - -### Transfer a Token - -```java -byte[] senderSecret = secret; -byte[] senderNonce = nonce; - -String recipientNametag = "RECIPIENT"; -byte[] recipientData = "Custom data".getBytes(StandardCharsets.UTF_8); -DataHash recipientDataHash = new DataHasher(HashAlgorithm.SHA256) - .update(recipientData) - .digest(); - -byte[] salt = randomBytes(32); - -// Submit transfer transaction -TransferCommitment transferCommitment = TransferCommitment.create( - token, - ProxyAddress.create(recipientNametag), - salt, - recipientDataHash, - null, - SigningService.createFromMaskedSecret(senderSecret, senderNonce) -); - -SubmitCommitmentResponse transferResponse = this.client.submitCommitment(transferCommitment).get(); -if (transferResponse.getStatus() != SubmitCommitmentStatus.SUCCESS) { - throw new Exception( - String.format( - "Failed to submit transfer commitment: %s", - transferResponse.getStatus() - ) - ); -} - -// Create transfer transaction -TransferTransaction transferTransaction = transferCommitment.toTransaction( - InclusionProofUtils.waitInclusionProof( - client, - trustBase, - transferCommitment - ).get() -); - -// Prepare info for sending to recipient -String transferTransactionJson = transferTransaction.toJson(); -String tokenJson = token.toJson(); -``` - -### Receive the token - -```java -String recipientNametag = "RECIPIENT"; -byte[] receiverSecret = "RECEIVER_SECRET".getBytes(); - -Token token = Token.fromJson("TOKEN JSON"); -TransferTransaction transaction = TransferTransaction.fromJson("TRANSFER TRANSACTION JSON"); - -// Create nametag token -TokenType nametagType = new TokenType(randomBytes(32)); -byte[] nametagNonce = randomBytes(32); -byte[] nametagSalt = randomBytes(32); - -MintCommitment nametagMintCommitment = MintCommitment.create( - new MintTransaction.NametagData( - recipientNametag, - nametagType, - MaskedPredicateReference.create( - nametagType, - SigningService.createFromMaskedSecret(receiverSecret, nametagNonce), - HashAlgorithm.SHA256, - nametagNonce - ).toAddress(), - nametagSalt, - UnmaskedPredicateReference.create( - token.getType(), - SigningService.createFromSecret(receiverSecret), - HashAlgorithm.SHA256 - ).toAddress() - ) -); - -// Submit nametag mint transaction using StateTransitionClient -SubmitCommitmentResponse nametagMintResponse = client.submitCommitment(nametagMintCommitment).get(); -if (nametagMintResponse.getStatus() != SubmitCommitmentStatus.SUCCESS) { - throw new Exception( - String.format( - "Failed to submit nametag mint commitment: %s", - nametagMintResponse.getStatus() - ) - ); -} - -// Wait for inclusion proof -InclusionProof inclusionProof = InclusionProofUtils.waitInclusionProof( - client, - trustBase, - nametagMintCommitment -).get(); - -Token nametagToken = Token.create( - trustBase, - new TokenState( - MaskedPredicate.create( - nametagMintCommitment.getTransactionData().getTokenId(), - nametagMintCommitment.getTransactionData().getTokenType(), - SigningService.createFromMaskedSecret(receiverSecret, nametagNonce), - HashAlgorithm.SHA256, - nametagNonce - ), - null - ), - nametagMintCommitment.toTransaction(inclusionProof) -); - -// Receiver finalizes the token -Token finalizedToken = client.finalizeTransaction( - trustBase, - token, - new TokenState( - UnmaskedPredicate.create( - token.getId(), - token.getType(), - SigningService.createFromSecret(receiverSecret), - HashAlgorithm.SHA256, - transaction.getData().getSalt() - ), - null - ), - transaction, - List.of(nametagToken) -); - -``` - ## Building from Source ### Clone the Repository @@ -410,7 +169,7 @@ Located in `src/test/java/org/unicitylabs/sdk/`: ./gradlew integrationTest # Specific test class -./gradlew test --tests "org.unicitylabs.sdk.api.RequestIdTest" +./gradlew test --tests "org.unicitylabs.sdk.api.StateIdTest" ``` ## License diff --git a/src/main/java/org/unicitylabs/sdk/StateTransitionClient.java b/src/main/java/org/unicitylabs/sdk/StateTransitionClient.java index 5bba9b8..26f5d50 100644 --- a/src/main/java/org/unicitylabs/sdk/StateTransitionClient.java +++ b/src/main/java/org/unicitylabs/sdk/StateTransitionClient.java @@ -1,27 +1,9 @@ package org.unicitylabs.sdk; -import java.util.List; -import java.util.Objects; +import org.unicitylabs.sdk.api.*; + import java.util.concurrent.CompletableFuture; -import org.unicitylabs.sdk.api.AggregatorClient; -import org.unicitylabs.sdk.api.InclusionProofResponse; -import org.unicitylabs.sdk.api.RequestId; -import org.unicitylabs.sdk.api.SubmitCommitmentResponse; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.predicate.Predicate; -import org.unicitylabs.sdk.predicate.PredicateEngineService; -import org.unicitylabs.sdk.signing.MintSigningService; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.token.TokenState; -import org.unicitylabs.sdk.transaction.InclusionProofVerificationStatus; -import org.unicitylabs.sdk.transaction.MintCommitment; -import org.unicitylabs.sdk.transaction.MintTransactionReason; -import org.unicitylabs.sdk.transaction.MintTransactionState; -import org.unicitylabs.sdk.transaction.TransferCommitment; -import org.unicitylabs.sdk.transaction.TransferTransaction; -import org.unicitylabs.sdk.verification.VerificationException; /** * Client for handling state transitions of tokens, including submitting commitments and finalizing transactions. @@ -43,159 +25,23 @@ public StateTransitionClient(AggregatorClient client) { } /** - * Submits a mint commitment to the aggregator. - * - * @param commitment The mint commitment to submit. - * @param The type of mint transaction data. - * @return A CompletableFuture that resolves to the response from the aggregator. - */ - public - CompletableFuture submitCommitment(MintCommitment commitment) { - return this.client.submitCommitment( - commitment.getRequestId(), - commitment.getTransactionData().calculateHash(), - commitment.getAuthenticator() - ); - } - - /** - * Submits a transfer commitment to the aggregator after verifying ownership. - * - * @param commitment The transfer commitment to submit. - * @return A CompletableFuture that resolves to the response from the aggregator. - * @throws IllegalArgumentException if ownership verification fails. - */ - public CompletableFuture submitCommitment( - TransferCommitment commitment - ) { - if ( - !PredicateEngineService.createPredicate( - commitment.getTransactionData().getSourceState().getPredicate() - ).isOwner(commitment.getAuthenticator().getPublicKey()) - ) { - throw new IllegalArgumentException( - "Ownership verification failed: Authenticator does not match source state predicate."); - } - - return this.client.submitCommitment(commitment.getRequestId(), commitment.getTransactionData() - .calculateHash(), commitment.getAuthenticator()); - } - - /** - * Finalizes a transaction by updating the token state based on the provided transaction data without nametags. + * Retrieves the inclusion proof for a given transaction. * - * @param trustBase The root trust base for inclusion proof verification. - * @param token The token to be updated. - * @param state The current state of the token. - * @param transaction The transaction containing transfer data. - * @param The type of mint transaction data. - * @return The updated token after applying the transaction. - * @throws VerificationException if verification fails during the update process. + * @param stateId The state ID of inclusion proof to retrieve. + * @return inclusion proof response from the aggregator. */ - public Token finalizeTransaction( - RootTrustBase trustBase, - Token token, - TokenState state, - TransferTransaction transaction - ) throws VerificationException { - return this.finalizeTransaction(trustBase, token, state, transaction, List.of()); - } - - /** - * Finalizes a transaction by updating the token state based on the provided transaction data and nametags. - * - * @param trustBase The root trust base for inclusion proof verification. - * @param token The token to be updated. - * @param state The current state of the token. - * @param transaction The transaction containing transfer data. - * @param nametags A list of tokens used as nametags in the transaction. - * @param The type of mint transaction data of token. - * @return The updated token after applying the transaction. - * @throws VerificationException if verification fails during the update process. - */ - public Token finalizeTransaction( - RootTrustBase trustBase, - Token token, - TokenState state, - TransferTransaction transaction, - List> nametags - ) throws VerificationException { - Objects.requireNonNull(token, "Token is null"); - - return token.update(trustBase, state, transaction, nametags); - } - - /** - * Retrieves the inclusion proof for a given request id. - * - * @param requestId The request ID of inclusion proof to retrieve. - * @return A CompletableFuture that resolves to the inclusion proof response from the aggregator. - */ - public CompletableFuture getInclusionProof(RequestId requestId) { - return this.client.getInclusionProof(requestId); - } - - /** - * Check if state is already spent for given request id. - * - * @param requestId request id - * @param trustBase root trust base - * @return A CompletableFuture that resolves to true if state is spent, false otherwise. - */ - public CompletableFuture isStateSpent(RequestId requestId, RootTrustBase trustBase) { - return this.getInclusionProof(requestId) - .thenApply(inclusionProof -> { - InclusionProofVerificationStatus result = inclusionProof.getInclusionProof().verify(requestId, trustBase); - switch (result) { - case OK: - return true; - case PATH_NOT_INCLUDED: - return false; - default: - throw new RuntimeException( - String.format("Inclusion proof verification failed with status %s", result) - ); - } - }); - } - - - /** - * Get inclusion proof for current token state. - * - * @param token token - * @param publicKey public key - * @param trustBase trustBase - * @return A CompletableFuture that resolves to the inclusion proof response from the aggregator. - */ - public CompletableFuture isStateSpent( - Token token, - byte[] publicKey, - RootTrustBase trustBase - ) { - Predicate predicate = PredicateEngineService.createPredicate(token.getState().getPredicate()); - if (!predicate.isOwner(publicKey)) { - throw new IllegalArgumentException("Given key is not owner of the token."); - } - - return this.isStateSpent(RequestId.create(publicKey, token.getState()), trustBase); + public CompletableFuture getInclusionProof(StateId stateId) { + return this.client.getInclusionProof(stateId); } /** - * Check if token id is already minted. + * Submits a certification request to the aggregator. * - * @param tokenId token id - * @param trustBase root trust base - * @return A CompletableFuture that resolves to true if token id is spent, false otherwise. + * @param certificationData The certification data to submit. + * @return certification response from the aggregator. */ - public CompletableFuture isMinted(TokenId tokenId, RootTrustBase trustBase) { - return this.isStateSpent( - RequestId.create( - MintSigningService.create(tokenId).getPublicKey(), - MintTransactionState.create(tokenId) - ), - trustBase - ); + public CompletableFuture submitCertificationRequest(CertificationData certificationData) { + return this.client.submitCertificationRequest(certificationData); } diff --git a/src/main/java/org/unicitylabs/sdk/address/Address.java b/src/main/java/org/unicitylabs/sdk/address/Address.java deleted file mode 100644 index 78a8a5f..0000000 --- a/src/main/java/org/unicitylabs/sdk/address/Address.java +++ /dev/null @@ -1,27 +0,0 @@ - -package org.unicitylabs.sdk.address; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - -/** - * Address interface. - */ -@JsonSerialize(using = AddressJson.Serializer.class) -@JsonDeserialize(using = AddressJson.Deserializer.class) -public interface Address { - - /** - * Get the address scheme. - * - * @return address scheme - */ - AddressScheme getScheme(); - - /** - * Get the address as a string. - * - * @return address string - */ - String getAddress(); -} diff --git a/src/main/java/org/unicitylabs/sdk/address/AddressFactory.java b/src/main/java/org/unicitylabs/sdk/address/AddressFactory.java deleted file mode 100644 index e1e9e62..0000000 --- a/src/main/java/org/unicitylabs/sdk/address/AddressFactory.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.unicitylabs.sdk.address; - -import java.util.Arrays; -import java.util.Objects; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.util.HexConverter; - -/** - * Factory for creating Address instances from string representations. - */ -public class AddressFactory { - - private AddressFactory() {} - - /** - * Create an Address from its string representation. - * - * @param address The address string. - * @return The corresponding Address instance. - * @throws IllegalArgumentException if the address format is invalid or does not match the - * expected format. - * @throws NullPointerException if the address is null. - */ - public static Address createAddress(String address) { - Objects.requireNonNull(address, "Address cannot be null"); - - String[] result = address.split("://", 2); - if (result.length != 2) { - throw new IllegalArgumentException("Invalid address format"); - } - - Address expectedAddress; - byte[] bytes = HexConverter.decode(result[1]); - - switch (AddressScheme.valueOf(result[0])) { - case DIRECT: - expectedAddress = DirectAddress.create( - DataHash.fromImprint(Arrays.copyOf(bytes, bytes.length - 4))); - break; - case PROXY: - expectedAddress = ProxyAddress.create(new TokenId(Arrays.copyOf(bytes, bytes.length - 4))); - break; - default: - throw new IllegalArgumentException("Invalid address scheme: " + result[0]); - } - - if (!expectedAddress.getAddress().equals(address)) { - throw new IllegalArgumentException("Address mismatch"); - } - - return expectedAddress; - } -} diff --git a/src/main/java/org/unicitylabs/sdk/address/AddressJson.java b/src/main/java/org/unicitylabs/sdk/address/AddressJson.java deleted file mode 100644 index ed91a20..0000000 --- a/src/main/java/org/unicitylabs/sdk/address/AddressJson.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.unicitylabs.sdk.address; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.exc.MismatchedInputException; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import java.io.IOException; -import org.unicitylabs.sdk.predicate.EncodedPredicate; - -/** - * Address serializer and deserializer implementation. - */ -public class AddressJson { - - private AddressJson() { - } - - /** - * Address serializer. - */ - public static class Serializer extends StdSerializer
{ - - /** - * Create serializer. - */ - public Serializer() { - super(Address.class); - } - - /** - * Serialize address. - * - * @param value addess - * @param gen json generator - * @param serializers serializer provider - * @throws IOException on serialization failure - */ - @Override - public void serialize(Address value, JsonGenerator gen, - SerializerProvider serializers) - throws IOException { - gen.writeObject(value.toString()); - } - } - - /** - * Address deserializer. - */ - public static class Deserializer extends StdDeserializer
{ - - /** - * Create deserializer. - */ - public Deserializer() { - super(Address.class); - } - - /** - * Deserialize address. - * - * @param p Parser used for reading JSON content - * @param ctx Context that can be used to access information about this deserialization - * activity. - * @return address - * @throws IOException on deserialization failure - */ - @Override - public Address deserialize(JsonParser p, DeserializationContext ctx) - throws IOException { - if (p.getCurrentToken() != JsonToken.VALUE_STRING) { - throw MismatchedInputException.from( - p, - EncodedPredicate.class, - "Expected string value" - ); - } - - try { - return AddressFactory.createAddress(p.readValueAs(String.class)); - } catch (Exception e) { - throw MismatchedInputException.from(p, EncodedPredicate.class, "Expected bytes"); - } - } - } -} - diff --git a/src/main/java/org/unicitylabs/sdk/address/AddressScheme.java b/src/main/java/org/unicitylabs/sdk/address/AddressScheme.java deleted file mode 100644 index ac781a5..0000000 --- a/src/main/java/org/unicitylabs/sdk/address/AddressScheme.java +++ /dev/null @@ -1,16 +0,0 @@ - -package org.unicitylabs.sdk.address; - -/** - * Address scheme. - */ -public enum AddressScheme { - /** - * Direct address scheme. - */ - DIRECT, - /** - * Nametag address scheme which redirects to DIRECT scheme eventually. - */ - PROXY -} diff --git a/src/main/java/org/unicitylabs/sdk/address/DirectAddress.java b/src/main/java/org/unicitylabs/sdk/address/DirectAddress.java deleted file mode 100644 index 6b29dfd..0000000 --- a/src/main/java/org/unicitylabs/sdk/address/DirectAddress.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.unicitylabs.sdk.address; - -import java.util.Arrays; -import java.util.Objects; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.util.HexConverter; - -/** - * Direct address implementation. - */ -public class DirectAddress implements Address { - - private final DataHash data; - private final byte[] checksum; - - private DirectAddress(DataHash data, byte[] checksum) { - this.data = data; - this.checksum = Arrays.copyOf(checksum, checksum.length); - } - - /** - * Create a direct address from a predicate reference. - * - * @param reference the data hash to create the address from - * @return the direct address - */ - public static DirectAddress create(DataHash reference) { - DataHash checksum = new DataHasher(HashAlgorithm.SHA256).update(reference.getImprint()) - .digest(); - return new DirectAddress(reference, Arrays.copyOf(checksum.getData(), 4)); - } - - @Override - public AddressScheme getScheme() { - return AddressScheme.DIRECT; - } - - @Override - public String getAddress() { - return this.toString(); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof DirectAddress)) { - return false; - } - DirectAddress that = (DirectAddress) o; - return Objects.equals(this.data, that.data) && Arrays.equals(this.checksum, - that.checksum); - } - - @Override - public int hashCode() { - return Objects.hash(this.data, Arrays.hashCode(checksum)); - } - - @Override - public String toString() { - return String.format( - "%s://%s%s", - AddressScheme.DIRECT, - HexConverter.encode(this.data.getImprint()), - HexConverter.encode(this.checksum)); - } -} \ No newline at end of file diff --git a/src/main/java/org/unicitylabs/sdk/address/ProxyAddress.java b/src/main/java/org/unicitylabs/sdk/address/ProxyAddress.java deleted file mode 100644 index e10a8ff..0000000 --- a/src/main/java/org/unicitylabs/sdk/address/ProxyAddress.java +++ /dev/null @@ -1,122 +0,0 @@ -package org.unicitylabs.sdk.address; - -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.util.HexConverter; - -/** - * Proxy address implementation. - */ -public class ProxyAddress implements Address { - - private final TokenId data; - private final byte[] checksum; - - private ProxyAddress(TokenId data, byte[] checksum) { - this.data = data; - this.checksum = Arrays.copyOf(checksum, checksum.length); - } - - /** - * Create a proxy address from a nametag string. - * - * @param name the nametag - * @return the proxy address - */ - public static ProxyAddress create(String name) { - return ProxyAddress.create(TokenId.fromNameTag(name)); - } - - /** - * Create a proxy address from a token ID. - * - * @param tokenId the token ID - * @return the proxy address - */ - public static ProxyAddress create(TokenId tokenId) { - DataHash checksum = new DataHasher(HashAlgorithm.SHA256).update(tokenId.getBytes()) - .digest(); - return new ProxyAddress(tokenId, Arrays.copyOf(checksum.getData(), 4)); - } - - @Override - public AddressScheme getScheme() { - return AddressScheme.PROXY; - } - - @Override - public String getAddress() { - return this.toString(); - } - - /** - * Resolve a proxy address to a direct address using a list of nametag tokens. - * - * @param inputAddress the input address to resolve - * @param nametags the list of nametag tokens - * @return the resolved direct address, or null if resolution fails - * @throws IllegalArgumentException if the nametags list contains null elements or duplicate - * addresses - */ - public static Address resolve(Address inputAddress, List> nametags) { - Map> nametagMap = new HashMap<>(); - for (Token token : nametags) { - if (token == null) { - throw new IllegalArgumentException("Nametag tokens list cannot contain null elements"); - } - - Address address = ProxyAddress.create(token.getId()); - if (nametagMap.containsKey(address)) { - throw new IllegalArgumentException( - "Nametag tokens list contains duplicate addresses: " + address); - } - nametagMap.put(address, token); - } - - Address targetAddress = inputAddress; - while (targetAddress.getScheme() != AddressScheme.DIRECT) { - Token nametag = nametagMap.get(targetAddress); - if (nametag == null || !nametag.getData().isPresent()) { - return null; - } - - targetAddress = AddressFactory.createAddress( - new String(nametag.getData().get(), StandardCharsets.UTF_8)); - } - - return targetAddress; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof ProxyAddress)) { - return false; - } - ProxyAddress that = (ProxyAddress) o; - return Objects.equals(this.data, that.data) && Arrays.equals(this.checksum, - that.checksum); - } - - @Override - public int hashCode() { - return Objects.hash(this.data, Arrays.hashCode(this.checksum)); - } - - @Override - public String toString() { - return String.format( - "%s://%s%s", - AddressScheme.PROXY, - HexConverter.encode(this.data.getBytes()), - HexConverter.encode(this.checksum)); - } -} \ No newline at end of file diff --git a/src/main/java/org/unicitylabs/sdk/api/AggregatorClient.java b/src/main/java/org/unicitylabs/sdk/api/AggregatorClient.java index 8808cdb..772a1c9 100644 --- a/src/main/java/org/unicitylabs/sdk/api/AggregatorClient.java +++ b/src/main/java/org/unicitylabs/sdk/api/AggregatorClient.java @@ -2,7 +2,6 @@ package org.unicitylabs.sdk.api; import java.util.concurrent.CompletableFuture; -import org.unicitylabs.sdk.hash.DataHash; /** * Aggregator client structure. @@ -10,25 +9,20 @@ public interface AggregatorClient { /** - * Submit commitment. + * Submit certification request. * - * @param requestId request id - * @param transactionHash transaction hash - * @param authenticator authenticator - * @return submit commitment response + * @param certificationData certification data + * @return certification response */ - CompletableFuture submitCommitment( - RequestId requestId, - DataHash transactionHash, - Authenticator authenticator); + CompletableFuture submitCertificationRequest(CertificationData certificationData); /** - * Get inclusion proof for request id. + * Get inclusion proof for state id. * - * @param requestId request id + * @param stateId state id * @return inclusion / non inclusion proof */ - CompletableFuture getInclusionProof(RequestId requestId); + CompletableFuture getInclusionProof(StateId stateId); /** * Get block height. diff --git a/src/main/java/org/unicitylabs/sdk/api/Authenticator.java b/src/main/java/org/unicitylabs/sdk/api/Authenticator.java deleted file mode 100644 index 97d106c..0000000 --- a/src/main/java/org/unicitylabs/sdk/api/Authenticator.java +++ /dev/null @@ -1,189 +0,0 @@ -package org.unicitylabs.sdk.api; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.serializer.UnicityObjectMapper; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.serializer.json.JsonSerializationException; -import org.unicitylabs.sdk.signing.Signature; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.util.HexConverter; - -/** - * Authenticator for transaction submission. - */ -public class Authenticator { - - private final String algorithm; - private final Signature signature; - private final DataHash stateHash; - private final byte[] publicKey; - - @JsonCreator - private Authenticator( - @JsonProperty("algorithm") String algorithm, - @JsonProperty("publicKey") byte[] publicKey, - @JsonProperty("signature") Signature signature, - @JsonProperty("stateHash") DataHash stateHash - ) { - this.algorithm = algorithm; - this.publicKey = Arrays.copyOf(publicKey, publicKey.length); - this.signature = signature; - this.stateHash = stateHash; - } - - /** - * Create authenticator from signing service. - * - * @param signingService signing service - * @param transactionHash transaction hash - * @param stateHash state hash - * @return authenticator - */ - public static Authenticator create( - SigningService signingService, - DataHash transactionHash, - DataHash stateHash - ) { - return new Authenticator( - signingService.getAlgorithm(), - signingService.getPublicKey(), - signingService.sign(transactionHash), - stateHash - ); - } - - /** - * Get signature. - * - * @return signature - */ - public Signature getSignature() { - return this.signature; - } - - /** - * Get algorithm. - * - * @return algorithm - */ - public String getAlgorithm() { - return this.algorithm; - } - - /** - * Get state hash. - * - * @return state hash - */ - public DataHash getStateHash() { - return this.stateHash; - } - - /** - * Get public key. - * - * @return public key - */ - public byte[] getPublicKey() { - return Arrays.copyOf(this.publicKey, this.publicKey.length); - } - - /** - * Verify if signature and data are correct. - * - * @param hash data hash - * @return true if successful - */ - public boolean verify(DataHash hash) { - return SigningService.verifyWithPublicKey(hash, this.signature.getBytes(), this.publicKey); - } - - /** - * Create authenticator from CBOR bytes. - * - * @param bytes CBOR bytes - * @return authenticator - */ - public static Authenticator fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); - - return new Authenticator( - CborDeserializer.readTextString(data.get(0)), - CborDeserializer.readByteString(data.get(1)), - Signature.decode(CborDeserializer.readByteString(data.get(2))), - DataHash.fromCbor(data.get(3)) - ); - } - - /** - * Convert authenticator to CBOR bytes. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeArray( - CborSerializer.encodeTextString(this.algorithm), - CborSerializer.encodeByteString(this.publicKey), - CborSerializer.encodeByteString(this.signature.encode()), - this.stateHash.toCbor() - ); - } - - /** - * Create authenticator from JSON string. - * - * @param input JSON string - * @return authenticator - */ - public static Authenticator fromJson(String input) { - try { - return UnicityObjectMapper.JSON.readValue(input, Authenticator.class); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(Authenticator.class, e); - } - } - - /** - * Convert authenticator to JSON string. - * - * @return JSON string - */ - public String toJson() { - try { - return UnicityObjectMapper.JSON.writeValueAsString(this); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(Authenticator.class, e); - } - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof Authenticator)) { - return false; - } - Authenticator that = (Authenticator) o; - return Objects.equals(this.algorithm, that.algorithm) - && Objects.equals(this.signature, that.signature) - && Objects.equals(this.stateHash, that.stateHash) - && Objects.deepEquals(this.publicKey, that.publicKey); - } - - @Override - public int hashCode() { - return Objects.hash(this.algorithm, this.signature, this.stateHash, - Arrays.hashCode(this.publicKey)); - } - - @Override - public String toString() { - return String.format("Authenticator{algorithm=%s, signature=%s, stateHash=%s, publicKey=%s}", - this.algorithm, this.signature, this.stateHash, HexConverter.encode(this.publicKey)); - } -} \ No newline at end of file diff --git a/src/main/java/org/unicitylabs/sdk/api/BlockHeightResponse.java b/src/main/java/org/unicitylabs/sdk/api/BlockHeightResponse.java index b734175..7254a65 100644 --- a/src/main/java/org/unicitylabs/sdk/api/BlockHeightResponse.java +++ b/src/main/java/org/unicitylabs/sdk/api/BlockHeightResponse.java @@ -4,11 +4,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import java.util.Objects; import org.unicitylabs.sdk.serializer.UnicityObjectMapper; import org.unicitylabs.sdk.serializer.json.JsonSerializationException; import org.unicitylabs.sdk.serializer.json.LongAsStringSerializer; +import java.util.Objects; + /** * Block height response. */ @@ -18,7 +19,7 @@ public class BlockHeightResponse { @JsonCreator private BlockHeightResponse( - @JsonProperty("blockNumber") long blockNumber + @JsonProperty("blockNumber") long blockNumber ) { this.blockNumber = blockNumber; } diff --git a/src/main/java/org/unicitylabs/sdk/api/CertificationData.java b/src/main/java/org/unicitylabs/sdk/api/CertificationData.java new file mode 100644 index 0000000..38d71c1 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/CertificationData.java @@ -0,0 +1,206 @@ +package org.unicitylabs.sdk.api; + +import org.unicitylabs.sdk.crypto.MintSigningService; +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.EncodedPredicate; +import org.unicitylabs.sdk.predicate.UnlockScript; +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; +import org.unicitylabs.sdk.transaction.MintTransaction; +import org.unicitylabs.sdk.transaction.Transaction; +import org.unicitylabs.sdk.util.HexConverter; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Certification data. + */ +public class CertificationData { + public static final long CBOR_TAG = 39031; + private static final int VERSION = 1; + + private final EncodedPredicate lockScript; + private final DataHash sourceStateHash; + private final DataHash transactionHash; + private final byte[] unlockScript; + + CertificationData( + EncodedPredicate lockScript, + DataHash sourceStateHash, + DataHash transactionHash, + byte[] unlockScript + ) { + this.lockScript = lockScript; + this.sourceStateHash = sourceStateHash; + this.transactionHash = transactionHash; + this.unlockScript = Arrays.copyOf(unlockScript, unlockScript.length); + } + + public int getVersion() { + return CertificationData.VERSION; + } + + /** + * Get lock script of certified transaction output. + * + * @return lock script + */ + public EncodedPredicate getLockScript() { + return this.lockScript; + } + + /** + * Get source state hash. + * + * @return source state hash + */ + public DataHash getSourceStateHash() { + return this.sourceStateHash; + } + + /** + * Get transaction hash. + * + * @return transaction hash + */ + public DataHash getTransactionHash() { + return this.transactionHash; + } + + /** + * Get unlock script used for certification. + * + * @return unlock script bytes + */ + public byte[] getUnlockScript() { + return Arrays.copyOf(this.unlockScript, this.unlockScript.length); + } + + /** + * Deserialize CertificationData from CBOR bytes. + * + * @param bytes CBOR bytes + * @return CertificationData + */ + public static CertificationData fromCbor(byte[] bytes) { + CborDeserializer.CborTag tag = CborDeserializer.decodeTag(bytes); + if (tag.getTag() != CertificationData.CBOR_TAG) { + throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); + } + List data = CborDeserializer.decodeArray(tag.getData(), 5); + + int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); + if (version != CertificationData.VERSION) { + throw new CborSerializationException(String.format("Unsupported version: %s", version)); + } + + return new CertificationData( + EncodedPredicate.fromCbor(data.get(1)), + new DataHash(HashAlgorithm.SHA256, CborDeserializer.decodeByteString(data.get(2))), + new DataHash(HashAlgorithm.SHA256, CborDeserializer.decodeByteString(data.get(3))), + CborDeserializer.decodeByteString(data.get(4)) + ); + } + + /** + * Build certification data for a mint transaction using the deterministic mint signing service. + * + * @param transaction mint transaction + * + * @return certification data + */ + public static CertificationData fromMintTransaction(MintTransaction transaction) { + Objects.requireNonNull(transaction, "transaction cannot be null"); + + SigningService signingService = MintSigningService.create(transaction.getTokenId()); + + return CertificationData.fromTransaction( + transaction, + SignaturePredicateUnlockScript.create(transaction, signingService).getSignature() + .encode() + ); + } + + /** + * Build certification data from a transaction and unlock script object. + * + * @param transaction transaction to certify + * @param unlockScript unlock script + * + * @return certification data + */ + public static CertificationData fromTransaction(Transaction transaction, UnlockScript unlockScript) { + Objects.requireNonNull(unlockScript, "unlockScript cannot be null"); + + return CertificationData.fromTransaction(transaction, unlockScript.encode()); + } + + /** + * Build certification data from a transaction and encoded unlock script bytes. + * + * @param transaction transaction to certify + * @param unlockScript encoded unlock script bytes + * + * @return certification data + */ + public static CertificationData fromTransaction(Transaction transaction, byte[] unlockScript) { + Objects.requireNonNull(transaction, "transaction cannot be null"); + Objects.requireNonNull(unlockScript, "unlockScript cannot be null"); + + return new CertificationData( + transaction.getLockScript(), + transaction.getSourceStateHash(), + transaction.calculateTransactionHash(), + unlockScript + ); + } + + /** + * Serialize certification data to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeTag( + CertificationData.CBOR_TAG, + CborSerializer.encodeArray( + CborSerializer.encodeUnsignedInteger(CertificationData.VERSION), + this.lockScript.toCbor(), + CborSerializer.encodeByteString(this.sourceStateHash.getData()), + CborSerializer.encodeByteString(this.transactionHash.getData()), + CborSerializer.encodeByteString(this.unlockScript) + ) + ); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof CertificationData)) { + return false; + } + CertificationData that = (CertificationData) o; + 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); + } + + @Override + public int hashCode() { + return Objects.hash(this.lockScript, this.sourceStateHash, this.transactionHash, Arrays.hashCode(this.unlockScript)); + } + + @Override + public String toString() { + return String.format( + "CertificationData{lockScript=%s, sourceStateHash=%s, transactionHash=%s, unlockScript=%s}", + this.lockScript, this.sourceStateHash, this.transactionHash, + HexConverter.encode(this.unlockScript)); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/api/CertificationRequest.java b/src/main/java/org/unicitylabs/sdk/api/CertificationRequest.java new file mode 100644 index 0000000..459f7f3 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/CertificationRequest.java @@ -0,0 +1,78 @@ +package org.unicitylabs.sdk.api; + +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; + +/** + * Submit certification request. + */ +public class CertificationRequest { + public static final long CBOR_TAG = 39030; + private static final int VERSION = 1; + + private final StateId stateId; + private final CertificationData certificationData; + + /** + * Create certification request. + * + * @param stateId state id + * @param certificationData transaction hash + */ + private CertificationRequest( + StateId stateId, + CertificationData certificationData + ) { + this.stateId = stateId; + this.certificationData = certificationData; + } + + public int getVersion() { + return CertificationRequest.VERSION; + } + + /** + * Get state id. + * + * @return state id + */ + public StateId getStateId() { + return this.stateId; + } + + /** + * Get certification data. + * + * @return certification data + */ + public CertificationData getCertificationData() { + return this.certificationData; + } + + /** + * Create certification request. + * + * @param certificationData certification data + * @return certification request + */ + public static CertificationRequest create(CertificationData certificationData) { + return new CertificationRequest(StateId.fromCertificationData(certificationData), + certificationData); + } + + /** + * Serialize request to a CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeTag( + CertificationRequest.CBOR_TAG, + CborSerializer.encodeArray( + CborSerializer.encodeUnsignedInteger(CertificationRequest.VERSION), + this.stateId.toCbor(), + this.certificationData.toCbor(), + CborSerializer.encodeUnsignedInteger(0) + ) + ); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/api/SubmitCommitmentResponse.java b/src/main/java/org/unicitylabs/sdk/api/CertificationResponse.java similarity index 60% rename from src/main/java/org/unicitylabs/sdk/api/SubmitCommitmentResponse.java rename to src/main/java/org/unicitylabs/sdk/api/CertificationResponse.java index 39d88c8..c7f08ed 100644 --- a/src/main/java/org/unicitylabs/sdk/api/SubmitCommitmentResponse.java +++ b/src/main/java/org/unicitylabs/sdk/api/CertificationResponse.java @@ -9,9 +9,9 @@ /** * Submit commitment response. */ -public class SubmitCommitmentResponse { +public class CertificationResponse { - private final SubmitCommitmentStatus status; + private final CertificationStatus status; /** * Create submit commitment response. @@ -19,8 +19,8 @@ public class SubmitCommitmentResponse { * @param status status */ @JsonCreator - public SubmitCommitmentResponse( - @JsonProperty("status") SubmitCommitmentStatus status + CertificationResponse( + @JsonProperty("status") CertificationStatus status ) { this.status = status; } @@ -30,21 +30,31 @@ public SubmitCommitmentResponse( * * @return status */ - public SubmitCommitmentStatus getStatus() { + public CertificationStatus getStatus() { return this.status; } + /** + * Create a new certification response. + * + * @param status Certification response status + * @return certification response + */ + public static CertificationResponse create(CertificationStatus status) { + return new CertificationResponse(status); + } + /** * Create submit commitment response from JSON string. * * @param input JSON string * @return submit commitment response */ - public static SubmitCommitmentResponse fromJson(String input) { + public static CertificationResponse fromJson(String input) { try { - return UnicityObjectMapper.JSON.readValue(input, SubmitCommitmentResponse.class); + return UnicityObjectMapper.JSON.readValue(input, CertificationResponse.class); } catch (JsonProcessingException e) { - throw new JsonSerializationException(SubmitCommitmentResponse.class, e); + throw new JsonSerializationException(CertificationResponse.class, e); } } @@ -57,7 +67,7 @@ public String toJson() { try { return UnicityObjectMapper.JSON.writeValueAsString(this); } catch (JsonProcessingException e) { - throw new JsonSerializationException(SubmitCommitmentResponse.class, e); + throw new JsonSerializationException(CertificationResponse.class, e); } } diff --git a/src/main/java/org/unicitylabs/sdk/api/CertificationStatus.java b/src/main/java/org/unicitylabs/sdk/api/CertificationStatus.java new file mode 100644 index 0000000..bc917e1 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/CertificationStatus.java @@ -0,0 +1,78 @@ +package org.unicitylabs.sdk.api; + +/** + * Status codes for certification. + */ +public enum CertificationStatus { + /** + * The certification request was accepted and stored. + */ + SUCCESS("SUCCESS"), + + /** + * The certification request failed because the state ID already exists. + */ + STATE_ID_EXISTS("STATE_ID_EXISTS"), + /** + * The certification request failed because the state ID does not match the expected format. + */ + STATE_ID_MISMATCH("STATE_ID_MISMATCH"), + /** + * The certification request failed because the signature verification failed. + */ + SIGNATURE_VERIFICATION_FAILED("SIGNATURE_VERIFICATION_FAILED"), + /** + * The certification request failed because signature has invalid format. + */ + INVALID_SIGNATURE_FORMAT("INVALID_SIGNATURE_FORMAT"), + /** + * The certification request failed because the public key has invalid format. + */ + INVALID_PUBLIC_KEY_FORMAT("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"), + /** + * The certification request failed because the transaction hash has invalid format. + */ + INVALID_TRANSACTION_HASH_FORMAT("INVALID_TRANSACTION_HASH_FORMAT"), + /** + * The certification request failed because the algorithm is not supported. + */ + UNSUPPORTED_ALGORITHM("UNSUPPORTED_ALGORITHM"), + /** + * The certification request failed because request was sent to invalid shard. + */ + INVALID_SHARD("INVALID_SHARD"); + + private final String value; + + CertificationStatus(String value) { + this.value = value; + } + + /** + * Get string value of the status. + * + * @return string value + */ + public String getValue() { + return value; + } + + /** + * Create status from string value. + * + * @param value string value + * @return status + */ + public static CertificationStatus fromString(String value) { + for (CertificationStatus status : CertificationStatus.values()) { + if (status.value.equalsIgnoreCase(value)) { + return status; + } + } + throw new IllegalArgumentException("Unknown status: " + value); + } +} \ No newline at end of file diff --git a/src/main/java/org/unicitylabs/sdk/api/InclusionCertificate.java b/src/main/java/org/unicitylabs/sdk/api/InclusionCertificate.java new file mode 100644 index 0000000..36b5a58 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/InclusionCertificate.java @@ -0,0 +1,165 @@ +package org.unicitylabs.sdk.api; + +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.smt.radix.FinalizedBranch; +import org.unicitylabs.sdk.smt.radix.FinalizedLeafBranch; +import org.unicitylabs.sdk.smt.radix.FinalizedNodeBranch; +import org.unicitylabs.sdk.util.BitString; +import org.unicitylabs.sdk.util.HexConverter; +import org.unicitylabs.sdk.util.LongConverter; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class InclusionCertificate { + private static final int BITMAP_SIZE = 32; + private static final int MAX_DEPTH = 255; + + private final byte[] bitmap; + private final List siblings; + + + private InclusionCertificate(byte[] bitmap, List siblings) { + this.bitmap = bitmap; + this.siblings = siblings; + } + + public static InclusionCertificate create(FinalizedNodeBranch root, byte[] key) { + FinalizedBranch node = root; + + ArrayList siblings = new ArrayList<>(); + byte[] bitmap = new byte[InclusionCertificate.BITMAP_SIZE]; + BigInteger keyPath = BitString.fromBytesReversedLSB(key).toBigInteger(); + + while (node != null) { + if (node instanceof FinalizedLeafBranch) { + FinalizedLeafBranch leaf = (FinalizedLeafBranch) node; + if (!Arrays.equals(leaf.getKey(), key)) { + throw new RuntimeException(String.format("Leaf not found for key: %s", HexConverter.encode(key))); + } + + return new InclusionCertificate(bitmap, siblings); + } + + FinalizedNodeBranch nodeBranch = (FinalizedNodeBranch) node; + boolean isRight = keyPath.testBit(nodeBranch.getDepth()); + FinalizedBranch sibling = isRight ? nodeBranch.getLeft() : nodeBranch.getRight(); + if (sibling != null) { + bitmap[nodeBranch.getDepth() / 8] |= (byte) (1 << nodeBranch.getDepth() % 8); + siblings.add(sibling.getHash()); + } + + node = isRight ? nodeBranch.getRight() : nodeBranch.getLeft(); + } + + throw new RuntimeException("Could not construct inclusion certificate: Invalid path"); + } + + public static InclusionCertificate decode(byte[] bytes) { + if (bytes.length < InclusionCertificate.BITMAP_SIZE) { + throw new IllegalArgumentException("Inclusion Certificate bitmap is invalid."); + } + + int siblingBytesLength = bytes.length - InclusionCertificate.BITMAP_SIZE; + if (siblingBytesLength % HashAlgorithm.SHA256.getLength() != 0) { + throw new IllegalArgumentException("Inclusion Certificate siblings are misaligned."); + } + + int siblingsCount = 0; + for (int i = 0; i < InclusionCertificate.BITMAP_SIZE; i++) { + int x = bytes[i] & 0xff; + x = x - ((x >>> 1) & 0x55); + x = (x & 0x33) + ((x >>> 2) & 0x33); + x = (x + (x >>> 4)) & 0x0f; + siblingsCount += x; + } + + if (siblingBytesLength / HashAlgorithm.SHA256.getLength() != siblingsCount) { + throw new IllegalArgumentException("Inclusion Certificate siblings count does not match bitmap."); + } + + ArrayList siblings = new ArrayList<>(); + for (int i = InclusionCertificate.BITMAP_SIZE; i < bytes.length; i += HashAlgorithm.SHA256.getLength()) { + siblings.add(new DataHash(HashAlgorithm.SHA256, Arrays.copyOfRange(bytes, i, i + HashAlgorithm.SHA256.getLength()))); + } + + return new InclusionCertificate(Arrays.copyOfRange(bytes, 0, InclusionCertificate.BITMAP_SIZE), siblings); + } + + public byte[] encode() { + byte[] bytes = new byte[InclusionCertificate.BITMAP_SIZE + this.siblings.size() * HashAlgorithm.SHA256.getLength()]; + System.arraycopy(this.bitmap, 0, bytes, 0, InclusionCertificate.BITMAP_SIZE); + int offset = InclusionCertificate.BITMAP_SIZE; + for (DataHash sibling : this.siblings) { + byte[] data = sibling.getData(); + System.arraycopy(data, 0, bytes, offset, data.length); + offset += data.length; + } + return bytes; + } + + public boolean verify(StateId leafKey, DataHash leafValue, DataHash expectedRootHash) { + byte[] key = leafKey.getData(); + byte[] value = leafValue.getData(); + + DataHash hash = new DataHasher(HashAlgorithm.SHA256) + .update(new byte[]{0x00}) + .update(key) + .update(value) + .digest(); + + BigInteger keyPath = BitString.fromBytesReversedLSB(key).toBigInteger(); + BigInteger bitmapPath = BitString.fromBytesReversedLSB(this.bitmap).toBigInteger(); + + int position = this.siblings.size(); + for (int depth = InclusionCertificate.MAX_DEPTH; depth >= 0; depth--) { + if (!bitmapPath.testBit(depth)) continue; + + position -= 1; + if (position < 0) return false; + + DataHash sibling = this.siblings.get(position); + + byte[] left, right; + if (keyPath.testBit(depth)) { + left = sibling.getData(); + right = hash.getData(); + } else { + left = hash.getData(); + right = sibling.getData(); + } + + hash = new DataHasher(HashAlgorithm.SHA256) + .update(new byte[]{0x01}) + .update(LongConverter.encode(depth)) + .update(left) + .update(right) + .digest(); + } + + return position == 0 && hash.equals(expectedRootHash); + } + + + @Override + public boolean equals(Object o) { + if (!(o instanceof InclusionCertificate)) return false; + InclusionCertificate that = (InclusionCertificate) o; + return Objects.deepEquals(this.bitmap, that.bitmap) && Objects.equals(this.siblings, that.siblings); + } + + @Override + public int hashCode() { + return Objects.hash(Arrays.hashCode(this.bitmap), this.siblings); + } + + @Override + public String toString() { + return String.format("InclusionCertificate{bitmap=%s, siblings=%s}", HexConverter.encode(this.bitmap), this.siblings); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java b/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java new file mode 100644 index 0000000..8944587 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java @@ -0,0 +1,135 @@ +package org.unicitylabs.sdk.api; + +import org.unicitylabs.sdk.api.bft.UnicityCertificate; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializationException; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Represents a proof of inclusion or non-inclusion in a sparse merkle tree. + */ +public class InclusionProof { + public static final long CBOR_TAG = 39033; + private static final int VERSION = 1; + + private final InclusionCertificate inclusionCertificate; + private final CertificationData certificationData; + private final UnicityCertificate unicityCertificate; + + InclusionProof( + CertificationData certificationData, + InclusionCertificate inclusionCertificate, + UnicityCertificate unicityCertificate + ) { + Objects.requireNonNull(unicityCertificate, "Unicity certificate cannot be null."); + + this.inclusionCertificate = inclusionCertificate; + this.certificationData = certificationData; + this.unicityCertificate = unicityCertificate; + } + + public int getVersion() { + return InclusionProof.VERSION; + } + + /** + * Get merkle tree path. + * + * @return merkle tree path + */ + public InclusionCertificate getInclusionCertificate() { + return this.inclusionCertificate; + } + + /** + * Get unicity certificate. + * + * @return unicity certificate + */ + public UnicityCertificate getUnicityCertificate() { + return this.unicityCertificate; + } + + /** + * Get certification data on inclusion proof, null on non inclusion proof. + * + * @return authenticator + */ + public Optional getCertificationData() { + return Optional.ofNullable(this.certificationData); + } + + /** + * Deserialize inclusion proof from CBOR bytes. + * + * @param bytes CBOR bytes + * @return inclusion proof + */ + public static InclusionProof fromCbor(byte[] bytes) { + CborDeserializer.CborTag tag = CborDeserializer.decodeTag(bytes); + if (tag.getTag() != InclusionProof.CBOR_TAG) { + throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); + } + List data = CborDeserializer.decodeArray(tag.getData(), 4); + + int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); + if (version != InclusionProof.VERSION) { + throw new CborSerializationException(String.format("Unsupported version: %s", version)); + } + + return new InclusionProof( + CborDeserializer.decodeNullable(data.get(1), CertificationData::fromCbor), + CborDeserializer.decodeNullable(data.get(2), (inclusionCertificate) -> + InclusionCertificate.decode(CborDeserializer.decodeByteString(inclusionCertificate)) + ), + UnicityCertificate.fromCbor(data.get(3)) + ); + } + + /** + * Serialize inclusion proof to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeTag( + InclusionProof.CBOR_TAG, + CborSerializer.encodeArray( + CborSerializer.encodeUnsignedInteger(InclusionProof.VERSION), + CborSerializer.encodeNullable(this.certificationData, CertificationData::toCbor), + CborSerializer.encodeNullable(this.inclusionCertificate, (inclusionCertificate) -> + CborSerializer.encodeByteString(inclusionCertificate.encode()) + ), + this.unicityCertificate.toCbor() + ) + ); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof InclusionProof)) { + return false; + } + InclusionProof that = (InclusionProof) o; + return Objects.equals(this.inclusionCertificate, that.inclusionCertificate) && Objects.equals(this.certificationData, that.certificationData) && Objects.equals(this.unicityCertificate, that.unicityCertificate); + } + + @Override + public int hashCode() { + return Objects.hash(this.inclusionCertificate, this.certificationData, this.unicityCertificate); + } + + @Override + public String toString() { + return String.format( + "InclusionProof{certificationData=%s, inclusionCertificate=%s, unicityCertificate=%s}", + this.inclusionCertificate, + this.certificationData, + this.unicityCertificate + ); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/api/InclusionProofRequest.java b/src/main/java/org/unicitylabs/sdk/api/InclusionProofRequest.java index 663ac20..dedb252 100644 --- a/src/main/java/org/unicitylabs/sdk/api/InclusionProofRequest.java +++ b/src/main/java/org/unicitylabs/sdk/api/InclusionProofRequest.java @@ -2,62 +2,37 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import org.unicitylabs.sdk.serializer.UnicityObjectMapper; -import org.unicitylabs.sdk.serializer.json.JsonSerializationException; + +import java.util.Arrays; +import java.util.Objects; /** * Inclusion proof request. */ public class InclusionProofRequest { - private final RequestId requestId; + private final byte[] stateId; /** * Create inclusion proof request. * - * @param requestId request id + * @param stateId state id */ @JsonCreator public InclusionProofRequest( - @JsonProperty("requestId") RequestId requestId + @JsonProperty("stateId") StateId stateId ) { - this.requestId = requestId; - } - - /** - * Get request id. - * - * @return request id - */ - public RequestId getRequestId() { - return this.requestId; - } + Objects.requireNonNull(stateId, "stateId cannot be null"); - /** - * Create request from JSON string. - * - * @param input JSON string - * @return inclusion proof request - */ - public static InclusionProofRequest fromJson(String input) { - try { - return UnicityObjectMapper.JSON.readValue(input, InclusionProofRequest.class); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(InclusionProofRequest.class, e); - } + this.stateId = stateId.getData(); } /** - * Convert request to JSON string. + * Get state id. * - * @return JSON string + * @return state id */ - public String toJson() { - try { - return UnicityObjectMapper.JSON.writeValueAsString(this); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(InclusionProofRequest.class, e); - } + public byte[] getStateId() { + return Arrays.copyOf(this.stateId, this.stateId.length); } } diff --git a/src/main/java/org/unicitylabs/sdk/api/InclusionProofResponse.java b/src/main/java/org/unicitylabs/sdk/api/InclusionProofResponse.java index 21b5d8c..d53e22e 100644 --- a/src/main/java/org/unicitylabs/sdk/api/InclusionProofResponse.java +++ b/src/main/java/org/unicitylabs/sdk/api/InclusionProofResponse.java @@ -1,17 +1,16 @@ package org.unicitylabs.sdk.api; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import org.unicitylabs.sdk.serializer.UnicityObjectMapper; -import org.unicitylabs.sdk.serializer.json.JsonSerializationException; -import org.unicitylabs.sdk.transaction.InclusionProof; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; + +import java.util.List; /** * Inclusion proof response. */ public class InclusionProofResponse { + private final long blockNumber; private final InclusionProof inclusionProof; /** @@ -19,11 +18,11 @@ public class InclusionProofResponse { * * @param inclusionProof inclusion proof */ - @JsonCreator - public InclusionProofResponse( - @JsonProperty("inclusionProof") - InclusionProof inclusionProof + InclusionProofResponse( + long blockNumber, + InclusionProof inclusionProof ) { + this.blockNumber = blockNumber; this.inclusionProof = inclusionProof; } @@ -37,29 +36,29 @@ public InclusionProof getInclusionProof() { } /** - * Create response from JSON string. + * Deserialize response from CBOR bytes. * - * @param input JSON string + * @param bytes CBOR bytes * @return inclusion proof response */ - public static InclusionProofResponse fromJson(String input) { - try { - return UnicityObjectMapper.JSON.readValue(input, InclusionProofResponse.class); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(InclusionProofResponse.class, e); - } + public static InclusionProofResponse fromCbor(byte[] bytes) { + List data = CborDeserializer.decodeArray(bytes, 2); + return new InclusionProofResponse( + CborDeserializer.decodeUnsignedInteger(data.get(0)).asLong(), + InclusionProof.fromCbor(data.get(1)) + ); } /** - * Convert response to JSON string. + * Serialize inclusion proof response to CBOR bytes. * - * @return JSON string + * @return CBOR bytes */ - public String toJson() { - try { - return UnicityObjectMapper.JSON.writeValueAsString(this); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(InclusionProofResponse.class, e); - } + public byte[] toCbor() { + return CborSerializer.encodeArray( + CborSerializer.encodeUnsignedInteger(this.blockNumber), + this.inclusionProof.toCbor() + ); } + } diff --git a/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java b/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java index 8606632..fd298fb 100644 --- a/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java +++ b/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java @@ -1,17 +1,21 @@ package org.unicitylabs.sdk.api; -import static com.google.common.net.HttpHeaders.AUTHORIZATION; +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; import java.util.concurrent.CompletableFuture; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.jsonrpc.JsonRpcHttpTransport; + +import static com.google.common.net.HttpHeaders.AUTHORIZATION; /** * 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; @@ -34,52 +38,52 @@ public JsonRpcAggregatorClient(String url) { * */ public JsonRpcAggregatorClient(String url, String apiKey) { - this.transport = new JsonRpcHttpTransport(url); + this.transport = new JsonRpcHttpTransport(Objects.requireNonNull(url, "url cannot be null")); this.apiKey = apiKey; } /** - * Submit commitment. + * Submit a certification request for a transaction state transition. * - * @param requestId request id - * @param transactionHash transaction hash - * @param authenticator authenticator - * @return submit commitment response + * @param certificationData certification payload + * + * @return asynchronous certification response */ - public CompletableFuture submitCommitment( - RequestId requestId, - DataHash transactionHash, - Authenticator authenticator + @Override + public CompletableFuture submitCertificationRequest( + CertificationData certificationData ) { - SubmitCommitmentRequest request = new SubmitCommitmentRequest( - requestId, - transactionHash, - authenticator, - false - ); + 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( - "submit_commitment", - request, - SubmitCommitmentResponse.class, - headers + "certification_request", + HexConverter.encode(request.toCbor()), + CertificationResponse.class, + headers ); } /** - * Get inclusion proof for request id. + * Get inclusion proof for state id. * - * @param requestId request id + * @param stateId state id * @return inclusion / non inclusion proof */ - public CompletableFuture getInclusionProof(RequestId requestId) { - InclusionProofRequest request = new InclusionProofRequest(requestId); + @Override + public CompletableFuture getInclusionProof(StateId stateId) { + InclusionProofRequest request = new InclusionProofRequest( + Objects.requireNonNull(stateId, "stateId cannot be null")); - return this.transport.request("get_inclusion_proof", request, InclusionProofResponse.class); + return this.transport + .request("get_inclusion_proof.v2", request, String.class) + .thenApply(response -> InclusionProofResponse.fromCbor(HexConverter.decode(response))); } /** @@ -87,8 +91,9 @@ public CompletableFuture getInclusionProof(RequestId req * * @return block height */ + @Override public CompletableFuture getBlockHeight() { return this.transport.request("get_block_height", Map.of(), BlockHeightResponse.class) - .thenApply(BlockHeightResponse::getBlockNumber); + .thenApply(BlockHeightResponse::getBlockNumber); } -} \ No newline at end of file +} diff --git a/src/main/java/org/unicitylabs/sdk/api/LeafValue.java b/src/main/java/org/unicitylabs/sdk/api/LeafValue.java deleted file mode 100644 index 8c2966b..0000000 --- a/src/main/java/org/unicitylabs/sdk/api/LeafValue.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.unicitylabs.sdk.api; - -import java.util.Arrays; -import java.util.Objects; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.util.HexConverter; - -/** - * Leaf value for merkle tree. - */ -public class LeafValue { - - private final byte[] bytes; - - private LeafValue(byte[] bytes) { - this.bytes = Arrays.copyOf(bytes, bytes.length); - } - - /** - * Create leaf value from authenticator and transaction hash. - * - * @param authenticator authenticator - * @param transactionHash transaction hash - * @return leaf value - */ - public static LeafValue create(Authenticator authenticator, DataHash transactionHash) { - DataHash hash = new DataHasher(HashAlgorithm.SHA256) - .update(authenticator.toCbor()) - .update(transactionHash.getImprint()) - .digest(); - - return new LeafValue(hash.getImprint()); - } - - /** - * Get leaf value as bytes. - * - * @return bytes - */ - public byte[] getBytes() { - return Arrays.copyOf(this.bytes, this.bytes.length); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof LeafValue)) { - return false; - } - LeafValue leafValue = (LeafValue) o; - return Objects.deepEquals(this.bytes, leafValue.bytes); - } - - @Override - public int hashCode() { - return Arrays.hashCode(this.bytes); - } - - @Override - public String toString() { - return String.format("LeafValue{%s}", HexConverter.encode(this.bytes)); - } -} \ No newline at end of file diff --git a/src/main/java/org/unicitylabs/sdk/api/RequestId.java b/src/main/java/org/unicitylabs/sdk/api/RequestId.java deleted file mode 100644 index fabe147..0000000 --- a/src/main/java/org/unicitylabs/sdk/api/RequestId.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.unicitylabs.sdk.api; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.serializer.UnicityObjectMapper; -import org.unicitylabs.sdk.serializer.json.JsonSerializationException; -import org.unicitylabs.sdk.token.TokenState; -import org.unicitylabs.sdk.transaction.MintTransaction; -import org.unicitylabs.sdk.util.BitString; -import org.unicitylabs.sdk.util.HexConverter; - -/** - * Represents a unique request identifier derived from a public key and state hash. - */ -@JsonDeserialize(using = RequestIdJson.Deserializer.class) -public class RequestId extends DataHash { - - /** - * Constructs a RequestId instance. - * - * @param hash The DataHash representing the request ID. - */ - protected RequestId(DataHash hash) { - super(hash.getAlgorithm(), hash.getData()); - } - - /** - * Creates a RequestId from public key and state. - * - * @param publicKey public key as a byte array. - * @param state token state. - * @return request id - */ - public static RequestId create(byte[] publicKey, TokenState state) { - return RequestId.create(publicKey, state.calculateHash()); - } - - /** - * Creates a RequestId from public key and hash. - * - * @param publicKey public key as a byte array. - * @param hash hash. - * @return request id - */ - public static RequestId create(byte[] publicKey, DataHash hash) { - return RequestId.create(publicKey, hash.getImprint()); - } - - /** - * Creates a RequestId from identifier bytes and hash imprint. - * - * @param id id bytes. - * @param stateBytes state bytes. - * @return request id. - */ - public static RequestId create(byte[] id, byte[] stateBytes) { - DataHasher hasher = new DataHasher(HashAlgorithm.SHA256); - hasher.update(id); - hasher.update(stateBytes); - - return new RequestId(hasher.digest()); - } - - /** - * Create a request id from JSON string. - * - * @param input JSON string - * @return request id - */ - public static RequestId fromJson(String input) { - try { - return UnicityObjectMapper.JSON.readValue(input, RequestId.class); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(RequestId.class, e); - } - } - - /** - * Converts the request id to a JSON string. - * - * @return JSON string - */ - public String toJson() { - try { - return UnicityObjectMapper.JSON.writeValueAsString(this); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(RequestId.class, e); - } - } - - /** - * Converts the RequestId to a BitString. - * - * @return The BitString representation of the RequestId. - */ - public BitString toBitString() { - return BitString.fromDataHash(this); - } - - /** - * Returns a string representation of the RequestId. - * - * @return The string representation. - */ - @Override - public String toString() { - return String.format("RequestId[%s]", HexConverter.encode(this.getImprint())); - } -} \ No newline at end of file diff --git a/src/main/java/org/unicitylabs/sdk/api/RequestIdJson.java b/src/main/java/org/unicitylabs/sdk/api/RequestIdJson.java deleted file mode 100644 index 2ee0bf5..0000000 --- a/src/main/java/org/unicitylabs/sdk/api/RequestIdJson.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.unicitylabs.sdk.api; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import java.io.IOException; -import org.unicitylabs.sdk.hash.DataHash; - -/** - * Request ID deserializer implementation. - */ -public class RequestIdJson { - - private RequestIdJson() { - } - - /** - * Request ID deserializer. - */ - public static class Deserializer extends StdDeserializer { - - /** - * Create deserializer. - */ - public Deserializer() { - super(RequestId.class); - } - - /** - * Deserialize request id. - * - * @param p Parser used for reading JSON content - * @param ctx Context that can be used to access information about this deserialization - * activity. - * @return request id - * @throws IOException on deserialization failure - */ - @Override - public RequestId deserialize(JsonParser p, DeserializationContext ctx) throws IOException { - return new RequestId(p.readValueAs(DataHash.class)); - } - } -} diff --git a/src/main/java/org/unicitylabs/sdk/api/StateId.java b/src/main/java/org/unicitylabs/sdk/api/StateId.java new file mode 100644 index 0000000..f0567d1 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/StateId.java @@ -0,0 +1,112 @@ +package org.unicitylabs.sdk.api; + +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.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.transaction.Transaction; +import org.unicitylabs.sdk.util.HexConverter; + +import java.util.Objects; + +/** + * Represents a state identifier for requests. + */ +public class StateId { + + private final DataHash hash; + + private StateId(DataHash hash) { + this.hash = hash; + } + + /** + * Returns the raw hash bytes of this state id. + * + * @return state id hash bytes + */ + public byte[] getData() { + return this.hash.getData(); + } + + /** + * Deserializes a state id from CBOR. + * + * @param bytes CBOR byte string containing SHA-256 hash bytes + * @return decoded state id + */ + public static StateId fromCbor(byte[] bytes) { + return new StateId( + new DataHash(HashAlgorithm.SHA256, CborDeserializer.decodeByteString(bytes))); + } + + /** + * Creates a state id from certification data. + * + * @param certificationData certification data carrying lock script and source state hash + * @return created state id + * @throws NullPointerException if {@code certificationData} is {@code null} + */ + public static StateId fromCertificationData(CertificationData certificationData) { + Objects.requireNonNull(certificationData, "Certification data cannot be null"); + + return StateId.create(certificationData.getLockScript(), + certificationData.getSourceStateHash()); + } + + /** + * Creates a state id from transaction data. + * + * @param transaction transaction carrying lock script and source state hash + * @return created state id + * @throws NullPointerException if {@code transaction} is {@code null} + */ + public static StateId fromTransaction(Transaction transaction) { + Objects.requireNonNull(transaction, "Transaction cannot be null"); + + return StateId.create(transaction.getLockScript(), transaction.getSourceStateHash()); + } + + private static StateId create(EncodedPredicate predicate, DataHash stateHash) { + DataHash hash = new DataHasher(HashAlgorithm.SHA256) + .update( + CborSerializer.encodeArray( + predicate.toCbor(), + CborSerializer.encodeByteString(stateHash.getData()) + ) + ) + .digest(); + + return new StateId(hash); + } + + /** + * Serializes this state id as a CBOR bytes. + * + * @return CBOR-encoded state id + */ + public byte[] toCbor() { + return CborSerializer.encodeByteString(this.getData()); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof StateId)) { + return false; + } + StateId stateId = (StateId) o; + return Objects.equals(this.hash, stateId.hash); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.hash); + } + + @Override + public String toString() { + return String.format("StateId[%s]", HexConverter.encode(this.getData())); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/api/SubmitCommitmentRequest.java b/src/main/java/org/unicitylabs/sdk/api/SubmitCommitmentRequest.java deleted file mode 100644 index 957a45d..0000000 --- a/src/main/java/org/unicitylabs/sdk/api/SubmitCommitmentRequest.java +++ /dev/null @@ -1,102 +0,0 @@ -package org.unicitylabs.sdk.api; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.serializer.UnicityObjectMapper; -import org.unicitylabs.sdk.serializer.json.JsonSerializationException; - -/** - * Submit commitment request. - */ -public class SubmitCommitmentRequest { - - private final RequestId requestId; - private final DataHash transactionHash; - private final Authenticator authenticator; - private final Boolean receipt; - - /** - * Create submit commitment request. - * - * @param requestId request id - * @param transactionHash transaction hash - * @param authenticator authenticator - * @param receipt get receipt - */ - @JsonCreator - public SubmitCommitmentRequest( - @JsonProperty("requestId") RequestId requestId, - @JsonProperty("transactionHash") DataHash transactionHash, - @JsonProperty("authenticator") Authenticator authenticator, - @JsonProperty("receipt") Boolean receipt) { - this.requestId = requestId; - this.transactionHash = transactionHash; - this.authenticator = authenticator; - this.receipt = receipt; - } - - /** - * Get request id. - * - * @return request id - */ - public RequestId getRequestId() { - return this.requestId; - } - - /** - * Get transaction hash. - * - * @return transaction hash - */ - public DataHash getTransactionHash() { - return this.transactionHash; - } - - /** - * Get authenticator. - * - * @return authenticator - */ - public Authenticator getAuthenticator() { - return this.authenticator; - } - - /** - * Is getting receipt from unicity service. - * - * @return true if receipt unicity service should return receipt - */ - public Boolean getReceipt() { - return this.receipt; - } - - /** - * Create submit commitment request from JSON string. - * - * @param input JSON string - * @return submit commitment request - */ - public static SubmitCommitmentRequest fromJson(String input) { - try { - return UnicityObjectMapper.JSON.readValue(input, SubmitCommitmentRequest.class); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(SubmitCommitmentRequest.class, e); - } - } - - /** - * Convert submit commitment request to JSON string. - * - * @return JSON string - */ - public String toJson() { - try { - return UnicityObjectMapper.JSON.writeValueAsString(this); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(SubmitCommitmentRequest.class, e); - } - } -} diff --git a/src/main/java/org/unicitylabs/sdk/api/SubmitCommitmentStatus.java b/src/main/java/org/unicitylabs/sdk/api/SubmitCommitmentStatus.java deleted file mode 100644 index f0f926c..0000000 --- a/src/main/java/org/unicitylabs/sdk/api/SubmitCommitmentStatus.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.unicitylabs.sdk.api; - -/** - * Status codes for submit commitment response. - */ -public enum SubmitCommitmentStatus { - /** - * The commitment was accepted and stored. - */ - SUCCESS("SUCCESS"), - /** - * Signature verification failed. - */ - AUTHENTICATOR_VERIFICATION_FAILED("AUTHENTICATOR_VERIFICATION_FAILED"), - /** - * Request identifier did not match the payload. - */ - REQUEST_ID_MISMATCH("REQUEST_ID_MISMATCH"), - /** - * A commitment with the same request id already exists. - */ - REQUEST_ID_EXISTS("REQUEST_ID_EXISTS"); - - private final String value; - - SubmitCommitmentStatus(String value) { - this.value = value; - } - - /** - * Get string value of the status. - * - * @return string value - */ - public String getValue() { - return value; - } - - /** - * Create status from string value. - * - * @param value string value - * @return status - */ - public static SubmitCommitmentStatus fromString(String value) { - for (SubmitCommitmentStatus status : SubmitCommitmentStatus.values()) { - if (status.value.equalsIgnoreCase(value)) { - return status; - } - } - throw new IllegalArgumentException("Unknown status: " + value); - } -} \ No newline at end of file diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java b/src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java new file mode 100644 index 0000000..d8a1a1e --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java @@ -0,0 +1,239 @@ +package org.unicitylabs.sdk.api.bft; + +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer.CborTag; +import org.unicitylabs.sdk.serializer.cbor.CborSerializationException; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.util.HexConverter; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Input record for UnicityCertificate. + */ +public class InputRecord { + public static final long CBOR_TAG = 39002; + private static final int VERSION = 1; + + private final long roundNumber; + private final long epoch; + private final byte[] previousHash; + private final byte[] hash; + private final byte[] summaryValue; + private final long timestamp; + private final byte[] blockHash; + private final long sumOfEarnedFees; + private final byte[] executedTransactionsHash; + + InputRecord( + long roundNumber, + long epoch, + byte[] previousHash, + byte[] hash, + byte[] summaryValue, + long timestamp, + byte[] blockHash, + long sumOfEarnedFees, + byte[] executedTransactionsHash + ) { + Objects.requireNonNull(hash, "Hash cannot be null"); + Objects.requireNonNull(summaryValue, "Summary value cannot be null"); + + this.roundNumber = roundNumber; + this.epoch = epoch; + this.previousHash = previousHash; + this.hash = hash; + this.summaryValue = summaryValue; + this.timestamp = timestamp; + this.blockHash = blockHash; + this.sumOfEarnedFees = sumOfEarnedFees; + this.executedTransactionsHash = executedTransactionsHash; + } + + public int getVersion() { + return InputRecord.VERSION; + } + + /** + * Get round number. + * + * @return round number + */ + public long getRoundNumber() { + return this.roundNumber; + } + + /** + * Get epoch. + * + * @return epoch + */ + public long getEpoch() { + return this.epoch; + } + + /** + * Get previous hash. + * + * @return previous hash or null if not set + */ + public byte[] getPreviousHash() { + return this.previousHash != null ? Arrays.copyOf(this.previousHash, this.previousHash.length) + : null; + } + + /** + * Get hash. + * + * @return hash + */ + public byte[] getHash() { + return Arrays.copyOf(this.hash, this.hash.length); + } + + /** + * Get summary value. + * + * @return summary value + */ + public byte[] getSummaryValue() { + return Arrays.copyOf(this.summaryValue, this.summaryValue.length); + } + + /** + * Get timestamp. + * + * @return timestamp + */ + public long getTimestamp() { + return this.timestamp; + } + + /** + * Get block hash. + * + * @return block hash or null if not set + */ + public byte[] getBlockHash() { + return this.blockHash != null ? Arrays.copyOf(this.blockHash, this.blockHash.length) : null; + } + + /** + * Get sum of earned fees. + * + * @return sum of earned fees + */ + public long getSumOfEarnedFees() { + return this.sumOfEarnedFees; + } + + /** + * Get executed transactions hash. + * + * @return executed transactions hash or null if not set + */ + public byte[] getExecutedTransactionsHash() { + return this.executedTransactionsHash != null ? Arrays.copyOf(this.executedTransactionsHash, + this.executedTransactionsHash.length) : null; + } + + /** + * Deserialize InputRecord from CBOR bytes. + * + * @param bytes CBOR bytes + * @return input record + */ + public static InputRecord fromCbor(byte[] bytes) { + CborTag tag = CborDeserializer.decodeTag(bytes); + if (tag.getTag() != InputRecord.CBOR_TAG) { + throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); + } + List data = CborDeserializer.decodeArray(tag.getData(), 10); + + int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); + if (version != InputRecord.VERSION) { + throw new CborSerializationException(String.format("Unsupported version: %s", version)); + } + + return new InputRecord( + CborDeserializer.decodeUnsignedInteger(data.get(1)).asLong(), + CborDeserializer.decodeUnsignedInteger(data.get(2)).asLong(), + CborDeserializer.decodeNullable(data.get(3), CborDeserializer::decodeByteString), + CborDeserializer.decodeByteString(data.get(4)), + CborDeserializer.decodeByteString(data.get(5)), + CborDeserializer.decodeUnsignedInteger(data.get(6)).asLong(), + CborDeserializer.decodeNullable(data.get(7), CborDeserializer::decodeByteString), + CborDeserializer.decodeUnsignedInteger(data.get(8)).asLong(), + CborDeserializer.decodeNullable(data.get(9), CborDeserializer::decodeByteString) + ); + } + + /** + * Serialize InputRecord to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeTag( + InputRecord.CBOR_TAG, + CborSerializer.encodeArray( + CborSerializer.encodeUnsignedInteger(InputRecord.VERSION), + CborSerializer.encodeUnsignedInteger(this.roundNumber), + CborSerializer.encodeUnsignedInteger(this.epoch), + CborSerializer.encodeNullable(this.previousHash, CborSerializer::encodeByteString), + CborSerializer.encodeByteString(this.hash), + CborSerializer.encodeByteString(this.summaryValue), + CborSerializer.encodeUnsignedInteger(this.timestamp), + CborSerializer.encodeNullable(this.blockHash, CborSerializer::encodeByteString), + CborSerializer.encodeUnsignedInteger(this.sumOfEarnedFees), + CborSerializer.encodeNullable(this.executedTransactionsHash, + CborSerializer::encodeByteString) + )); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof InputRecord)) { + return false; + } + InputRecord that = (InputRecord) o; + return Objects.equals(this.roundNumber, that.roundNumber) + && Objects.equals(this.epoch, that.epoch) + && Objects.deepEquals(this.previousHash, that.previousHash) + && Objects.deepEquals(this.hash, that.hash) + && Objects.deepEquals(this.summaryValue, that.summaryValue) + && Objects.equals(this.timestamp, that.timestamp) + && Objects.deepEquals(this.blockHash, that.blockHash) + && Objects.equals(this.sumOfEarnedFees, that.sumOfEarnedFees) + && Objects.deepEquals(this.executedTransactionsHash, that.executedTransactionsHash); + } + + @Override + public int hashCode() { + 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), + this.sumOfEarnedFees, Arrays.hashCode(this.executedTransactionsHash)); + } + + @Override + public String toString() { + return String.format("InputRecord{roundNumber=%s, epoch=%s, previousHash=%s, " + + "hash=%s, summaryValue=%s, timestamp=%s, blockHash=%s, sumOfEarnedFees=%s, " + + "executedTransactionsHash=%s}", + this.roundNumber, + this.epoch, + this.previousHash != null ? HexConverter.encode(this.previousHash) : null, + HexConverter.encode(this.hash), + HexConverter.encode(this.summaryValue), + this.timestamp, + this.blockHash != null ? HexConverter.encode(this.blockHash) : null, + this.sumOfEarnedFees, + this.executedTransactionsHash != null ? HexConverter.encode(this.executedTransactionsHash) + : null + ); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/bft/RootTrustBase.java b/src/main/java/org/unicitylabs/sdk/api/bft/RootTrustBase.java similarity index 76% rename from src/main/java/org/unicitylabs/sdk/bft/RootTrustBase.java rename to src/main/java/org/unicitylabs/sdk/api/bft/RootTrustBase.java index 0e3a7ef..b4fe31d 100644 --- a/src/main/java/org/unicitylabs/sdk/bft/RootTrustBase.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/RootTrustBase.java @@ -1,17 +1,18 @@ -package org.unicitylabs.sdk.bft; +package org.unicitylabs.sdk.api.bft; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.unicitylabs.sdk.serializer.UnicityObjectMapper; +import org.unicitylabs.sdk.serializer.json.JsonSerializationException; +import org.unicitylabs.sdk.serializer.json.LongAsStringSerializer; + import java.util.Arrays; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; -import org.unicitylabs.sdk.serializer.UnicityObjectMapper; -import org.unicitylabs.sdk.serializer.json.JsonSerializationException; -import org.unicitylabs.sdk.serializer.json.LongAsStringSerializer; /** * Root trust base information. @@ -31,16 +32,16 @@ public class RootTrustBase { @JsonCreator RootTrustBase( - @JsonProperty("version") long version, - @JsonProperty("networkId") int networkId, - @JsonProperty("epoch") long epoch, - @JsonProperty("epochStartRound") long epochStartRound, - @JsonProperty("rootNodes") Set rootNodes, - @JsonProperty("quorumThreshold") long quorumThreshold, - @JsonProperty("stateHash") byte[] stateHash, - @JsonProperty("changeRecordHash") byte[] changeRecordHash, - @JsonProperty("previousEntryHash") byte[] previousEntryHash, - @JsonProperty("signatures") Map signatures + @JsonProperty("version") long version, + @JsonProperty("networkId") int networkId, + @JsonProperty("epoch") long epoch, + @JsonProperty("epochStartRound") long epochStartRound, + @JsonProperty("rootNodes") Set rootNodes, + @JsonProperty("quorumThreshold") long quorumThreshold, + @JsonProperty("stateHash") byte[] stateHash, + @JsonProperty("changeRecordHash") byte[] changeRecordHash, + @JsonProperty("previousEntryHash") byte[] previousEntryHash, + @JsonProperty("signatures") Map signatures ) { this.version = version; this.networkId = networkId; @@ -50,16 +51,16 @@ public class RootTrustBase { this.quorumThreshold = quorumThreshold; this.stateHash = Arrays.copyOf(stateHash, stateHash.length); this.changeRecordHash = changeRecordHash == null - ? null - : Arrays.copyOf(changeRecordHash, changeRecordHash.length); + ? null + : Arrays.copyOf(changeRecordHash, changeRecordHash.length); this.previousEntryHash = previousEntryHash == null - ? null - : Arrays.copyOf(previousEntryHash, previousEntryHash.length); + ? null + : Arrays.copyOf(previousEntryHash, previousEntryHash.length); this.signatures = signatures.entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - e -> Arrays.copyOf(e.getValue(), e.getValue().length) - )); + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> Arrays.copyOf(e.getValue(), e.getValue().length) + )); } /** @@ -136,8 +137,8 @@ public byte[] getStateHash() { */ public byte[] getChangeRecordHash() { return this.changeRecordHash == null - ? null - : Arrays.copyOf(this.changeRecordHash, this.changeRecordHash.length); + ? null + : Arrays.copyOf(this.changeRecordHash, this.changeRecordHash.length); } /** @@ -147,8 +148,8 @@ public byte[] getChangeRecordHash() { */ public byte[] getPreviousEntryHash() { return this.previousEntryHash == null - ? null - : Arrays.copyOf(this.previousEntryHash, this.previousEntryHash.length); + ? null + : Arrays.copyOf(this.previousEntryHash, this.previousEntryHash.length); } /** @@ -158,11 +159,11 @@ public byte[] getPreviousEntryHash() { */ public Map getSignatures() { return Map.copyOf( - this.signatures.entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - e -> Arrays.copyOf(e.getValue(), e.getValue().length) - )) + this.signatures.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> Arrays.copyOf(e.getValue(), e.getValue().length) + )) ); } @@ -204,9 +205,9 @@ public static class NodeInfo { @JsonCreator NodeInfo( - @JsonProperty("nodeId") String nodeId, - @JsonProperty("sigKey") byte[] signingKey, - @JsonProperty("stake") long stakedAmount + @JsonProperty("nodeId") String nodeId, + @JsonProperty("sigKey") byte[] signingKey, + @JsonProperty("stake") long stakedAmount ) { this.nodeId = nodeId; this.signingKey = Arrays.copyOf(signingKey, signingKey.length); diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/ShardId.java b/src/main/java/org/unicitylabs/sdk/api/bft/ShardId.java new file mode 100644 index 0000000..0509ec0 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/bft/ShardId.java @@ -0,0 +1,127 @@ +package org.unicitylabs.sdk.api.bft; + +import org.unicitylabs.sdk.serializer.cbor.CborSerializationException; + +import java.util.Arrays; +import java.util.Objects; + +public class ShardId { + + private final byte[] bits; + private final int length; + + private ShardId(byte[] bits, int length) { + this.bits = Arrays.copyOf(bits, bits.length); + this.length = length; + } + + public byte[] getBits() { + return Arrays.copyOf(this.bits, this.bits.length); + } + + public int getLength() { + return this.length; + } + + public static ShardId decode(byte[] data) { + if (data.length == 0) { + throw new CborSerializationException("Invalid ShardId encoding: empty input"); + } + + int lastByte = data[data.length - 1] & 0xff; + + for (int i = 8; i > 0; i--) { + if ((lastByte & 1) == 1) { + if (i == 1) { + return new ShardId( + Arrays.copyOfRange(data, 0, data.length - 1), + (data.length - 1) * 8 + ); + } + + byte[] bits = Arrays.copyOfRange(data, 0, data.length); + bits[data.length - 1] = (byte) (((lastByte >> 1) << (8 - i + 1)) & 0xff); + return new ShardId(bits, (data.length - 1) * 8 + i - 1); + } + + lastByte >>= 1; + } + + throw new CborSerializationException( + "Invalid ShardId encoding: last byte doesnt contain end marker"); + } + + public byte[] encode() { + int byteCount = this.length / 8; + int bitCount = this.length % 8; + byte[] result = new byte[byteCount + 1]; + System.arraycopy(this.bits, 0, result, 0, byteCount); + if (bitCount == 0) { + result[byteCount] = (byte) 0b10000000; + } else { + int v = this.bits[byteCount] & (~(0xff >> bitCount) & 0xff); + result[byteCount] = (byte) ((v | (1 << (7 - bitCount))) & 0xff); + } + return result; + } + + public int getBit(int index) { + if (index < 0 || index >= this.length) { + throw new IndexOutOfBoundsException("ShardId bit index out of bounds"); + } + return ((this.bits[index / 8] & 0xff) >> (7 - (index % 8))) & 1; + } + + public boolean isPrefixOf(byte[] data) { + if (data.length * 8 < this.length) { + return false; + } + + int fullBytes = this.length / 8; + int remainingBits = this.length % 8; + + for (int i = 0; i < fullBytes; i++) { + if (this.bits[i] != data[i]) { + return false; + } + } + + if (remainingBits > 0) { + int mask = 0xff & (0xff << (8 - remainingBits)); + return (this.bits[fullBytes] & mask) == (data[fullBytes] & mask); + } + + return true; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ShardId)) { + return false; + } + ShardId that = (ShardId) o; + return this.length == that.length && Arrays.equals(this.bits, that.bits); + } + + @Override + public int hashCode() { + return Objects.hash(this.length, Arrays.hashCode(this.bits)); + } + + @Override + public String toString() { + int fullBytes = this.length / 8; + int remainingBits = this.length % 8; + StringBuilder result = new StringBuilder(); + for (int i = 0; i < fullBytes; i++) { + String bin = Integer.toBinaryString(this.bits[i] & 0xff); + result.append("00000000", 0, 8 - bin.length()).append(bin); + } + if (remainingBits > 0) { + String bin = Integer.toBinaryString(this.bits[fullBytes] & 0xff); + String padded = "00000000".substring(0, 8 - bin.length()) + bin; + result.append(padded, 0, remainingBits); + } + return result.toString(); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java b/src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java new file mode 100644 index 0000000..94227b3 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java @@ -0,0 +1,122 @@ +package org.unicitylabs.sdk.api.bft; + +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializationException; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Shard tree certificate. + */ +public class ShardTreeCertificate { + public static final long CBOR_TAG = 39003; + private static final int VERSION = 1; + + private final ShardId shard; + private final List siblingHashList; + + ShardTreeCertificate(ShardId shard, List siblingHashList) { + Objects.requireNonNull(shard, "Shard cannot be null"); + Objects.requireNonNull(siblingHashList, "Sibling hash list cannot be null"); + + this.shard = shard; + this.siblingHashList = siblingHashList.stream() + .map(hash -> Arrays.copyOf(hash, hash.length)) + .collect(Collectors.toList()); + } + + public int getVersion() { + return ShardTreeCertificate.VERSION; + } + + /** + * Get shard. + * + * @return shard + */ + public ShardId getShard() { + return this.shard; + } + + /** + * Get sibling hash list. + * + * @return sibling hash list + */ + public List getSiblingHashList() { + return this.siblingHashList.stream() + .map(hash -> Arrays.copyOf(hash, hash.length)) + .collect(Collectors.toList()); + } + + /** + * Deserialize shard tree certificate from CBOR bytes. + * + * @param bytes CBOR bytes + * @return shard tree certificate + */ + public static ShardTreeCertificate fromCbor(byte[] bytes) { + CborDeserializer.CborTag tag = CborDeserializer.decodeTag(bytes); + if (tag.getTag() != ShardTreeCertificate.CBOR_TAG) { + throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); + } + List data = CborDeserializer.decodeArray(tag.getData(), 3); + + int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); + if (version != ShardTreeCertificate.VERSION) { + throw new CborSerializationException(String.format("Unsupported version: %s", version)); + } + + return new ShardTreeCertificate( + ShardId.decode(CborDeserializer.decodeByteString(data.get(1))), + CborDeserializer.decodeArray(data.get(2)).stream() + .map(CborDeserializer::decodeByteString) + .collect(Collectors.toList()) + ); + } + + /** + * Serialize shard tree certificate to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeTag( + ShardTreeCertificate.CBOR_TAG, + CborSerializer.encodeArray( + CborSerializer.encodeUnsignedInteger(ShardTreeCertificate.VERSION), + CborSerializer.encodeByteString(this.shard.encode()), + CborSerializer.encodeArray( + this.siblingHashList.stream() + .map(CborSerializer::encodeByteString) + .toArray(byte[][]::new) + ) + ) + ); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ShardTreeCertificate)) { + return false; + } + ShardTreeCertificate that = (ShardTreeCertificate) o; + return Objects.deepEquals(this.shard, that.shard) + && Objects.equals(this.siblingHashList, that.siblingHashList); + } + + @Override + public int hashCode() { + return Objects.hash(this.shard, this.siblingHashList); + } + + @Override + public String toString() { + return String.format("ShardTreeCertificate{shard=%s, siblingHashList=%s}", + this.shard, this.siblingHashList); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java new file mode 100644 index 0000000..4900501 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java @@ -0,0 +1,247 @@ +package org.unicitylabs.sdk.api.bft; + +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.CborSerializationException; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.util.HexConverter; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Unicity certificate. + */ +public class UnicityCertificate { + public static final long CBOR_TAG = 39001; + private static final int VERSION = 1; + + private final InputRecord inputRecord; + private final byte[] technicalRecordHash; + private final byte[] shardConfigurationHash; + private final ShardTreeCertificate shardTreeCertificate; + private final UnicityTreeCertificate unicityTreeCertificate; + private final UnicitySeal unicitySeal; + + UnicityCertificate( + InputRecord inputRecord, + byte[] technicalRecordHash, + byte[] shardConfigurationHash, + ShardTreeCertificate shardTreeCertificate, + UnicityTreeCertificate unicityTreeCertificate, + UnicitySeal unicitySeal + ) { + Objects.requireNonNull(inputRecord, "Input record cannot be null"); + Objects.requireNonNull(shardConfigurationHash, "Shard configuration hash cannot be null"); + Objects.requireNonNull(shardTreeCertificate, "Shard tree certificate cannot be null"); + Objects.requireNonNull(unicityTreeCertificate, "Unicity tree certificate cannot be null"); + Objects.requireNonNull(unicitySeal, "Unicity seal cannot be null"); + + this.inputRecord = inputRecord; + this.technicalRecordHash = technicalRecordHash != null + ? Arrays.copyOf(technicalRecordHash, technicalRecordHash.length) + : null; + this.shardConfigurationHash = Arrays.copyOf( + shardConfigurationHash, + shardConfigurationHash.length + ); + this.shardTreeCertificate = shardTreeCertificate; + this.unicityTreeCertificate = unicityTreeCertificate; + this.unicitySeal = unicitySeal; + } + + /** + * Get the certificate version. + * + * @return certificate version + */ + public int getVersion() { + return UnicityCertificate.VERSION; + } + + /** + * Get the input record. + * + * @return input record + */ + public InputRecord getInputRecord() { + return this.inputRecord; + } + + /** + * Get the technical record hash. + * + * @return technical record hash + */ + public byte[] getTechnicalRecordHash() { + return this.technicalRecordHash != null + ? Arrays.copyOf(this.technicalRecordHash, this.technicalRecordHash.length) + : null; + } + + /** + * Get the shard configuration hash. + * + * @return shard configuration hash + */ + public byte[] getShardConfigurationHash() { + return Arrays.copyOf(this.shardConfigurationHash, this.shardConfigurationHash.length); + } + + /** + * Get the shard tree certificate. + * + * @return shard tree certificate + */ + public ShardTreeCertificate getShardTreeCertificate() { + return this.shardTreeCertificate; + } + + /** + * Get the unicity tree certificate. + * + * @return unicity tree certificate + */ + public UnicityTreeCertificate getUnicityTreeCertificate() { + return this.unicityTreeCertificate; + } + + /** + * Get the unicity seal. + * + * @return unicity seal + */ + public UnicitySeal getUnicitySeal() { + return this.unicitySeal; + } + + /** + * Calculate the root hash of the shard tree certificate. + * + * @param inputRecord input record + * @param technicalRecordHash technical record hash + * @param shardConfigurationHash shard configuration hash + * @param shardTreeCertificate shard tree certificate + * @return root hash + */ + public static DataHash calculateShardTreeCertificateRootHash( + InputRecord inputRecord, + byte[] technicalRecordHash, + byte[] shardConfigurationHash, + ShardTreeCertificate shardTreeCertificate + ) { + + DataHash rootHash = new DataHasher(HashAlgorithm.SHA256) + .update(inputRecord.toCbor()) + .update( + CborSerializer.encodeNullable(technicalRecordHash, CborSerializer::encodeByteString)) + .update(CborSerializer.encodeByteString(shardConfigurationHash)) + .digest(); + + ShardId shardId = shardTreeCertificate.getShard(); + List siblingHashes = shardTreeCertificate.getSiblingHashList(); + for (int i = 0; i < siblingHashes.size(); i++) { + boolean isRight = shardId.getBit(shardId.getLength() - 1 - i) == 1; + if (isRight) { + rootHash = new DataHasher(HashAlgorithm.SHA256) + .update(CborSerializer.encodeByteString(siblingHashes.get(i))) + .update(CborSerializer.encodeByteString(rootHash.getData())) + .digest(); + } else { + rootHash = new DataHasher(HashAlgorithm.SHA256) + .update(CborSerializer.encodeByteString(rootHash.getData())) + .update(CborSerializer.encodeByteString(siblingHashes.get(i))) + .digest(); + } + } + + return rootHash; + + } + + /** + * Deserialize unicity certificate from CBOR bytes. + * + * @param bytes CBOR bytes + * @return unicity certificate + */ + public static UnicityCertificate fromCbor(byte[] bytes) { + CborDeserializer.CborTag tag = CborDeserializer.decodeTag(bytes); + if (tag.getTag() != UnicityCertificate.CBOR_TAG) { + throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); + } + List data = CborDeserializer.decodeArray(tag.getData(), 7); + + int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); + if (version != UnicityCertificate.VERSION) { + throw new CborSerializationException(String.format("Unsupported version: %s", version)); + } + + return new UnicityCertificate( + InputRecord.fromCbor(data.get(1)), + CborDeserializer.decodeNullable(data.get(2), CborDeserializer::decodeByteString), + CborDeserializer.decodeByteString(data.get(3)), + ShardTreeCertificate.fromCbor(data.get(4)), + UnicityTreeCertificate.fromCbor(data.get(5)), + UnicitySeal.fromCbor(data.get(6)) + ); + } + + /** + * Serialize unicity certificate to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeTag( + UnicityCertificate.CBOR_TAG, + CborSerializer.encodeArray( + CborSerializer.encodeUnsignedInteger(UnicityCertificate.VERSION), + this.inputRecord.toCbor(), + CborSerializer.encodeNullable(this.technicalRecordHash, + CborSerializer::encodeByteString), + CborSerializer.encodeByteString(this.shardConfigurationHash), + this.shardTreeCertificate.toCbor(), + this.unicityTreeCertificate.toCbor(), + this.unicitySeal.toCbor() + )); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof UnicityCertificate)) { + return false; + } + UnicityCertificate that = (UnicityCertificate) o; + return Objects.equals(this.inputRecord, that.inputRecord) + && Objects.deepEquals(this.technicalRecordHash, that.technicalRecordHash) + && Objects.deepEquals(this.shardConfigurationHash, that.shardConfigurationHash) + && Objects.equals(this.shardTreeCertificate, that.shardTreeCertificate) + && Objects.equals(this.unicityTreeCertificate, that.unicityTreeCertificate) + && Objects.equals(this.unicitySeal, that.unicitySeal); + } + + @Override + public int hashCode() { + return Objects.hash(this.inputRecord, Arrays.hashCode(this.technicalRecordHash), + Arrays.hashCode(this.shardConfigurationHash), this.shardTreeCertificate, + this.unicityTreeCertificate, this.unicitySeal); + } + + @Override + public String toString() { + return String.format("UnicityCertificate{inputRecord=%s, technicalRecordHash=%s, " + + "shardConfigurationHash=%s, shardTreeCertificate=%s, unicityTreeCertificate=%s, " + + "unicitySeal=%s}", + this.inputRecord, + this.technicalRecordHash != null ? HexConverter.encode(this.technicalRecordHash) : null, + HexConverter.encode(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 new file mode 100644 index 0000000..3759ffe --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java @@ -0,0 +1,292 @@ +package org.unicitylabs.sdk.api.bft; + +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer.CborTag; +import org.unicitylabs.sdk.serializer.cbor.CborSerializationException; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer.CborMap; +import org.unicitylabs.sdk.util.HexConverter; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * UnicitySeal represents a seal in the Unicity BFT system, containing metadata and signatures. + */ +public class UnicitySeal { + public static final long CBOR_TAG = 39005; + private static final int VERSION = 1; + + private final short networkId; + private final long rootChainRoundNumber; + private final long epoch; + private final long timestamp; + private final byte[] previousHash; // nullable + private final byte[] hash; + private final Set signatures; + + UnicitySeal( + short networkId, + long rootChainRoundNumber, + long epoch, + long timestamp, + byte[] previousHash, + byte[] hash, + Set signatures + ) { + Objects.requireNonNull(hash, "Hash cannot be null"); + + this.networkId = networkId; + this.rootChainRoundNumber = rootChainRoundNumber; + this.epoch = epoch; + this.timestamp = timestamp; + this.previousHash = previousHash; + this.hash = hash; + this.signatures = signatures == null + ? null + : Set.copyOf(signatures); + } + + /** + * Create a new UnicitySeal instance with the provided signatures. + * + * @param signatures the signatures to include in the new UnicitySeal + * @return a new UnicitySeal instance with the specified signatures + */ + public UnicitySeal withSignatures(Set signatures) { + return new UnicitySeal( + this.networkId, + this.rootChainRoundNumber, + this.epoch, + this.timestamp, + this.previousHash, + this.hash, + signatures + ); + } + + public int getVersion() { + return UnicitySeal.VERSION; + } + + /** + * Get the network ID. + * + * @return network ID + */ + public short getNetworkId() { + return this.networkId; + } + + /** + * Get the root chain round number. + * + * @return root chain round number + */ + public long getRootChainRoundNumber() { + return this.rootChainRoundNumber; + } + + /** + * Get the epoch. + * + * @return epoch + */ + public long getEpoch() { + return this.epoch; + } + + /** + * Get the timestamp. + * + * @return timestamp + */ + public long getTimestamp() { + return this.timestamp; + } + + /** + * Get the previous hash. + * + * @return previous hash or null if not set + */ + public byte[] getPreviousHash() { + return this.previousHash != null ? Arrays.copyOf(this.previousHash, this.previousHash.length) + : null; + } + + /** + * Get the hash. + * + * @return hash + */ + public byte[] getHash() { + return Arrays.copyOf(this.hash, this.hash.length); + } + + /** + * Get the signatures. + * + * @return signatures + */ + public Set getSignatures() { + return this.signatures; + } + + /** + * Deserialize unicity seal from CBOR bytes. + * + * @param bytes CBOR bytes + * @return unicity seal + */ + public static UnicitySeal fromCbor(byte[] bytes) { + CborTag tag = CborDeserializer.decodeTag(bytes); + if (tag.getTag() != UnicitySeal.CBOR_TAG) { + throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); + } + List data = CborDeserializer.decodeArray(tag.getData(), 8); + + int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); + if (version != UnicitySeal.VERSION) { + throw new CborSerializationException(String.format("Unsupported version: %s", version)); + } + + return new UnicitySeal( + CborDeserializer.decodeUnsignedInteger(data.get(1)).asShort(), + CborDeserializer.decodeUnsignedInteger(data.get(2)).asLong(), + CborDeserializer.decodeUnsignedInteger(data.get(3)).asLong(), + CborDeserializer.decodeUnsignedInteger(data.get(4)).asLong(), + CborDeserializer.decodeNullable(data.get(5), CborDeserializer::decodeByteString), + CborDeserializer.decodeByteString(data.get(6)), + CborDeserializer.decodeMap(data.get(7)).stream() + .map(entry -> new SignatureEntry( + CborDeserializer.decodeTextString(entry.getKey()), + CborDeserializer.decodeByteString(entry.getValue()) + )) + .collect(Collectors.toSet()) + ); + } + + /** + * Serialize unicity seal to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeTag( + UnicitySeal.CBOR_TAG, + CborSerializer.encodeArray( + CborSerializer.encodeUnsignedInteger(UnicitySeal.VERSION), + CborSerializer.encodeUnsignedInteger(this.networkId), + CborSerializer.encodeUnsignedInteger(this.rootChainRoundNumber), + CborSerializer.encodeUnsignedInteger(this.epoch), + CborSerializer.encodeUnsignedInteger(this.timestamp), + CborSerializer.encodeNullable(this.previousHash, CborSerializer::encodeByteString), + CborSerializer.encodeByteString(this.hash), + CborSerializer.encodeNullable( + this.signatures, + (signatures) -> CborSerializer.encodeMap( + new CborMap( + signatures.stream() + .map(entry -> new CborMap.Entry( + CborSerializer.encodeTextString(entry.getKey()), + CborSerializer.encodeByteString(entry.getSignature()) + ) + ) + .collect(Collectors.toSet()) + ) + ) + ) + ) + ); + } + + /** + * Convert unicity seal to CBOR bytes without signatures. + * + * @return CBOR bytes without signatures + */ + public byte[] toCborWithoutSignatures() { + return new UnicitySeal( + this.networkId, + this.rootChainRoundNumber, + this.epoch, + this.timestamp, + this.previousHash, + this.hash, + null + ).toCbor(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof UnicitySeal)) { + return false; + } + UnicitySeal that = (UnicitySeal) o; + return Objects.equals(this.networkId, that.networkId) + && Objects.equals(this.rootChainRoundNumber, that.rootChainRoundNumber) + && Objects.equals(this.epoch, that.epoch) + && Objects.equals(this.timestamp, that.timestamp) + && Objects.deepEquals(this.previousHash, that.previousHash) + && Objects.deepEquals(this.hash, that.hash) + && Objects.equals(this.signatures, that.signatures); + } + + @Override + public int hashCode() { + return Objects.hash(this.networkId, this.rootChainRoundNumber, this.epoch, + this.timestamp, + Arrays.hashCode(this.previousHash), Arrays.hashCode(this.hash), this.signatures); + } + + @Override + public String toString() { + return String.format( + "UnicitySeal{networkId=%s, rootChainRoundNumber=%s, epoch=%s, timestamp=%s, " + + "previousHash=%s, hash=%s, signatures=%s", + this.networkId, + this.rootChainRoundNumber, + this.epoch, + this.timestamp, + this.previousHash != null ? HexConverter.encode(this.previousHash) : null, + HexConverter.encode(this.hash), + this.signatures.stream().map(SignatureEntry::toString).collect(Collectors.toList()) + ); + } + + public static final class SignatureEntry { + private final String key; + private final byte[] signature; + + SignatureEntry(String key, byte[] signature) { + this.key = key; + this.signature = signature; + } + + public String getKey() { + return this.key; + } + + public byte[] getSignature() { + return Arrays.copyOf(this.signature, this.signature.length); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof SignatureEntry)) return false; + SignatureEntry that = (SignatureEntry) o; + return Objects.equals(this.key, that.key) && Objects.deepEquals(this.signature, that.signature); + } + + @Override + public int hashCode() { + return Objects.hash(this.key); + } + + @Override + public String toString() { + return String.format("SignatureEntry{key=%s, signature=%s}", this.key, HexConverter.encode(this.signature)); + } + } +} diff --git a/src/main/java/org/unicitylabs/sdk/bft/UnicityTreeCertificate.java b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityTreeCertificate.java similarity index 53% rename from src/main/java/org/unicitylabs/sdk/bft/UnicityTreeCertificate.java rename to src/main/java/org/unicitylabs/sdk/api/bft/UnicityTreeCertificate.java index afff5da..0f979a6 100644 --- a/src/main/java/org/unicitylabs/sdk/bft/UnicityTreeCertificate.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityTreeCertificate.java @@ -1,31 +1,31 @@ -package org.unicitylabs.sdk.bft; +package org.unicitylabs.sdk.api.bft; + +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.util.HexConverter; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer.CborTag; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.util.HexConverter; /** * Unicity tree certificate. */ public class UnicityTreeCertificate { + public static final long CBOR_TAG = 39004; + private static final int VERSION = 1; - private final int version; private final int partitionIdentifier; private final List steps; UnicityTreeCertificate( - int version, - int partitionIdentifier, - List steps + int partitionIdentifier, + List steps ) { Objects.requireNonNull(steps, "Steps cannot be null"); - this.version = version; this.partitionIdentifier = partitionIdentifier; this.steps = List.copyOf(steps); } @@ -36,7 +36,7 @@ public class UnicityTreeCertificate { * @return version */ public int getVersion() { - return this.version; + return UnicityTreeCertificate.VERSION; } /** @@ -58,39 +58,46 @@ public List getSteps() { } /** - * Create certificate from CBOR bytes. + * Deserialize certificate from CBOR bytes. * * @param bytes CBOR bytes * @return certificate */ public static UnicityTreeCertificate fromCbor(byte[] bytes) { - CborTag tag = CborDeserializer.readTag(bytes); - List data = CborDeserializer.readArray(tag.getData()); + CborDeserializer.CborTag tag = CborDeserializer.decodeTag(bytes); + if (tag.getTag() != UnicityTreeCertificate.CBOR_TAG) { + throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); + } + List data = CborDeserializer.decodeArray(tag.getData(), 3); + + int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); + if (version != UnicityTreeCertificate.VERSION) { + throw new CborSerializationException(String.format("Unsupported version: %s", version)); + } return new UnicityTreeCertificate( - CborDeserializer.readUnsignedInteger(data.get(0)).asInt(), - CborDeserializer.readUnsignedInteger(data.get(1)).asInt(), - CborDeserializer.readArray(data.get(2)).stream() - .map(HashStep::fromCbor) - .collect(Collectors.toList()) + CborDeserializer.decodeUnsignedInteger(data.get(1)).asInt(), + CborDeserializer.decodeArray(data.get(2)).stream() + .map(HashStep::fromCbor) + .collect(Collectors.toList()) ); } /** - * Convert certificate to CBOR bytes. + * Serialize certificate to CBOR bytes. * * @return CBOR bytes */ public byte[] toCbor() { return CborSerializer.encodeTag( - 1014, - CborSerializer.encodeArray( - CborSerializer.encodeUnsignedInteger(this.version), - CborSerializer.encodeUnsignedInteger(this.partitionIdentifier), - CborSerializer.encodeArray(this.steps.stream() - .map(HashStep::toCbor) - .toArray(byte[][]::new)) - )); + UnicityTreeCertificate.CBOR_TAG, + CborSerializer.encodeArray( + CborSerializer.encodeUnsignedInteger(UnicityTreeCertificate.VERSION), + CborSerializer.encodeUnsignedInteger(this.partitionIdentifier), + CborSerializer.encodeArray(this.steps.stream() + .map(HashStep::toCbor) + .toArray(byte[][]::new)) + )); } @Override @@ -99,20 +106,18 @@ public boolean equals(Object o) { return false; } UnicityTreeCertificate that = (UnicityTreeCertificate) o; - return Objects.equals(this.version, that.version) && Objects.equals( - this.partitionIdentifier, that.partitionIdentifier) && Objects.equals(this.steps, - that.steps); + return Objects.equals(this.partitionIdentifier, that.partitionIdentifier) + && Objects.equals(this.steps, that.steps); } @Override public int hashCode() { - return Objects.hash(this.version, this.partitionIdentifier, this.steps); + return Objects.hash(this.partitionIdentifier, this.steps); } @Override public String toString() { - return String.format("UnicityTreeCertificate{version=%s, partitionIdentifier=%s, steps=%s", - this.version, this.partitionIdentifier, this.steps); + return String.format("UnicityTreeCertificate{partitionIdentifier=%s, steps=%s", this.partitionIdentifier, this.steps); } /** @@ -149,29 +154,29 @@ public byte[] getHash() { } /** - * Create hash step from CBOR bytes. + * Deserialize hash step from CBOR bytes. * * @param bytes CBOR bytes * @return hash step */ public static HashStep fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new HashStep( - CborDeserializer.readUnsignedInteger(data.get(0)).asInt(), - CborDeserializer.readByteString(data.get(1)) + CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(), + CborDeserializer.decodeByteString(data.get(1)) ); } /** - * Convert hash step to CBOR bytes. + * Serialize hash step to CBOR bytes. * * @return CBOR bytes */ public byte[] toCbor() { return CborSerializer.encodeArray( - CborSerializer.encodeUnsignedInteger(this.key), - CborSerializer.encodeByteString(this.hash) + CborSerializer.encodeUnsignedInteger(this.key), + CborSerializer.encodeByteString(this.hash) ); } @@ -181,8 +186,8 @@ public boolean equals(Object o) { return false; } HashStep hashStep = (HashStep) o; - return Objects.equals(this.key, hashStep.key) && Objects.deepEquals(this.hash, - hashStep.hash); + return Objects.equals(this.key, hashStep.key) + && Objects.deepEquals(this.hash, hashStep.hash); } @Override @@ -193,7 +198,7 @@ public int hashCode() { @Override public String toString() { return String.format("UnicityTreeCertificate.HashStep{key=%s, hash=%s", - this.key, HexConverter.encode(this.hash)); + this.key, HexConverter.encode(this.hash)); } } } diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/verification/UnicityCertificateVerification.java b/src/main/java/org/unicitylabs/sdk/api/bft/verification/UnicityCertificateVerification.java new file mode 100644 index 0000000..4b8daad --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/bft/verification/UnicityCertificateVerification.java @@ -0,0 +1,48 @@ +package org.unicitylabs.sdk.api.bft.verification; + +import org.unicitylabs.sdk.api.InclusionProof; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.api.bft.verification.rule.UnicitySealHashMatchesWithRootHashRule; +import org.unicitylabs.sdk.api.bft.verification.rule.UnicitySealQuorumSignaturesVerificationRule; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + + +import java.util.ArrayList; + +/** + * Verifies unicity certificate within an inclusion proof. + */ +public class UnicityCertificateVerification { + + private UnicityCertificateVerification() { + } + + /** + * Runs unicity certificate verification rules against the provided inclusion proof. + * + * @param trustBase trust base used for quorum signature verification + * @param inclusionProof inclusion proof containing the certificate and seal + * @return verification result aggregating rule outcomes + */ + public static UnicityCertificateVerificationResult verify(RootTrustBase trustBase, + InclusionProof inclusionProof) { + ArrayList> results = new ArrayList<>(); + VerificationResult result = UnicitySealHashMatchesWithRootHashRule.verify(inclusionProof.getUnicityCertificate()); + results.add(result); + if (result.getStatus() != VerificationStatus.OK) { + return UnicityCertificateVerificationResult.fail(results); + } + + result = UnicitySealQuorumSignaturesVerificationRule.verify(trustBase, + inclusionProof.getUnicityCertificate() + .getUnicitySeal()); + results.add(result); + if (result.getStatus() != VerificationStatus.OK) { + return UnicityCertificateVerificationResult.fail(results); + } + + return UnicityCertificateVerificationResult.ok(results); + } + +} diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/verification/UnicityCertificateVerificationResult.java b/src/main/java/org/unicitylabs/sdk/api/bft/verification/UnicityCertificateVerificationResult.java new file mode 100644 index 0000000..df1a38a --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/bft/verification/UnicityCertificateVerificationResult.java @@ -0,0 +1,37 @@ +package org.unicitylabs.sdk.api.bft.verification; + +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +import java.util.List; + +/** + * Verification result type for unicity certificate verification. + */ +public class UnicityCertificateVerificationResult extends VerificationResult { + + private UnicityCertificateVerificationResult(VerificationStatus status, + List> results) { + super("UnicityCertificateVerification", status, "", results); + } + + /** + * Creates a failed unicity certificate verification result. + * + * @param results detailed rule verification results + * @return failed verification result + */ + public static UnicityCertificateVerificationResult fail(List> results) { + return new UnicityCertificateVerificationResult(VerificationStatus.FAIL, results); + } + + /** + * Creates a successful unicity certificate verification result. + * + * @param results detailed rule verification results + * @return successful verification result + */ + public static UnicityCertificateVerificationResult ok(List> results) { + return new UnicityCertificateVerificationResult(VerificationStatus.OK, results); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/InputRecordCurrentHashVerificationRule.java b/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/InputRecordCurrentHashVerificationRule.java new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealHashMatchesWithRootHashRule.java b/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealHashMatchesWithRootHashRule.java new file mode 100644 index 0000000..4731f23 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealHashMatchesWithRootHashRule.java @@ -0,0 +1,95 @@ +package org.unicitylabs.sdk.api.bft.verification.rule; + +import com.google.common.primitives.UnsignedBytes; +import org.unicitylabs.sdk.api.bft.UnicityCertificate; +import org.unicitylabs.sdk.api.bft.UnicityTreeCertificate; +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.CborSerializer; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +/** + * Rule to verify that the UnicitySeal hash matches the root hash of the UnicityTreeCertificate. + */ +public class UnicitySealHashMatchesWithRootHashRule { + + private UnicitySealHashMatchesWithRootHashRule() { + } + + /** + * Verifies that the unicity seal hash matches the recomputed root hash of the unicity tree. + * + * @param unicityCertificate unicity certificate containing tree and seal data + * @return verification result with {@link VerificationStatus#OK} on match, otherwise fail + */ + public static VerificationResult verify( + UnicityCertificate unicityCertificate) { + DataHash shardTreeCertificateRootHash = UnicityCertificate + .calculateShardTreeCertificateRootHash( + unicityCertificate.getInputRecord(), + unicityCertificate.getTechnicalRecordHash(), + unicityCertificate.getShardConfigurationHash(), + unicityCertificate.getShardTreeCertificate() + ); + + UnicityTreeCertificate unicityTreeCertificate = unicityCertificate.getUnicityTreeCertificate(); + byte[] key = ByteBuffer.allocate(4) + .order(ByteOrder.BIG_ENDIAN) + .putInt(unicityTreeCertificate.getPartitionIdentifier()) + .array(); + + DataHash result = new DataHasher(HashAlgorithm.SHA256) + .update(CborSerializer.encodeByteString(new byte[]{(byte) 0x01})) // LEAF + .update(CborSerializer.encodeByteString(key)) + .update( + CborSerializer.encodeByteString( + new DataHasher(HashAlgorithm.SHA256) + .update( + CborSerializer.encodeByteString(shardTreeCertificateRootHash.getData()) + ) + .digest() + .getData() + ) + ) + .digest(); + + for (UnicityTreeCertificate.HashStep step : unicityTreeCertificate.getSteps()) { + byte[] stepKey = ByteBuffer.allocate(4) + .order(ByteOrder.BIG_ENDIAN) + .putInt(step.getKey()) + .array(); + + DataHasher hasher = new DataHasher(HashAlgorithm.SHA256) + .update(CborSerializer.encodeByteString(new byte[]{(byte) 0x00})) // NODE + .update(CborSerializer.encodeByteString(stepKey)); + + if (UnsignedBytes.lexicographicalComparator().compare(key, stepKey) > 0) { + hasher + .update(CborSerializer.encodeByteString(step.getHash())) + .update(CborSerializer.encodeByteString(result.getData())); + } else { + hasher + .update(CborSerializer.encodeByteString(result.getData())) + .update(CborSerializer.encodeByteString(step.getHash())); + } + + result = hasher.digest(); + } + + byte[] unicitySealHash = unicityCertificate.getUnicitySeal().getHash(); + + if (!Arrays.equals(unicitySealHash, result.getData())) { + return new VerificationResult<>("UnicitySealHashMatchesWithRootHashRule", + VerificationStatus.FAIL); + } + + return new VerificationResult<>("UnicitySealHashMatchesWithRootHashRule", + VerificationStatus.OK); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.java b/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.java new file mode 100644 index 0000000..c0469ba --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.java @@ -0,0 +1,110 @@ +package org.unicitylabs.sdk.api.bft.verification.rule; + +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.api.bft.RootTrustBase.NodeInfo; +import org.unicitylabs.sdk.api.bft.UnicitySeal; +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.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Rule to verify that the UnicitySeal contains valid quorum signatures. + */ +public class UnicitySealQuorumSignaturesVerificationRule { + + private UnicitySealQuorumSignaturesVerificationRule() { + } + + /** + * Verifies unicity seal signatures and checks that the quorum threshold is reached. + * + * @param trustBase trust base containing root nodes and quorum threshold + * @param unicitySeal unicity seal with node signatures + * @return verification result with per-signature details + */ + public static VerificationResult verify(RootTrustBase trustBase, + UnicitySeal unicitySeal) { + List> results = new ArrayList<>(); + DataHash hash = new DataHasher(HashAlgorithm.SHA256) + .update(unicitySeal.toCborWithoutSignatures()) + .digest(); + int successful = 0; + for (UnicitySeal.SignatureEntry entry : unicitySeal.getSignatures()) { + String nodeId = entry.getKey(); + byte[] signature = entry.getSignature(); + + VerificationResult result = UnicitySealQuorumSignaturesVerificationRule.verifySignature( + trustBase, + nodeId, + signature, + hash.getData() + ); + results.add(result); + + if (result.getStatus() == VerificationStatus.OK) { + successful++; + } + } + + if (successful >= trustBase.getQuorumThreshold()) { + return new VerificationResult<>( + "UnicitySealQuorumSignaturesVerificationRule", + VerificationStatus.OK, + "Unicity quorum signatures verification threshold reached", + results + ); + } + + return new VerificationResult<>( + "UnicitySealQuorumSignaturesVerificationRule", + VerificationStatus.FAIL, + "Unicity quorum treshold was not reached", + results + ); + } + + private static VerificationResult verifySignature( + RootTrustBase trustBase, + String nodeId, + byte[] signature, + byte[] hash + ) { + NodeInfo node = trustBase.getRootNodes().stream() + .filter(n -> n.getNodeId().equals(nodeId)) + .findFirst() + .orElse(null); + + if (node == null) { + return new VerificationResult<>( + String.format("SignatureVerificationRule[%s]", nodeId), + VerificationStatus.FAIL, + "No root node defined" + ); + } + + if (!SigningService.verifyWithPublicKey( + hash, + Arrays.copyOf(signature, signature.length - 1), + node.getSigningKey() + )) { + return new VerificationResult<>( + String.format("SignatureVerificationRule[%s]", nodeId), + VerificationStatus.FAIL, + "Signature verification failed" + ); + } + + return new VerificationResult<>( + String.format("SignatureVerificationRule[%s]", nodeId), + VerificationStatus.OK + ); + } + +} diff --git a/src/main/java/org/unicitylabs/sdk/jsonrpc/JsonRpcError.java b/src/main/java/org/unicitylabs/sdk/api/jsonrpc/JsonRpcError.java similarity index 80% rename from src/main/java/org/unicitylabs/sdk/jsonrpc/JsonRpcError.java rename to src/main/java/org/unicitylabs/sdk/api/jsonrpc/JsonRpcError.java index 8ae66bf..d35d327 100644 --- a/src/main/java/org/unicitylabs/sdk/jsonrpc/JsonRpcError.java +++ b/src/main/java/org/unicitylabs/sdk/api/jsonrpc/JsonRpcError.java @@ -1,4 +1,4 @@ -package org.unicitylabs.sdk.jsonrpc; +package org.unicitylabs.sdk.api.jsonrpc; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -13,8 +13,8 @@ public class JsonRpcError { @JsonCreator JsonRpcError( - @JsonProperty("code") int code, - @JsonProperty("message") String message + @JsonProperty("code") int code, + @JsonProperty("message") String message ) { this.code = code; this.message = message; diff --git a/src/main/java/org/unicitylabs/sdk/jsonrpc/JsonRpcHttpTransport.java b/src/main/java/org/unicitylabs/sdk/api/jsonrpc/JsonRpcHttpTransport.java similarity index 68% rename from src/main/java/org/unicitylabs/sdk/jsonrpc/JsonRpcHttpTransport.java rename to src/main/java/org/unicitylabs/sdk/api/jsonrpc/JsonRpcHttpTransport.java index 98d4bb5..879da98 100644 --- a/src/main/java/org/unicitylabs/sdk/jsonrpc/JsonRpcHttpTransport.java +++ b/src/main/java/org/unicitylabs/sdk/api/jsonrpc/JsonRpcHttpTransport.java @@ -1,19 +1,13 @@ -package org.unicitylabs.sdk.jsonrpc; +package org.unicitylabs.sdk.api.jsonrpc; + +import okhttp3.*; +import org.unicitylabs.sdk.serializer.UnicityObjectMapper; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; -import org.unicitylabs.sdk.serializer.UnicityObjectMapper; /** * JSON-RPC HTTP service. @@ -45,7 +39,7 @@ public JsonRpcHttpTransport(String url) { * @return future with result */ public CompletableFuture request(String method, Object params, Class resultType) { - return request(method, params, resultType, Map.of()); + return this.request(method, params, resultType, Map.of()); } /** @@ -59,27 +53,27 @@ public CompletableFuture request(String method, Object params, Class r * @return future with result */ public CompletableFuture request( - String method, - Object params, - Class resultType, - Map> headers + String method, + Object params, + Class resultType, + Map> headers ) { CompletableFuture future = new CompletableFuture<>(); try { Request.Builder requestBuilder = new Request.Builder() - .url(this.url) - .post( - RequestBody.create( - UnicityObjectMapper.JSON.writeValueAsString( - new JsonRpcRequest(method, params) - ), - JsonRpcHttpTransport.MEDIA_TYPE_JSON) - ); + .url(this.url) + .post( + RequestBody.create( + UnicityObjectMapper.JSON.writeValueAsString( + new JsonRpcRequest(method, params) + ), + JsonRpcHttpTransport.MEDIA_TYPE_JSON) + ); headers.forEach((header, values) -> - values.forEach(value -> - requestBuilder.addHeader(header, value))); + values.forEach(value -> + requestBuilder.addHeader(header, value))); Request request = requestBuilder.build(); @@ -99,17 +93,17 @@ public void onResponse(Call call, Response response) { } JsonRpcResponse data = UnicityObjectMapper.JSON.readValue( - body != null ? body.string() : "", - UnicityObjectMapper.JSON.getTypeFactory() - .constructParametricType(JsonRpcResponse.class, resultType) + body != null ? body.string() : "", + UnicityObjectMapper.JSON.getTypeFactory() + .constructParametricType(JsonRpcResponse.class, resultType) ); if (data.getError() != null) { future.completeExceptionally( - new JsonRpcNetworkException( - data.getError().getCode(), - data.getError().getMessage() - ) + new JsonRpcNetworkException( + data.getError().getCode(), + data.getError().getMessage() + ) ); return; } @@ -117,7 +111,7 @@ public void onResponse(Call call, Response response) { future.complete(data.getResult()); } catch (Exception e) { future.completeExceptionally( - new RuntimeException("Failed to parse JSON-RPC response", e)); + new RuntimeException("Failed to parse JSON-RPC response", e)); } } }); diff --git a/src/main/java/org/unicitylabs/sdk/jsonrpc/JsonRpcNetworkException.java b/src/main/java/org/unicitylabs/sdk/api/jsonrpc/JsonRpcNetworkException.java similarity index 95% rename from src/main/java/org/unicitylabs/sdk/jsonrpc/JsonRpcNetworkException.java rename to src/main/java/org/unicitylabs/sdk/api/jsonrpc/JsonRpcNetworkException.java index bcacc7e..debddef 100644 --- a/src/main/java/org/unicitylabs/sdk/jsonrpc/JsonRpcNetworkException.java +++ b/src/main/java/org/unicitylabs/sdk/api/jsonrpc/JsonRpcNetworkException.java @@ -1,5 +1,5 @@ -package org.unicitylabs.sdk.jsonrpc; +package org.unicitylabs.sdk.api.jsonrpc; /** * JSON RPC network exception. diff --git a/src/main/java/org/unicitylabs/sdk/jsonrpc/JsonRpcRequest.java b/src/main/java/org/unicitylabs/sdk/api/jsonrpc/JsonRpcRequest.java similarity index 88% rename from src/main/java/org/unicitylabs/sdk/jsonrpc/JsonRpcRequest.java rename to src/main/java/org/unicitylabs/sdk/api/jsonrpc/JsonRpcRequest.java index 4e06a39..5ec8598 100644 --- a/src/main/java/org/unicitylabs/sdk/jsonrpc/JsonRpcRequest.java +++ b/src/main/java/org/unicitylabs/sdk/api/jsonrpc/JsonRpcRequest.java @@ -1,8 +1,9 @@ -package org.unicitylabs.sdk.jsonrpc; +package org.unicitylabs.sdk.api.jsonrpc; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonGetter; import com.fasterxml.jackson.annotation.JsonProperty; + import java.util.UUID; /** @@ -33,9 +34,9 @@ public JsonRpcRequest(String method, Object params) { */ @JsonCreator public JsonRpcRequest( - @JsonProperty("id") UUID id, - @JsonProperty("method") String method, - @JsonProperty("params") Object params + @JsonProperty("id") UUID id, + @JsonProperty("method") String method, + @JsonProperty("params") Object params ) { this.id = id; this.method = method; diff --git a/src/main/java/org/unicitylabs/sdk/jsonrpc/JsonRpcResponse.java b/src/main/java/org/unicitylabs/sdk/api/jsonrpc/JsonRpcResponse.java similarity index 81% rename from src/main/java/org/unicitylabs/sdk/jsonrpc/JsonRpcResponse.java rename to src/main/java/org/unicitylabs/sdk/api/jsonrpc/JsonRpcResponse.java index 89df5ba..88a1f9c 100644 --- a/src/main/java/org/unicitylabs/sdk/jsonrpc/JsonRpcResponse.java +++ b/src/main/java/org/unicitylabs/sdk/api/jsonrpc/JsonRpcResponse.java @@ -1,7 +1,8 @@ -package org.unicitylabs.sdk.jsonrpc; +package org.unicitylabs.sdk.api.jsonrpc; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; + import java.util.Objects; import java.util.UUID; @@ -19,10 +20,10 @@ public class JsonRpcResponse { @JsonCreator JsonRpcResponse( - @JsonProperty("jsonrpc") String version, - @JsonProperty("result") T result, - @JsonProperty("error") JsonRpcError error, - @JsonProperty("id") UUID id + @JsonProperty("jsonrpc") String version, + @JsonProperty("result") T result, + @JsonProperty("error") JsonRpcError error, + @JsonProperty("id") UUID id ) { if (!"2.0".equals(version)) { throw new IllegalArgumentException("Invalid JSON-RPC version: " + version); @@ -77,8 +78,8 @@ public boolean equals(Object o) { } JsonRpcResponse that = (JsonRpcResponse) o; return Objects.equals(this.version, that.version) && Objects.equals(this.result, - that.result) && Objects.equals(this.error, that.error) && Objects.equals(this.id, - that.id); + that.result) && Objects.equals(this.error, that.error) && Objects.equals(this.id, + that.id); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/bft/InputRecord.java b/src/main/java/org/unicitylabs/sdk/bft/InputRecord.java deleted file mode 100644 index 6c35aa3..0000000 --- a/src/main/java/org/unicitylabs/sdk/bft/InputRecord.java +++ /dev/null @@ -1,237 +0,0 @@ -package org.unicitylabs.sdk.bft; - -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer.CborTag; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.util.HexConverter; - -/** - * Input record for UnicityCertificate. - */ -public class InputRecord { - - private final int version; - private final long roundNumber; - private final long epoch; - private final byte[] previousHash; - private final byte[] hash; - private final byte[] summaryValue; - private final long timestamp; - private final byte[] blockHash; - private final long sumOfEarnedFees; - private final byte[] executedTransactionsHash; - - InputRecord( - int version, - long roundNumber, - long epoch, - byte[] previousHash, - byte[] hash, - byte[] summaryValue, - long timestamp, - byte[] blockHash, - long sumOfEarnedFees, - byte[] executedTransactionsHash - ) { - Objects.requireNonNull(hash, "Hash cannot be null"); - Objects.requireNonNull(summaryValue, "Summary value cannot be null"); - - this.version = version; - this.roundNumber = roundNumber; - this.epoch = epoch; - this.previousHash = previousHash; - this.hash = hash; - this.summaryValue = summaryValue; - this.timestamp = timestamp; - this.blockHash = blockHash; - this.sumOfEarnedFees = sumOfEarnedFees; - this.executedTransactionsHash = executedTransactionsHash; - } - - /** - * Get version. - * - * @return version - */ - public int getVersion() { - return this.version; - } - - /** - * Get round number. - * - * @return round number - */ - public long getRoundNumber() { - return this.roundNumber; - } - - /** - * Get epoch. - * - * @return epoch - */ - public long getEpoch() { - return this.epoch; - } - - /** - * Get previous hash. - * - * @return previous hash or null if not set - */ - public byte[] getPreviousHash() { - return this.previousHash != null ? Arrays.copyOf(this.previousHash, this.previousHash.length) - : null; - } - - /** - * Get hash. - * - * @return hash - */ - public byte[] getHash() { - return Arrays.copyOf(this.hash, this.hash.length); - } - - /** - * Get summary value. - * - * @return summary value - */ - public byte[] getSummaryValue() { - return Arrays.copyOf(this.summaryValue, this.summaryValue.length); - } - - /** - * Get timestamp. - * - * @return timestamp - */ - public long getTimestamp() { - return this.timestamp; - } - - /** - * Get block hash. - * - * @return block hash or null if not set - */ - public byte[] getBlockHash() { - return this.blockHash != null ? Arrays.copyOf(this.blockHash, this.blockHash.length) : null; - } - - /** - * Get sum of earned fees. - * - * @return sum of earned fees - */ - public long getSumOfEarnedFees() { - return this.sumOfEarnedFees; - } - - /** - * Get executed transactions hash. - * - * @return executed transactions hash or null if not set - */ - public byte[] getExecutedTransactionsHash() { - return this.executedTransactionsHash != null ? Arrays.copyOf(this.executedTransactionsHash, - this.executedTransactionsHash.length) : null; - } - - /** - * Create InputRecord from CBOR bytes. - * - * @param bytes CBOR bytes - * @return input record - */ - public static InputRecord fromCbor(byte[] bytes) { - CborTag tag = CborDeserializer.readTag(bytes); - List data = CborDeserializer.readArray(tag.getData()); - - return new InputRecord( - CborDeserializer.readUnsignedInteger(data.get(0)).asInt(), - CborDeserializer.readUnsignedInteger(data.get(1)).asLong(), - CborDeserializer.readUnsignedInteger(data.get(2)).asLong(), - CborDeserializer.readOptional(data.get(3), CborDeserializer::readByteString), - CborDeserializer.readByteString(data.get(4)), - CborDeserializer.readByteString(data.get(5)), - CborDeserializer.readUnsignedInteger(data.get(6)).asLong(), - CborDeserializer.readOptional(data.get(7), CborDeserializer::readByteString), - CborDeserializer.readUnsignedInteger(data.get(8)).asLong(), - CborDeserializer.readOptional(data.get(9), CborDeserializer::readByteString) - ); - } - - /** - * Convert InputRecord to CBOR bytes. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeTag( - 1008, - CborSerializer.encodeArray( - CborSerializer.encodeUnsignedInteger(this.version), - CborSerializer.encodeUnsignedInteger(this.roundNumber), - CborSerializer.encodeUnsignedInteger(this.epoch), - CborSerializer.encodeOptional(this.previousHash, CborSerializer::encodeByteString), - CborSerializer.encodeByteString(this.hash), - CborSerializer.encodeByteString(this.summaryValue), - CborSerializer.encodeUnsignedInteger(this.timestamp), - CborSerializer.encodeOptional(this.blockHash, CborSerializer::encodeByteString), - CborSerializer.encodeUnsignedInteger(this.sumOfEarnedFees), - CborSerializer.encodeOptional(this.executedTransactionsHash, - CborSerializer::encodeByteString) - )); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof InputRecord)) { - return false; - } - InputRecord that = (InputRecord) o; - return Objects.equals(this.version, that.version) && Objects.equals(this.roundNumber, - that.roundNumber) && Objects.equals(this.epoch, that.epoch) - && Objects.deepEquals(this.previousHash, that.previousHash) - && Objects.deepEquals(this.hash, that.hash) && Objects.deepEquals(this.summaryValue, - that.summaryValue) && Objects.equals(this.timestamp, that.timestamp) - && Objects.deepEquals(this.blockHash, that.blockHash) && Objects.equals( - this.sumOfEarnedFees, that.sumOfEarnedFees) && Objects.deepEquals( - this.executedTransactionsHash, that.executedTransactionsHash); - } - - @Override - public int hashCode() { - return Objects.hash(this.version, this.roundNumber, this.epoch, - Arrays.hashCode(this.previousHash), - Arrays.hashCode(this.hash), Arrays.hashCode(this.summaryValue), this.timestamp, - Arrays.hashCode(this.blockHash), - this.sumOfEarnedFees, Arrays.hashCode(this.executedTransactionsHash)); - } - - @Override - public String toString() { - return String.format("InputRecord{version=%s, roundNumber=%s, epoch=%s, previousHash=%s, " - + "hash=%s, summaryValue=%s, timestamp=%s, blockHash=%s, sumOfEarnedFees=%s, " - + "executedTransactionsHash=%s}", - this.version, - this.roundNumber, - this.epoch, - this.previousHash != null ? HexConverter.encode(this.previousHash) : null, - HexConverter.encode(this.hash), - HexConverter.encode(this.summaryValue), - this.timestamp, - this.blockHash != null ? HexConverter.encode(this.blockHash) : null, - this.sumOfEarnedFees, - this.executedTransactionsHash != null ? HexConverter.encode(this.executedTransactionsHash) - : null - ); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/bft/ShardTreeCertificate.java b/src/main/java/org/unicitylabs/sdk/bft/ShardTreeCertificate.java deleted file mode 100644 index fbc2cb3..0000000 --- a/src/main/java/org/unicitylabs/sdk/bft/ShardTreeCertificate.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.unicitylabs.sdk.bft; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.util.HexConverter; - -/** - * Shard tree certificate. - */ -public class ShardTreeCertificate { - - private final byte[] shard; - private final List siblingHashList; - - ShardTreeCertificate(byte[] shard, List siblingHashList) { - Objects.requireNonNull(shard, "Shard cannot be null"); - Objects.requireNonNull(siblingHashList, "Sibling hash list cannot be null"); - - this.shard = Arrays.copyOf(shard, shard.length); - this.siblingHashList = siblingHashList.stream() - .map(hash -> Arrays.copyOf(hash, hash.length)) - .collect(Collectors.toList()); - } - - /** - * Get shard. - * - * @return shard - */ - public byte[] getShard() { - return Arrays.copyOf(this.shard, this.shard.length); - } - - /** - * Get sibling hash list. - * - * @return sibling hash list - */ - public List getSiblingHashList() { - return this.siblingHashList.stream() - .map(hash -> Arrays.copyOf(hash, hash.length)) - .collect(Collectors.toList()); - } - - /** - * Create shard tree certificate from CBOR bytes. - * - * @param bytes CBOR bytes - * @return shard tree certificate - */ - public static ShardTreeCertificate fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); - - return new ShardTreeCertificate( - CborDeserializer.readByteString(data.get(0)), - CborDeserializer.readArray(data.get(1)).stream() - .map(CborDeserializer::readByteString) - .collect(Collectors.toList()) - ); - } - - /** - * Convert shard tree certificate to CBOR bytes. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeArray( - CborSerializer.encodeByteString(this.shard), - CborSerializer.encodeArray( - this.siblingHashList.stream() - .map(CborSerializer::encodeByteString) - .toArray(byte[][]::new) - ) - ); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof ShardTreeCertificate)) { - return false; - } - ShardTreeCertificate that = (ShardTreeCertificate) o; - return Objects.deepEquals(this.shard, that.shard) && Objects.equals( - this.siblingHashList, that.siblingHashList); - } - - @Override - public int hashCode() { - return Objects.hash(Arrays.hashCode(this.shard), this.siblingHashList); - } - - @Override - public String toString() { - return String.format("ShardTreeCertificate{shard=%s, siblingHashList=%s}", - HexConverter.encode(this.shard), this.siblingHashList); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/bft/UnicityCertificate.java b/src/main/java/org/unicitylabs/sdk/bft/UnicityCertificate.java deleted file mode 100644 index bc3f19d..0000000 --- a/src/main/java/org/unicitylabs/sdk/bft/UnicityCertificate.java +++ /dev/null @@ -1,245 +0,0 @@ -package org.unicitylabs.sdk.bft; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer.CborTag; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.util.HexConverter; - -/** - * Unicity certificate. - */ -@JsonSerialize(using = UnicityCertificateJson.Serializer.class) -@JsonDeserialize(using = UnicityCertificateJson.Deserializer.class) -public class UnicityCertificate { - - private final int version; - private final InputRecord inputRecord; - private final byte[] technicalRecordHash; - private final byte[] shardConfigurationHash; - private final ShardTreeCertificate shardTreeCertificate; - private final UnicityTreeCertificate unicityTreeCertificate; - private final UnicitySeal unicitySeal; - - UnicityCertificate( - int version, - InputRecord inputRecord, - byte[] technicalRecordHash, - byte[] shardConfigurationHash, - ShardTreeCertificate shardTreeCertificate, - UnicityTreeCertificate unicityTreeCertificate, - UnicitySeal unicitySeal - ) { - Objects.requireNonNull(inputRecord, "Input record cannot be null"); - Objects.requireNonNull(shardConfigurationHash, "Shard configuration hash cannot be null"); - Objects.requireNonNull(shardTreeCertificate, "Shard tree certificate cannot be null"); - Objects.requireNonNull(unicityTreeCertificate, "Unicity tree certificate cannot be null"); - Objects.requireNonNull(unicitySeal, "Unicity seal cannot be null"); - - this.version = version; - this.inputRecord = inputRecord; - this.technicalRecordHash = technicalRecordHash != null - ? Arrays.copyOf(technicalRecordHash, technicalRecordHash.length) - : null; - this.shardConfigurationHash = Arrays.copyOf( - shardConfigurationHash, - shardConfigurationHash.length - ); - this.shardTreeCertificate = shardTreeCertificate; - this.unicityTreeCertificate = unicityTreeCertificate; - this.unicitySeal = unicitySeal; - } - - /** - * Get the certificate version. - * - * @return certificate version - */ - public int getVersion() { - return this.version; - } - - /** - * Get the input record. - * - * @return input record - */ - public InputRecord getInputRecord() { - return this.inputRecord; - } - - /** - * Get the technical record hash. - * - * @return technical record hash - */ - public byte[] getTechnicalRecordHash() { - return this.technicalRecordHash != null - ? Arrays.copyOf(this.technicalRecordHash, this.technicalRecordHash.length) - : null; - } - - /** - * Get the shard configuration hash. - * - * @return shard configuration hash - */ - public byte[] getShardConfigurationHash() { - return Arrays.copyOf(this.shardConfigurationHash, this.shardConfigurationHash.length); - } - - /** - * Get the shard tree certificate. - * - * @return shard tree certificate - */ - public ShardTreeCertificate getShardTreeCertificate() { - return this.shardTreeCertificate; - } - - /** - * Get the unicity tree certificate. - * - * @return unicity tree certificate - */ - public UnicityTreeCertificate getUnicityTreeCertificate() { - return this.unicityTreeCertificate; - } - - /** - * Get the unicity seal. - * - * @return unicity seal - */ - public UnicitySeal getUnicitySeal() { - return this.unicitySeal; - } - - /** - * Calculate the root hash of the shard tree certificate. - * - * @param inputRecord input record - * @param technicalRecordHash technical record hash - * @param shardConfigurationHash shard configuration hash - * @param shardTreeCertificate shard tree certificate - * @return root hash - */ - public static DataHash calculateShardTreeCertificateRootHash( - InputRecord inputRecord, - byte[] technicalRecordHash, - byte[] shardConfigurationHash, - ShardTreeCertificate shardTreeCertificate - ) { - - DataHash rootHash = new DataHasher(HashAlgorithm.SHA256) - .update(inputRecord.toCbor()) - .update( - CborSerializer.encodeOptional(technicalRecordHash, CborSerializer::encodeByteString)) - .update(CborSerializer.encodeByteString(shardConfigurationHash)) - .digest(); - - byte[] shardId = shardTreeCertificate.getShard(); - List siblingHashes = shardTreeCertificate.getSiblingHashList(); - for (int i = 0; i < siblingHashes.size(); i++) { - boolean isRight = shardId[(shardId.length - 1) - (i / 8)] == 1; - if (isRight) { - rootHash = new DataHasher(HashAlgorithm.SHA256) - .update(siblingHashes.get(i)) - .update(rootHash.getData()) - .digest(); - } else { - rootHash = new DataHasher(HashAlgorithm.SHA256) - .update(rootHash.getData()) - .update(siblingHashes.get(i)) - .digest(); - } - } - - return rootHash; - - } - - /** - * Create unicity certificate from CBOR bytes. - * - * @param bytes CBOR bytes - * @return unicity certificate - */ - public static UnicityCertificate fromCbor(byte[] bytes) { - CborTag tag = CborDeserializer.readTag(bytes); - List data = CborDeserializer.readArray(tag.getData()); - - return new UnicityCertificate( - CborDeserializer.readUnsignedInteger(data.get(0)).asInt(), - InputRecord.fromCbor(data.get(1)), - CborDeserializer.readOptional(data.get(2), CborDeserializer::readByteString), - CborDeserializer.readByteString(data.get(3)), - ShardTreeCertificate.fromCbor(data.get(4)), - UnicityTreeCertificate.fromCbor(data.get(5)), - UnicitySeal.fromCbor(data.get(6)) - ); - } - - /** - * Convert unicity certificate to CBOR bytes. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeTag( - 1007, - CborSerializer.encodeArray( - CborSerializer.encodeUnsignedInteger(this.version), - this.inputRecord.toCbor(), - CborSerializer.encodeOptional(this.technicalRecordHash, - CborSerializer::encodeByteString), - CborSerializer.encodeByteString(this.shardConfigurationHash), - this.shardTreeCertificate.toCbor(), - this.unicityTreeCertificate.toCbor(), - this.unicitySeal.toCbor() - )); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof UnicityCertificate)) { - return false; - } - UnicityCertificate that = (UnicityCertificate) o; - return Objects.equals(this.version, that.version) && Objects.equals(this.inputRecord, - that.inputRecord) && Objects.deepEquals(this.technicalRecordHash, - that.technicalRecordHash) && Objects.deepEquals(this.shardConfigurationHash, - that.shardConfigurationHash) && Objects.equals(this.shardTreeCertificate, - that.shardTreeCertificate) && Objects.equals(this.unicityTreeCertificate, - that.unicityTreeCertificate) && Objects.equals(this.unicitySeal, that.unicitySeal); - } - - @Override - public int hashCode() { - return Objects.hash(this.version, this.inputRecord, Arrays.hashCode(this.technicalRecordHash), - Arrays.hashCode(this.shardConfigurationHash), this.shardTreeCertificate, - this.unicityTreeCertificate, this.unicitySeal); - } - - @Override - public String toString() { - return String.format("UnicityCertificate{version=%s, inputRecord=%s, technicalRecordHash=%s, " - + "shardConfigurationHash=%s, shardTreeCertificate=%s, unicityTreeCertificate=%s, " - + "unicitySeal=%s}", - this.version, - this.inputRecord, - this.technicalRecordHash != null ? HexConverter.encode(this.technicalRecordHash) : null, - HexConverter.encode(this.shardConfigurationHash), - this.shardTreeCertificate, - this.unicityTreeCertificate, - this.unicitySeal - ); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/bft/UnicityCertificateJson.java b/src/main/java/org/unicitylabs/sdk/bft/UnicityCertificateJson.java deleted file mode 100644 index 298e286..0000000 --- a/src/main/java/org/unicitylabs/sdk/bft/UnicityCertificateJson.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.unicitylabs.sdk.bft; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import java.io.IOException; - -/** - * Unicity certificate serializer and deserializer implementation. - */ -public class UnicityCertificateJson { - - private UnicityCertificateJson() { - } - - /** - * Unicity certificate serializer. - */ - public static class Serializer extends StdSerializer { - - /** - * Create serializer. - */ - public Serializer() { - super(UnicityCertificate.class); - } - - /** - * Serialize unicity certificate. - * - * @param value unicity certificate - * @param gen json generator - * @param serializers serializer provider - * @throws IOException on serialization failure - */ - @Override - public void serialize(UnicityCertificate value, JsonGenerator gen, - SerializerProvider serializers) - throws IOException { - gen.writeObject(value.toCbor()); - } - } - - /** - * Unicity certificate deserializer. - */ - public static class Deserializer extends StdDeserializer { - - /** - * Create deserializer. - */ - public Deserializer() { - super(UnicityCertificate.class); - } - - /** - * Deserialize unicity certificate. - * - * @param p json parser - * @param ctx deserialization context - * @return unicity certificate - * @throws IOException on deserialization failure - */ - @Override - public UnicityCertificate deserialize(JsonParser p, DeserializationContext ctx) - throws IOException { - return UnicityCertificate.fromCbor(p.readValueAs(byte[].class)); - } - } -} - diff --git a/src/main/java/org/unicitylabs/sdk/bft/UnicitySeal.java b/src/main/java/org/unicitylabs/sdk/bft/UnicitySeal.java deleted file mode 100644 index 7222206..0000000 --- a/src/main/java/org/unicitylabs/sdk/bft/UnicitySeal.java +++ /dev/null @@ -1,294 +0,0 @@ -package org.unicitylabs.sdk.bft; - -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer.CborTag; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer.CborMap; -import org.unicitylabs.sdk.util.HexConverter; - -/** - * UnicitySeal represents a seal in the Unicity BFT system, containing metadata and signatures. - */ -public class UnicitySeal { - - private final int version; - private final short networkId; - private final long rootChainRoundNumber; - private final long epoch; - private final long timestamp; - private final byte[] previousHash; // nullable - private final byte[] hash; - private final LinkedHashMap signatures; - - UnicitySeal( - int version, - short networkId, - long rootChainRoundNumber, - long epoch, - long timestamp, - byte[] previousHash, - byte[] hash, - Map signatures - ) { - Objects.requireNonNull(hash, "Hash cannot be null"); - - this.version = version; - this.networkId = networkId; - this.rootChainRoundNumber = rootChainRoundNumber; - this.epoch = epoch; - this.timestamp = timestamp; - this.previousHash = previousHash; - this.hash = hash; - this.signatures = signatures == null - ? null - : signatures.entrySet().stream() - .map(entry -> Map.entry( - entry.getKey(), - Arrays.copyOf(entry.getValue(), entry.getValue().length) - ) - ) - .collect( - Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue, - (e1, e2) -> e1, - LinkedHashMap::new - ) - ); - } - - /** - * Create a new UnicitySeal instance with the provided signatures. - * - * @param signatures the signatures to include in the new UnicitySeal - * @return a new UnicitySeal instance with the specified signatures - */ - public UnicitySeal withSignatures(Map signatures) { - return new UnicitySeal( - this.version, - this.networkId, - this.rootChainRoundNumber, - this.epoch, - this.timestamp, - this.previousHash, - this.hash, - signatures - ); - } - - /** - * Get the version. - * - * @return version - */ - public int getVersion() { - return this.version; - } - - /** - * Get the network ID. - * - * @return network ID - */ - public short getNetworkId() { - return this.networkId; - } - - /** - * Get the root chain round number. - * - * @return root chain round number - */ - public long getRootChainRoundNumber() { - return this.rootChainRoundNumber; - } - - /** - * Get the epoch. - * - * @return epoch - */ - public long getEpoch() { - return this.epoch; - } - - /** - * Get the timestamp. - * - * @return timestamp - */ - public long getTimestamp() { - return this.timestamp; - } - - /** - * Get the previous hash. - * - * @return previous hash or null if not set - */ - public byte[] getPreviousHash() { - return this.previousHash != null ? Arrays.copyOf(this.previousHash, this.previousHash.length) - : null; - } - - /** - * Get the hash. - * - * @return hash - */ - public byte[] getHash() { - return Arrays.copyOf(this.hash, this.hash.length); - } - - /** - * Get the signatures. - * - * @return signatures - */ - public Map getSignatures() { - return this.signatures == null - ? null - : this.signatures.entrySet().stream() - .map(entry -> Map.entry( - entry.getKey(), - Arrays.copyOf(entry.getValue(), entry.getValue().length) - ) - ) - .collect( - Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue, - (e1, e2) -> e1, - LinkedHashMap::new - ) - ); - } - - /** - * Create unicity seal from CBOR bytes. - * - * @param bytes CBOR bytes - * @return unicity seal - */ - public static UnicitySeal fromCbor(byte[] bytes) { - CborTag tag = CborDeserializer.readTag(bytes); - List data = CborDeserializer.readArray(tag.getData()); - - return new UnicitySeal( - CborDeserializer.readUnsignedInteger(data.get(0)).asInt(), - CborDeserializer.readUnsignedInteger(data.get(1)).asShort(), - CborDeserializer.readUnsignedInteger(data.get(2)).asLong(), - CborDeserializer.readUnsignedInteger(data.get(3)).asLong(), - CborDeserializer.readUnsignedInteger(data.get(4)).asLong(), - CborDeserializer.readOptional(data.get(5), CborDeserializer::readByteString), - CborDeserializer.readByteString(data.get(6)), - CborDeserializer.readMap(data.get(7)).stream() - .collect( - Collectors.toMap( - entry -> CborDeserializer.readTextString(entry.getKey()), - entry -> CborDeserializer.readByteString(entry.getValue() - ) - ) - ) - ); - } - - /** - * Convert unicity seal to CBOR bytes. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeTag( - 1001, - CborSerializer.encodeArray( - CborSerializer.encodeUnsignedInteger(this.version), - CborSerializer.encodeUnsignedInteger(this.networkId), - CborSerializer.encodeUnsignedInteger(this.rootChainRoundNumber), - CborSerializer.encodeUnsignedInteger(this.epoch), - CborSerializer.encodeUnsignedInteger(this.timestamp), - CborSerializer.encodeOptional(this.previousHash, CborSerializer::encodeByteString), - CborSerializer.encodeByteString(this.hash), - CborSerializer.encodeOptional( - this.signatures, - (signatures) -> CborSerializer.encodeMap( - new CborMap( - signatures.entrySet().stream() - .map(entry -> new CborMap.Entry( - CborSerializer.encodeTextString(entry.getKey()), - CborSerializer.encodeByteString(entry.getValue()) - ) - ) - .collect(Collectors.toSet()) - ) - ) - ) - ) - ); - } - - /** - * Convert unicity seal to CBOR bytes without signatures. - * - * @return CBOR bytes without signatures - */ - public byte[] toCborWithoutSignatures() { - return new UnicitySeal( - this.version, - this.networkId, - this.rootChainRoundNumber, - this.epoch, - this.timestamp, - this.previousHash, - this.hash, - null - ).toCbor(); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof UnicitySeal)) { - return false; - } - UnicitySeal that = (UnicitySeal) o; - return Objects.equals(this.version, that.version) && Objects.equals(this.networkId, - that.networkId) && Objects.equals(this.rootChainRoundNumber, that.rootChainRoundNumber) - && Objects.equals(this.epoch, that.epoch) && Objects.equals(this.timestamp, - that.timestamp) && Objects.deepEquals(this.previousHash, that.previousHash) - && Objects.deepEquals(this.hash, that.hash) && Objects.equals(this.signatures, - that.signatures); - } - - @Override - public int hashCode() { - return Objects.hash(this.version, this.networkId, this.rootChainRoundNumber, this.epoch, - this.timestamp, - Arrays.hashCode(this.previousHash), Arrays.hashCode(this.hash), this.signatures); - } - - @Override - public String toString() { - return String.format( - "UnicitySeal{version=%s, networkId=%s, rootChainRoundNumber=%s, epoch=%s, timestamp=%s, " - + "previousHash=%s, hash=%s, signatures=%s", - this.version, - this.networkId, - this.rootChainRoundNumber, - this.epoch, - this.timestamp, - this.previousHash != null ? HexConverter.encode(this.previousHash) : null, - HexConverter.encode(this.hash), - this.signatures.entrySet() - .stream() - .map(entry -> String.format("%s: %s", entry.getKey(), - HexConverter.encode(entry.getValue()))) - .collect(Collectors.toList()) - ); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/bft/verification/UnicityCertificateVerificationContext.java b/src/main/java/org/unicitylabs/sdk/bft/verification/UnicityCertificateVerificationContext.java deleted file mode 100644 index 07d77ee..0000000 --- a/src/main/java/org/unicitylabs/sdk/bft/verification/UnicityCertificateVerificationContext.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.unicitylabs.sdk.bft.verification; - -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.bft.UnicityCertificate; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.verification.VerificationContext; - -/** - * Unicity certificate verification context. - */ -public class UnicityCertificateVerificationContext implements VerificationContext { - - private final DataHash inputHash; - private final UnicityCertificate unicityCertificate; - private final RootTrustBase trustBase; - - - /** - * Create unicity certificate verification context. - * - * @param inputHash input record hash - * @param unicityCertificate unicity certificate - * @param trustBase root trust base - */ - public UnicityCertificateVerificationContext( - DataHash inputHash, - UnicityCertificate unicityCertificate, - RootTrustBase trustBase - ) { - this.inputHash = inputHash; - this.unicityCertificate = unicityCertificate; - this.trustBase = trustBase; - } - - /** - * Get input record hash. - * - * @return input record hash - */ - public DataHash getInputHash() { - return this.inputHash; - } - - /** - * Get unicity certificate. - * - * @return unicity certificate - */ - public UnicityCertificate getUnicityCertificate() { - return this.unicityCertificate; - } - - /** - * Get root trust base. - * - * @return root trust base - */ - public RootTrustBase getTrustBase() { - return this.trustBase; - } - - -} diff --git a/src/main/java/org/unicitylabs/sdk/bft/verification/UnicityCertificateVerificationRule.java b/src/main/java/org/unicitylabs/sdk/bft/verification/UnicityCertificateVerificationRule.java deleted file mode 100644 index 8024f36..0000000 --- a/src/main/java/org/unicitylabs/sdk/bft/verification/UnicityCertificateVerificationRule.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.unicitylabs.sdk.bft.verification; - -import org.unicitylabs.sdk.bft.verification.rule.InputRecordCurrentHashVerificationRule; -import org.unicitylabs.sdk.bft.verification.rule.UnicitySealHashMatchesWithRootHashRule; -import org.unicitylabs.sdk.bft.verification.rule.UnicitySealQuorumSignaturesVerificationRule; -import org.unicitylabs.sdk.verification.CompositeVerificationRule; - -/** - * Unicity certificate verification rule. - */ -public class UnicityCertificateVerificationRule extends - CompositeVerificationRule { - - /** - * Create unicity certificate verification rule. - */ - public UnicityCertificateVerificationRule() { - super("Verify unicity certificate", - new InputRecordCurrentHashVerificationRule( - new UnicitySealHashMatchesWithRootHashRule( - new UnicitySealQuorumSignaturesVerificationRule(), - null - ), - null - )); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/bft/verification/rule/InputRecordCurrentHashVerificationRule.java b/src/main/java/org/unicitylabs/sdk/bft/verification/rule/InputRecordCurrentHashVerificationRule.java deleted file mode 100644 index ffe9baf..0000000 --- a/src/main/java/org/unicitylabs/sdk/bft/verification/rule/InputRecordCurrentHashVerificationRule.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.unicitylabs.sdk.bft.verification.rule; - -import org.unicitylabs.sdk.bft.verification.UnicityCertificateVerificationContext; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.verification.VerificationResult; -import org.unicitylabs.sdk.verification.VerificationRule; - -/** - * Input record current hash verification rule. - */ -public class InputRecordCurrentHashVerificationRule extends - VerificationRule { - - /** - * Create the rule without any subsequent rules. - */ - public InputRecordCurrentHashVerificationRule() { - this(null, null); - } - - /** - * Create the rule with subsequent rules for success and failure. - * - * @param onSuccessRule rule to execute on success - * @param onFailureRule rule to execute on failure - */ - public InputRecordCurrentHashVerificationRule( - VerificationRule onSuccessRule, - VerificationRule onFailureRule - ) { - super( - "Verifying input record if current hash matches input hash.", - onSuccessRule, - onFailureRule - ); - } - - @Override - public VerificationResult verify(UnicityCertificateVerificationContext context) { - if (context.getInputHash() - .equals(DataHash.fromImprint(context.getUnicityCertificate().getInputRecord().getHash()))) { - return VerificationResult.success(); - } - - return VerificationResult.fail("Input record current hash does not match input hash."); - } - -} diff --git a/src/main/java/org/unicitylabs/sdk/bft/verification/rule/UnicitySealHashMatchesWithRootHashRule.java b/src/main/java/org/unicitylabs/sdk/bft/verification/rule/UnicitySealHashMatchesWithRootHashRule.java deleted file mode 100644 index 2258986..0000000 --- a/src/main/java/org/unicitylabs/sdk/bft/verification/rule/UnicitySealHashMatchesWithRootHashRule.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.unicitylabs.sdk.bft.verification.rule; - -import com.google.common.primitives.UnsignedBytes; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import org.unicitylabs.sdk.bft.UnicityCertificate; -import org.unicitylabs.sdk.bft.UnicityTreeCertificate; -import org.unicitylabs.sdk.bft.verification.UnicityCertificateVerificationContext; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.verification.VerificationResult; -import org.unicitylabs.sdk.verification.VerificationRule; - -/** - * Rule to verify that the UnicitySeal hash matches the root hash of the UnicityTreeCertificate. - */ -public class UnicitySealHashMatchesWithRootHashRule extends - VerificationRule { - - /** - * Create the rule without any subsequent rules. - */ - public UnicitySealHashMatchesWithRootHashRule() { - this(null, null); - } - - /** - * Create the rule with subsequent rules for success and failure. - * - * @param onSuccessRule rule to execute on success - * @param onFailureRule rule to execute on failure - */ - public UnicitySealHashMatchesWithRootHashRule( - VerificationRule onSuccessRule, - VerificationRule onFailureRule - ) { - super( - "Verifying UnicitySeal hash matches with tree root hash.", - onSuccessRule, - onFailureRule - ); - } - - @Override - public VerificationResult verify(UnicityCertificateVerificationContext context) { - DataHash shardTreeCertificateRootHash = UnicityCertificate - .calculateShardTreeCertificateRootHash( - context.getUnicityCertificate().getInputRecord(), - context.getUnicityCertificate().getTechnicalRecordHash(), - context.getUnicityCertificate().getShardConfigurationHash(), - context.getUnicityCertificate().getShardTreeCertificate() - ); - - if (shardTreeCertificateRootHash == null) { - return VerificationResult.fail("Could not calculate shard tree certificate root hash."); - } - - UnicityTreeCertificate unicityTreeCertificate = context.getUnicityCertificate() - .getUnicityTreeCertificate(); - byte[] key = ByteBuffer.allocate(4) - .order(ByteOrder.BIG_ENDIAN) - .putInt(unicityTreeCertificate.getPartitionIdentifier()) - .array(); - - DataHash result = new DataHasher(HashAlgorithm.SHA256) - .update(CborSerializer.encodeByteString(new byte[]{(byte) 0x01})) // LEAF - .update(CborSerializer.encodeByteString(key)) - .update( - CborSerializer.encodeByteString( - new DataHasher(HashAlgorithm.SHA256) - .update( - CborSerializer.encodeByteString(shardTreeCertificateRootHash.getData()) - ) - .digest() - .getData() - ) - ) - .digest(); - - for (UnicityTreeCertificate.HashStep step : unicityTreeCertificate.getSteps()) { - byte[] stepKey = ByteBuffer.allocate(4) - .order(ByteOrder.BIG_ENDIAN) - .putInt(step.getKey()) - .array(); - - DataHasher hasher = new DataHasher(HashAlgorithm.SHA256) - .update(CborSerializer.encodeByteString(new byte[]{(byte) 0x00})) // NODE - .update(CborSerializer.encodeByteString(stepKey)); - - if (UnsignedBytes.lexicographicalComparator().compare(key, stepKey) > 0) { - hasher - .update(CborSerializer.encodeByteString(step.getHash())) - .update(CborSerializer.encodeByteString(result.getData())); - } else { - hasher - .update(CborSerializer.encodeByteString(result.getData())) - .update(CborSerializer.encodeByteString(step.getHash())); - } - - result = hasher.digest(); - } - - byte[] unicitySealHash = context.getUnicityCertificate().getUnicitySeal().getHash(); - - if (UnsignedBytes.lexicographicalComparator().compare(unicitySealHash, result.getData()) - != 0) { - return VerificationResult.fail("Unicity seal hash does not match tree root."); - } - - return VerificationResult.success(); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.java b/src/main/java/org/unicitylabs/sdk/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.java deleted file mode 100644 index 9fde5d2..0000000 --- a/src/main/java/org/unicitylabs/sdk/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.java +++ /dev/null @@ -1,110 +0,0 @@ -package org.unicitylabs.sdk.bft.verification.rule; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.bft.UnicitySeal; -import org.unicitylabs.sdk.bft.verification.UnicityCertificateVerificationContext; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.verification.VerificationResult; -import org.unicitylabs.sdk.verification.VerificationRule; - -/** - * Rule to verify that the UnicitySeal contains valid quorum signatures. - */ -public class UnicitySealQuorumSignaturesVerificationRule extends - VerificationRule { - - /** - * Create the rule without any subsequent rules. - */ - public UnicitySealQuorumSignaturesVerificationRule() { - this(null, null); - } - - /** - * Create the rule with subsequent rules for success and failure. - * - * @param onSuccessRule rule to execute on success - * @param onFailureRule rule to execute on failure - */ - public UnicitySealQuorumSignaturesVerificationRule( - VerificationRule onSuccessRule, - VerificationRule onFailureRule - ) { - super( - "Verifying UnicitySeal quorum signatures.", - onSuccessRule, - onFailureRule - ); - } - - @Override - public VerificationResult verify(UnicityCertificateVerificationContext context) { - UnicitySeal unicitySeal = context.getUnicityCertificate().getUnicitySeal(); - RootTrustBase trustBase = context.getTrustBase(); - - List results = new ArrayList<>(); - DataHash hash = new DataHasher(HashAlgorithm.SHA256) - .update(unicitySeal.toCborWithoutSignatures()) - .digest(); - int successful = 0; - for (Map.Entry entry : unicitySeal.getSignatures().entrySet()) { - String nodeId = entry.getKey(); - byte[] signature = entry.getValue(); - - VerificationResult result = UnicitySealQuorumSignaturesVerificationRule.verifySignature( - trustBase.getRootNodes().stream() - .filter(node -> node.getNodeId().equals(nodeId)) - .findFirst() - .orElse(null), - signature, - hash.getData() - ); - results.add( - VerificationResult.fromChildren( - String.format("Verifying node '%s' signature.", nodeId), - List.of(result) - ) - ); - - if (result.isSuccessful()) { - successful++; - } - } - - if (successful >= trustBase.getQuorumThreshold()) { - return VerificationResult.success(results); - } - - return VerificationResult.fail("Quorum threshold not reached.", results); - } - - private static VerificationResult verifySignature( - RootTrustBase.NodeInfo node, - byte[] signature, - byte[] hash - ) { - if (node == null) { - return VerificationResult.fail("No root node defined."); - } - - if (!SigningService.verifyWithPublicKey( - hash, - Arrays.copyOf(signature, signature.length - 1), - node.getSigningKey() - )) { - return VerificationResult.fail( - "Signature verification failed." - ); - } - - return VerificationResult.success(); - } - -} diff --git a/src/main/java/org/unicitylabs/sdk/crypto/MintSigningService.java b/src/main/java/org/unicitylabs/sdk/crypto/MintSigningService.java new file mode 100644 index 0000000..ccdd3b5 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/crypto/MintSigningService.java @@ -0,0 +1,43 @@ +package org.unicitylabs.sdk.crypto; + +import org.unicitylabs.sdk.crypto.hash.DataHasher; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.util.HexConverter; + +import java.util.Objects; + +/** + * Factory for the deterministic signing key used by mint transactions. + */ +public class MintSigningService { + + private static final byte[] MINTER_SECRET = HexConverter.decode( + "495f414d5f554e4956455253414c5f4d494e5445525f464f525f"); + + private MintSigningService() { + } + + /** + * Create a signing service for the provided token id. + * + * @param tokenId token id + * + * @return signing service + */ + public static SigningService create(TokenId tokenId) { + Objects.requireNonNull(tokenId, "Token ID cannot be null"); + + return new SigningService( + new DataHasher(HashAlgorithm.SHA256) + .update(CborSerializer.encodeArray( + CborSerializer.encodeByteString(MintSigningService.MINTER_SECRET), + tokenId.toCbor())) + .digest() + .getData() + ); + } + +} diff --git a/src/main/java/org/unicitylabs/sdk/hash/DataHash.java b/src/main/java/org/unicitylabs/sdk/crypto/hash/DataHash.java similarity index 72% rename from src/main/java/org/unicitylabs/sdk/hash/DataHash.java rename to src/main/java/org/unicitylabs/sdk/crypto/hash/DataHash.java index 98ea412..7b521cb 100644 --- a/src/main/java/org/unicitylabs/sdk/hash/DataHash.java +++ b/src/main/java/org/unicitylabs/sdk/crypto/hash/DataHash.java @@ -1,22 +1,16 @@ -package org.unicitylabs.sdk.hash; +package org.unicitylabs.sdk.crypto.hash; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import java.util.Arrays; -import java.util.Objects; -import org.unicitylabs.sdk.serializer.UnicityObjectMapper; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.serializer.json.JsonSerializationException; import org.unicitylabs.sdk.util.HexConverter; +import java.util.Arrays; +import java.util.Objects; + /** * DataHash represents a hash of data using a specific hash algorithm. */ -@JsonSerialize(using = DataHashJson.Serializer.class) -@JsonDeserialize(using = DataHashJson.Deserializer.class) public class DataHash { private final byte[] data; @@ -33,6 +27,10 @@ public DataHash(HashAlgorithm algorithm, byte[] data) { Objects.requireNonNull(algorithm, "algorithm cannot be null"); Objects.requireNonNull(data, "data cannot be null"); + if (data.length != algorithm.getLength()) { + throw new IllegalArgumentException("Invalid data length for the specified hash algorithm."); + } + this.data = Arrays.copyOf(data, data.length); this.algorithm = algorithm; } @@ -94,44 +92,17 @@ public byte[] getImprint() { } /** - * Create data hash from JSON string. - * - * @param input json string - * @return data hash - */ - public static DataHash fromJson(String input) { - try { - return UnicityObjectMapper.JSON.readValue(input, DataHash.class); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(DataHash.class, e); - } - } - - /** - * Convert data hash to JSON string. - * - * @return JSON string - */ - public String toJson() { - try { - return UnicityObjectMapper.JSON.writeValueAsString(this); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(DataHash.class, e); - } - } - - /** - * Create data hash from CBOR bytes. + * Deserialize data hash from CBOR bytes. * * @param bytes CBOR bytes * @return data hash */ public static DataHash fromCbor(byte[] bytes) { - return DataHash.fromImprint(CborDeserializer.readByteString(bytes)); + return DataHash.fromImprint(CborDeserializer.decodeByteString(bytes)); } /** - * Convert data hash to CBOR bytes. + * Serialize data hash to CBOR bytes. * * @return CBOR bytes */ diff --git a/src/main/java/org/unicitylabs/sdk/hash/DataHasher.java b/src/main/java/org/unicitylabs/sdk/crypto/hash/DataHasher.java similarity index 90% rename from src/main/java/org/unicitylabs/sdk/hash/DataHasher.java rename to src/main/java/org/unicitylabs/sdk/crypto/hash/DataHasher.java index c48c280..4b570d5 100644 --- a/src/main/java/org/unicitylabs/sdk/hash/DataHasher.java +++ b/src/main/java/org/unicitylabs/sdk/crypto/hash/DataHasher.java @@ -1,11 +1,11 @@ -package org.unicitylabs.sdk.hash; +package org.unicitylabs.sdk.crypto.hash; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** - * DataHasher is a utility class for hashing data using a specified hash algorithm. It provides - * methods to update the hash with data and to retrieve the final hash. + * DataHasher is a utility class for hashing data using a specified hash algorithm. It provides methods to update the + * hash with data and to retrieve the final hash. */ public class DataHasher { diff --git a/src/main/java/org/unicitylabs/sdk/hash/HashAlgorithm.java b/src/main/java/org/unicitylabs/sdk/crypto/hash/HashAlgorithm.java similarity index 73% rename from src/main/java/org/unicitylabs/sdk/hash/HashAlgorithm.java rename to src/main/java/org/unicitylabs/sdk/crypto/hash/HashAlgorithm.java index 7ac74a8..30d57de 100644 --- a/src/main/java/org/unicitylabs/sdk/hash/HashAlgorithm.java +++ b/src/main/java/org/unicitylabs/sdk/crypto/hash/HashAlgorithm.java @@ -1,4 +1,4 @@ -package org.unicitylabs.sdk.hash; +package org.unicitylabs.sdk.crypto.hash; /** * Hash algorithm representation. @@ -7,30 +7,32 @@ public enum HashAlgorithm { /** * SHA2-256 hash algorithm. */ - SHA256(0, "SHA-256"), + SHA256(0, "SHA-256", 32), /** * SHA2-224 hash algorithm. */ - SHA224(1, "SHA-224"), + SHA224(1, "SHA-224", 28), /** * SHA2-384 hash algorithm. */ - SHA384(2, "SHA-384"), + SHA384(2, "SHA-384", 48), /** * SHA2-512 hash algorithm. */ - SHA512(3, "SHA-512"), + SHA512(3, "SHA-512", 64), /** * RIPEMD160 hash algorithm. */ - RIPEMD160(4, "RIPEMD160"); + RIPEMD160(4, "RIPEMD160", 20); private final int value; private final String algorithm; + private final int length; - HashAlgorithm(int value, String algorithm) { + HashAlgorithm(int value, String algorithm, int length) { this.value = value; this.algorithm = algorithm; + this.length = length; } /** @@ -51,6 +53,15 @@ public String getAlgorithm() { return this.algorithm; } + /** + * Hash algorithm length in bytes. + * + * @return length + */ + public int getLength() { + return this.length; + } + /** * Get HashAlgorithm from its numeric value. * diff --git a/src/main/java/org/unicitylabs/sdk/hash/UnsupportedHashAlgorithmException.java b/src/main/java/org/unicitylabs/sdk/crypto/hash/UnsupportedHashAlgorithmException.java similarity index 94% rename from src/main/java/org/unicitylabs/sdk/hash/UnsupportedHashAlgorithmException.java rename to src/main/java/org/unicitylabs/sdk/crypto/hash/UnsupportedHashAlgorithmException.java index 4b0f29f..88eca98 100644 --- a/src/main/java/org/unicitylabs/sdk/hash/UnsupportedHashAlgorithmException.java +++ b/src/main/java/org/unicitylabs/sdk/crypto/hash/UnsupportedHashAlgorithmException.java @@ -1,5 +1,5 @@ -package org.unicitylabs.sdk.hash; +package org.unicitylabs.sdk.crypto.hash; /** * Throw given error when hash algorithm is not supported by data hasher. diff --git a/src/main/java/org/unicitylabs/sdk/signing/Signature.java b/src/main/java/org/unicitylabs/sdk/crypto/secp256k1/Signature.java similarity index 77% rename from src/main/java/org/unicitylabs/sdk/signing/Signature.java rename to src/main/java/org/unicitylabs/sdk/crypto/secp256k1/Signature.java index 677d767..05ad847 100644 --- a/src/main/java/org/unicitylabs/sdk/signing/Signature.java +++ b/src/main/java/org/unicitylabs/sdk/crypto/secp256k1/Signature.java @@ -1,16 +1,15 @@ -package org.unicitylabs.sdk.signing; +package org.unicitylabs.sdk.crypto.secp256k1; + +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.util.HexConverter; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.util.Arrays; import java.util.Objects; -import org.unicitylabs.sdk.util.HexConverter; /** * Signature implementation for signing service, this contains public key recovery byte as well. */ -@JsonSerialize(using = SignatureJson.Serializer.class) -@JsonDeserialize(using = SignatureJson.Deserializer.class) public class Signature { private final byte[] bytes; @@ -51,6 +50,15 @@ public byte[] encode() { return signature; } + /** + * Serialize Signature to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeByteString(this.encode()); + } + /** * Decodes a byte array into a Signature object. * @@ -67,6 +75,16 @@ public static Signature decode(byte[] input) { return new Signature(bytes, recovery); } + /** + * Deserialize Signature from CBOR bytes. + * + * @param bytes CBOR bytes + * @return signature + */ + public static Signature fromCbor(byte[] bytes) { + return Signature.decode(CborDeserializer.decodeByteString(bytes)); + } + @Override public boolean equals(Object o) { if (!(o instanceof Signature)) { @@ -84,6 +102,6 @@ public int hashCode() { @Override public String toString() { return String.format("Signature{bytes=%s, recovery=%s}", HexConverter.encode(this.bytes), - this.recovery); + this.recovery); } } diff --git a/src/main/java/org/unicitylabs/sdk/signing/SigningService.java b/src/main/java/org/unicitylabs/sdk/crypto/secp256k1/SigningService.java similarity index 85% rename from src/main/java/org/unicitylabs/sdk/signing/SigningService.java rename to src/main/java/org/unicitylabs/sdk/crypto/secp256k1/SigningService.java index eb0eae4..855b32d 100644 --- a/src/main/java/org/unicitylabs/sdk/signing/SigningService.java +++ b/src/main/java/org/unicitylabs/sdk/crypto/secp256k1/SigningService.java @@ -1,8 +1,5 @@ -package org.unicitylabs.sdk.signing; +package org.unicitylabs.sdk.crypto.secp256k1; -import java.math.BigInteger; -import java.security.SecureRandom; -import java.util.Arrays; import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.params.ECDomainParameters; import org.bouncycastle.crypto.params.ECPrivateKeyParameters; @@ -13,9 +10,11 @@ import org.bouncycastle.jce.spec.ECParameterSpec; import org.bouncycastle.math.ec.ECCurve; import org.bouncycastle.math.ec.ECPoint; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; +import org.unicitylabs.sdk.crypto.hash.DataHash; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Arrays; /** * Default signing service. @@ -25,10 +24,10 @@ public class SigningService { private static final String CURVE_NAME = "secp256k1"; private static final ECParameterSpec EC_SPEC = ECNamedCurveTable.getParameterSpec(CURVE_NAME); private static final ECDomainParameters EC_DOMAIN_PARAMETERS = new ECDomainParameters( - EC_SPEC.getCurve(), - EC_SPEC.getG(), - EC_SPEC.getN(), - EC_SPEC.getH() + EC_SPEC.getCurve(), + EC_SPEC.getG(), + EC_SPEC.getN(), + EC_SPEC.getH() ); private final ECPrivateKeyParameters privateKey; @@ -46,7 +45,7 @@ public SigningService(byte[] privateKey) { BigInteger privateKeyAsBigInt = new BigInteger(1, privateKey); if (privateKeyAsBigInt.compareTo(BigInteger.ONE) < 0 - || privateKeyAsBigInt.compareTo(EC_SPEC.getN()) >= 0) { + || privateKeyAsBigInt.compareTo(EC_SPEC.getN()) >= 0) { throw new IllegalArgumentException("Invalid private key: must be in range [1, N)"); } @@ -54,8 +53,8 @@ public SigningService(byte[] privateKey) { ECPoint q = EC_SPEC.getG().multiply(privateKeyAsBigInt); this.publicKey = q.getEncoded(true); // compressed format this.privateKey = new ECPrivateKeyParameters( - privateKeyAsBigInt, - EC_DOMAIN_PARAMETERS + privateKeyAsBigInt, + EC_DOMAIN_PARAMETERS ); } @@ -65,7 +64,7 @@ public SigningService(byte[] privateKey) { * @return public key bytes */ public byte[] getPublicKey() { - return Arrays.copyOf(publicKey, publicKey.length); + return Arrays.copyOf(this.publicKey, this.publicKey.length); } /** @@ -90,30 +89,12 @@ public static byte[] generatePrivateKey() { } /** - * Create signing service from secret. + * Generate a signing service instance with a randomly generated private key. * - * @param secret secret bytes - * @return signing service + * @return signing service instance */ - public static SigningService createFromSecret(byte[] secret) { - return SigningService.createFromMaskedSecret(secret, null); - } - - /** - * Create signing service from secret and nonce. - * - * @param secret secret bytes - * @param nonce nonce bytes - * @return signing service - */ - public static SigningService createFromMaskedSecret(byte[] secret, byte[] nonce) { - DataHasher hasher = new DataHasher(HashAlgorithm.SHA256); - hasher.update(secret); - if (nonce != null) { - hasher.update(nonce); - } - - return new SigningService(hasher.digest().getData()); + public static SigningService generate() { + return new SigningService(SigningService.generatePrivateKey()); } /** @@ -224,10 +205,10 @@ private byte[] toFixedLength(BigInteger value, int length) { * Recover public key from signature for a specific recovery ID. */ private static ECPoint recoverFromSignature( - int recId, - BigInteger r, - BigInteger s, - byte[] message + int recId, + BigInteger r, + BigInteger s, + byte[] message ) { BigInteger n = EC_DOMAIN_PARAMETERS.getN(); BigInteger x = r; @@ -255,8 +236,8 @@ private static ECPoint recoverFromSignature( ECPoint point1 = y.multiply(s); ECPoint point2 = EC_DOMAIN_PARAMETERS.getG().multiply(e); return point1 - .subtract(point2) - .multiply(r.modInverse(n)); + .subtract(point2) + .multiply(r.modInverse(n)); } diff --git a/src/main/java/org/unicitylabs/sdk/hash/DataHashJson.java b/src/main/java/org/unicitylabs/sdk/hash/DataHashJson.java deleted file mode 100644 index 0682fc4..0000000 --- a/src/main/java/org/unicitylabs/sdk/hash/DataHashJson.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.unicitylabs.sdk.hash; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import java.io.IOException; - -/** - * Data hash serializer and deserializer implementation. - */ -public class DataHashJson { - - private DataHashJson() { - } - - /** - * Data hash serializer. - */ - public static class Serializer extends StdSerializer { - - /** - * Create serializer. - */ - public Serializer() { - super(DataHash.class); - } - - /** - * Serialize data hash. - * - * @param value data hash - * @param gen json generator - * @param serializers serializer provider - * @throws IOException on serialization failure - */ - @Override - public void serialize(DataHash value, JsonGenerator gen, SerializerProvider serializers) - throws IOException { - gen.writeObject(value.getImprint()); - } - } - - /** - * Data hash deserializer. - */ - public static class Deserializer extends StdDeserializer { - - /** - * Create deserializer. - */ - public Deserializer() { - super(DataHash.class); - } - - /** - * Deserialize data hash. - * - * @param p Parser used for reading JSON content - * @param ctx Context that can be used to access information about this deserialization - * activity. - * @return data hash - * @throws IOException on deserialization failure - */ - @Override - public DataHash deserialize(JsonParser p, DeserializationContext ctx) throws IOException { - return DataHash.fromImprint(p.readValueAs(byte[].class)); - } - } -} - diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePath.java b/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePath.java deleted file mode 100644 index 9925ee4..0000000 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePath.java +++ /dev/null @@ -1,254 +0,0 @@ -package org.unicitylabs.sdk.mtree.sum; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonGetter; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import java.math.BigInteger; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.mtree.MerkleTreePathVerificationResult; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.serializer.json.BigIntegerAsStringSerializer; -import org.unicitylabs.sdk.util.BigIntegerConverter; - -/** - * Path in a sparse merkle sum tree. - */ -public class SparseMerkleSumTreePath { - - private final DataHash rootHash; - private final List steps; - - @JsonCreator - SparseMerkleSumTreePath( - @JsonProperty("root") DataHash rootHash, - @JsonProperty("steps") List steps - ) { - Objects.requireNonNull(rootHash, "root cannot be null"); - Objects.requireNonNull(steps, "steps cannot be null"); - - this.rootHash = rootHash; - this.steps = List.copyOf(steps); - } - - /** - * Get root hash. - * - * @return root hash - */ - @JsonGetter("root") - public DataHash getRootHash() { - return this.rootHash; - } - - /** - * Get steps of the path from leaf to the root. - * - * @return steps - */ - public List getSteps() { - return this.steps; - } - - /** - * Verify the path against the given state ID. - * - * @param stateId state ID to verify against - * @return result of the verification - */ - public MerkleTreePathVerificationResult verify(BigInteger stateId) { - if (this.steps.size() == 0) { - return new MerkleTreePathVerificationResult(false, false); - } - - SparseMerkleSumTreePathStep step = this.steps.get(0); - byte[] currentData; - BigInteger currentPath = step.getPath(); - BigInteger currentSum = step.getValue(); - if (step.getPath().compareTo(BigInteger.ONE) > 0) { - DataHash hash = new DataHasher(this.rootHash.getAlgorithm()) - .update( - CborSerializer.encodeArray( - CborSerializer.encodeByteString(BigIntegerConverter.encode(step.getPath())), - CborSerializer.encodeOptional( - step.getData().orElse(null), - CborSerializer::encodeByteString - ), - CborSerializer.encodeByteString(BigIntegerConverter.encode(step.getValue())) - ) - ) - .digest(); - - currentData = hash.getData(); - } else { - currentPath = BigInteger.ONE; - currentData = step.getData().orElse(null); - } - - SparseMerkleSumTreePathStep previousStep = step; - for (int i = 1; i < this.steps.size(); i++) { - step = this.steps.get(i); - boolean isRight = previousStep.getPath().testBit(0); - - byte[] leftHash = isRight ? step.getData().orElse(null) : currentData; - byte[] rightHash = isRight ? currentData : step.getData().orElse(null); - BigInteger leftCounter = isRight ? step.getValue() : currentSum; - BigInteger rightCounter = isRight ? currentSum : step.getValue(); - - DataHash hash = new DataHasher(this.rootHash.getAlgorithm()) - .update( - CborSerializer.encodeArray( - CborSerializer.encodeByteString(BigIntegerConverter.encode(step.getPath())), - CborSerializer.encodeOptional(leftHash, CborSerializer::encodeByteString), - CborSerializer.encodeByteString(BigIntegerConverter.encode(leftCounter)), - CborSerializer.encodeOptional(rightHash, CborSerializer::encodeByteString), - CborSerializer.encodeByteString(BigIntegerConverter.encode(rightCounter)) - ) - ) - .digest(); - - currentData = hash.getData(); - - int length = step.getPath().bitLength() - 1; - if (length < 0) { - return new MerkleTreePathVerificationResult(false, false); - } - currentPath = currentPath.shiftLeft(length) - .or(step.getPath().and(BigInteger.ONE.shiftLeft(length).subtract(BigInteger.ONE))); - currentSum = currentSum.add(step.getValue()); - previousStep = step; - } - - boolean pathValid = currentData != null - && this.rootHash.equals(new DataHash(this.rootHash.getAlgorithm(), currentData)); - boolean pathIncluded = currentPath.compareTo(stateId) == 0; - - return new MerkleTreePathVerificationResult(pathValid, pathIncluded); - } - - /** - * Create path from CBOR bytes. - * - * @param bytes CBOR bytes - * @return path - */ - public static SparseMerkleSumTreePath fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); - - return new SparseMerkleSumTreePath( - DataHash.fromCbor(data.get(0)), - CborDeserializer.readArray(data.get(1)).stream() - .map(SparseMerkleSumTreePathStep::fromCbor) - .collect(Collectors.toList()) - ); - } - - /** - * Convert path to CBOR bytes. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeArray( - this.rootHash.toCbor(), - CborSerializer.encodeArray( - this.steps.stream() - .map(SparseMerkleSumTreePathStep::toCbor) - .toArray(byte[][]::new) - ) - ); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof SparseMerkleSumTreePath)) { - return false; - } - SparseMerkleSumTreePath that = (SparseMerkleSumTreePath) o; - return Objects.equals(this.rootHash, that.rootHash) && Objects.equals(this.steps, that.steps); - } - - @Override - public int hashCode() { - return Objects.hash(this.rootHash, this.steps); - } - - @Override - 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; - - @JsonCreator - Root( - @JsonProperty("hash") DataHash hash, - @JsonProperty("counter") 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 - */ - @JsonSerialize(using = BigIntegerAsStringSerializer.class) - 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.readArray(bytes); - - return new Root( - DataHash.fromCbor(data.get(0)), - BigIntegerConverter.decode(CborDeserializer.readByteString(data.get(1))) - ); - } - - /** - * Convert 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/payment/PaymentData.java b/src/main/java/org/unicitylabs/sdk/payment/PaymentData.java new file mode 100644 index 0000000..850abda --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/PaymentData.java @@ -0,0 +1,24 @@ +package org.unicitylabs.sdk.payment; + +import org.unicitylabs.sdk.payment.asset.Asset; + +import java.util.Set; + +/** + * Represents payment payload data. + */ +public interface PaymentData { + /** + * Returns the assets included in this payment payload. + * + * @return set of assets + */ + Set getAssets(); + + /** + * Encodes this payment payload into bytes. + * + * @return encoded payment data + */ + byte[] encode(); +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/PaymentDataDeserializer.java b/src/main/java/org/unicitylabs/sdk/payment/PaymentDataDeserializer.java new file mode 100644 index 0000000..25f2ec0 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/PaymentDataDeserializer.java @@ -0,0 +1,15 @@ +package org.unicitylabs.sdk.payment; + +/** + * Functional contract for decoding encoded payment data. + */ +@FunctionalInterface +public interface PaymentDataDeserializer { + /** + * Decodes payment data bytes into a {@link PaymentData} instance. + * + * @param data encoded payment data bytes + * @return decoded payment data + */ + PaymentData decode(byte[] data); +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitAssetProof.java b/src/main/java/org/unicitylabs/sdk/payment/SplitAssetProof.java new file mode 100644 index 0000000..29654c2 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitAssetProof.java @@ -0,0 +1,115 @@ +package org.unicitylabs.sdk.payment; + +import org.unicitylabs.sdk.payment.asset.AssetId; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.smt.plain.SparseMerkleTreePath; +import org.unicitylabs.sdk.smt.sum.SparseMerkleSumTreePath; + +import java.util.List; +import java.util.Objects; + +/** + * Proof material for one split reason entry. + */ +public final class SplitAssetProof { + private final AssetId assetId; + private final SparseMerkleTreePath aggregationPath; + private final SparseMerkleSumTreePath assetTreePath; + + private SplitAssetProof( + AssetId assetId, + SparseMerkleTreePath aggregationPath, + SparseMerkleSumTreePath assetTreePath + ) { + this.assetId = assetId; + this.aggregationPath = aggregationPath; + this.assetTreePath = assetTreePath; + } + + /** + * Get asset id referenced by this proof. + * + * @return asset id + */ + public AssetId getAssetId() { + return this.assetId; + } + + /** + * Get sparse merkle path in the aggregation tree. + * + * @return aggregation path + */ + public SparseMerkleTreePath getAggregationPath() { + return this.aggregationPath; + } + + /** + * Get sparse merkle sum tree path for the asset tree. + * + * @return asset tree path + */ + public SparseMerkleSumTreePath getAssetTreePath() { + return this.assetTreePath; + } + + /** + * Create split reason proof. + * + * @param assetId asset id + * @param aggregationPath aggregation path + * @param assetTreePath asset tree path + * + * @return split reason proof + */ + public static SplitAssetProof create( + AssetId assetId, + SparseMerkleTreePath aggregationPath, + SparseMerkleSumTreePath assetTreePath + ) { + return new SplitAssetProof(assetId, aggregationPath, assetTreePath); + } + + /** + * Deserialize split reason proof from CBOR bytes. + * + * @param bytes CBOR bytes + * + * @return split reason proof + */ + public static SplitAssetProof fromCbor(byte[] bytes) { + List data = CborDeserializer.decodeArray(bytes, 3); + + return new SplitAssetProof( + AssetId.fromCbor(data.get(0)), + SparseMerkleTreePath.fromCbor(data.get(1)), + SparseMerkleSumTreePath.fromCbor(data.get(2)) + ); + } + + /** + * Serialize split reason proof to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeArray( + this.assetId.toCbor(), + this.aggregationPath.toCbor(), + this.assetTreePath.toCbor() + ); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof SplitAssetProof)) return false; + SplitAssetProof that = (SplitAssetProof) o; + return Objects.equals(this.assetId, that.assetId); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.assetId); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustification.java b/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustification.java new file mode 100644 index 0000000..6f68714 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustification.java @@ -0,0 +1,101 @@ +package org.unicitylabs.sdk.payment; + +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializationException; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.transaction.Token; + +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Mint justification for a split-output token, carrying the burn token of the source and the + * inclusion proofs that link each output asset back to the burned source aggregation tree. + */ +public final class SplitMintJustification { + public static final long CBOR_TAG = 39044; + + private final Token token; + private final List proofs; + + private SplitMintJustification( + Token token, + List proofs + ) { + this.token = token; + this.proofs = proofs; + } + + /** + * Get the burn token whose split produced this justification. + * + * @return burn token + */ + public Token getToken() { + return this.token; + } + + /** + * Get the inclusion proofs supporting this split mint justification. + * + * @return proofs + */ + public List getProofs() { + return this.proofs; + } + + /** + * Create a split mint justification. + * + * @param token burn token of the source token being split + * @param proofs inclusion proofs supporting split eligibility + * + * @return split mint justification + */ + public static SplitMintJustification create(Token token, Set proofs) { + Objects.requireNonNull(token, "token cannot be null"); + Objects.requireNonNull(proofs, "proofs cannot be null"); + + if (proofs.isEmpty()) { + throw new IllegalArgumentException("proofs cannot be empty"); + } + + return new SplitMintJustification(token, List.copyOf(proofs)); + } + + /** + * Deserialize split mint justification from CBOR bytes. + * + * @param bytes CBOR bytes + * + * @return split mint justification + */ + public static SplitMintJustification fromCbor(byte[] bytes) { + CborDeserializer.CborTag tag = CborDeserializer.decodeTag(bytes); + if (tag.getTag() != SplitMintJustification.CBOR_TAG) { + throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); + } + List data = CborDeserializer.decodeArray(tag.getData(), 2); + return SplitMintJustification.create( + Token.fromCbor(data.get(0)), + CborDeserializer.decodeArray(data.get(1)).stream().map(SplitAssetProof::fromCbor).collect(Collectors.toSet()) + ); + } + + /** + * Serialize split mint justification to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeTag( + SplitMintJustification.CBOR_TAG, + CborSerializer.encodeArray( + this.token.toCbor(), + CborSerializer.encodeArray(this.proofs.stream().map(SplitAssetProof::toCbor).toArray(byte[][]::new)) + ) + ); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java b/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java new file mode 100644 index 0000000..a78a9db --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitMintJustificationVerifier.java @@ -0,0 +1,208 @@ +package org.unicitylabs.sdk.payment; + +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.payment.asset.Asset; +import org.unicitylabs.sdk.payment.asset.AssetId; +import org.unicitylabs.sdk.predicate.EncodedPredicate; +import org.unicitylabs.sdk.predicate.builtin.BurnPredicate; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.smt.MerkleTreePathVerificationResult; +import org.unicitylabs.sdk.transaction.CertifiedMintTransaction; +import org.unicitylabs.sdk.transaction.Transaction; +import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifier; +import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifierService; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public class SplitMintJustificationVerifier implements MintJustificationVerifier { + private final RootTrustBase trustBase; + private final PredicateVerifierService predicateVerifier; + private final PaymentDataDeserializer decodePaymentData; + + public SplitMintJustificationVerifier( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + PaymentDataDeserializer decodePaymentData + ) { + this.trustBase = Objects.requireNonNull(trustBase, "trustBase cannot be null"); + this.predicateVerifier = Objects.requireNonNull(predicateVerifier, "predicateVerifier cannot be null"); + this.decodePaymentData = Objects.requireNonNull(decodePaymentData, "decodePaymentData cannot be null"); + } + + @Override + public long getTag() { + return SplitMintJustification.CBOR_TAG; + } + + @Override + public VerificationResult verify(CertifiedMintTransaction transaction, MintJustificationVerifierService mintJustificationVerifier) { + Objects.requireNonNull(transaction, "transaction cannot be null"); + Objects.requireNonNull(mintJustificationVerifier, "mintJustificationVerifierService cannot be null"); + + byte[] justificationBytes = transaction.getJustification().orElse(null); + if (justificationBytes == null) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Transaction has no justification." + ); + } + + SplitMintJustification justification = SplitMintJustification.fromCbor(justificationBytes); + byte[] paymentDataBytes = transaction.getData().orElse(null); + PaymentData paymentData = paymentDataBytes != null ? this.decodePaymentData.decode(paymentDataBytes) : null; + + if (paymentData == null || paymentData.getAssets() == null) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Assets data is missing." + ); + } + + VerificationResult verificationResult = justification.getToken() + .verify(trustBase, predicateVerifier, mintJustificationVerifier); + if (verificationResult.getStatus() != VerificationStatus.OK) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Burn token verification failed.", + verificationResult + ); + } + + Map assets = new HashMap<>(); + for (Asset asset : paymentData.getAssets()) { + if (asset == null) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Asset data is missing." + ); + } + + AssetId assetId = asset.getId(); + if (assets.putIfAbsent(assetId, asset) != null) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + String.format("Duplicate asset id %s found in asset data.", assetId) + ); + } + } + + if (assets.size() != justification.getProofs().size()) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Total amount of assets differ in token and proofs." + ); + } + + Set validatedAssets = new HashSet<>(); + Transaction burnTokenLastTransaction = justification.getToken().getLatestTransaction(); + DataHash root = justification.getProofs().get(0).getAggregationPath().getRootHash(); + for (SplitAssetProof proof : justification.getProofs()) { + 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()) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + String.format("Aggregation path verification failed for asset: %s", proof.getAssetId()) + ); + } + + MerkleTreePathVerificationResult assetTreePathResult = proof.getAssetTreePath() + .verify(transaction.getTokenId().toBitString().toBigInteger()); + if (!assetTreePathResult.isSuccessful()) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + String.format("Asset tree path verification failed for token: %s", transaction.getTokenId()) + ); + } + + if (!proof.getAggregationPath().getRootHash().equals(root)) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Current proof is not derived from the same asset tree as other proofs." + ); + } + + if (!Arrays.equals( + proof.getAssetTreePath().getRootHash().getImprint(), + proof.getAggregationPath().getSteps().get(0).getData().orElse(null) + )) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Asset tree root does not match aggregation path leaf." + ); + } + + Asset asset = assets.get(proof.getAssetId()); + + if (asset == null) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + String.format("Asset id %s not found in asset data.", proof.getAssetId()) + ); + } + + BigInteger amount = asset.getValue(); + + if (!proof.getAssetTreePath().getSteps().get(0).getValue().equals(amount)) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + String.format("Asset amount for asset id %s does not match asset tree leaf.", proof.getAssetId()) + ); + } + + EncodedPredicate expectedRecipient = EncodedPredicate.fromPredicate( + BurnPredicate.create(proof.getAggregationPath().getRootHash().getImprint()) + ); + + if (!expectedRecipient.equals(burnTokenLastTransaction.getRecipient())) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Aggregation path root does not match burn predicate." + ); + } + } + + if (validatedAssets.size() != assets.size()) { + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.FAIL, + "Some assets proofs are missing from the token." + ); + } + + return new VerificationResult<>( + "SplitMintJustificationVerificationRule", + VerificationStatus.OK + ); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitResult.java b/src/main/java/org/unicitylabs/sdk/payment/SplitResult.java new file mode 100644 index 0000000..ac2afb0 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitResult.java @@ -0,0 +1,46 @@ +package org.unicitylabs.sdk.payment; + +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.transaction.TransferTransaction; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +/** + * Result of token split generation containing burn transaction and per-token proofs. + */ +public class SplitResult { + + private final TransferTransaction burnTransaction; + private final Map> proofs; + + SplitResult(TransferTransaction burnTransaction, Map> proofs) { + this.burnTransaction = burnTransaction; + this.proofs = Map.copyOf( + proofs.entrySet().stream() + .collect( + Collectors.toMap(Entry::getKey, value -> List.copyOf(value.getValue())) + ) + ); + } + + /** + * Get the burn transaction that anchors split proofs. + * + * @return burn transaction + */ + public TransferTransaction getBurnTransaction() { + return this.burnTransaction; + } + + /** + * Get proofs grouped by resulting token id. + * + * @return split proofs map + */ + public Map> getProofs() { + return this.proofs; + } +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java b/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java new file mode 100644 index 0000000..39cdda3 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java @@ -0,0 +1,144 @@ +package org.unicitylabs.sdk.payment; + +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.payment.asset.Asset; +import org.unicitylabs.sdk.payment.asset.AssetId; +import org.unicitylabs.sdk.predicate.builtin.BurnPredicate; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.smt.BranchExistsException; +import org.unicitylabs.sdk.smt.LeafOutOfBoundsException; +import org.unicitylabs.sdk.smt.plain.SparseMerkleTree; +import org.unicitylabs.sdk.smt.plain.SparseMerkleTreeRootNode; +import org.unicitylabs.sdk.smt.sum.SparseMerkleSumTree; +import org.unicitylabs.sdk.smt.sum.SparseMerkleSumTreeRootNode; +import org.unicitylabs.sdk.transaction.Token; +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.transaction.TransferTransaction; + +import java.security.SecureRandom; +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +/** + * Utilities for creating and verifying token split proofs. + */ +public class TokenSplit { + + private static final SecureRandom RANDOM = new SecureRandom(); + + private TokenSplit() { + } + + /** + * Create split proofs and burn transaction for provided target token distributions. + * + * @param token source token being split + * @param paymentDataDeserializer payment data decoder for source token payload + * @param splitTokens destination token ids and their asset allocations + * + * @return split result containing burn transaction and proof map + * + * @throws LeafOutOfBoundsException if a leaf path is invalid for merkle tree insertion + * @throws BranchExistsException if duplicate branches are inserted into a merkle tree + */ + public static SplitResult split( + Token token, + PaymentDataDeserializer paymentDataDeserializer, + Map> splitTokens + ) throws LeafOutOfBoundsException, BranchExistsException { + Objects.requireNonNull(token, "Token cannot be null"); + Objects.requireNonNull(paymentDataDeserializer, "Payment data deserializer cannot be null"); + Objects.requireNonNull(splitTokens, "Split tokens cannot be null"); + byte[] paymentDataBytes = token.getGenesis().getData().orElse(null); + if (paymentDataBytes == null) { + throw new IllegalArgumentException("Token genesis data must be present"); + } + + HashMap trees = new HashMap(); + for (Entry> entry : splitTokens.entrySet()) { + Objects.requireNonNull(entry, "Split token entry cannot be null"); + Objects.requireNonNull(entry.getKey(), "Split token id cannot be null"); + for (Asset asset : entry.getValue()) { + Objects.requireNonNull(asset, "Split token asset cannot be null"); + + SparseMerkleSumTree tree = trees.computeIfAbsent(asset.getId(), + v -> new SparseMerkleSumTree(HashAlgorithm.SHA256)); + tree.addLeaf( + entry.getKey().toBitString().toBigInteger(), + new SparseMerkleSumTree.LeafValue(asset.getId().getBytes(), asset.getValue()) + ); + } + } + + PaymentData paymentData = paymentDataDeserializer.decode(paymentDataBytes); + Map assets = paymentData.getAssets().stream() + .collect(Collectors.toMap( + Asset::getId, + asset -> asset, + (a, b) -> { + throw new IllegalArgumentException( + "Payment data contains multiple assets with the same id: " + a.getId()); + } + ) + ); + + if (trees.size() != assets.size()) { + throw new IllegalArgumentException("Token and split tokens asset counts differ."); + } + + SparseMerkleTree aggregationTree = new SparseMerkleTree(HashAlgorithm.SHA256); + HashMap assetTreeRoots = new HashMap(); + for (Entry entry : trees.entrySet()) { + Asset tokenAsset = assets.get(entry.getKey()); + if (tokenAsset == null) { + throw new IllegalArgumentException(String.format("Token did not contain asset %s.", entry.getKey())); + } + + SparseMerkleSumTreeRootNode root = entry.getValue().calculateRoot(); + if (!root.getValue().equals(tokenAsset.getValue())) { + throw new IllegalArgumentException( + String.format( + "Token contained %s %s assets, but tree has %s", + tokenAsset.getValue(), + tokenAsset.getId(), + root.getValue() + ) + ); + } + + assetTreeRoots.put(tokenAsset.getId(), root); + aggregationTree.addLeaf(tokenAsset.getId().toBitString().toBigInteger(), root.getRootHash().getImprint()); + } + + SparseMerkleTreeRootNode aggregationRoot = aggregationTree.calculateRoot(); + BurnPredicate burnPredicate = BurnPredicate.create(aggregationRoot.getRootHash().getImprint()); + byte[] stateMask = new byte[32]; + RANDOM.nextBytes(stateMask); + + TransferTransaction burnTransaction = TransferTransaction.create( + token, + burnPredicate, + stateMask, + CborSerializer.encodeNull() + ); + + HashMap> proofs = new HashMap>(); + for (Entry> entry : splitTokens.entrySet()) { + proofs.put( + entry.getKey(), + List.copyOf( + entry.getValue().stream().map(asset -> SplitAssetProof.create( + asset.getId(), + aggregationRoot.getPath(asset.getId().toBitString().toBigInteger()), + assetTreeRoots.get(asset.getId()).getPath(entry.getKey().toBitString().toBigInteger()) + ) + ).collect(Collectors.toList()) + ) + ); + } + + return new SplitResult(burnTransaction, proofs); + } + +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java b/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java new file mode 100644 index 0000000..ee253dd --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java @@ -0,0 +1,97 @@ +package org.unicitylabs.sdk.payment.asset; + +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.util.BigIntegerConverter; + +import java.math.BigInteger; +import java.util.List; +import java.util.Objects; + +/** + * Represents an asset with an ID and a value. + */ +public final class Asset { + + private final BigInteger value; + private final AssetId id; + + /** + * Create a new asset with the given ID and value. + * + * @param id asset ID + * @param value asset value + */ + public Asset(AssetId id, BigInteger value) { + this.id = Objects.requireNonNull(id, "Asset ID cannot be null"); + this.value = Objects.requireNonNull(value, "Asset value cannot be null"); + + if (this.value.compareTo(BigInteger.ZERO) < 0) { + throw new IllegalArgumentException("Asset value cannot be negative"); + } + } + + /** + * Get asset ID. + * + * @return asset ID + */ + public AssetId getId() { + return this.id; + } + + /** + * Get asset value. + * + * @return asset value + */ + public BigInteger getValue() { + return this.value; + } + + /** + * Deserialize asset from CBOR bytes. + * + * @param bytes CBOR bytes + * @return asset + */ + public static Asset fromCbor(byte[] bytes) { + List data = CborDeserializer.decodeArray(bytes, 2); + + return new Asset( + AssetId.fromCbor(data.get(0)), + BigIntegerConverter.decode(CborDeserializer.decodeByteString(data.get(1))) + ); + } + + /** + * Serialize asset to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeArray( + this.id.toCbor(), + CborSerializer.encodeByteString(BigIntegerConverter.encode(this.value)) + ); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Asset)) { + return false; + } + Asset asset = (Asset) o; + return Objects.equals(this.id, asset.id); + } + + @Override + public int hashCode() { + return Objects.hash(this.id); + } + + @Override + public String toString() { + return String.format("Asset{value=%s, id=%s}", this.value, this.id); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/asset/AssetId.java b/src/main/java/org/unicitylabs/sdk/payment/asset/AssetId.java new file mode 100644 index 0000000..0d8ad4d --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/asset/AssetId.java @@ -0,0 +1,84 @@ +package org.unicitylabs.sdk.payment.asset; + +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.util.BitString; +import org.unicitylabs.sdk.util.HexConverter; + +import java.util.Arrays; +import java.util.Objects; + +/** + * Unique identifier of an asset. + */ +public class AssetId { + private final byte[] bytes; + + /** + * Create asset id from bytes. + * + * @param bytes asset id bytes + */ + public AssetId(byte[] bytes) { + Objects.requireNonNull(bytes, "Asset id cannot be null"); + + this.bytes = Arrays.copyOf(bytes, bytes.length); + } + + /** + * Get asset id bytes. + * + * @return asset id bytes + */ + public byte[] getBytes() { + return Arrays.copyOf(this.bytes, this.bytes.length); + } + + /** + * Deserialize asset id from CBOR bytes. + * + * @param bytes CBOR bytes + * + * @return asset id + */ + public static AssetId fromCbor(byte[] bytes) { + return new AssetId(CborDeserializer.decodeByteString(bytes)); + } + + /** + * Convert asset id to bit string form used by sparse merkle trees. + * + * @return bit string + */ + public BitString toBitString() { + return BitString.fromBytes(this.bytes); + } + + /** + * Serialize asset id to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeByteString(this.bytes); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AssetId)) { + return false; + } + AssetId assetId = (AssetId) o; + return Arrays.equals(this.bytes, assetId.bytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(this.bytes); + } + + @Override + public String toString() { + return String.format("AssetId{bytes=%s}", HexConverter.encode(this.bytes)); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/EncodedPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/EncodedPredicate.java index af598d8..ef35844 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/EncodedPredicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/EncodedPredicate.java @@ -1,72 +1,91 @@ package org.unicitylabs.sdk.predicate; +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.util.HexConverter; + import java.util.Arrays; import java.util.List; import java.util.Objects; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.util.HexConverter; /** - * Predicate structure before passing it to predicate engine. + * Generic predicate representation that stores engine, code, and parameters as encoded bytes. */ -public class EncodedPredicate implements SerializablePredicate { +public class EncodedPredicate implements Predicate { + public static final long CBOR_TAG = 39032; - private final PredicateEngineType engine; + private final PredicateEngine engine; private final byte[] code; private final byte[] parameters; - - EncodedPredicate(PredicateEngineType engine, byte[] code, byte[] parameters) { - Objects.requireNonNull(code, "Code must not be null"); - Objects.requireNonNull(parameters, "Parameters must not be null"); - + private EncodedPredicate(PredicateEngine engine, byte[] code, byte[] parameters) { this.engine = engine; - this.code = Arrays.copyOf(code, code.length); - this.parameters = Arrays.copyOf(parameters, parameters.length); + this.code = code; + this.parameters = parameters; + } + + @Override + public PredicateEngine getEngine() { + return this.engine; } /** - * Get predicate engine. + * Deserializes an encoded predicate from CBOR. * - * @return predicate engine + * @param bytes CBOR-encoded predicate bytes + * @return decoded encoded predicate */ - public PredicateEngineType getEngine() { - return this.engine; + public static EncodedPredicate fromCbor(byte[] bytes) { + CborDeserializer.CborTag tag = CborDeserializer.decodeTag(bytes); + if (tag.getTag() != EncodedPredicate.CBOR_TAG) { + throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); + } + List data = CborDeserializer.decodeArray(tag.getData(), 3); + PredicateEngine engine = PredicateEngine.fromId( + CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt()); + + return new EncodedPredicate( + engine, + CborDeserializer.decodeByteString(data.get(1)), + CborDeserializer.decodeByteString(data.get(2)) + ); } /** - * Encode predicate code. + * Creates an encoded predicate snapshot from any predicate implementation. * - * @return encoded code + * @param predicate source predicate + * @return encoded predicate containing engine, code, and parameters */ + public static EncodedPredicate fromPredicate(Predicate predicate) { + return new EncodedPredicate(predicate.getEngine(), predicate.encodeCode(), + predicate.encodeParameters()); + } + @Override - public byte[] encode() { + public byte[] encodeCode() { return Arrays.copyOf(this.code, this.code.length); } - /** - * Encode predicate parameters. - * - * @return encoded parameters - */ @Override public byte[] encodeParameters() { return Arrays.copyOf(this.parameters, this.parameters.length); } /** - * Create encoded predicate from CBOR bytes. + * Serializes this predicate into CBOR. * - * @param bytes CBOR bytes - * @return encoded predicate + * @return CBOR-encoded predicate bytes */ - public static EncodedPredicate fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); - - return new EncodedPredicate( - PredicateEngineType.values()[CborDeserializer.readUnsignedInteger(data.get(0)).asInt()], - CborDeserializer.readByteString(data.get(1)), - CborDeserializer.readByteString(data.get(2)) + public byte[] toCbor() { + return CborSerializer.encodeTag( + EncodedPredicate.CBOR_TAG, + CborSerializer.encodeArray( + CborSerializer.encodeUnsignedInteger(this.engine.getId()), + CborSerializer.encodeByteString(this.code), + CborSerializer.encodeByteString(this.parameters) + ) ); } @@ -75,9 +94,9 @@ public boolean equals(Object o) { if (!(o instanceof EncodedPredicate)) { return false; } - EncodedPredicate predicate = (EncodedPredicate) o; - return this.engine == predicate.engine && Objects.deepEquals(this.code, predicate.code) - && Objects.deepEquals(this.parameters, predicate.parameters); + EncodedPredicate that = (EncodedPredicate) o; + return this.engine == that.engine && Arrays.equals(this.code, that.code) && Arrays.equals( + this.parameters, that.parameters); } @Override @@ -87,7 +106,11 @@ public int hashCode() { @Override public String toString() { - return String.format("Predicate{engine=%s, code=%s, parameters=%s}", this.engine, - HexConverter.encode(this.code), HexConverter.encode(this.parameters)); + return String.format( + "EncodedPredicate{engine=%s, code=%s, parameters=%s}", + this.engine, + HexConverter.encode(this.code), + HexConverter.encode(this.parameters) + ); } -} \ No newline at end of file +} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java b/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java index 2a9b95d..1a6d8e0 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java @@ -1,45 +1,28 @@ package org.unicitylabs.sdk.predicate; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.transaction.TransferTransaction; - /** - * Predicate structure. + * Base contract for all predicate implementations. */ -public interface Predicate extends SerializablePredicate { - - /** - * Calculate predicate hash representation. - * - * @return predicate hash - */ - DataHash calculateHash(); +public interface Predicate { /** - * Get predicate as reference. + * Returns the predicate engine used by this predicate. * - * @return predicate reference + * @return the predicate engine */ - PredicateReference getReference(); + PredicateEngine getEngine(); /** - * Is given public key owner of current predicate. + * Encodes the predicate type/code portion. * - * @param publicKey public key of potential owner - * @return true if is owner + * @return encoded predicate code bytes */ - boolean isOwner(byte[] publicKey); + byte[] encodeCode(); /** - * Verify if predicate is valid for given token state. + * Encodes the predicate parameter payload. * - * @param token current token state - * @param transaction current transaction - * @param trustBase trust base to verify against. - * @return true if successful + * @return encoded predicate parameter bytes */ - boolean verify(Token token, TransferTransaction transaction, RootTrustBase trustBase); + byte[] encodeParameters(); } - diff --git a/src/main/java/org/unicitylabs/sdk/predicate/PredicateEngine.java b/src/main/java/org/unicitylabs/sdk/predicate/PredicateEngine.java index 54ee980..0954c24 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/PredicateEngine.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/PredicateEngine.java @@ -1,15 +1,40 @@ package org.unicitylabs.sdk.predicate; /** - * Predicate engine structure. + * Enumerates supported predicate engines and their numeric identifiers. */ -public interface PredicateEngine { +public enum PredicateEngine { + /** Engine for built-in predicate implementations. */ + BUILT_IN(1); + + private final int id; + + PredicateEngine(int id) { + this.id = id; + } + + /** + * Returns the numeric identifier of this predicate engine. + * + * @return predicate engine id + */ + public int getId() { + return this.id; + } /** - * Create predicate from serializable predicate. + * Resolves a predicate engine from its numeric identifier. * - * @param predicate serializable predicate. - * @return parsed predicate + * @param id predicate engine id + * @return matching predicate engine + * @throws IllegalArgumentException if the id is not mapped to a predicate engine */ - Predicate create(SerializablePredicate predicate); + public static PredicateEngine fromId(int id) { + for (PredicateEngine engine : PredicateEngine.values()) { + if (engine.id == id) { + return engine; + } + } + throw new IllegalArgumentException("Invalid predicate engine: " + id); + } } diff --git a/src/main/java/org/unicitylabs/sdk/predicate/PredicateEngineService.java b/src/main/java/org/unicitylabs/sdk/predicate/PredicateEngineService.java deleted file mode 100644 index 6856729..0000000 --- a/src/main/java/org/unicitylabs/sdk/predicate/PredicateEngineService.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.unicitylabs.sdk.predicate; - -import java.util.HashMap; -import org.unicitylabs.sdk.predicate.embedded.EmbeddedPredicateEngine; - -/** - * Predefined predicate engines service to create predicates. - */ -public class PredicateEngineService { - - private static final HashMap ENGINES = new HashMap<>() { - { - put(PredicateEngineType.EMBEDDED, new EmbeddedPredicateEngine()); - } - }; - - private PredicateEngineService() { - } - - /** - * Create predicate from serializable predicate. - * - * @param predicate serializable predicate - * @return parsed predicate - */ - public static Predicate createPredicate(SerializablePredicate predicate) { - PredicateEngine engine = PredicateEngineService.ENGINES.get(predicate.getEngine()); - if (engine == null) { - throw new IllegalArgumentException( - "Unsupported predicate engine type: " + predicate.getEngine()); - } - - return engine.create(predicate); - } - - -} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/PredicateEngineType.java b/src/main/java/org/unicitylabs/sdk/predicate/PredicateEngineType.java deleted file mode 100644 index 56bda67..0000000 --- a/src/main/java/org/unicitylabs/sdk/predicate/PredicateEngineType.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.unicitylabs.sdk.predicate; - -/** - * Predicate engine type. - */ -public enum PredicateEngineType { - /** - * Embedded predicate engine. - */ - EMBEDDED, -} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/PredicateReference.java b/src/main/java/org/unicitylabs/sdk/predicate/PredicateReference.java deleted file mode 100644 index 682b8f7..0000000 --- a/src/main/java/org/unicitylabs/sdk/predicate/PredicateReference.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.unicitylabs.sdk.predicate; - -import org.unicitylabs.sdk.address.Address; -import org.unicitylabs.sdk.hash.DataHash; - -/** - * Predicate reference interface. - */ -public interface PredicateReference { - - /** - * Get predicate reference as hash. - * - * @return reference hash - */ - DataHash getHash(); - - /** - * Get predicate reference as address. - * - * @return reference address - */ - Address toAddress(); -} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/SerializablePredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/SerializablePredicate.java deleted file mode 100644 index 7adc34f..0000000 --- a/src/main/java/org/unicitylabs/sdk/predicate/SerializablePredicate.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.unicitylabs.sdk.predicate; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - -/** - * Serializable predicate structure. - */ -@JsonSerialize(using = SerializablePredicateJson.Serializer.class) -@JsonDeserialize(using = SerializablePredicateJson.Deserializer.class) -public interface SerializablePredicate { - - /** - * Get predicate engine. - * - * @return predicate engine - */ - PredicateEngineType getEngine(); - - /** - * Get predicate code as bytes. - * - * @return predicate code bytes - */ - byte[] encode(); - - /** - * Get predicate parameters as bytes. - * - * @return parameters bytes - */ - byte[] encodeParameters(); -} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/SerializablePredicateJson.java b/src/main/java/org/unicitylabs/sdk/predicate/SerializablePredicateJson.java deleted file mode 100644 index 50b53db..0000000 --- a/src/main/java/org/unicitylabs/sdk/predicate/SerializablePredicateJson.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.unicitylabs.sdk.predicate; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.exc.MismatchedInputException; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import java.io.IOException; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; - -/** - * Predicate serializer and deserializer implementation. - */ -public class SerializablePredicateJson { - - private SerializablePredicateJson() { - } - - /** - * Predicate serializer. - */ - public static class Serializer extends StdSerializer { - - /** - * Create predicate serializer. - */ - public Serializer() { - super(SerializablePredicate.class); - } - - /** - * Serialize predicate. - * - * @param value predicate - * @param gen json generator - * @param serializers serializer provider - * @throws IOException on serialization failure - */ - @Override - public void serialize(SerializablePredicate value, JsonGenerator gen, - SerializerProvider serializers) - throws IOException { - gen.writeObject( - CborSerializer.encodeArray( - CborSerializer.encodeUnsignedInteger(value.getEngine().ordinal()), - CborSerializer.encodeByteString(value.encode()), - CborSerializer.encodeByteString(value.encodeParameters()) - ) - ); - } - } - - /** - * Predicate deserializer. - */ - public static class Deserializer extends StdDeserializer { - - /** - * Create predicate deserializer. - */ - public Deserializer() { - super(SerializablePredicate.class); - } - - /** - * Deserialize predicate. - * - * @param p Parser used for reading JSON content - * @param ctx Context that can be used to access information about this deserialization - * activity. - * @return predicate - * @throws IOException on deserialization failure - */ - @Override - public SerializablePredicate deserialize(JsonParser p, DeserializationContext ctx) - throws IOException { - if (p.getCurrentToken() != JsonToken.VALUE_STRING) { - throw MismatchedInputException.from( - p, - EncodedPredicate.class, - "Expected string value" - ); - } - - try { - return EncodedPredicate.fromCbor(p.readValueAs(byte[].class)); - } catch (Exception e) { - throw MismatchedInputException.from(p, EncodedPredicate.class, "Expected bytes"); - } - } - } -} - diff --git a/src/main/java/org/unicitylabs/sdk/predicate/UnlockScript.java b/src/main/java/org/unicitylabs/sdk/predicate/UnlockScript.java new file mode 100644 index 0000000..bbd95ae --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/predicate/UnlockScript.java @@ -0,0 +1,13 @@ +package org.unicitylabs.sdk.predicate; + +/** + * Contract for predicate unlock script payloads. + */ +public interface UnlockScript { + /** + * Encodes this unlock script into bytes. + * + * @return encoded unlock script + */ + byte[] encode(); +} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicate.java new file mode 100644 index 0000000..e69ed5f --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicate.java @@ -0,0 +1,36 @@ +package org.unicitylabs.sdk.predicate.builtin; + +import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.PredicateEngine; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; + +/** + * Base contract for predicates represented by a built-in predicate type. + */ +public interface BuiltInPredicate extends Predicate { + + /** + * Returns the built-in type identifier for this predicate. + * + * @return the built-in predicate type + */ + BuiltInPredicateType getType(); + + /** + * Returns the predicate engine used by all built-in predicates. + * + * @return {@link PredicateEngine#BUILT_IN} + */ + default PredicateEngine getEngine() { + return PredicateEngine.BUILT_IN; + } + + /** + * Encodes this predicate type id as an unsigned CBOR integer. + * + * @return the encoded predicate type id + */ + default byte[] encodeCode() { + return CborSerializer.encodeUnsignedInteger(this.getType().getId()); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java new file mode 100644 index 0000000..ffb0222 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BuiltInPredicateType.java @@ -0,0 +1,44 @@ +package org.unicitylabs.sdk.predicate.builtin; + +/** + * Enumerates supported built-in predicate types and their numeric identifiers. + */ +public enum BuiltInPredicateType { + /** Predicate that locks state to a public key. */ + SIGNATURE(0x01), + /** Predicate that marks state as unspendable (burned). */ + BURN(0x02), + /** Predicate that references a Unicity identifier. */ + UNICITY_ID(0x100); + + private final int id; + + BuiltInPredicateType(int id) { + this.id = id; + } + + /** + * Returns the numeric identifier of this predicate type. + * + * @return predicate type id + */ + public int getId() { + return this.id; + } + + /** + * Resolves a predicate type from its numeric identifier. + * + * @param id the predicate type id + * @return the matching {@link BuiltInPredicateType} + * @throws IllegalArgumentException if the id is not mapped to a built-in type + */ + public static BuiltInPredicateType fromId(int id) { + for (BuiltInPredicateType type : BuiltInPredicateType.values()) { + if (type.id == id) { + return type; + } + } + throw new IllegalArgumentException("Invalid predicate type: " + id); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java new file mode 100644 index 0000000..d0a7263 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java @@ -0,0 +1,82 @@ +package org.unicitylabs.sdk.predicate.builtin; + +import org.unicitylabs.sdk.predicate.EncodedPredicate; +import org.unicitylabs.sdk.predicate.PredicateEngine; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; + +import java.util.Arrays; +import java.util.Objects; + +/** + * Built-in predicate representing a burn operation. + */ +public class BurnPredicate implements BuiltInPredicate { + private final byte[] reason; + + private BurnPredicate(byte[] reason) { + this.reason = Arrays.copyOf(reason, reason.length); + } + + /** + * Returns the built-in predicate type. + * + * @return {@link BuiltInPredicateType#BURN} + */ + public BuiltInPredicateType getType() { + return BuiltInPredicateType.BURN; + } + + /** + * Returns the burn reason bytes. + * + * @return a defensive copy of the burn reason + */ + public byte[] getReason() { + return Arrays.copyOf(this.reason, this.reason.length); + } + + /** + * Creates a burn predicate from the provided reason bytes. + * + * @param reason burn reason bytes + * @return created burn predicate + * @throws NullPointerException if {@code reason} is {@code null} + */ + public static BurnPredicate create(byte[] reason) { + Objects.requireNonNull(reason, "Reason cannot be null"); + + return new BurnPredicate(reason); + } + + /** + * Converts a generic predicate into a {@link BurnPredicate}. + * + * @param predicate predicate to convert + * @return converted burn predicate + * @throws IllegalArgumentException if the predicate engine is not built-in or predicate type is not burn + */ + public static BurnPredicate fromPredicate(EncodedPredicate predicate) { + PredicateEngine engine = predicate.getEngine(); + if (engine != PredicateEngine.BUILT_IN) { + throw new IllegalArgumentException("Predicate engine must be BUILT_IN."); + } + + BuiltInPredicateType type = BuiltInPredicateType.fromId( + CborDeserializer.decodeUnsignedInteger(predicate.encodeCode()).asInt()); + if (type != BuiltInPredicateType.BURN) { + throw new IllegalArgumentException("Predicate type must be BURN."); + } + + return new BurnPredicate(predicate.encodeParameters()); + } + + /** + * Encodes burn predicate parameters. + * + * @return burn reason bytes + */ + @Override + public byte[] encodeParameters() { + return this.getReason(); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/DefaultBuiltInPredicateVerifier.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/DefaultBuiltInPredicateVerifier.java new file mode 100644 index 0000000..cb08129 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/DefaultBuiltInPredicateVerifier.java @@ -0,0 +1,77 @@ +package org.unicitylabs.sdk.predicate.builtin; + +import org.unicitylabs.sdk.crypto.hash.DataHash; +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.SignaturePredicateVerifier; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifier; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Default {@link PredicateVerifier} implementation for built-in predicates. + */ +public class DefaultBuiltInPredicateVerifier implements PredicateVerifier { + + private final Map verifiers; + + + /** + * Creates a verifier registry from built-in predicate verifiers. + * + * @param verifiers verifiers to register, one per predicate type + * @throws IllegalArgumentException if multiple verifiers are provided for the same type + */ + public DefaultBuiltInPredicateVerifier( + List verifiers) { + Map result = new HashMap<>(); + for (BuiltInPredicateVerifier verifier : verifiers) { + if (result.containsKey(verifier.getType())) { + throw new IllegalArgumentException("Duplicate verifier for type " + verifier.getType()); + } + + result.put(verifier.getType(), verifier); + } + + this.verifiers = result; + } + + @Override + public PredicateEngine getPredicateEngine() { + return PredicateEngine.BUILT_IN; + } + + /** + * Creates the default built-in predicate verifier set. + * + * @return default built-in predicate verifier + */ + public static DefaultBuiltInPredicateVerifier create() { + return new DefaultBuiltInPredicateVerifier( + List.of( + new SignaturePredicateVerifier() + ) + ); + } + + @Override + public VerificationResult verify(EncodedPredicate predicate, + DataHash sourceStateHash, + DataHash transactionHash, byte[] unlockScript) { + BuiltInPredicateType type = BuiltInPredicateType.fromId( + CborDeserializer.decodeUnsignedInteger(predicate.encodeCode()).asInt()); + + BuiltInPredicateVerifier verifier = this.verifiers.get(type); + if (verifier == null) { + throw new IllegalArgumentException("No verifier registered for predicate type: " + type); + } + + return verifier.verify(predicate, sourceStateHash, transactionHash, unlockScript); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/SignaturePredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/SignaturePredicate.java new file mode 100644 index 0000000..e53e6e5 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/SignaturePredicate.java @@ -0,0 +1,101 @@ +package org.unicitylabs.sdk.predicate.builtin; + +import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +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; + +import java.util.Arrays; +import java.util.Objects; + +/** + * Built-in predicate that locks an output to a secp256k1 public key. + */ +public class SignaturePredicate implements BuiltInPredicate { + + private final byte[] publicKey; + + private SignaturePredicate(byte[] publicKey) { + this.publicKey = publicKey; + } + + /** + * Get public key bytes. + * + * @return public key bytes + */ + public byte[] getPublicKey() { + return Arrays.copyOf(this.publicKey, this.publicKey.length); + } + + /** + * Get built-in predicate type. + * + * @return predicate type + */ + public BuiltInPredicateType getType() { + return BuiltInPredicateType.SIGNATURE; + } + + /** + * Create predicate from public key bytes. + * + * @param publicKey public key bytes + * + * @return pay-to-public-key predicate + */ + public static SignaturePredicate create(byte[] publicKey) { + return new SignaturePredicate(Arrays.copyOf(publicKey, publicKey.length)); + } + + /** + * Parse pay-to-public-key predicate from generic predicate. + * + * @param predicate generic predicate + * + * @return pay-to-public-key predicate + */ + public static SignaturePredicate fromPredicate(EncodedPredicate predicate) { + PredicateEngine engine = predicate.getEngine(); + if (engine != PredicateEngine.BUILT_IN) { + throw new IllegalArgumentException("Predicate engine must be BUILT_IN."); + } + + BuiltInPredicateType type = BuiltInPredicateType.fromId( + CborDeserializer.decodeUnsignedInteger(predicate.encodeCode()).asInt()); + if (type != BuiltInPredicateType.SIGNATURE) { + throw new IllegalArgumentException("Predicate type must be SIGNATURE."); + } + + return new SignaturePredicate(predicate.encodeParameters()); + } + + /** + * Create predicate from signing service public key. + * + * @param signingService signing service + * + * @return pay-to-public-key predicate + */ + public static SignaturePredicate fromSigningService(SigningService signingService) { + Objects.requireNonNull(signingService, "Signing service cannot be null"); + return new SignaturePredicate(signingService.getPublicKey()); + } + + /** + * Encode predicate parameters. + * + * @return encoded parameter bytes + */ + @Override + public byte[] encodeParameters() { + return this.getPublicKey(); + } + + @Override + public String toString() { + return String.format("SignaturePredicate{publicKey=%s}", HexConverter.encode(this.publicKey)); + } + +} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/SignaturePredicateUnlockScript.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/SignaturePredicateUnlockScript.java new file mode 100644 index 0000000..01fb55b --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/SignaturePredicateUnlockScript.java @@ -0,0 +1,69 @@ +package org.unicitylabs.sdk.predicate.builtin; + +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.crypto.secp256k1.Signature; +import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.predicate.UnlockScript; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.transaction.Transaction; + +/** + * Unlock script for {@link SignaturePredicate} containing a transaction signature. + */ +public class SignaturePredicateUnlockScript implements UnlockScript { + + private final Signature signature; + + private SignaturePredicateUnlockScript(Signature signature) { + this.signature = signature; + } + + /** + * Returns the unlock signature. + * + * @return signature used to unlock the predicate + */ + public Signature getSignature() { + return this.signature; + } + + /** + * Creates an unlock script by signing the source-state and transaction-hash payload. + * + * @param transaction transaction being authorized + * @param signingService signing service used to produce the signature + * @return created unlock script + */ + public static SignaturePredicateUnlockScript create( + Transaction transaction, + SigningService signingService + ) { + DataHash hash = new DataHasher(HashAlgorithm.SHA256) + .update( + CborSerializer.encodeArray( + CborSerializer.encodeByteString(transaction.getSourceStateHash().getData()), + CborSerializer.encodeByteString(transaction.calculateTransactionHash().getData()) + ) + ) + .digest(); + + return new SignaturePredicateUnlockScript(signingService.sign(hash)); + } + + /** + * Decodes an unlock script from encoded signature bytes. + * + * @param bytes encoded signature bytes + * @return decoded unlock script + */ + public static SignaturePredicateUnlockScript decode(byte[] bytes) { + return new SignaturePredicateUnlockScript(Signature.decode(bytes)); + } + + @Override + public byte[] encode() { + return this.signature.encode(); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/BuiltInPredicateVerifier.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/BuiltInPredicateVerifier.java new file mode 100644 index 0000000..f618275 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/BuiltInPredicateVerifier.java @@ -0,0 +1,32 @@ +package org.unicitylabs.sdk.predicate.builtin.verification; + +import org.unicitylabs.sdk.crypto.hash.DataHash; +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; + +/** + * Verifier contract for a specific built-in predicate type. + */ +public interface BuiltInPredicateVerifier { + + /** + * Returns the built-in predicate type handled by this verifier. + * + * @return supported built-in predicate type + */ + BuiltInPredicateType getType(); + + /** + * Verifies that the provided unlock script satisfies the predicate in the current context. + * + * @param predicate the predicate to verify + * @param sourceStateHash hash of the source state + * @param transactionHash hash of the transaction being validated + * @param unlockScript unlock script bytes provided for the predicate + * @return verification result with status and optional diagnostics + */ + VerificationResult verify(EncodedPredicate predicate, DataHash sourceStateHash, + DataHash transactionHash, byte[] unlockScript); +} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/SignaturePredicateVerifier.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/SignaturePredicateVerifier.java new file mode 100644 index 0000000..f3c0c54 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/SignaturePredicateVerifier.java @@ -0,0 +1,58 @@ +package org.unicitylabs.sdk.predicate.builtin.verification; + +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.crypto.secp256k1.Signature; +import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.predicate.EncodedPredicate; +import org.unicitylabs.sdk.predicate.builtin.BuiltInPredicateType; +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 SignaturePredicate} unlock scripts using secp256k1 signatures. + */ +public class SignaturePredicateVerifier implements BuiltInPredicateVerifier { + + /** + * Creates a verifier instance for pay-to-public-key predicates. + */ + public SignaturePredicateVerifier() { + } + + @Override + public BuiltInPredicateType getType() { + return BuiltInPredicateType.SIGNATURE; + } + + + @Override + public VerificationResult verify(EncodedPredicate encodedPredicate, + DataHash sourceStateHash, + DataHash transactionHash, byte[] unlockScript) { + SignaturePredicate predicate = SignaturePredicate.fromPredicate(encodedPredicate); + + boolean result = SigningService.verifyWithPublicKey( + new DataHasher(HashAlgorithm.SHA256) + .update( + CborSerializer.encodeArray( + CborSerializer.encodeByteString(sourceStateHash.getData()), + CborSerializer.encodeByteString(transactionHash.getData()) + ) + ) + .digest(), + Signature.decode(unlockScript).getBytes(), + predicate.getPublicKey() + ); + + if (!result) { + return new VerificationResult<>("SignaturePredicateVerifier", VerificationStatus.FAIL, + "Signature verification failed."); + } + + return new VerificationResult<>("SignaturePredicateVerifier", VerificationStatus.OK); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/embedded/BurnPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/embedded/BurnPredicate.java deleted file mode 100644 index ad3a1f9..0000000 --- a/src/main/java/org/unicitylabs/sdk/predicate/embedded/BurnPredicate.java +++ /dev/null @@ -1,138 +0,0 @@ -package org.unicitylabs.sdk.predicate.embedded; - -import java.util.List; -import java.util.Objects; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.predicate.Predicate; -import org.unicitylabs.sdk.predicate.PredicateEngineType; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.token.TokenType; -import org.unicitylabs.sdk.transaction.TransferTransaction; - -/** - * Burn predicate implementation. - */ -public class BurnPredicate implements Predicate { - - private final TokenId tokenId; - private final TokenType tokenType; - private final DataHash burnReason; - - /** - * Create burn predicate. - * - * @param tokenId token id - * @param tokenType token type - * @param reason burn reason as coin aggregation tree hash - */ - public BurnPredicate(TokenId tokenId, TokenType tokenType, DataHash reason) { - Objects.requireNonNull(tokenId, "Token id cannot be null"); - Objects.requireNonNull(tokenType, "Token type cannot be null"); - Objects.requireNonNull(reason, "Burn reason cannot be null"); - - this.tokenId = tokenId; - this.tokenType = tokenType; - this.burnReason = reason; - } - - /** - * Get token id. - * - * @return token id - */ - public TokenId getTokenId() { - return this.tokenId; - } - - /** - * Get token type. - * - * @return token type - */ - public TokenType getTokenType() { - return this.tokenType; - } - - /** - * Get burn reason. - * - * @return burn reason - */ - public DataHash getReason() { - return this.burnReason; - } - - @Override - public boolean isOwner(byte[] publicKey) { - return false; - } - - @Override - public boolean verify(Token token, TransferTransaction transaction, RootTrustBase trustBase) { - return false; - } - - @Override - public DataHash calculateHash() { - return new DataHasher(HashAlgorithm.SHA256) - .update( - CborSerializer.encodeArray( - this.getReference().getHash().toCbor(), - this.tokenId.toCbor() - ) - ) - .digest(); - } - - /** - * Create burn predicate from CBOR bytes. - * - * @param bytes CBOR bytes - * @return burn predicate - */ - public static BurnPredicate fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); - - return new BurnPredicate( - TokenId.fromCbor(data.get(0)), - TokenType.fromCbor(data.get(1)), - DataHash.fromCbor(data.get(2)) - ); - } - - @Override - public BurnPredicateReference getReference() { - return BurnPredicateReference.create(this.tokenType, this.burnReason); - } - - @Override - public PredicateEngineType getEngine() { - return PredicateEngineType.EMBEDDED; - } - - @Override - public byte[] encode() { - return EmbeddedPredicateType.BURN.getBytes(); - } - - @Override - public byte[] encodeParameters() { - return CborSerializer.encodeArray( - this.tokenId.toCbor(), - this.tokenType.toCbor(), - this.burnReason.toCbor() - ); - } - - @Override - public String toString() { - return String.format("BurnPredicate{tokenId=%s, tokenType=%s, burnReason=%s}", this.tokenId, - this.tokenType, this.burnReason); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/embedded/BurnPredicateReference.java b/src/main/java/org/unicitylabs/sdk/predicate/embedded/BurnPredicateReference.java deleted file mode 100644 index 675e876..0000000 --- a/src/main/java/org/unicitylabs/sdk/predicate/embedded/BurnPredicateReference.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.unicitylabs.sdk.predicate.embedded; - -import java.util.Objects; -import org.unicitylabs.sdk.address.DirectAddress; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.predicate.PredicateReference; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.token.TokenType; - -/** - * Burn predicate reference. - */ -public class BurnPredicateReference implements PredicateReference { - - private final DataHash hash; - - private BurnPredicateReference(DataHash hash) { - this.hash = hash; - } - - /** - * Get burn predicate reference hash. - * - * @return reference hash - */ - public DataHash getHash() { - return this.hash; - } - - /** - * Create burn predicate reference. - * - * @param tokenType token type - * @param burnReason burn reason - * @return predicate reference - */ - public static BurnPredicateReference create(TokenType tokenType, DataHash burnReason) { - Objects.requireNonNull(tokenType, "Token type cannot be null"); - Objects.requireNonNull(burnReason, "Burn reason cannot be null"); - - return new BurnPredicateReference( - new DataHasher(HashAlgorithm.SHA256) - .update( - CborSerializer.encodeArray( - CborSerializer.encodeByteString(EmbeddedPredicateType.BURN.getBytes()), - CborSerializer.encodeByteString(tokenType.toCbor()), - CborSerializer.encodeByteString(burnReason.getImprint()) - ) - ) - .digest() - ); - } - - /** - * Convert predicate reference to address. - * - * @return predicate address - */ - public DirectAddress toAddress() { - return DirectAddress.create(this.hash); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/embedded/DefaultPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/embedded/DefaultPredicate.java deleted file mode 100644 index 8d732e0..0000000 --- a/src/main/java/org/unicitylabs/sdk/predicate/embedded/DefaultPredicate.java +++ /dev/null @@ -1,248 +0,0 @@ -package org.unicitylabs.sdk.predicate.embedded; - -import java.util.Arrays; -import java.util.Objects; -import org.unicitylabs.sdk.api.Authenticator; -import org.unicitylabs.sdk.api.RequestId; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.predicate.Predicate; -import org.unicitylabs.sdk.predicate.PredicateEngineType; -import org.unicitylabs.sdk.predicate.PredicateReference; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.token.TokenType; -import org.unicitylabs.sdk.transaction.InclusionProofVerificationStatus; -import org.unicitylabs.sdk.transaction.TransferTransaction; -import org.unicitylabs.sdk.util.HexConverter; - -/** - * Base class for unmasked and masked predicates. - */ -public abstract class DefaultPredicate implements Predicate { - - private final EmbeddedPredicateType type; - private final TokenId tokenId; - private final TokenType tokenType; - private final byte[] publicKey; - private final String signingAlgorithm; - private final HashAlgorithm hashAlgorithm; - private final byte[] nonce; - - /** - * Create default functionality for masked and unmasked predicate. - * - * @param type predicate type - * @param tokenId token id - * @param tokenType token type - * @param publicKey public key - * @param signingAlgorithm signing algorithm - * @param hashAlgorithm hash algorithm - * @param nonce predicate nonce - */ - protected DefaultPredicate( - EmbeddedPredicateType type, - TokenId tokenId, - TokenType tokenType, - byte[] publicKey, - String signingAlgorithm, - HashAlgorithm hashAlgorithm, - byte[] nonce) { - Objects.requireNonNull(type, "Predicate type cannot be null"); - Objects.requireNonNull(tokenId, "TokenId cannot be null"); - Objects.requireNonNull(tokenType, "TokenType cannot be null"); - Objects.requireNonNull(publicKey, "Public key cannot be null"); - Objects.requireNonNull(signingAlgorithm, "Signing algorithm cannot be null"); - Objects.requireNonNull(hashAlgorithm, "Hash algorithm cannot be null"); - Objects.requireNonNull(nonce, "Nonce cannot be null"); - - this.type = type; - this.tokenId = tokenId; - this.tokenType = tokenType; - this.publicKey = Arrays.copyOf(publicKey, publicKey.length); - this.signingAlgorithm = signingAlgorithm; - this.hashAlgorithm = hashAlgorithm; - this.nonce = Arrays.copyOf(nonce, nonce.length); - } - - /** - * Get predicate type. - * - * @return predicate type - */ - public EmbeddedPredicateType getType() { - return this.type; - } - - /** - * Get token id. - * - * @return token id - */ - public TokenId getTokenId() { - return this.tokenId; - } - - /** - * Get token type. - * - * @return token type - */ - public TokenType getTokenType() { - return this.tokenType; - } - - /** - * Get public key associated with predicate. - * - * @return public key - */ - public byte[] getPublicKey() { - return Arrays.copyOf(this.publicKey, this.publicKey.length); - } - - /** - * Get signing algorithm used with predicate. - * - * @return signing algorithm - */ - public String getSigningAlgorithm() { - return this.signingAlgorithm; - } - - /** - * Get hash algorithm used with predicate. - * - * @return hash algorithm - */ - public HashAlgorithm getHashAlgorithm() { - return this.hashAlgorithm; - } - - /** - * Get predicate nonce. - * - * @return predicate nonce - */ - public byte[] getNonce() { - return Arrays.copyOf(this.nonce, this.nonce.length); - } - - @Override - public DataHash calculateHash() { - return new DataHasher(HashAlgorithm.SHA256) - .update( - CborSerializer.encodeArray( - this.getReference().getHash().toCbor(), - this.tokenId.toCbor(), - CborSerializer.encodeByteString(this.getNonce()) - ) - ) - .digest(); - } - - /** - * Get predicate reference. - * - * @return predicate reference - */ - public abstract PredicateReference getReference(); - - @Override - public boolean isOwner(byte[] publicKey) { - return Arrays.equals(this.publicKey, publicKey); - } - - @Override - public boolean verify(Token token, TransferTransaction transaction, RootTrustBase trustBase) { - if (!this.tokenId.equals(token.getId()) || !this.tokenType.equals(token.getType())) { - return false; - } - - Authenticator authenticator = transaction.getInclusionProof().getAuthenticator().orElse(null); - - if (authenticator == null) { - return false; - } - - if (!Arrays.equals(authenticator.getPublicKey(), this.publicKey)) { - return false; - } - - DataHash transactionHash = transaction.getData().calculateHash(); - if (!authenticator.verify(transactionHash)) { - return false; - } - - RequestId requestId = RequestId.create(this.publicKey, transaction.getData().getSourceState()); - return transaction.getInclusionProof().verify( - requestId, - trustBase - ) == InclusionProofVerificationStatus.OK; - } - - @Override - public PredicateEngineType getEngine() { - return PredicateEngineType.EMBEDDED; - } - - @Override - public byte[] encode() { - return this.type.getBytes(); - } - - @Override - public byte[] encodeParameters() { - return CborSerializer.encodeArray( - this.tokenId.toCbor(), - this.tokenType.toCbor(), - CborSerializer.encodeByteString(this.publicKey), - CborSerializer.encodeTextString(this.signingAlgorithm), - CborSerializer.encodeUnsignedInteger(this.hashAlgorithm.getValue()), - CborSerializer.encodeByteString(this.nonce) - ); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof DefaultPredicate)) { - return false; - } - DefaultPredicate that = (DefaultPredicate) o; - return this.type == that.type && Objects.equals(this.tokenId, that.tokenId) - && Objects.equals(this.tokenType, that.tokenType) - && Objects.deepEquals(this.publicKey, that.publicKey) - && Objects.equals(this.signingAlgorithm, that.signingAlgorithm) - && this.hashAlgorithm == that.hashAlgorithm - && Arrays.equals(this.nonce, that.nonce); - } - - @Override - public int hashCode() { - return Objects.hash(this.type, this.tokenId, this.tokenType, Arrays.hashCode(this.publicKey), - this.signingAlgorithm, this.hashAlgorithm, Arrays.hashCode(nonce)); - } - - @Override - public String toString() { - return String.format( - "DefaultPredicate{" - + "type=%s, " - + "tokenId=%s, " - + "tokenType=%s, " - + "publicKey=%s, " - + "algorithm=%s, " - + "hashAlgorithm=%s, " - + "nonce=%s}", - this.type, - this.tokenId, - this.tokenType, - HexConverter.encode(this.publicKey), - this.signingAlgorithm, - this.hashAlgorithm, - HexConverter.encode(this.nonce)); - } -} \ No newline at end of file diff --git a/src/main/java/org/unicitylabs/sdk/predicate/embedded/EmbeddedPredicateEngine.java b/src/main/java/org/unicitylabs/sdk/predicate/embedded/EmbeddedPredicateEngine.java deleted file mode 100644 index 70322fe..0000000 --- a/src/main/java/org/unicitylabs/sdk/predicate/embedded/EmbeddedPredicateEngine.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.unicitylabs.sdk.predicate.embedded; - -import org.unicitylabs.sdk.predicate.Predicate; -import org.unicitylabs.sdk.predicate.PredicateEngine; -import org.unicitylabs.sdk.predicate.SerializablePredicate; - -/** - * Embedded predicate engine implementation. - */ -public class EmbeddedPredicateEngine implements PredicateEngine { - - /** - * Create embedded predicate engine. - */ - public EmbeddedPredicateEngine() {} - - /** - * Create predicate from embedded predicate engine. - * - * @param predicate serializable predicate. - * @return predicate - */ - public Predicate create(SerializablePredicate predicate) { - EmbeddedPredicateType type = EmbeddedPredicateType.fromBytes(predicate.encode()); - switch (type) { - case MASKED: - if (predicate instanceof MaskedPredicate) { - return (MaskedPredicate) predicate; - } - - return MaskedPredicate.fromCbor(predicate.encodeParameters()); - case UNMASKED: - if (predicate instanceof UnmaskedPredicate) { - return (UnmaskedPredicate) predicate; - } - - return UnmaskedPredicate.fromCbor(predicate.encodeParameters()); - case BURN: - if (predicate instanceof BurnPredicate) { - return (BurnPredicate) predicate; - } - - return BurnPredicate.fromCbor(predicate.encodeParameters()); - default: - throw new IllegalArgumentException("Unknown predicate type: " + type); - } - } -} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/embedded/EmbeddedPredicateType.java b/src/main/java/org/unicitylabs/sdk/predicate/embedded/EmbeddedPredicateType.java deleted file mode 100644 index 45de559..0000000 --- a/src/main/java/org/unicitylabs/sdk/predicate/embedded/EmbeddedPredicateType.java +++ /dev/null @@ -1,54 +0,0 @@ - -package org.unicitylabs.sdk.predicate.embedded; - -import java.util.Arrays; - -/** - * Embedded predicate types. - */ -public enum EmbeddedPredicateType { - /** - * Unmasked predicate type. - */ - UNMASKED(new byte[]{0x0}), - /** - * Masked predicate type. - */ - MASKED(new byte[]{0x1}), - /** - * Burn predicate type. - */ - BURN(new byte[]{0x2}); - - private final byte[] bytes; - - EmbeddedPredicateType(byte[] bytes) { - this.bytes = bytes; - } - - /** - * Get embedded predicate encoded code bytes. - * - * @return encoded code bytes - */ - public byte[] getBytes() { - return Arrays.copyOf(this.bytes, this.bytes.length); - } - - /** - * Create embedded predicate type from bytes. - * - * @param bytes predicate type bytes - * @return predicate type - */ - public static EmbeddedPredicateType fromBytes(byte[] bytes) { - for (EmbeddedPredicateType type : EmbeddedPredicateType.values()) { - if (Arrays.equals(bytes, type.getBytes())) { - return type; - } - } - - throw new RuntimeException("Invalid embedded predicate type"); - } - -} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/embedded/MaskedPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/embedded/MaskedPredicate.java deleted file mode 100644 index 3e62247..0000000 --- a/src/main/java/org/unicitylabs/sdk/predicate/embedded/MaskedPredicate.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.unicitylabs.sdk.predicate.embedded; - -import java.util.List; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.token.TokenType; - -/** - * Masked predicate. - */ -public class MaskedPredicate extends DefaultPredicate { - - /** - * Create masked predicate. - * - * @param tokenId token id - * @param tokenType token type - * @param publicKey predicate public key - * @param signingAlgorithm signing algorithm - * @param hashAlgorithm hash algorithm - * @param nonce predicate nonce - */ - public MaskedPredicate( - TokenId tokenId, - TokenType tokenType, - byte[] publicKey, - String signingAlgorithm, - HashAlgorithm hashAlgorithm, - byte[] nonce) { - super( - EmbeddedPredicateType.MASKED, - tokenId, - tokenType, - publicKey, - signingAlgorithm, - hashAlgorithm, - nonce - ); - } - - /** - * Create masked predicate from signing service. - * - * @param tokenId token id - * @param tokenType token type - * @param signingService signing service - * @param hashAlgorithm hash algorithm - * @param nonce predicate nonce - * @return predicate - */ - public static MaskedPredicate create( - TokenId tokenId, - TokenType tokenType, - SigningService signingService, - HashAlgorithm hashAlgorithm, - byte[] nonce) { - return new MaskedPredicate(tokenId, tokenType, signingService.getPublicKey(), - signingService.getAlgorithm(), hashAlgorithm, nonce); - } - - /** - * Create masked predicate from CBOR bytes. - * - * @param bytes CBOR bytes. - * @return predicate - */ - public static MaskedPredicate fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); - - return new MaskedPredicate( - TokenId.fromCbor(data.get(0)), - TokenType.fromCbor(data.get(1)), - CborDeserializer.readByteString(data.get(2)), - CborDeserializer.readTextString(data.get(3)), - HashAlgorithm.fromValue(CborDeserializer.readUnsignedInteger(data.get(4)).asInt()), - CborDeserializer.readByteString(data.get(5)) - ); - } - - @Override - public MaskedPredicateReference getReference() { - return MaskedPredicateReference.create( - this.getTokenType(), - this.getSigningAlgorithm(), - this.getPublicKey(), - this.getHashAlgorithm(), - this.getNonce() - ); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/embedded/MaskedPredicateReference.java b/src/main/java/org/unicitylabs/sdk/predicate/embedded/MaskedPredicateReference.java deleted file mode 100644 index b2ad5b1..0000000 --- a/src/main/java/org/unicitylabs/sdk/predicate/embedded/MaskedPredicateReference.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.unicitylabs.sdk.predicate.embedded; - -import java.util.Objects; -import org.unicitylabs.sdk.address.DirectAddress; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.predicate.PredicateReference; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.TokenType; - -/** - * Masked predicate reference. - */ -public class MaskedPredicateReference implements PredicateReference { - - private final DataHash hash; - - private MaskedPredicateReference(DataHash hash) { - this.hash = hash; - } - - /** - * Get predicate hash. - * - * @return predicate hash - */ - public DataHash getHash() { - return this.hash; - } - - /** - * Create masked predicate reference. - * - * @param tokenType token type - * @param signingAlgorithm signing algorithm - * @param publicKey predicate public key - * @param hashAlgorithm hash algorithm - * @param nonce predicate nonce - * @return predicate reference - */ - public static MaskedPredicateReference create( - TokenType tokenType, - String signingAlgorithm, - byte[] publicKey, - HashAlgorithm hashAlgorithm, - byte[] nonce - ) { - Objects.requireNonNull(tokenType, "Token type cannot be null"); - Objects.requireNonNull(signingAlgorithm, "Signing algorithm cannot be null"); - Objects.requireNonNull(publicKey, "Public key cannot be null"); - Objects.requireNonNull(hashAlgorithm, "Hash algorithm cannot be null"); - Objects.requireNonNull(nonce, "Nonce cannot be null"); - - return new MaskedPredicateReference( - new DataHasher(HashAlgorithm.SHA256) - .update( - CborSerializer.encodeArray( - CborSerializer.encodeByteString(EmbeddedPredicateType.MASKED.getBytes()), - CborSerializer.encodeByteString(tokenType.toCbor()), - CborSerializer.encodeTextString(signingAlgorithm), - CborSerializer.encodeUnsignedInteger(hashAlgorithm.getValue()), - CborSerializer.encodeByteString(publicKey), - CborSerializer.encodeByteString(nonce) - ) - ) - .digest() - ); - } - - /** - * Create predicate reference from signing service. - * - * @param tokenType token type - * @param signingService signing service - * @param hashAlgorithm hash algorithm - * @param nonce predicate nonce - * @return predicate reference - */ - public static MaskedPredicateReference create(TokenType tokenType, SigningService signingService, - HashAlgorithm hashAlgorithm, byte[] nonce) { - return MaskedPredicateReference.create( - tokenType, - signingService.getAlgorithm(), - signingService.getPublicKey(), - hashAlgorithm, - nonce - ); - } - - /** - * Convert predicate reference to address. - * - * @return predicate address - */ - public DirectAddress toAddress() { - return DirectAddress.create(this.hash); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/embedded/UnmaskedPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/embedded/UnmaskedPredicate.java deleted file mode 100644 index c019cf0..0000000 --- a/src/main/java/org/unicitylabs/sdk/predicate/embedded/UnmaskedPredicate.java +++ /dev/null @@ -1,123 +0,0 @@ - -package org.unicitylabs.sdk.predicate.embedded; - -import java.util.List; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.signing.Signature; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.token.TokenType; -import org.unicitylabs.sdk.transaction.TransferTransaction; - -/** - * Unmasked predicate. - */ -public class UnmaskedPredicate extends DefaultPredicate { - - UnmaskedPredicate( - TokenId tokenId, - TokenType tokenType, - byte[] publicKey, - String signingAlgorithm, - HashAlgorithm hashAlgorithm, - byte[] nonce - ) { - super(EmbeddedPredicateType.UNMASKED, tokenId, tokenType, publicKey, signingAlgorithm, - hashAlgorithm, nonce); - } - - /** - * Create unmasked predicate. - * - * @param tokenId token id - * @param tokenType token type - * @param signingService signing service - * @param hashAlgorithm hash algorithm - * @param salt received transaction salt - * @return unmasked predicate - */ - public static UnmaskedPredicate create( - TokenId tokenId, - TokenType tokenType, - SigningService signingService, - HashAlgorithm hashAlgorithm, - byte[] salt - ) { - Signature nonce = signingService.sign( - new DataHasher(HashAlgorithm.SHA256).update(salt).digest()); - - return new UnmaskedPredicate( - tokenId, - tokenType, - signingService.getPublicKey(), - signingService.getAlgorithm(), - hashAlgorithm, - nonce.getBytes()); - } - - /** - * Verify token state for current transaction. - * - * @param token current token state - * @param transaction current transaction - * @param trustBase trust base to verify against. - * @return true if successful - */ - @Override - public boolean verify( - Token token, - TransferTransaction transaction, - RootTrustBase trustBase - ) { - List transactions = token.getTransactions(); - - return super.verify(token, transaction, trustBase) && SigningService.verifyWithPublicKey( - new DataHasher(HashAlgorithm.SHA256) - .update( - transactions.isEmpty() - ? token.getGenesis().getData().getSalt() - : transactions.get(transactions.size() - 1).getData().getSalt() - ) - .digest(), - this.getNonce(), - this.getPublicKey() - ); - } - - /** - * Create predicate from CBOR bytes. - * - * @param bytes CBOR bytes - * @return predicate - */ - public static UnmaskedPredicate fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); - - return new UnmaskedPredicate( - TokenId.fromCbor(data.get(0)), - TokenType.fromCbor(data.get(1)), - CborDeserializer.readByteString(data.get(2)), - CborDeserializer.readTextString(data.get(3)), - HashAlgorithm.fromValue(CborDeserializer.readUnsignedInteger(data.get(4)).asInt()), - CborDeserializer.readByteString(data.get(5)) - ); - } - - /** - * Convert predicate to CBOR bytes. - * - * @return CBOR bytes - */ - public UnmaskedPredicateReference getReference() { - return UnmaskedPredicateReference.create( - this.getTokenType(), - this.getSigningAlgorithm(), - this.getPublicKey(), - this.getHashAlgorithm() - ); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/embedded/UnmaskedPredicateReference.java b/src/main/java/org/unicitylabs/sdk/predicate/embedded/UnmaskedPredicateReference.java deleted file mode 100644 index 3041222..0000000 --- a/src/main/java/org/unicitylabs/sdk/predicate/embedded/UnmaskedPredicateReference.java +++ /dev/null @@ -1,97 +0,0 @@ -package org.unicitylabs.sdk.predicate.embedded; - -import java.util.Objects; -import org.unicitylabs.sdk.address.DirectAddress; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.predicate.PredicateReference; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.TokenType; - -/** - * Unmasked predicate reference. - */ -public class UnmaskedPredicateReference implements PredicateReference { - - private final DataHash hash; - - private UnmaskedPredicateReference(DataHash hash) { - this.hash = hash; - } - - /** - * Get predicate hash. - * - * @return predicate hash - */ - public DataHash getHash() { - return this.hash; - } - - /** - * Create predicate reference. - * - * @param tokenType token type - * @param signingAlgorithm signing algorithm - * @param publicKey predicate public key - * @param hashAlgorithm hash algorithm - * @return predicate reference - */ - public static UnmaskedPredicateReference create( - TokenType tokenType, - String signingAlgorithm, - byte[] publicKey, - HashAlgorithm hashAlgorithm - ) { - Objects.requireNonNull(tokenType, "Token type cannot be null"); - Objects.requireNonNull(signingAlgorithm, "Signing algorithm cannot be null"); - Objects.requireNonNull(publicKey, "Public key cannot be null"); - Objects.requireNonNull(hashAlgorithm, "Hash algorithm cannot be null"); - - return new UnmaskedPredicateReference( - new DataHasher(HashAlgorithm.SHA256) - .update( - CborSerializer.encodeArray( - CborSerializer.encodeByteString(EmbeddedPredicateType.UNMASKED.getBytes()), - CborSerializer.encodeByteString(tokenType.toCbor()), - CborSerializer.encodeTextString(signingAlgorithm), - CborSerializer.encodeUnsignedInteger(hashAlgorithm.getValue()), - CborSerializer.encodeByteString(publicKey) - ) - ) - .digest() - ); - } - - /** - * Create predicate reference from signing service. - * - * @param tokenType token type - * @param signingService signing service - * @param hashAlgorithm hash algorithm - * @return predicate reference - */ - public static UnmaskedPredicateReference create( - TokenType tokenType, - SigningService signingService, - HashAlgorithm hashAlgorithm - ) { - return UnmaskedPredicateReference.create( - tokenType, - signingService.getAlgorithm(), - signingService.getPublicKey(), - hashAlgorithm - ); - } - - /** - * Convert predicate reference to address. - * - * @return predicate address - */ - public DirectAddress toAddress() { - return DirectAddress.create(this.hash); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifier.java b/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifier.java new file mode 100644 index 0000000..915eb09 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifier.java @@ -0,0 +1,32 @@ +package org.unicitylabs.sdk.predicate.verification; + +import org.unicitylabs.sdk.crypto.hash.DataHash; +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; + +/** + * Verifier contract for predicates handled by a specific predicate engine. + */ +public interface PredicateVerifier { + + /** + * Returns the predicate engine supported by this verifier. + * + * @return supported predicate engine + */ + PredicateEngine getPredicateEngine(); + + /** + * Verifies a predicate in the context of a source state, transaction, and unlock script. + * + * @param predicate predicate to verify + * @param sourceStateHash hash of the source state + * @param transactionHash hash of the transaction being validated + * @param unlockScript unlock script bytes + * @return verification result with status and diagnostics + */ + VerificationResult verify(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 new file mode 100644 index 0000000..42b5416 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifierService.java @@ -0,0 +1,78 @@ +package org.unicitylabs.sdk.predicate.verification; + +import org.unicitylabs.sdk.crypto.hash.DataHash; +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; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +import java.util.HashMap; +import java.util.Map; + +/** + * Service registry that routes predicate verification to engine-specific verifiers. + */ +public class PredicateVerifierService { + + private final Map verifiers = new HashMap<>(); + + private PredicateVerifierService() { + + } + + /** + * Creates a predicate verifier service with default verifier registrations. + * + * @return initialized predicate verifier service + */ + public static PredicateVerifierService create() { + PredicateVerifierService verifier = new PredicateVerifierService(); + verifier.addVerifier(DefaultBuiltInPredicateVerifier.create()); + + return verifier; + } + + /** + * Registers a predicate verifier for its predicate engine. + * + * @param verifier verifier to register + * @return this service instance + * @throws RuntimeException if a verifier is already registered for the same predicate engine + */ + public PredicateVerifierService addVerifier(PredicateVerifier verifier) { + if (this.verifiers.containsKey(verifier.getPredicateEngine())) { + throw new RuntimeException("Predicate verifier already registered for predicate engine: " + + verifier.getPredicateEngine()); + } + + this.verifiers.put(verifier.getPredicateEngine(), verifier); + + return this; + } + + /** + * Verifies a predicate by dispatching to a verifier registered for its engine. + * + * @param predicate predicate to verify + * @param sourceStateHash hash of the source state + * @param transactionHash hash of the transaction being verified + * @param unlockScript unlock script bytes + * @return verification result from the engine-specific verifier + * @throws IllegalArgumentException if no verifier is registered for the predicate engine + */ + public VerificationResult verify( + EncodedPredicate predicate, + DataHash sourceStateHash, + DataHash transactionHash, + byte[] unlockScript + ) { + PredicateVerifier verifier = this.verifiers.get(predicate.getEngine()); + if (verifier == null) { + throw new IllegalArgumentException( + "No verifier registered for predicate engine: " + predicate.getEngine()); + } + + return verifier.verify(predicate, sourceStateHash, transactionHash, unlockScript); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/serializer/UnicityObjectMapper.java b/src/main/java/org/unicitylabs/sdk/serializer/UnicityObjectMapper.java index 39fee09..50e9fce 100644 --- a/src/main/java/org/unicitylabs/sdk/serializer/UnicityObjectMapper.java +++ b/src/main/java/org/unicitylabs/sdk/serializer/UnicityObjectMapper.java @@ -16,7 +16,8 @@ public class UnicityObjectMapper { */ public static final ObjectMapper JSON = createJsonObjectMapper(); - private UnicityObjectMapper() {} + private UnicityObjectMapper() { + } private static ObjectMapper createJsonObjectMapper() { SimpleModule module = new SimpleModule(); 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 f921db5..1d38d5a 100644 --- a/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializer.java +++ b/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializer.java @@ -1,15 +1,11 @@ package org.unicitylabs.sdk.serializer.cbor; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.function.Function; import org.unicitylabs.sdk.serializer.cbor.CborSerializer.CborMap; import org.unicitylabs.sdk.serializer.cbor.CborSerializer.CborMap.Entry; +import java.util.*; +import java.util.function.Function; + /** * CBOR deserialization utilities. */ @@ -18,22 +14,27 @@ public class CborDeserializer { private static final byte MAJOR_TYPE_MASK = (byte) 0b11100000; private static final byte ADDITIONAL_INFORMATION_MASK = (byte) 0b00011111; - private CborDeserializer() {} + 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 readOptional(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); } /** @@ -42,9 +43,12 @@ public static T readOptional(byte[] data, Function reader) { * @param data bytes * @return unsigned number */ - public static CborNumber readUnsignedInteger(byte[] data) { + 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); } /** @@ -53,9 +57,12 @@ public static CborNumber readUnsignedInteger(byte[] data) { * @param data bytes * @return bytes */ - public static byte[] readByteString(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; } /** @@ -64,10 +71,12 @@ public static byte[] readByteString(byte[] data) { * @param data bytes * @return text */ - public static String readTextString(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); } /** @@ -76,7 +85,7 @@ public static String readTextString(byte[] data) { * @param data bytes * @return CBOR element array */ - public static List readArray(byte[] data) { + public static List decodeArray(byte[] data) { CborReader reader = new CborReader(data); long length = reader.readLength(CborMajorType.ARRAY); @@ -84,26 +93,58 @@ public static List readArray(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. * * @param data bytes * @return CBOR element map */ - public static Set readMap(byte[] data) { + public static Set decodeMap(byte[] data) { CborReader reader = new CborReader(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,10 +155,13 @@ public static Set readMap(byte[] data) { * @param data bytes * @return CBOR tag */ - public static CborTag readTag(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); } /** @@ -126,12 +170,15 @@ public static CborTag readTag(byte[] data) { * @param data bytes * @return boolean */ - public static boolean readBoolean(byte[] data) { - byte byteValue = new CborReader(data).readByte(); - if (byteValue == (byte) 0xf5) { + public static boolean decodeBoolean(byte[] data) { + 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."); @@ -148,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."); @@ -171,29 +226,37 @@ 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 - & CborDeserializer.ADDITIONAL_INFORMATION_MASK); + & CborDeserializer.ADDITIONAL_INFORMATION_MASK); if (Byte.compareUnsigned(additionalInformation, (byte) 24) < 0) { return additionalInformation; } 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; @@ -202,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; } @@ -211,7 +281,7 @@ public byte[] readRawCbor() { } CborMajorType majorType = CborMajorType.fromType( - this.data[this.position] & CborDeserializer.MAJOR_TYPE_MASK); + this.data[this.position] & CborDeserializer.MAJOR_TYPE_MASK); int position = this.position; int length = (int) this.readLength(majorType); switch (majorType) { 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 f3c5cc3..9cb62f5 100644 --- a/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborSerializer.java +++ b/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborSerializer.java @@ -3,11 +3,7 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.function.Function; /** @@ -26,7 +22,7 @@ private CborSerializer() { * @param value type * @return bytes */ - public static byte[] encodeOptional(T data, Function encoder) { + public static byte[] encodeNullable(T data, Function encoder) { if (data == null) { return new byte[]{(byte) 0xf6}; } @@ -48,7 +44,7 @@ public static byte[] encodeUnsignedInteger(long input) { byte[] result = new byte[1 + bytes.length]; System.arraycopy(bytes, 0, result, 1, bytes.length); result[0] = (byte) (CborMajorType.UNSIGNED_INTEGER.getType() - | CborSerializer.getAdditionalInformationBits(bytes.length)); + | CborSerializer.getAdditionalInformationBits(bytes.length)); return result; } @@ -155,7 +151,7 @@ public static byte[] encodeTag(long tag, byte[] input) { byte[] bytes = CborSerializer.getUnsignedLongAsPaddedBytes(tag); byte[] result = new byte[1 + bytes.length + input.length]; result[0] = (byte) (CborMajorType.TAG.getType() - | CborSerializer.getAdditionalInformationBits(bytes.length)); + | CborSerializer.getAdditionalInformationBits(bytes.length)); System.arraycopy(bytes, 0, result, 1, bytes.length); System.arraycopy(input, 0, result, 1 + bytes.length, input.length); @@ -193,7 +189,7 @@ private static byte[] encodeRawArray(byte[] input, int length, CborMajorType typ byte[] lengthBytes = CborSerializer.getUnsignedLongAsPaddedBytes(length); byte[] result = new byte[1 + lengthBytes.length + input.length]; result[0] = (byte) (type.getType() - | CborSerializer.getAdditionalInformationBits(lengthBytes.length)); + | CborSerializer.getAdditionalInformationBits(lengthBytes.length)); System.arraycopy(lengthBytes, 0, result, 1, lengthBytes.length); System.arraycopy(input, 0, result, 1 + lengthBytes.length, input.length); @@ -211,8 +207,8 @@ private static byte[] getUnsignedLongAsPaddedBytes(long input) { } ByteBuffer buffer = ByteBuffer - .allocate((int) Math.pow(2, (int) Math.ceil(Math.log(length) / Math.log(2)))) - .order(ByteOrder.BIG_ENDIAN); + .allocate((int) Math.pow(2, (int) Math.ceil(Math.log(length) / Math.log(2)))) + .order(ByteOrder.BIG_ENDIAN); if (length <= 1) { buffer.put((byte) input); } else if (length <= 2) { @@ -240,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; - }); + } } /** @@ -264,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/serializer/json/BigIntegerAsStringSerializer.java b/src/main/java/org/unicitylabs/sdk/serializer/json/BigIntegerAsStringSerializer.java deleted file mode 100644 index bc13c4b..0000000 --- a/src/main/java/org/unicitylabs/sdk/serializer/json/BigIntegerAsStringSerializer.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.unicitylabs.sdk.serializer.json; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import java.io.IOException; -import java.math.BigInteger; - -/** - * Serializes a BigInteger value as a JSON string. - */ -public class BigIntegerAsStringSerializer extends JsonSerializer { - - /** - * Create BigInteger serializer. - */ - public BigIntegerAsStringSerializer() { - super(); - } - - @Override - public void serialize(BigInteger value, JsonGenerator gen, SerializerProvider serializers) - throws IOException { - gen.writeString(value.toString()); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/serializer/json/ByteArrayJson.java b/src/main/java/org/unicitylabs/sdk/serializer/json/ByteArrayJson.java index a65444a..76f8e82 100644 --- a/src/main/java/org/unicitylabs/sdk/serializer/json/ByteArrayJson.java +++ b/src/main/java/org/unicitylabs/sdk/serializer/json/ByteArrayJson.java @@ -8,9 +8,10 @@ import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.exc.MismatchedInputException; import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import java.io.IOException; import org.unicitylabs.sdk.util.HexConverter; +import java.io.IOException; + /** * Byte array serializer and deserializer implementation. */ @@ -41,7 +42,7 @@ public Serializer() { */ @Override public void serialize(byte[] value, JsonGenerator gen, SerializerProvider serializers) - throws IOException { + throws IOException { gen.writeString(HexConverter.encode(value)); } } @@ -62,8 +63,7 @@ public Deserializer() { * Deserialize byte array. * * @param p Parser used for reading JSON content - * @param ctx Context that can be used to access information about this deserialization - * activity. + * @param ctx Context that can be used to access information about this deserialization activity. * @return bytes * @throws IOException on deserialization failure */ @@ -71,7 +71,7 @@ public Deserializer() { public byte[] deserialize(JsonParser p, DeserializationContext ctx) throws IOException { if (p.getCurrentToken() != JsonToken.VALUE_STRING) { throw MismatchedInputException.from(p, byte[].class, - "Expected hex string value"); + "Expected hex string value"); } try { diff --git a/src/main/java/org/unicitylabs/sdk/serializer/json/LongAsStringSerializer.java b/src/main/java/org/unicitylabs/sdk/serializer/json/LongAsStringSerializer.java index 8163313..6969399 100644 --- a/src/main/java/org/unicitylabs/sdk/serializer/json/LongAsStringSerializer.java +++ b/src/main/java/org/unicitylabs/sdk/serializer/json/LongAsStringSerializer.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; + import java.io.IOException; /** @@ -19,7 +20,7 @@ public LongAsStringSerializer() { @Override public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) - throws IOException { + throws IOException { gen.writeString(value.toString()); } } diff --git a/src/main/java/org/unicitylabs/sdk/signing/MintSigningService.java b/src/main/java/org/unicitylabs/sdk/signing/MintSigningService.java deleted file mode 100644 index cc33d2a..0000000 --- a/src/main/java/org/unicitylabs/sdk/signing/MintSigningService.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.unicitylabs.sdk.signing; - -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.util.HexConverter; - -/** - * Signing service for minting operations. - */ -public class MintSigningService { - private static final byte[] MINTER_SECRET = HexConverter.decode( - "495f414d5f554e4956455253414c5f4d494e5445525f464f525f"); - - private MintSigningService() {} - - /** - * Create signing service for minting operations. - * - * @param tokenId token identifier - * @return signing service - */ - public static SigningService create(TokenId tokenId) { - return SigningService.createFromMaskedSecret(MINTER_SECRET, tokenId.getBytes()); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/signing/SignatureJson.java b/src/main/java/org/unicitylabs/sdk/signing/SignatureJson.java deleted file mode 100644 index bdcd698..0000000 --- a/src/main/java/org/unicitylabs/sdk/signing/SignatureJson.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.unicitylabs.sdk.signing; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import java.io.IOException; - -/** - * Signature serializer and deserializer implementation. - */ -public class SignatureJson { - - private SignatureJson() { - } - - /** - * Signature serializer. - */ - public static class Serializer extends StdSerializer { - - /** - * Create serializer. - */ - public Serializer() { - super(Signature.class); - } - - /** - * Serialize signature. - * - * @param value signature - * @param gen json generator - * @param serializers serializer provider - * @throws IOException on serialization failure - */ - @Override - public void serialize(Signature value, JsonGenerator gen, - SerializerProvider serializers) - throws IOException { - gen.writeObject(value.encode()); - } - } - - /** - * Signature deserializer. - */ - public static class Deserializer extends StdDeserializer { - - /** - * Create deserializer. - */ - public Deserializer() { - super(Signature.class); - } - - /** - * Deserialize signature. - * - * @param p Parser used for reading JSON content - * @param ctx Context that can be used to access information about this deserialization - * activity. - * @return signature - * @throws IOException on deserialization failure - */ - @Override - public Signature deserialize(JsonParser p, DeserializationContext ctx) - throws IOException { - return Signature.decode(p.readValueAs(byte[].class)); - } - } -} - diff --git a/src/main/java/org/unicitylabs/sdk/mtree/BranchExistsException.java b/src/main/java/org/unicitylabs/sdk/smt/BranchExistsException.java similarity index 90% rename from src/main/java/org/unicitylabs/sdk/mtree/BranchExistsException.java rename to src/main/java/org/unicitylabs/sdk/smt/BranchExistsException.java index b5694b7..c6c87aa 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/BranchExistsException.java +++ b/src/main/java/org/unicitylabs/sdk/smt/BranchExistsException.java @@ -1,4 +1,4 @@ -package org.unicitylabs.sdk.mtree; +package org.unicitylabs.sdk.smt; /** * Exception thrown when a branch already exists at a given path in the merkle tree. diff --git a/src/main/java/org/unicitylabs/sdk/mtree/CommonPath.java b/src/main/java/org/unicitylabs/sdk/smt/CommonPath.java similarity index 94% rename from src/main/java/org/unicitylabs/sdk/mtree/CommonPath.java rename to src/main/java/org/unicitylabs/sdk/smt/CommonPath.java index 177438b..6a4f1a6 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/CommonPath.java +++ b/src/main/java/org/unicitylabs/sdk/smt/CommonPath.java @@ -1,4 +1,4 @@ -package org.unicitylabs.sdk.mtree; +package org.unicitylabs.sdk.smt; import java.math.BigInteger; import java.util.Objects; @@ -61,7 +61,7 @@ public static CommonPath create(BigInteger path1, BigInteger path2) { int length = 0; while (Objects.equals(path1.and(mask), path2.and(mask)) && path.compareTo(path1) < 0 - && path.compareTo(path2) < 0) { + && path.compareTo(path2) < 0) { mask = mask.shiftLeft(1); length += 1; path = mask.or(mask.subtract(BigInteger.ONE).and(path1)); diff --git a/src/main/java/org/unicitylabs/sdk/mtree/LeafOutOfBoundsException.java b/src/main/java/org/unicitylabs/sdk/smt/LeafOutOfBoundsException.java similarity index 87% rename from src/main/java/org/unicitylabs/sdk/mtree/LeafOutOfBoundsException.java rename to src/main/java/org/unicitylabs/sdk/smt/LeafOutOfBoundsException.java index d392a44..1516ae5 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/LeafOutOfBoundsException.java +++ b/src/main/java/org/unicitylabs/sdk/smt/LeafOutOfBoundsException.java @@ -1,4 +1,4 @@ -package org.unicitylabs.sdk.mtree; +package org.unicitylabs.sdk.smt; /** * Exception when leaf is out of bounds. diff --git a/src/main/java/org/unicitylabs/sdk/mtree/MerkleTreePathVerificationResult.java b/src/main/java/org/unicitylabs/sdk/smt/MerkleTreePathVerificationResult.java similarity index 95% rename from src/main/java/org/unicitylabs/sdk/mtree/MerkleTreePathVerificationResult.java rename to src/main/java/org/unicitylabs/sdk/smt/MerkleTreePathVerificationResult.java index 2613d0a..982fb5a 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/MerkleTreePathVerificationResult.java +++ b/src/main/java/org/unicitylabs/sdk/smt/MerkleTreePathVerificationResult.java @@ -1,4 +1,4 @@ -package org.unicitylabs.sdk.mtree; +package org.unicitylabs.sdk.smt; import java.util.Objects; @@ -51,7 +51,7 @@ public boolean isSuccessful() { @Override public String toString() { return String.format("MerkleTreePathVerificationResult{pathValid=%b, pathIncluded=%b}", - pathValid, pathIncluded); + pathValid, pathIncluded); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/mtree/plain/Branch.java b/src/main/java/org/unicitylabs/sdk/smt/plain/Branch.java similarity index 80% rename from src/main/java/org/unicitylabs/sdk/mtree/plain/Branch.java rename to src/main/java/org/unicitylabs/sdk/smt/plain/Branch.java index 2cb6f43..3a887f5 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/Branch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/plain/Branch.java @@ -1,7 +1,8 @@ -package org.unicitylabs.sdk.mtree.plain; +package org.unicitylabs.sdk.smt.plain; + +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import java.math.BigInteger; -import org.unicitylabs.sdk.hash.HashAlgorithm; /** * Sparse merkle tree branch structure. diff --git a/src/main/java/org/unicitylabs/sdk/mtree/plain/FinalizedBranch.java b/src/main/java/org/unicitylabs/sdk/smt/plain/FinalizedBranch.java similarity index 67% rename from src/main/java/org/unicitylabs/sdk/mtree/plain/FinalizedBranch.java rename to src/main/java/org/unicitylabs/sdk/smt/plain/FinalizedBranch.java index a6c884f..6cdd85c 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/FinalizedBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/plain/FinalizedBranch.java @@ -1,6 +1,6 @@ -package org.unicitylabs.sdk.mtree.plain; +package org.unicitylabs.sdk.smt.plain; -import org.unicitylabs.sdk.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.DataHash; /** * Finalized branch in sparse merkle tree. diff --git a/src/main/java/org/unicitylabs/sdk/mtree/plain/FinalizedLeafBranch.java b/src/main/java/org/unicitylabs/sdk/smt/plain/FinalizedLeafBranch.java similarity index 75% rename from src/main/java/org/unicitylabs/sdk/mtree/plain/FinalizedLeafBranch.java rename to src/main/java/org/unicitylabs/sdk/smt/plain/FinalizedLeafBranch.java index ebe9374..cf7792c 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/FinalizedLeafBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/plain/FinalizedLeafBranch.java @@ -1,13 +1,14 @@ -package org.unicitylabs.sdk.mtree.plain; +package org.unicitylabs.sdk.smt.plain; + +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.CborSerializer; +import org.unicitylabs.sdk.util.BigIntegerConverter; import java.math.BigInteger; import java.util.Arrays; import java.util.Objects; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.util.BigIntegerConverter; /** * Finalized leaf branch in a sparse merkle tree. @@ -33,18 +34,18 @@ private FinalizedLeafBranch(BigInteger path, byte[] value, DataHash hash) { * @return finalized leaf branch */ public static FinalizedLeafBranch create( - BigInteger path, - byte[] value, - HashAlgorithm hashAlgorithm + BigInteger path, + byte[] value, + HashAlgorithm hashAlgorithm ) { DataHash hash = new DataHasher(hashAlgorithm) - .update( - CborSerializer.encodeArray( - CborSerializer.encodeByteString(BigIntegerConverter.encode(path)), - CborSerializer.encodeByteString(value) + .update( + CborSerializer.encodeArray( + CborSerializer.encodeByteString(BigIntegerConverter.encode(path)), + CborSerializer.encodeByteString(value) + ) ) - ) - .digest(); + .digest(); return new FinalizedLeafBranch(path, value, hash); } diff --git a/src/main/java/org/unicitylabs/sdk/mtree/plain/FinalizedNodeBranch.java b/src/main/java/org/unicitylabs/sdk/smt/plain/FinalizedNodeBranch.java similarity index 56% rename from src/main/java/org/unicitylabs/sdk/mtree/plain/FinalizedNodeBranch.java rename to src/main/java/org/unicitylabs/sdk/smt/plain/FinalizedNodeBranch.java index c790060..f176570 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/FinalizedNodeBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/plain/FinalizedNodeBranch.java @@ -1,13 +1,14 @@ -package org.unicitylabs.sdk.mtree.plain; +package org.unicitylabs.sdk.smt.plain; -import java.math.BigInteger; -import java.util.Objects; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; +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.CborSerializer; import org.unicitylabs.sdk.util.BigIntegerConverter; +import java.math.BigInteger; +import java.util.Objects; + /** * Finalized node branch in a sparse merkle tree. */ @@ -19,10 +20,10 @@ class FinalizedNodeBranch implements NodeBranch, FinalizedBranch { private final DataHash hash; private FinalizedNodeBranch( - BigInteger path, - FinalizedBranch left, - FinalizedBranch right, - DataHash hash + BigInteger path, + FinalizedBranch left, + FinalizedBranch right, + DataHash hash ) { this.path = path; this.left = left; @@ -40,30 +41,30 @@ private FinalizedNodeBranch( * @return finalized node branch */ public static FinalizedNodeBranch create( - BigInteger path, - FinalizedBranch left, - FinalizedBranch right, - HashAlgorithm hashAlgorithm + BigInteger path, + FinalizedBranch left, + FinalizedBranch right, + HashAlgorithm hashAlgorithm ) { DataHash hash = new DataHasher(hashAlgorithm) - .update( - CborSerializer.encodeArray( - CborSerializer.encodeByteString(BigIntegerConverter.encode(path)), - CborSerializer.encodeOptional( - left == null - ? null - : left.getHash().getData(), - CborSerializer::encodeByteString - ), - CborSerializer.encodeOptional( - right == null - ? null - : right.getHash().getData(), - CborSerializer::encodeByteString - ) + .update( + CborSerializer.encodeArray( + CborSerializer.encodeByteString(BigIntegerConverter.encode(path)), + CborSerializer.encodeNullable( + left == null + ? null + : left.getHash().getData(), + CborSerializer::encodeByteString + ), + CborSerializer.encodeNullable( + right == null + ? null + : right.getHash().getData(), + CborSerializer::encodeByteString + ) + ) ) - ) - .digest(); + .digest(); return new FinalizedNodeBranch(path, left, right, hash); } @@ -100,7 +101,7 @@ public boolean equals(Object o) { } FinalizedNodeBranch that = (FinalizedNodeBranch) o; return Objects.equals(this.path, that.path) && Objects.equals(this.left, that.left) - && Objects.equals(this.right, that.right); + && Objects.equals(this.right, that.right); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/mtree/plain/LeafBranch.java b/src/main/java/org/unicitylabs/sdk/smt/plain/LeafBranch.java similarity index 83% rename from src/main/java/org/unicitylabs/sdk/mtree/plain/LeafBranch.java rename to src/main/java/org/unicitylabs/sdk/smt/plain/LeafBranch.java index f8650e5..c5752d6 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/LeafBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/plain/LeafBranch.java @@ -1,4 +1,4 @@ -package org.unicitylabs.sdk.mtree.plain; +package org.unicitylabs.sdk.smt.plain; /** * Leaf branch in a sparse merkle tree. diff --git a/src/main/java/org/unicitylabs/sdk/mtree/plain/NodeBranch.java b/src/main/java/org/unicitylabs/sdk/smt/plain/NodeBranch.java similarity index 86% rename from src/main/java/org/unicitylabs/sdk/mtree/plain/NodeBranch.java rename to src/main/java/org/unicitylabs/sdk/smt/plain/NodeBranch.java index 2c32323..5e98afc 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/NodeBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/plain/NodeBranch.java @@ -1,4 +1,4 @@ -package org.unicitylabs.sdk.mtree.plain; +package org.unicitylabs.sdk.smt.plain; /** * Node branch in merkle tree. diff --git a/src/main/java/org/unicitylabs/sdk/mtree/plain/PendingLeafBranch.java b/src/main/java/org/unicitylabs/sdk/smt/plain/PendingLeafBranch.java similarity index 92% rename from src/main/java/org/unicitylabs/sdk/mtree/plain/PendingLeafBranch.java rename to src/main/java/org/unicitylabs/sdk/smt/plain/PendingLeafBranch.java index 88022ab..286a13f 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/PendingLeafBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/plain/PendingLeafBranch.java @@ -1,9 +1,10 @@ -package org.unicitylabs.sdk.mtree.plain; +package org.unicitylabs.sdk.smt.plain; + +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import java.math.BigInteger; import java.util.Arrays; import java.util.Objects; -import org.unicitylabs.sdk.hash.HashAlgorithm; /** * Pending leaf branch in a sparse merkle tree. diff --git a/src/main/java/org/unicitylabs/sdk/mtree/plain/PendingNodeBranch.java b/src/main/java/org/unicitylabs/sdk/smt/plain/PendingNodeBranch.java similarity index 81% rename from src/main/java/org/unicitylabs/sdk/mtree/plain/PendingNodeBranch.java rename to src/main/java/org/unicitylabs/sdk/smt/plain/PendingNodeBranch.java index c206dfd..8b39cc0 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/PendingNodeBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/plain/PendingNodeBranch.java @@ -1,8 +1,9 @@ -package org.unicitylabs.sdk.mtree.plain; +package org.unicitylabs.sdk.smt.plain; + +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import java.math.BigInteger; import java.util.Objects; -import org.unicitylabs.sdk.hash.HashAlgorithm; /** * Pending node branch in a sparse merkle tree. @@ -44,10 +45,10 @@ public Branch getRight() { @Override public FinalizedNodeBranch finalize(HashAlgorithm hashAlgorithm) { return FinalizedNodeBranch.create( - this.path, - this.left.finalize(hashAlgorithm), - this.right.finalize(hashAlgorithm), - hashAlgorithm + this.path, + this.left.finalize(hashAlgorithm), + this.right.finalize(hashAlgorithm), + hashAlgorithm ); } @@ -58,7 +59,7 @@ public boolean equals(Object o) { } PendingNodeBranch that = (PendingNodeBranch) o; return Objects.equals(this.path, that.path) && Objects.equals(this.left, that.left) - && Objects.equals(this.right, that.right); + && Objects.equals(this.right, that.right); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTree.java b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTree.java similarity index 66% rename from src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTree.java rename to src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTree.java index 96abb35..ce5109a 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTree.java +++ b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTree.java @@ -1,11 +1,12 @@ -package org.unicitylabs.sdk.mtree.plain; +package org.unicitylabs.sdk.smt.plain; + +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.smt.BranchExistsException; +import org.unicitylabs.sdk.smt.CommonPath; +import org.unicitylabs.sdk.smt.LeafOutOfBoundsException; import java.math.BigInteger; import java.util.Arrays; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.mtree.BranchExistsException; -import org.unicitylabs.sdk.mtree.CommonPath; -import org.unicitylabs.sdk.mtree.LeafOutOfBoundsException; /** * Sparse Merkle tree implementation. @@ -31,12 +32,12 @@ public SparseMerkleTree(HashAlgorithm hashAlgorithm) { * * @param path path of the leaf * @param data data of the leaf - * @throws BranchExistsException if branch already exists at the path - * @throws LeafOutOfBoundsException if leaf is out of bounds - * @throws IllegalArgumentException if path is less than 1 + * @throws BranchExistsException if branch already exists at the path + * @throws LeafOutOfBoundsException if leaf is out of bounds + * @throws IllegalArgumentException if path is less than 1 */ public synchronized void addLeaf(BigInteger path, byte[] data) - throws BranchExistsException, LeafOutOfBoundsException { + throws BranchExistsException, LeafOutOfBoundsException { if (path.compareTo(BigInteger.ONE) < 0) { throw new IllegalArgumentException("Path must be greater than 0"); } @@ -44,8 +45,8 @@ public synchronized void addLeaf(BigInteger path, byte[] data) boolean isRight = path.testBit(0); Branch branch = isRight ? this.right : this.left; Branch result = branch != null - ? SparseMerkleTree.buildTree(branch, path, Arrays.copyOf(data, data.length)) - : new PendingLeafBranch(path, Arrays.copyOf(data, data.length)); + ? SparseMerkleTree.buildTree(branch, path, Arrays.copyOf(data, data.length)) + : new PendingLeafBranch(path, Arrays.copyOf(data, data.length)); if (isRight) { this.right = result; @@ -69,7 +70,7 @@ public synchronized SparseMerkleTreeRootNode calculateRoot() { } private static Branch buildTree(Branch branch, BigInteger remainingPath, byte[] value) - throws BranchExistsException, LeafOutOfBoundsException { + throws BranchExistsException, LeafOutOfBoundsException { CommonPath commonPath = CommonPath.create(remainingPath, branch.getPath()); boolean isRight = remainingPath.shiftRight(commonPath.getLength()).testBit(0); @@ -85,11 +86,11 @@ private static Branch buildTree(Branch branch, BigInteger remainingPath, byte[] LeafBranch leafBranch = (LeafBranch) branch; LeafBranch oldBranch = new PendingLeafBranch( - branch.getPath().shiftRight(commonPath.getLength()), leafBranch.getValue()); + branch.getPath().shiftRight(commonPath.getLength()), leafBranch.getValue()); LeafBranch newBranch = new PendingLeafBranch(remainingPath.shiftRight(commonPath.getLength()), - value); + value); return new PendingNodeBranch(commonPath.getPath(), isRight ? oldBranch : newBranch, - isRight ? newBranch : oldBranch); + isRight ? newBranch : oldBranch); } NodeBranch nodeBranch = (NodeBranch) branch; @@ -97,23 +98,23 @@ private static Branch buildTree(Branch branch, BigInteger remainingPath, byte[] // if node branch is split in the middle if (commonPath.getPath().compareTo(branch.getPath()) < 0) { LeafBranch newBranch = new PendingLeafBranch(remainingPath.shiftRight(commonPath.getLength()), - value); + value); NodeBranch oldBranch = new PendingNodeBranch( - branch.getPath().shiftRight(commonPath.getLength()), nodeBranch.getLeft(), - nodeBranch.getRight()); + branch.getPath().shiftRight(commonPath.getLength()), nodeBranch.getLeft(), + nodeBranch.getRight()); return new PendingNodeBranch(commonPath.getPath(), isRight ? oldBranch : newBranch, - isRight ? newBranch : oldBranch); + isRight ? newBranch : oldBranch); } if (isRight) { return new PendingNodeBranch(nodeBranch.getPath(), nodeBranch.getLeft(), - SparseMerkleTree.buildTree(nodeBranch.getRight(), - remainingPath.shiftRight(commonPath.getLength()), value)); + SparseMerkleTree.buildTree(nodeBranch.getRight(), + remainingPath.shiftRight(commonPath.getLength()), value)); } return new PendingNodeBranch(nodeBranch.getPath(), - SparseMerkleTree.buildTree(nodeBranch.getLeft(), - remainingPath.shiftRight(commonPath.getLength()), value), nodeBranch.getRight()); + SparseMerkleTree.buildTree(nodeBranch.getLeft(), + remainingPath.shiftRight(commonPath.getLength()), value), nodeBranch.getRight()); } } diff --git a/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePath.java b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePath.java similarity index 50% rename from src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePath.java rename to src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePath.java index 53b9012..1dcd88e 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePath.java +++ b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePath.java @@ -1,21 +1,16 @@ -package org.unicitylabs.sdk.mtree.plain; +package org.unicitylabs.sdk.smt.plain; + +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.DataHasher; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.smt.MerkleTreePathVerificationResult; +import org.unicitylabs.sdk.util.BigIntegerConverter; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonGetter; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; import java.math.BigInteger; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.mtree.MerkleTreePathVerificationResult; -import org.unicitylabs.sdk.serializer.UnicityObjectMapper; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.serializer.json.JsonSerializationException; -import org.unicitylabs.sdk.util.BigIntegerConverter; /** * Sparse merkle tree path for selected path. @@ -25,13 +20,7 @@ public class SparseMerkleTreePath { private final DataHash rootHash; private final List steps; - @JsonCreator - SparseMerkleTreePath( - @JsonProperty("root") - DataHash rootHash, - @JsonProperty("steps") - List steps - ) { + SparseMerkleTreePath(DataHash rootHash, List steps) { Objects.requireNonNull(rootHash, "rootHash cannot be null"); Objects.requireNonNull(steps, "steps cannot be null"); @@ -44,7 +33,6 @@ public class SparseMerkleTreePath { * * @return root hash */ - @JsonGetter("root") public DataHash getRootHash() { return this.rootHash; } @@ -61,10 +49,10 @@ public List getSteps() { /** * Verify merkle tree path against given path. * - * @param requestId path + * @param stateId path * @return MerkleTreePathVerificationResult */ - public MerkleTreePathVerificationResult verify(BigInteger requestId) { + public MerkleTreePathVerificationResult verify(BigInteger stateId) { if (this.steps.isEmpty()) { return new MerkleTreePathVerificationResult(false, false); } @@ -74,16 +62,16 @@ public MerkleTreePathVerificationResult verify(BigInteger requestId) { BigInteger currentPath = step.getPath(); if (step.getPath().compareTo(BigInteger.ONE) > 0) { DataHash hash = new DataHasher(this.rootHash.getAlgorithm()) - .update( - CborSerializer.encodeArray( - CborSerializer.encodeByteString(BigIntegerConverter.encode(step.getPath())), - CborSerializer.encodeOptional( - step.getData().orElse(null), - CborSerializer::encodeByteString - ) + .update( + CborSerializer.encodeArray( + CborSerializer.encodeByteString(BigIntegerConverter.encode(step.getPath())), + CborSerializer.encodeNullable( + step.getData().orElse(null), + CborSerializer::encodeByteString + ) + ) ) - ) - .digest(); + .digest(); currentData = hash.getData(); } else { @@ -100,14 +88,14 @@ public MerkleTreePathVerificationResult verify(BigInteger requestId) { byte[] right = isRight ? currentData : step.getData().orElse(null); DataHash hash = new DataHasher(this.rootHash.getAlgorithm()) - .update( - CborSerializer.encodeArray( - CborSerializer.encodeByteString(BigIntegerConverter.encode(step.getPath())), - CborSerializer.encodeOptional(left, CborSerializer::encodeByteString), - CborSerializer.encodeOptional(right, CborSerializer::encodeByteString) - ) + .update( + CborSerializer.encodeArray( + CborSerializer.encodeByteString(BigIntegerConverter.encode(step.getPath())), + CborSerializer.encodeNullable(left, CborSerializer::encodeByteString), + CborSerializer.encodeNullable(right, CborSerializer::encodeByteString) + ) ) - .digest(); + .digest(); currentData = hash.getData(); @@ -116,77 +104,50 @@ public MerkleTreePathVerificationResult verify(BigInteger requestId) { return new MerkleTreePathVerificationResult(false, false); } currentPath = currentPath.shiftLeft(length) - .or(step.getPath().and(BigInteger.ONE.shiftLeft(length).subtract(BigInteger.ONE))); + .or(step.getPath().and(BigInteger.ONE.shiftLeft(length).subtract(BigInteger.ONE))); previousStep = step; } boolean pathValid = currentData != null - && this.rootHash.equals(new DataHash(this.rootHash.getAlgorithm(), currentData)); - boolean pathIncluded = currentPath.compareTo(requestId) == 0; + && this.rootHash.equals(new DataHash(this.rootHash.getAlgorithm(), currentData)); + boolean pathIncluded = currentPath.compareTo(stateId) == 0; return new MerkleTreePathVerificationResult(pathValid, pathIncluded); } /** - * Create sparse merkle tree path from CBOR bytes. + * Deserialize sparse merkle tree path from CBOR bytes. * * @param bytes CBOR bytes * @return path */ public static SparseMerkleTreePath fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new SparseMerkleTreePath( - DataHash.fromCbor(data.get(0)), - CborDeserializer.readArray(data.get(1)).stream() - .map(SparseMerkleTreePathStep::fromCbor) - .collect(Collectors.toList()) + DataHash.fromCbor(data.get(0)), + CborDeserializer.decodeArray(data.get(1)).stream() + .map(SparseMerkleTreePathStep::fromCbor) + .collect(Collectors.toList()) ); } /** - * Convert sparse merkle tree path to CBOR bytes. + * Serialize sparse merkle tree path to CBOR bytes. * * @return CBOR bytes */ public byte[] toCbor() { return CborSerializer.encodeArray( - this.rootHash.toCbor(), - CborSerializer.encodeArray( - this.steps.stream() - .map(SparseMerkleTreePathStep::toCbor) - .toArray(byte[][]::new) - ) + this.rootHash.toCbor(), + CborSerializer.encodeArray( + this.steps.stream() + .map(SparseMerkleTreePathStep::toCbor) + .toArray(byte[][]::new) + ) ); } - /** - * Create sparse merkle tree path from JSON string. - * - * @param input JSON string - * @return path - */ - public static SparseMerkleTreePath fromJson(String input) { - try { - return UnicityObjectMapper.JSON.readValue(input, SparseMerkleTreePath.class); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(SparseMerkleTreePath.class, e); - } - } - - /** - * Convert sparse merkle tree path to JSON string. - * - * @return JSON string - */ - public String toJson() { - try { - return UnicityObjectMapper.JSON.writeValueAsString(this); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(SparseMerkleTreePath.class, e); - } - } - @Override public boolean equals(Object o) { if (!(o instanceof SparseMerkleTreePath)) { diff --git a/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePathStep.java b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePathStep.java similarity index 64% rename from src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePathStep.java rename to src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePathStep.java index 1006adf..931c850 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePathStep.java +++ b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePathStep.java @@ -1,18 +1,15 @@ -package org.unicitylabs.sdk.mtree.plain; +package org.unicitylabs.sdk.smt.plain; + +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.util.BigIntegerConverter; +import org.unicitylabs.sdk.util.HexConverter; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.math.BigInteger; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Optional; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.serializer.json.BigIntegerAsStringSerializer; -import org.unicitylabs.sdk.util.BigIntegerConverter; -import org.unicitylabs.sdk.util.HexConverter; /** * Sparse Merkle tree path step. @@ -28,10 +25,9 @@ public class SparseMerkleTreePathStep { * @param path step path, must be greater than or equal to zero * @param data step data */ - @JsonCreator public SparseMerkleTreePathStep( - @JsonProperty("path") BigInteger path, - @JsonProperty("data") byte[] data + BigInteger path, + byte[] data ) { Objects.requireNonNull(path, "path cannot be null"); if (path.compareTo(BigInteger.ZERO) < 0) { @@ -47,7 +43,6 @@ public SparseMerkleTreePathStep( * * @return step path */ - @JsonSerialize(using = BigIntegerAsStringSerializer.class) public BigInteger getPath() { return this.path; } @@ -62,29 +57,29 @@ public Optional getData() { } /** - * Create sparse Merkle tree path step from CBOR bytes. + * Deserialize sparse Merkle tree path step from CBOR bytes. * * @param bytes CBOR bytes * @return sparse Merkle tree path step */ public static SparseMerkleTreePathStep fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 2); return new SparseMerkleTreePathStep( - BigIntegerConverter.decode(CborDeserializer.readByteString(data.get(0))), - CborDeserializer.readOptional(data.get(1), CborDeserializer::readByteString) + BigIntegerConverter.decode(CborDeserializer.decodeByteString(data.get(0))), + CborDeserializer.decodeNullable(data.get(1), CborDeserializer::decodeByteString) ); } /** - * Convert sparse Merkle tree path step to CBOR bytes. + * Serialize sparse Merkle tree path step to CBOR bytes. * * @return CBOR bytes */ public byte[] toCbor() { return CborSerializer.encodeArray( - CborSerializer.encodeByteString(BigIntegerConverter.encode(this.path)), - CborSerializer.encodeOptional(this.data, CborSerializer::encodeByteString) + CborSerializer.encodeByteString(BigIntegerConverter.encode(this.path)), + CborSerializer.encodeNullable(this.data, CborSerializer::encodeByteString) ); } @@ -105,9 +100,9 @@ public int hashCode() { @Override public String toString() { return String.format( - "MerkleTreePathStep{path=%s, data=%s}", - this.path.toString(2), - this.data == null ? "null" : HexConverter.encode(this.data) + "MerkleTreePathStep{path=%s, data=%s}", + this.path.toString(2), + this.data == null ? "null" : HexConverter.encode(this.data) ); } } diff --git a/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreeRootNode.java b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreeRootNode.java similarity index 59% rename from src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreeRootNode.java rename to src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreeRootNode.java index 76cba44..e1dbece 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreeRootNode.java +++ b/src/main/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreeRootNode.java @@ -1,12 +1,13 @@ -package org.unicitylabs.sdk.mtree.plain; +package org.unicitylabs.sdk.smt.plain; + +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.smt.CommonPath; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.mtree.CommonPath; /** * Sparse merkle tree state for given root. @@ -20,12 +21,12 @@ private SparseMerkleTreeRootNode(FinalizedNodeBranch root) { } static SparseMerkleTreeRootNode create( - FinalizedBranch left, - FinalizedBranch right, - HashAlgorithm hashAlgorithm + FinalizedBranch left, + FinalizedBranch right, + HashAlgorithm hashAlgorithm ) { return new SparseMerkleTreeRootNode( - FinalizedNodeBranch.create(BigInteger.ONE, left, right, hashAlgorithm) + FinalizedNodeBranch.create(BigInteger.ONE, left, right, hashAlgorithm) ); } @@ -46,8 +47,8 @@ public DataHash getRootHash() { */ public SparseMerkleTreePath getPath(BigInteger path) { return new SparseMerkleTreePath( - this.root.getHash(), - SparseMerkleTreeRootNode.generatePath(path, this.root) + this.root.getHash(), + SparseMerkleTreeRootNode.generatePath(path, this.root) ); } @@ -66,8 +67,8 @@ public int hashCode() { } private static List generatePath( - BigInteger remainingPath, - FinalizedBranch parent + BigInteger remainingPath, + FinalizedBranch parent ) { if (parent instanceof LeafBranch) { LeafBranch leaf = (LeafBranch) parent; @@ -79,20 +80,20 @@ private static List generatePath( remainingPath = remainingPath.shiftRight(commonPath.getLength()); if (commonPath.getPath().compareTo(parent.getPath()) != 0 - || remainingPath.compareTo(BigInteger.ONE) == 0) { + || remainingPath.compareTo(BigInteger.ONE) == 0) { return List.of( - new SparseMerkleTreePathStep( - BigInteger.ZERO, - node.getLeft() == null - ? null - : node.getLeft().getHash().getData() - ), - new SparseMerkleTreePathStep( - node.getPath(), - node.getRight() == null - ? null - : node.getRight().getHash().getData() - ) + new SparseMerkleTreePathStep( + BigInteger.ZERO, + node.getLeft() == null + ? null + : node.getLeft().getHash().getData() + ), + new SparseMerkleTreePathStep( + node.getPath(), + node.getRight() == null + ? null + : node.getRight().getHash().getData() + ) ); } @@ -101,22 +102,22 @@ private static List generatePath( FinalizedBranch siblingBranch = isRight ? node.getLeft() : node.getRight(); SparseMerkleTreePathStep step = new SparseMerkleTreePathStep( - node.getPath(), - siblingBranch == null ? null : siblingBranch.getHash().getData() + node.getPath(), + siblingBranch == null ? null : siblingBranch.getHash().getData() ); if (branch == null) { return List.of( - new SparseMerkleTreePathStep( - isRight ? BigInteger.ONE : BigInteger.ZERO, - null - ), - step + new SparseMerkleTreePathStep( + isRight ? BigInteger.ONE : BigInteger.ZERO, + null + ), + step ); } List list = new ArrayList<>( - SparseMerkleTreeRootNode.generatePath(remainingPath, branch) + SparseMerkleTreeRootNode.generatePath(remainingPath, branch) ); list.add(step); diff --git a/src/main/java/org/unicitylabs/sdk/smt/radix/Branch.java b/src/main/java/org/unicitylabs/sdk/smt/radix/Branch.java new file mode 100644 index 0000000..0b3beb0 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/smt/radix/Branch.java @@ -0,0 +1,26 @@ +package org.unicitylabs.sdk.smt.radix; + +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; + +import java.math.BigInteger; + +/** + * Sparse merkle tree branch structure. + */ +public interface Branch { + + /** + * Get branch path from leaf to root. + * + * @return path + */ + BigInteger getPath(); + + /** + * Finalize current branch. + * + * @param hashAlgorithm hash algorithm + * @return finalized branch + */ + FinalizedBranch finalize(HashAlgorithm hashAlgorithm); +} diff --git a/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedBranch.java new file mode 100644 index 0000000..603090b --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedBranch.java @@ -0,0 +1,16 @@ +package org.unicitylabs.sdk.smt.radix; + +import org.unicitylabs.sdk.crypto.hash.DataHash; + +/** + * Finalized branch in sparse merkle tree. + */ +public interface FinalizedBranch extends Branch { + + /** + * Get hash of the branch. + * + * @return hash + */ + DataHash getHash(); +} diff --git a/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedLeafBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedLeafBranch.java new file mode 100644 index 0000000..6e282c5 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedLeafBranch.java @@ -0,0 +1,80 @@ +package org.unicitylabs.sdk.smt.radix; + +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.DataHasher; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Objects; + +public class FinalizedLeafBranch implements LeafBranch, FinalizedBranch { + + private final BigInteger path; + private final byte[] key; + private final byte[] value; + private final DataHash hash; + + private FinalizedLeafBranch(BigInteger path, byte[] key, byte[] value, DataHash hash) { + this.path = path; + this.key = Arrays.copyOf(key, key.length); + this.value = Arrays.copyOf(value, value.length); + this.hash = hash; + } + + @Override + public BigInteger getPath() { + return this.path; + } + + @Override + public byte[] getKey() { + return Arrays.copyOf(this.key, this.key.length); + } + + @Override + public byte[] getValue() { + return Arrays.copyOf(this.value, this.value.length); + } + + @Override + public DataHash getHash() { + return this.hash; + } + + @Override + public FinalizedLeafBranch finalize(HashAlgorithm hashAlgorithm) { + return this; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof FinalizedLeafBranch)) { + return false; + } + FinalizedLeafBranch that = (FinalizedLeafBranch) o; + return Objects.equals(this.path, that.path) && Arrays.equals(this.value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(this.path, Arrays.hashCode(this.value)); + } + + public static FinalizedLeafBranch fromPendingLeaf( + HashAlgorithm hashAlgorithm, + PendingLeafBranch leaf + ) { + byte[] key = leaf.getKey(); + byte[] value = leaf.getValue(); + + + DataHash hash = new DataHasher(hashAlgorithm) + .update(new byte[]{0x00}) + .update(key) + .update(value) + .digest(); + + return new FinalizedLeafBranch(leaf.getPath(), key, value, hash); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedNodeBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedNodeBranch.java new file mode 100644 index 0000000..3e3d095 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/smt/radix/FinalizedNodeBranch.java @@ -0,0 +1,86 @@ +package org.unicitylabs.sdk.smt.radix; + +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.util.LongConverter; + +import java.math.BigInteger; + +public class FinalizedNodeBranch implements NodeBranch, FinalizedBranch { + private final BigInteger path; + private final int depth; + private final FinalizedBranch left; + private final FinalizedBranch right; + private final DataHash hash; + + private FinalizedNodeBranch( + BigInteger path, + int depth, + FinalizedBranch left, + FinalizedBranch right, + DataHash hash + ) { + this.path = path; + this.depth = depth; + this.left = left; + this.right = right; + this.hash = hash; + } + + @Override + public BigInteger getPath() { + return this.path; + } + + @Override + public int getDepth() { + return this.depth; + } + + @Override + public FinalizedBranch getLeft() { + return this.left; + } + + @Override + public FinalizedBranch getRight() { + return this.right; + } + + @Override + public DataHash getHash() { + return this.hash; + } + + public static FinalizedNodeBranch fromPendingNode(HashAlgorithm hashAlgorithm, PendingNodeBranch node) { + FinalizedBranch left = node.getLeft() != null ? node.getLeft().finalize(hashAlgorithm) : null; + FinalizedBranch right = node.getRight() != null ? node.getRight().finalize(hashAlgorithm) : null; + + if (left == null && right == null) { + return new FinalizedNodeBranch(node.getPath(), node.getDepth(), left, right, new DataHash(HashAlgorithm.SHA256, new byte[32])); + } + + if (left != null && right == null) { + return new FinalizedNodeBranch(node.getPath(), node.getDepth(), left, right, left.getHash()); + } + + if (left == null) { + return new FinalizedNodeBranch(node.getPath(), node.getDepth(), left, right, right.getHash()); + } + + DataHash hash = new DataHasher(hashAlgorithm) + .update(new byte[]{0x01}) + .update(LongConverter.encode(node.getDepth())) + .update(left.getHash().getData()) + .update(right.getHash().getData()) + .digest(); + + return new FinalizedNodeBranch(node.getPath(), node.getDepth(), left, right, hash); + } + + @Override + public FinalizedNodeBranch finalize(HashAlgorithm hashAlgorithm) { + return this; + } +} diff --git a/src/main/java/org/unicitylabs/sdk/smt/radix/LeafBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radix/LeafBranch.java new file mode 100644 index 0000000..4cda9b5 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/smt/radix/LeafBranch.java @@ -0,0 +1,16 @@ +package org.unicitylabs.sdk.smt.radix; + +/** + * Leaf branch in a sparse merkle tree. + */ +public interface LeafBranch extends Branch { + + byte[] getKey(); + + /** + * Get value stored in the leaf. + * + * @return value stored in the leaf + */ + byte[] getValue(); +} diff --git a/src/main/java/org/unicitylabs/sdk/smt/radix/NodeBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radix/NodeBranch.java new file mode 100644 index 0000000..109130f --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/smt/radix/NodeBranch.java @@ -0,0 +1,23 @@ +package org.unicitylabs.sdk.smt.radix; + +/** + * Node branch in merkle tree. + */ +public interface NodeBranch extends Branch { + + int getDepth(); + + /** + * Get left branch. + * + * @return left branch + */ + Branch getLeft(); + + /** + * Get right branch. + * + * @return right branch + */ + Branch getRight(); +} diff --git a/src/main/java/org/unicitylabs/sdk/smt/radix/PendingLeafBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radix/PendingLeafBranch.java new file mode 100644 index 0000000..3b4c9d1 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/smt/radix/PendingLeafBranch.java @@ -0,0 +1,54 @@ +package org.unicitylabs.sdk.smt.radix; + +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Objects; + +public class PendingLeafBranch implements LeafBranch { + private final BigInteger path; + private final byte[] key; + private final byte[] value; + + public PendingLeafBranch(BigInteger path, byte[] key, byte[] value) { + this.path = path; + this.key = Arrays.copyOf(key, key.length); + this.value = Arrays.copyOf(value, value.length); + } + + @Override + public BigInteger getPath() { + return this.path; + } + + @Override + public byte[] getKey() { + return Arrays.copyOf(this.key, this.key.length); + } + + @Override + public byte[] getValue() { + return Arrays.copyOf(this.value, this.value.length); + } + + @Override + public FinalizedLeafBranch finalize(HashAlgorithm hashAlgorithm) { + return FinalizedLeafBranch.fromPendingLeaf(hashAlgorithm, this); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof PendingLeafBranch)) { + return false; + } + + PendingLeafBranch that = (PendingLeafBranch) o; + return Objects.equals(this.path, that.path) && Objects.deepEquals(this.value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(this.path, Arrays.hashCode(this.value)); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/smt/radix/PendingNodeBranch.java b/src/main/java/org/unicitylabs/sdk/smt/radix/PendingNodeBranch.java new file mode 100644 index 0000000..6c34f49 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/smt/radix/PendingNodeBranch.java @@ -0,0 +1,47 @@ +package org.unicitylabs.sdk.smt.radix; + +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; + +import java.math.BigInteger; + +public class PendingNodeBranch implements NodeBranch { + private final BigInteger path; + private final int depth; + private final Branch left; + private final Branch right; + + public PendingNodeBranch(BigInteger path, int depth, Branch left, Branch right) { + this.path = path; + this.depth = depth; + this.left = left; + this.right = right; + } + + @Override + public BigInteger getPath() { + return this.path; + } + + @Override + public int getDepth() { + return this.depth; + } + + @Override + public Branch getLeft() { + return this.left; + } + + @Override + public Branch getRight() { + return this.right; + } + + @Override + public FinalizedNodeBranch finalize(HashAlgorithm hashAlgorithm) { + return FinalizedNodeBranch.fromPendingNode( + hashAlgorithm, + this + ); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/smt/radix/SparseMerkleTree.java b/src/main/java/org/unicitylabs/sdk/smt/radix/SparseMerkleTree.java new file mode 100644 index 0000000..99205df --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/smt/radix/SparseMerkleTree.java @@ -0,0 +1,126 @@ +package org.unicitylabs.sdk.smt.radix; + +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.smt.BranchExistsException; +import org.unicitylabs.sdk.smt.CommonPath; +import org.unicitylabs.sdk.smt.LeafOutOfBoundsException; +import org.unicitylabs.sdk.util.BitString; + +import java.math.BigInteger; + +/** + * Sparse Merkle tree implementation. + */ +public class SparseMerkleTree { + + private Branch left = null; + private Branch right = null; + + private final HashAlgorithm hashAlgorithm; + + /** + * Create sparse Merkle tree with given hash algorithm. + * + * @param hashAlgorithm hash algorithm + */ + public SparseMerkleTree(HashAlgorithm hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + } + + /** + * Add leaf to the tree at given path. + * + * @param key path of the leaf + * @param data data of the leaf + * @throws BranchExistsException if branch already exists at the path + * @throws LeafOutOfBoundsException if leaf is out of bounds + * @throws IllegalArgumentException if path is less than 1 + */ + public synchronized void addLeaf(byte[] key, byte[] data) + throws BranchExistsException, LeafOutOfBoundsException { + BigInteger path = BitString.fromBytesReversedLSB(key).toBigInteger(); + + if (path.compareTo(BigInteger.ONE) <= 0) { + throw new IllegalArgumentException("Path must be greater than 0"); + } + + boolean isRight = path.testBit(0); + Branch branch = isRight ? this.right : this.left; + Branch result = branch != null + ? SparseMerkleTree.buildTree(branch, path, 0, key, data) + : new PendingLeafBranch(path, key, data); + + if (isRight) { + this.right = result; + } else { + this.left = result; + } + } + + /** + * Calculate root of the tree. + * + * @return root node and its state + */ + public synchronized FinalizedNodeBranch calculateRoot() { + FinalizedBranch left = this.left != null ? this.left.finalize(this.hashAlgorithm) : null; + FinalizedBranch right = this.right != null ? this.right.finalize(this.hashAlgorithm) : null; + this.left = left; + this.right = right; + + return new PendingNodeBranch(BigInteger.ONE, 0, left, right).finalize(hashAlgorithm); + } + + private static Branch buildTree(Branch branch, BigInteger remainingPath, int depth, byte[] key, + byte[] value) throws BranchExistsException, LeafOutOfBoundsException { + CommonPath commonPath = CommonPath.create(remainingPath, branch.getPath()); + int commonPathLength = commonPath.getLength(); + boolean isRight = remainingPath.shiftRight(commonPathLength).testBit(0); + + if (commonPath.getPath().equals(remainingPath)) { + throw new BranchExistsException(); + } + + if (branch instanceof LeafBranch) { + if (commonPath.getPath().equals(branch.getPath())) { + throw new LeafOutOfBoundsException(); + } + + LeafBranch leafBranch = (LeafBranch) branch; + + LeafBranch oldBranch = new PendingLeafBranch( + branch.getPath().shiftRight(commonPathLength), leafBranch.getKey(), + leafBranch.getValue()); + LeafBranch newBranch = new PendingLeafBranch( + remainingPath.shiftRight(commonPathLength), key, value); + return new PendingNodeBranch(commonPath.getPath(), depth + commonPathLength, + isRight ? oldBranch : newBranch, isRight ? newBranch : oldBranch); + } + + NodeBranch nodeBranch = (NodeBranch) branch; + + // if node branch is split in the middle + if (commonPath.getPath().compareTo(branch.getPath()) < 0) { + LeafBranch newBranch = new PendingLeafBranch( + remainingPath.shiftRight(commonPathLength), key, value); + NodeBranch oldBranch = new PendingNodeBranch( + branch.getPath().shiftRight(commonPathLength), nodeBranch.getDepth(), + nodeBranch.getLeft(), nodeBranch.getRight()); + return new PendingNodeBranch(commonPath.getPath(), depth + commonPathLength, + isRight ? oldBranch : newBranch, isRight ? newBranch : oldBranch); + } + + if (isRight) { + return new PendingNodeBranch(nodeBranch.getPath(), nodeBranch.getDepth(), + nodeBranch.getLeft(), + SparseMerkleTree.buildTree(nodeBranch.getRight(), + remainingPath.shiftRight(commonPathLength), depth + commonPathLength, key, value)); + } + + return new PendingNodeBranch(nodeBranch.getPath(), nodeBranch.getDepth(), + SparseMerkleTree.buildTree(nodeBranch.getLeft(), + remainingPath.shiftRight(commonPathLength), depth + commonPathLength, key, value), + nodeBranch.getRight()); + } +} + diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/Branch.java b/src/main/java/org/unicitylabs/sdk/smt/sum/Branch.java similarity index 80% rename from src/main/java/org/unicitylabs/sdk/mtree/sum/Branch.java rename to src/main/java/org/unicitylabs/sdk/smt/sum/Branch.java index 1818db3..ad1c518 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/Branch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/Branch.java @@ -1,7 +1,8 @@ -package org.unicitylabs.sdk.mtree.sum; +package org.unicitylabs.sdk.smt.sum; + +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import java.math.BigInteger; -import org.unicitylabs.sdk.hash.HashAlgorithm; /** * Branch in a sparse merkle sum tree. diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/FinalizedBranch.java b/src/main/java/org/unicitylabs/sdk/smt/sum/FinalizedBranch.java similarity index 78% rename from src/main/java/org/unicitylabs/sdk/mtree/sum/FinalizedBranch.java rename to src/main/java/org/unicitylabs/sdk/smt/sum/FinalizedBranch.java index ad8475a..2163141 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/FinalizedBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/FinalizedBranch.java @@ -1,7 +1,8 @@ -package org.unicitylabs.sdk.mtree.sum; +package org.unicitylabs.sdk.smt.sum; + +import org.unicitylabs.sdk.crypto.hash.DataHash; import java.math.BigInteger; -import org.unicitylabs.sdk.hash.DataHash; /** * Finalized branch in sparse merkle sum tree. diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/FinalizedLeafBranch.java b/src/main/java/org/unicitylabs/sdk/smt/sum/FinalizedLeafBranch.java similarity index 70% rename from src/main/java/org/unicitylabs/sdk/mtree/sum/FinalizedLeafBranch.java rename to src/main/java/org/unicitylabs/sdk/smt/sum/FinalizedLeafBranch.java index 50ba112..29a43a0 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/FinalizedLeafBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/FinalizedLeafBranch.java @@ -1,14 +1,15 @@ -package org.unicitylabs.sdk.mtree.sum; +package org.unicitylabs.sdk.smt.sum; -import java.math.BigInteger; -import java.util.Objects; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.mtree.sum.SparseMerkleSumTree.LeafValue; +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.CborSerializer; +import org.unicitylabs.sdk.smt.sum.SparseMerkleSumTree.LeafValue; import org.unicitylabs.sdk.util.BigIntegerConverter; +import java.math.BigInteger; +import java.util.Objects; + /** * Finalized leaf branch in a sparse merkle sum tree. */ @@ -33,19 +34,19 @@ private FinalizedLeafBranch(BigInteger path, LeafValue value, DataHash hash) { * @return finalized leaf branch */ public static FinalizedLeafBranch create( - BigInteger path, - LeafValue value, - HashAlgorithm hashAlgorithm + BigInteger path, + LeafValue value, + HashAlgorithm hashAlgorithm ) { DataHash hash = new DataHasher(hashAlgorithm) - .update( - CborSerializer.encodeArray( - CborSerializer.encodeByteString(BigIntegerConverter.encode(path)), - CborSerializer.encodeByteString(value.getValue()), - CborSerializer.encodeByteString(BigIntegerConverter.encode(value.getCounter())) + .update( + CborSerializer.encodeArray( + CborSerializer.encodeByteString(BigIntegerConverter.encode(path)), + CborSerializer.encodeByteString(value.getValue()), + CborSerializer.encodeByteString(BigIntegerConverter.encode(value.getCounter())) + ) ) - ) - .digest(); + .digest(); return new FinalizedLeafBranch(path, value, hash); } diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/FinalizedNodeBranch.java b/src/main/java/org/unicitylabs/sdk/smt/sum/FinalizedNodeBranch.java similarity index 66% rename from src/main/java/org/unicitylabs/sdk/mtree/sum/FinalizedNodeBranch.java rename to src/main/java/org/unicitylabs/sdk/smt/sum/FinalizedNodeBranch.java index 2093a7f..da42dcd 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/FinalizedNodeBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/FinalizedNodeBranch.java @@ -1,13 +1,14 @@ -package org.unicitylabs.sdk.mtree.sum; +package org.unicitylabs.sdk.smt.sum; -import java.math.BigInteger; -import java.util.Objects; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; +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.CborSerializer; import org.unicitylabs.sdk.util.BigIntegerConverter; +import java.math.BigInteger; +import java.util.Objects; + /** * Finalized node branch in a sparse merkle sum tree. */ @@ -20,11 +21,11 @@ class FinalizedNodeBranch implements NodeBranch, FinalizedBranch { private final DataHash hash; private FinalizedNodeBranch( - BigInteger path, - FinalizedBranch left, - FinalizedBranch right, - BigInteger counter, - DataHash hash + BigInteger path, + FinalizedBranch left, + FinalizedBranch right, + BigInteger counter, + DataHash hash ) { this.path = path; this.left = left; @@ -43,10 +44,10 @@ private FinalizedNodeBranch( * @return finalized node branch */ public static FinalizedNodeBranch create( - BigInteger path, - FinalizedBranch left, - FinalizedBranch right, - HashAlgorithm hashAlgorithm + BigInteger path, + FinalizedBranch left, + FinalizedBranch right, + HashAlgorithm hashAlgorithm ) { byte[] leftHash = left == null ? null : left.getHash().getData(); byte[] rightHash = right == null ? null : right.getHash().getData(); @@ -54,16 +55,16 @@ public static FinalizedNodeBranch create( BigInteger rightCounter = right == null ? BigInteger.ZERO : right.getCounter(); DataHash hash = new DataHasher(hashAlgorithm) - .update( - CborSerializer.encodeArray( - CborSerializer.encodeByteString(BigIntegerConverter.encode(path)), - CborSerializer.encodeOptional(leftHash, CborSerializer::encodeByteString), - CborSerializer.encodeByteString(BigIntegerConverter.encode(leftCounter)), - CborSerializer.encodeOptional(rightHash, CborSerializer::encodeByteString), - CborSerializer.encodeByteString(BigIntegerConverter.encode(rightCounter)) + .update( + CborSerializer.encodeArray( + CborSerializer.encodeByteString(BigIntegerConverter.encode(path)), + CborSerializer.encodeNullable(leftHash, CborSerializer::encodeByteString), + CborSerializer.encodeByteString(BigIntegerConverter.encode(leftCounter)), + CborSerializer.encodeNullable(rightHash, CborSerializer::encodeByteString), + CborSerializer.encodeByteString(BigIntegerConverter.encode(rightCounter)) + ) ) - ) - .digest(); + .digest(); BigInteger counter = leftCounter.add(rightCounter); @@ -107,7 +108,7 @@ public boolean equals(Object o) { } FinalizedNodeBranch that = (FinalizedNodeBranch) o; return Objects.equals(this.path, that.path) && Objects.equals(this.left, that.left) - && Objects.equals(this.right, that.right); + && Objects.equals(this.right, that.right); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/LeafBranch.java b/src/main/java/org/unicitylabs/sdk/smt/sum/LeafBranch.java similarity index 66% rename from src/main/java/org/unicitylabs/sdk/mtree/sum/LeafBranch.java rename to src/main/java/org/unicitylabs/sdk/smt/sum/LeafBranch.java index b57e43a..29e7d9a 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/LeafBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/LeafBranch.java @@ -1,6 +1,6 @@ -package org.unicitylabs.sdk.mtree.sum; +package org.unicitylabs.sdk.smt.sum; -import org.unicitylabs.sdk.mtree.sum.SparseMerkleSumTree.LeafValue; +import org.unicitylabs.sdk.smt.sum.SparseMerkleSumTree.LeafValue; /** * Leaf branch in a sparse merkle sum tree. diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/NodeBranch.java b/src/main/java/org/unicitylabs/sdk/smt/sum/NodeBranch.java similarity index 87% rename from src/main/java/org/unicitylabs/sdk/mtree/sum/NodeBranch.java rename to src/main/java/org/unicitylabs/sdk/smt/sum/NodeBranch.java index 1ba9a2b..357a968 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/NodeBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/NodeBranch.java @@ -1,4 +1,4 @@ -package org.unicitylabs.sdk.mtree.sum; +package org.unicitylabs.sdk.smt.sum; /** * Node branch in sparse merkle sum tree. diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/PendingLeafBranch.java b/src/main/java/org/unicitylabs/sdk/smt/sum/PendingLeafBranch.java similarity index 87% rename from src/main/java/org/unicitylabs/sdk/mtree/sum/PendingLeafBranch.java rename to src/main/java/org/unicitylabs/sdk/smt/sum/PendingLeafBranch.java index bbdeec0..f4f10c4 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/PendingLeafBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/PendingLeafBranch.java @@ -1,9 +1,10 @@ -package org.unicitylabs.sdk.mtree.sum; +package org.unicitylabs.sdk.smt.sum; + +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.smt.sum.SparseMerkleSumTree.LeafValue; import java.math.BigInteger; import java.util.Objects; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.mtree.sum.SparseMerkleSumTree.LeafValue; /** * Pending leaf branch in a sparse merkle sum tree. diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/PendingNodeBranch.java b/src/main/java/org/unicitylabs/sdk/smt/sum/PendingNodeBranch.java similarity index 86% rename from src/main/java/org/unicitylabs/sdk/mtree/sum/PendingNodeBranch.java rename to src/main/java/org/unicitylabs/sdk/smt/sum/PendingNodeBranch.java index f17cded..891dca6 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/PendingNodeBranch.java +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/PendingNodeBranch.java @@ -1,8 +1,9 @@ -package org.unicitylabs.sdk.mtree.sum; +package org.unicitylabs.sdk.smt.sum; + +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import java.math.BigInteger; import java.util.Objects; -import org.unicitylabs.sdk.hash.HashAlgorithm; /** * Pending node branch in a sparse merkle sum tree. @@ -44,7 +45,7 @@ public Branch getRight() { @Override public FinalizedNodeBranch finalize(HashAlgorithm hashAlgorithm) { return FinalizedNodeBranch.create(this.path, this.left.finalize(hashAlgorithm), - this.right.finalize(hashAlgorithm), hashAlgorithm); + this.right.finalize(hashAlgorithm), hashAlgorithm); } @Override @@ -54,7 +55,7 @@ public boolean equals(Object o) { } PendingNodeBranch that = (PendingNodeBranch) o; return Objects.equals(this.path, that.path) && Objects.equals(this.left, that.left) - && Objects.equals(this.right, that.right); + && Objects.equals(this.right, that.right); } @Override diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTree.java b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTree.java similarity index 80% rename from src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTree.java rename to src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTree.java index 565ed09..467fb3a 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTree.java +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTree.java @@ -1,12 +1,13 @@ -package org.unicitylabs.sdk.mtree.sum; +package org.unicitylabs.sdk.smt.sum; + +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.smt.BranchExistsException; +import org.unicitylabs.sdk.smt.CommonPath; +import org.unicitylabs.sdk.smt.LeafOutOfBoundsException; import java.math.BigInteger; import java.util.Arrays; import java.util.Objects; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.mtree.BranchExistsException; -import org.unicitylabs.sdk.mtree.CommonPath; -import org.unicitylabs.sdk.mtree.LeafOutOfBoundsException; /** * Sparse Merkle Sum Tree implementation. @@ -34,12 +35,11 @@ public SparseMerkleSumTree(HashAlgorithm hashAlgorithm) { * @param value value stored in the leaf * @throws BranchExistsException if a branch already exists at the given path * @throws LeafOutOfBoundsException if a leaf already exists at the given path - * @throws IllegalArgumentException if the path is less than or equal to 0 or if the counter is - * negative + * @throws IllegalArgumentException if the path is less than or equal to 0 or if the counter is negative * @throws NullPointerException if the path or value is null */ public synchronized void addLeaf(BigInteger path, LeafValue value) - throws BranchExistsException, LeafOutOfBoundsException { + throws BranchExistsException, LeafOutOfBoundsException { Objects.requireNonNull(path, "Path cannot be null"); Objects.requireNonNull(value, "Value cannot be null"); @@ -54,8 +54,8 @@ public synchronized void addLeaf(BigInteger path, LeafValue value) boolean isRight = path.testBit(0); Branch branch = isRight ? this.right : this.left; Branch result = branch != null - ? SparseMerkleSumTree.buildTree(branch, path, value) - : new PendingLeafBranch(path, value); + ? SparseMerkleSumTree.buildTree(branch, path, value) + : new PendingLeafBranch(path, value); if (isRight) { this.right = result; @@ -79,7 +79,7 @@ public synchronized SparseMerkleSumTreeRootNode calculateRoot() { } private static Branch buildTree(Branch branch, BigInteger remainingPath, LeafValue value) - throws BranchExistsException, LeafOutOfBoundsException { + throws BranchExistsException, LeafOutOfBoundsException { CommonPath commonPath = CommonPath.create(remainingPath, branch.getPath()); boolean isRight = remainingPath.shiftRight(commonPath.getLength()).testBit(0); @@ -95,11 +95,11 @@ private static Branch buildTree(Branch branch, BigInteger remainingPath, LeafVal LeafBranch leafBranch = (LeafBranch) branch; LeafBranch oldBranch = new PendingLeafBranch( - branch.getPath().shiftRight(commonPath.getLength()), leafBranch.getValue()); + branch.getPath().shiftRight(commonPath.getLength()), leafBranch.getValue()); LeafBranch newBranch = new PendingLeafBranch(remainingPath.shiftRight(commonPath.getLength()), - value); + value); return new PendingNodeBranch(commonPath.getPath(), isRight ? oldBranch : newBranch, - isRight ? newBranch : oldBranch); + isRight ? newBranch : oldBranch); } NodeBranch nodeBranch = (NodeBranch) branch; @@ -107,23 +107,23 @@ private static Branch buildTree(Branch branch, BigInteger remainingPath, LeafVal // if node branch is split in the middle if (commonPath.getPath().compareTo(branch.getPath()) < 0) { LeafBranch newBranch = new PendingLeafBranch(remainingPath.shiftRight(commonPath.getLength()), - value); + value); NodeBranch oldBranch = new PendingNodeBranch( - branch.getPath().shiftRight(commonPath.getLength()), nodeBranch.getLeft(), - nodeBranch.getRight()); + branch.getPath().shiftRight(commonPath.getLength()), nodeBranch.getLeft(), + nodeBranch.getRight()); return new PendingNodeBranch(commonPath.getPath(), isRight ? oldBranch : newBranch, - isRight ? newBranch : oldBranch); + isRight ? newBranch : oldBranch); } if (isRight) { return new PendingNodeBranch(nodeBranch.getPath(), nodeBranch.getLeft(), - SparseMerkleSumTree.buildTree(nodeBranch.getRight(), - remainingPath.shiftRight(commonPath.getLength()), value)); + SparseMerkleSumTree.buildTree(nodeBranch.getRight(), + remainingPath.shiftRight(commonPath.getLength()), value)); } return new PendingNodeBranch(nodeBranch.getPath(), - SparseMerkleSumTree.buildTree(nodeBranch.getLeft(), - remainingPath.shiftRight(commonPath.getLength()), value), nodeBranch.getRight()); + SparseMerkleSumTree.buildTree(nodeBranch.getLeft(), + remainingPath.shiftRight(commonPath.getLength()), value), nodeBranch.getRight()); } /** diff --git a/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePath.java b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePath.java new file mode 100644 index 0000000..da2e867 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePath.java @@ -0,0 +1,179 @@ +package org.unicitylabs.sdk.smt.sum; + +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.DataHasher; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.smt.MerkleTreePathVerificationResult; +import org.unicitylabs.sdk.util.BigIntegerConverter; + +import java.math.BigInteger; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Path in a sparse merkle sum tree. + */ +public class SparseMerkleSumTreePath { + + private final DataHash rootHash; + private final List steps; + + SparseMerkleSumTreePath( + DataHash rootHash, + List steps + ) { + Objects.requireNonNull(rootHash, "root cannot be null"); + Objects.requireNonNull(steps, "steps cannot be null"); + + this.rootHash = rootHash; + this.steps = List.copyOf(steps); + } + + /** + * Get root hash. + * + * @return root hash + */ + public DataHash getRootHash() { + return this.rootHash; + } + + /** + * Get steps of the path from leaf to the root. + * + * @return steps + */ + public List getSteps() { + return this.steps; + } + + /** + * Verify the path against the given state ID. + * + * @param stateId state ID to verify against + * @return result of the verification + */ + public MerkleTreePathVerificationResult verify(BigInteger stateId) { + if (this.steps.isEmpty()) { + return new MerkleTreePathVerificationResult(false, false); + } + + SparseMerkleSumTreePathStep step = this.steps.get(0); + byte[] currentData; + BigInteger currentPath = step.getPath(); + BigInteger currentSum = step.getValue(); + if (step.getPath().compareTo(BigInteger.ONE) > 0) { + DataHash hash = new DataHasher(this.rootHash.getAlgorithm()) + .update( + CborSerializer.encodeArray( + CborSerializer.encodeByteString(BigIntegerConverter.encode(step.getPath())), + CborSerializer.encodeNullable( + step.getData().orElse(null), + CborSerializer::encodeByteString + ), + CborSerializer.encodeByteString(BigIntegerConverter.encode(step.getValue())) + ) + ) + .digest(); + + currentData = hash.getData(); + } else { + currentPath = BigInteger.ONE; + currentData = step.getData().orElse(null); + } + + SparseMerkleSumTreePathStep previousStep = step; + for (int i = 1; i < this.steps.size(); i++) { + step = this.steps.get(i); + boolean isRight = previousStep.getPath().testBit(0); + + byte[] leftHash = isRight ? step.getData().orElse(null) : currentData; + byte[] rightHash = isRight ? currentData : step.getData().orElse(null); + BigInteger leftCounter = isRight ? step.getValue() : currentSum; + BigInteger rightCounter = isRight ? currentSum : step.getValue(); + + DataHash hash = new DataHasher(this.rootHash.getAlgorithm()) + .update( + CborSerializer.encodeArray( + CborSerializer.encodeByteString(BigIntegerConverter.encode(step.getPath())), + CborSerializer.encodeNullable(leftHash, CborSerializer::encodeByteString), + CborSerializer.encodeByteString(BigIntegerConverter.encode(leftCounter)), + CborSerializer.encodeNullable(rightHash, CborSerializer::encodeByteString), + CborSerializer.encodeByteString(BigIntegerConverter.encode(rightCounter)) + ) + ) + .digest(); + + currentData = hash.getData(); + + int length = step.getPath().bitLength() - 1; + if (length < 0) { + return new MerkleTreePathVerificationResult(false, false); + } + currentPath = currentPath.shiftLeft(length) + .or(step.getPath().and(BigInteger.ONE.shiftLeft(length).subtract(BigInteger.ONE))); + currentSum = currentSum.add(step.getValue()); + previousStep = step; + } + + boolean pathValid = currentData != null + && this.rootHash.equals(new DataHash(this.rootHash.getAlgorithm(), currentData)); + boolean pathIncluded = currentPath.compareTo(stateId) == 0; + + return new MerkleTreePathVerificationResult(pathValid, pathIncluded); + } + + /** + * Create path from CBOR bytes. + * + * @param bytes CBOR bytes + * @return path + */ + public static SparseMerkleSumTreePath fromCbor(byte[] bytes) { + List data = CborDeserializer.decodeArray(bytes, 2); + + return new SparseMerkleSumTreePath( + DataHash.fromCbor(data.get(0)), + CborDeserializer.decodeArray(data.get(1)).stream() + .map(SparseMerkleSumTreePathStep::fromCbor) + .collect(Collectors.toList()) + ); + } + + /** + * Serialize path to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeArray( + this.rootHash.toCbor(), + CborSerializer.encodeArray( + this.steps.stream() + .map(SparseMerkleSumTreePathStep::toCbor) + .toArray(byte[][]::new) + ) + ); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof SparseMerkleSumTreePath)) { + return false; + } + SparseMerkleSumTreePath that = (SparseMerkleSumTreePath) o; + return Objects.equals(this.rootHash, that.rootHash) && Objects.equals(this.steps, that.steps); + } + + @Override + public int hashCode() { + return Objects.hash(this.rootHash, this.steps); + } + + @Override + public String toString() { + return String.format("MerkleTreePath{rootHash=%s, steps=%s}", this.rootHash, this.steps); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePathStep.java b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePathStep.java similarity index 59% rename from src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePathStep.java rename to src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePathStep.java index f4cd109..532fce8 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePathStep.java +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreePathStep.java @@ -1,18 +1,15 @@ -package org.unicitylabs.sdk.mtree.sum; +package org.unicitylabs.sdk.smt.sum; + +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.util.BigIntegerConverter; +import org.unicitylabs.sdk.util.HexConverter; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.math.BigInteger; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Optional; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.serializer.json.BigIntegerAsStringSerializer; -import org.unicitylabs.sdk.util.BigIntegerConverter; -import org.unicitylabs.sdk.util.HexConverter; /** * Step in a sparse merkle sum tree path. @@ -23,11 +20,10 @@ public class SparseMerkleSumTreePathStep { private final byte[] data; private final BigInteger value; - @JsonCreator SparseMerkleSumTreePathStep( - @JsonProperty("path") BigInteger path, - @JsonProperty("data") byte[] data, - @JsonProperty("value") BigInteger value + BigInteger path, + byte[] data, + BigInteger value ) { Objects.requireNonNull(path, "path cannot be null"); Objects.requireNonNull(value, "value cannot be null"); @@ -42,7 +38,6 @@ public class SparseMerkleSumTreePathStep { * * @return path */ - @JsonSerialize(using = BigIntegerAsStringSerializer.class) public BigInteger getPath() { return this.path; } @@ -61,37 +56,36 @@ public Optional getData() { * * @return value */ - @JsonSerialize(using = BigIntegerAsStringSerializer.class) public BigInteger getValue() { return this.value; } /** - * Create a step from CBOR bytes. + * Deserialize a step from CBOR bytes. * * @param bytes CBOR bytes * @return step */ public static SparseMerkleSumTreePathStep fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); + List data = CborDeserializer.decodeArray(bytes, 3); return new SparseMerkleSumTreePathStep( - BigIntegerConverter.decode(CborDeserializer.readByteString(data.get(0))), - CborDeserializer.readOptional(data.get(1), CborDeserializer::readByteString), - BigIntegerConverter.decode(CborDeserializer.readByteString(data.get(2))) + BigIntegerConverter.decode(CborDeserializer.decodeByteString(data.get(0))), + CborDeserializer.decodeNullable(data.get(1), CborDeserializer::decodeByteString), + BigIntegerConverter.decode(CborDeserializer.decodeByteString(data.get(2))) ); } /** - * Convert step to CBOR bytes. + * Serialize step to CBOR bytes. * * @return CBOR bytes */ public byte[] toCbor() { return CborSerializer.encodeArray( - CborSerializer.encodeByteString(BigIntegerConverter.encode(this.path)), - CborSerializer.encodeOptional(this.data, CborSerializer::encodeByteString), - CborSerializer.encodeByteString(BigIntegerConverter.encode(this.value)) + CborSerializer.encodeByteString(BigIntegerConverter.encode(this.path)), + CborSerializer.encodeNullable(this.data, CborSerializer::encodeByteString), + CborSerializer.encodeByteString(BigIntegerConverter.encode(this.value)) ); } @@ -102,7 +96,7 @@ public boolean equals(Object o) { } SparseMerkleSumTreePathStep that = (SparseMerkleSumTreePathStep) o; return Objects.equals(this.path, that.path) && Arrays.equals(this.data, that.data) - && Objects.equals(this.value, that.value); + && Objects.equals(this.value, that.value); } @Override @@ -113,9 +107,9 @@ public int hashCode() { @Override public String toString() { return String.format("MerkleTreePathStep{path=%s, data=%s, value=%s}", - this.path.toString(2), - this.data == null ? null : HexConverter.encode(this.data), - this.value + this.path.toString(2), + this.data == null ? null : HexConverter.encode(this.data), + this.value ); } } diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreeRootNode.java b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreeRootNode.java similarity index 53% rename from src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreeRootNode.java rename to src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreeRootNode.java index 40522f8..9e7d710 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreeRootNode.java +++ b/src/main/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreeRootNode.java @@ -1,12 +1,13 @@ -package org.unicitylabs.sdk.mtree.sum; +package org.unicitylabs.sdk.smt.sum; + +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.smt.CommonPath; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.mtree.CommonPath; /** * Sparse Merkle Sum Tree root node. @@ -20,12 +21,12 @@ private SparseMerkleSumTreeRootNode(FinalizedNodeBranch root) { } static SparseMerkleSumTreeRootNode create( - FinalizedBranch left, - FinalizedBranch right, - HashAlgorithm hashAlgorithm + FinalizedBranch left, + FinalizedBranch right, + HashAlgorithm hashAlgorithm ) { return new SparseMerkleSumTreeRootNode( - FinalizedNodeBranch.create(BigInteger.ONE, left, right, hashAlgorithm) + FinalizedNodeBranch.create(BigInteger.ONE, left, right, hashAlgorithm) ); } @@ -55,8 +56,8 @@ public BigInteger getValue() { */ public SparseMerkleSumTreePath getPath(BigInteger path) { return new SparseMerkleSumTreePath( - this.root.getHash(), - SparseMerkleSumTreeRootNode.generatePath(path, this.root) + this.root.getHash(), + SparseMerkleSumTreeRootNode.generatePath(path, this.root) ); } @@ -75,15 +76,15 @@ public int hashCode() { } private static List generatePath( - BigInteger remainingPath, - FinalizedBranch parent + BigInteger remainingPath, + FinalizedBranch parent ) { if (parent instanceof LeafBranch) { LeafBranch leaf = (LeafBranch) parent; return List.of(new SparseMerkleSumTreePathStep( - leaf.getPath(), - leaf.getValue().getValue(), - leaf.getValue().getCounter() + leaf.getPath(), + leaf.getValue().getValue(), + leaf.getValue().getCounter() )); } @@ -92,26 +93,26 @@ private static List generatePath( remainingPath = remainingPath.shiftRight(commonPath.getLength()); if (commonPath.getPath().compareTo(parent.getPath()) != 0 - || remainingPath.compareTo(BigInteger.ONE) == 0) { + || remainingPath.compareTo(BigInteger.ONE) == 0) { return List.of( - new SparseMerkleSumTreePathStep( - BigInteger.ZERO, - node.getLeft() == null - ? null - : node.getLeft().getHash().getData(), - node.getLeft() == null - ? BigInteger.ZERO - : node.getLeft().getCounter() - ), - new SparseMerkleSumTreePathStep( - node.getPath(), - node.getRight() == null - ? null - : node.getRight().getHash().getData(), - node.getRight() == null - ? BigInteger.ZERO - : node.getRight().getCounter() - ) + new SparseMerkleSumTreePathStep( + BigInteger.ZERO, + node.getLeft() == null + ? null + : node.getLeft().getHash().getData(), + node.getLeft() == null + ? BigInteger.ZERO + : node.getLeft().getCounter() + ), + new SparseMerkleSumTreePathStep( + node.getPath(), + node.getRight() == null + ? null + : node.getRight().getHash().getData(), + node.getRight() == null + ? BigInteger.ZERO + : node.getRight().getCounter() + ) ); } @@ -120,24 +121,24 @@ private static List generatePath( FinalizedBranch siblingBranch = isRight ? node.getLeft() : node.getRight(); SparseMerkleSumTreePathStep step = new SparseMerkleSumTreePathStep( - node.getPath(), - siblingBranch == null ? null : siblingBranch.getHash().getData(), - siblingBranch == null ? BigInteger.ZERO : siblingBranch.getCounter() + node.getPath(), + siblingBranch == null ? null : siblingBranch.getHash().getData(), + siblingBranch == null ? BigInteger.ZERO : siblingBranch.getCounter() ); if (branch == null) { return List.of( - new SparseMerkleSumTreePathStep( - isRight ? BigInteger.ONE : BigInteger.ZERO, - null, - BigInteger.ZERO - ), - step + new SparseMerkleSumTreePathStep( + isRight ? BigInteger.ONE : BigInteger.ZERO, + null, + BigInteger.ZERO + ), + step ); } List list = new ArrayList<>( - SparseMerkleSumTreeRootNode.generatePath(remainingPath, branch) + SparseMerkleSumTreeRootNode.generatePath(remainingPath, branch) ); list.add(step); diff --git a/src/main/java/org/unicitylabs/sdk/token/Token.java b/src/main/java/org/unicitylabs/sdk/token/Token.java deleted file mode 100644 index 83e2943..0000000 --- a/src/main/java/org/unicitylabs/sdk/token/Token.java +++ /dev/null @@ -1,449 +0,0 @@ -package org.unicitylabs.sdk.token; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; -import org.unicitylabs.sdk.address.Address; -import org.unicitylabs.sdk.address.ProxyAddress; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.predicate.Predicate; -import org.unicitylabs.sdk.predicate.PredicateEngineService; -import org.unicitylabs.sdk.serializer.UnicityObjectMapper; -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.serializer.json.JsonSerializationException; -import org.unicitylabs.sdk.token.fungible.TokenCoinData; -import org.unicitylabs.sdk.transaction.MintTransaction; -import org.unicitylabs.sdk.transaction.MintTransactionReason; -import org.unicitylabs.sdk.transaction.Transaction; -import org.unicitylabs.sdk.transaction.TransferTransaction; -import org.unicitylabs.sdk.verification.VerificationException; -import org.unicitylabs.sdk.verification.VerificationResult; - -/** - * Token representation. - * - * @param mint transaction reason for current token. - */ -public class Token { - - /** - * Current token representation version. - */ - public static final String TOKEN_VERSION = "2.0"; - - private final TokenState state; - private final MintTransaction genesis; - private final List transactions; - private final List> nametags; - - @JsonCreator - Token( - @JsonProperty("state") - TokenState state, - @JsonProperty("genesis") - MintTransaction genesis, - @JsonProperty("transactions") - List transactions, - @JsonProperty("nametags") - List> nametags - ) { - Objects.requireNonNull(state, "State cannot be null"); - Objects.requireNonNull(genesis, "Genesis cannot be null"); - Objects.requireNonNull(transactions, "Transactions list cannot be null"); - Objects.requireNonNull(nametags, "Nametag tokens list cannot be null"); - - this.state = state; - this.genesis = genesis; - this.transactions = List.copyOf(transactions); - this.nametags = List.copyOf(nametags); - } - - /** - * Get token id from genesis. - * - * @return token id - */ - @JsonIgnore - public TokenId getId() { - return this.genesis.getData().getTokenId(); - } - - /** - * Get token type from genesis. - * - * @return token type - */ - @JsonIgnore - public TokenType getType() { - return this.genesis.getData().getTokenType(); - } - - /** - * Get token immutable data from genesis. - * - * @return token immutable data - */ - @JsonIgnore - public Optional getData() { - return this.genesis.getData().getTokenData(); - } - - /** - * Get token coins data from genesis. - * - * @return token coins data - */ - @JsonIgnore - public Optional getCoins() { - return this.genesis.getData().getCoinData(); - } - - /** - * Get token version. - * - * @return token version - */ - @JsonProperty(access = JsonProperty.Access.READ_ONLY) - public String getVersion() { - return TOKEN_VERSION; - } - - /** - * Get token current state. - * - * @return token state - */ - public TokenState getState() { - return this.state; - } - - /** - * Get token genesis. - * - * @return token genesis - */ - public MintTransaction getGenesis() { - return this.genesis; - } - - /** - * Get token transactions. - * - * @return token transactions - */ - public List getTransactions() { - return this.transactions; - } - - /** - * Get token current state nametags. - * - * @return nametags - */ - public List> getNametags() { - return this.nametags; - } - - /** - * Create token from mint transaction and initial state. Also verify if state is correct. - * - * @param trustBase trust base for mint transaction verification - * @param state initial state - * @param transaction mint transaction - * @param mint transaction reason - * @return token - * @throws VerificationException if token state is invalid - */ - public static Token create( - RootTrustBase trustBase, - TokenState state, - MintTransaction transaction - ) throws VerificationException { - return Token.create(trustBase, state, transaction, List.of()); - } - - /** - * Create token state from mint transaction, initial state and nametags. Also verify if state is - * correct. - * - * @param trustBase trust base for mint transaction verification - * @param state initial state - * @param transaction mint transaction - * @param nametags nametags associated with transaction - * @param mint transaction reason - * @return token - * @throws VerificationException if token state is invalid - */ - public static Token create( - RootTrustBase trustBase, - TokenState state, - MintTransaction transaction, - List> nametags - ) throws VerificationException { - Objects.requireNonNull(state, "State cannot be null"); - Objects.requireNonNull(transaction, "Genesis cannot be null"); - Objects.requireNonNull(trustBase, "Trust base cannot be null"); - Objects.requireNonNull(nametags, "Nametag tokens cannot be null"); - - Token token = new Token<>(state, transaction, List.of(), nametags); - VerificationResult result = token.verify(trustBase); - if (!result.isSuccessful()) { - throw new VerificationException("Token verification failed", result); - } - - return token; - } - - /** - * Update token to next state with given transfer transaction. - * - * @param trustBase trust base to verify latest state - * @param state current state - * @param transaction latest transaction - * @param nametags nametags associated with transaction - * @return tokest with latest state - * @throws VerificationException if token state is invalid - */ - public Token update( - RootTrustBase trustBase, - TokenState state, - TransferTransaction transaction, - List> nametags - ) throws VerificationException { - Objects.requireNonNull(state, "State cannot be null"); - Objects.requireNonNull(transaction, "Transaction cannot be null"); - Objects.requireNonNull(nametags, "Nametag tokens cannot be null"); - Objects.requireNonNull(trustBase, "Trust base cannot be null"); - - VerificationResult result = transaction.verify(trustBase, this); - - if (!result.isSuccessful()) { - throw new VerificationException("Transaction verification failed", result); - } - - LinkedList transactions = new LinkedList<>(this.transactions); - transactions.add(transaction); - Token token = new Token<>(state, this.genesis, transactions, nametags); - - result = token.verifyNametagTokens(trustBase); - if (!result.isSuccessful()) { - throw new VerificationException("Nametag tokens verification failed", result); - } - - result = token.verifyRecipient(); - if (!result.isSuccessful()) { - throw new VerificationException("Recipient verification failed", result); - } - - result = token.verifyRecipientData(); - if (!result.isSuccessful()) { - throw new VerificationException("Recipient data verification failed", result); - } - - return token; - } - - /** - * Verify current token state against trustbase. - * - * @param trustBase trust base to verify state against - * @return verification result - */ - public VerificationResult verify(RootTrustBase trustBase) { - List results = new ArrayList<>(); - results.add( - VerificationResult.fromChildren( - "Genesis verification", - List.of(this.genesis.verify(trustBase)) - ) - ); - - for (int i = 0; i < this.transactions.size(); i++) { - TransferTransaction transaction = this.transactions.get(i); - results.add( - transaction.verify( - trustBase, - new Token<>( - transaction.getData().getSourceState(), - this.genesis, - this.transactions.subList(0, i), - transaction.getData().getNametags() - ) - ) - ); - } - - results.add( - VerificationResult.fromChildren( - "Current state verification", - List.of( - this.verifyNametagTokens(trustBase), - this.verifyRecipient(), - this.verifyRecipientData() - ) - ) - ); - - return VerificationResult.fromChildren("Token verification", results); - } - - /** - * Verify token nametag tokens against trust base. - * - * @param trustBase trust base to verify against - * @return verification result - */ - public VerificationResult verifyNametagTokens(RootTrustBase trustBase) { - return VerificationResult.fromChildren( - "Nametag verification", - this.nametags.stream() - .map(token -> token.verify(trustBase)) - .collect(Collectors.toList())); - } - - /** - * Verify if token owner is the result of last transaction. - * - * @return verification result - */ - public VerificationResult verifyRecipient() { - Predicate predicate = PredicateEngineService.createPredicate(this.state.getPredicate()); - Address expectedRecipient = predicate.getReference().toAddress(); - - Transaction previousTransaction = this.transactions.isEmpty() - ? this.genesis - : this.transactions.get(this.transactions.size() - 1); - - Address transactionRecipient = ProxyAddress.resolve( - previousTransaction.getData().getRecipient(), this.nametags); - return VerificationResult.fromChildren("Recipient verification", List.of( - expectedRecipient.equals(transactionRecipient) - ? VerificationResult.success() - : VerificationResult.fail("Recipient address mismatch") - )); - } - - /** - * Verify if token state data matches last transaction recipient data hash. - * - * @return verification result - */ - public VerificationResult verifyRecipientData() { - Transaction previousTransaction = this.transactions.isEmpty() - ? this.genesis - : this.transactions.get(this.transactions.size() - 1); - - return VerificationResult.fromChildren("Recipient data verification", List.of( - previousTransaction.containsRecipientData(this.state.getData().orElse(null)) - ? VerificationResult.success() - : VerificationResult.fail( - "State data hash does not match previous transaction recipient data hash") - )); - } - - /** - * Create token from CBOR bytes. - * - * @param bytes CBOR bytes - * @return token - */ - public static Token fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); - String version = CborDeserializer.readTextString(data.get(0)); - if (!Token.TOKEN_VERSION.equals(version)) { - throw new CborSerializationException("Invalid version: " + version); - } - - return new Token<>( - TokenState.fromCbor(data.get(1)), - MintTransaction.fromCbor(data.get(2)), - CborDeserializer.readArray(data.get(3)).stream() - .map(TransferTransaction::fromCbor) - .collect(Collectors.toList()), - CborDeserializer.readArray(data.get(4)).stream() - .map(Token::fromCbor) - .collect(Collectors.toList()) - ); - } - - /** - * Convert token to CBOR bytes. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeArray( - CborSerializer.encodeTextString(TOKEN_VERSION), - this.state.toCbor(), - this.genesis.toCbor(), - CborSerializer.encodeArray( - this.transactions.stream() - .map(TransferTransaction::toCbor) - .toArray(byte[][]::new) - ), - CborSerializer.encodeArray( - this.nametags.stream() - .map(Token::toCbor) - .toArray(byte[][]::new) - ) - ); - } - - /** - * Create token from JSON string. - * - * @param input JSON string - * @return token - */ - public static Token fromJson(String input) { - try { - return UnicityObjectMapper.JSON.readValue(input, Token.class); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(Token.class, e); - } - } - - /** - * Convert token to JSON string. - * - * @return JSON string - */ - public String toJson() { - try { - return UnicityObjectMapper.JSON.writeValueAsString(this); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(Token.class, e); - } - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof Token)) { - return false; - } - Token token = (Token) o; - return Objects.equals(this.state, token.state) && Objects.equals(this.genesis, - token.genesis) && Objects.equals(this.transactions, token.transactions) - && Objects.equals(this.nametags, token.nametags); - } - - @Override - public int hashCode() { - return Objects.hash(this.state, this.genesis, this.transactions, this.nametags); - } - - @Override - public String toString() { - return String.format("Token{state=%s, genesis=%s, transactions=%s, nametags=%s}", - this.state, this.genesis, this.transactions, this.nametags); - } -} \ No newline at end of file diff --git a/src/main/java/org/unicitylabs/sdk/token/TokenIdJson.java b/src/main/java/org/unicitylabs/sdk/token/TokenIdJson.java deleted file mode 100644 index 72f6eb9..0000000 --- a/src/main/java/org/unicitylabs/sdk/token/TokenIdJson.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.unicitylabs.sdk.token; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.exc.MismatchedInputException; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import java.io.IOException; - -/** - * Token id serializer and deserializer implementation. - */ -public class TokenIdJson { - - private TokenIdJson() { - } - - /** - * Token id serializer. - */ - public static class Serializer extends StdSerializer { - - /** - * Create token id serializer. - */ - public Serializer() { - super(TokenId.class); - } - - - /** - * Serialize token id. - * - * @param value token id - * @param gen json generator - * @param serializers serializer provider - * @throws IOException on serialization failure - */ - @Override - public void serialize(TokenId value, JsonGenerator gen, - SerializerProvider serializers) - throws IOException { - gen.writeObject(value.getBytes()); - } - } - - /** - * Token id deserializer. - */ - public static class Deserializer extends StdDeserializer { - - /** - * Create token id deserializer. - */ - public Deserializer() { - super(TokenId.class); - } - - - /** - * Deserialize token id. - * - * @param p Parser used for reading JSON content - * @param ctx Context that can be used to access information about this deserialization - * activity. - * @return token id - * @throws IOException on deserialization failure - */ - @Override - public TokenId deserialize(JsonParser p, DeserializationContext ctx) - throws IOException { - if (p.getCurrentToken() != JsonToken.VALUE_STRING) { - throw MismatchedInputException.from( - p, - TokenId.class, - "Expected string value" - ); - } - - try { - return new TokenId(p.readValueAs(byte[].class)); - } catch (Exception e) { - throw MismatchedInputException.from(p, TokenId.class, "Expected bytes"); - } - } - } -} - diff --git a/src/main/java/org/unicitylabs/sdk/token/TokenState.java b/src/main/java/org/unicitylabs/sdk/token/TokenState.java deleted file mode 100644 index 3c3d93a..0000000 --- a/src/main/java/org/unicitylabs/sdk/token/TokenState.java +++ /dev/null @@ -1,135 +0,0 @@ -package org.unicitylabs.sdk.token; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.predicate.EncodedPredicate; -import org.unicitylabs.sdk.predicate.SerializablePredicate; -import org.unicitylabs.sdk.predicate.SerializablePredicateJson; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.util.HexConverter; - -/** - * Represents a snapshot of token ownership and associated data. - */ -public class TokenState { - - private final SerializablePredicate predicate; - private final byte[] data; - - /** - * Create token state. - * - * @param predicate current state predicate - * @param data current state data - */ - @JsonCreator - public TokenState( - @JsonSerialize(using = SerializablePredicateJson.Serializer.class) - @JsonDeserialize(using = SerializablePredicateJson.Deserializer.class) - @JsonProperty("predicate") SerializablePredicate predicate, - @JsonProperty("data") byte[] data - ) { - Objects.requireNonNull(predicate, "Predicate cannot be null"); - - this.predicate = predicate; - this.data = data != null ? Arrays.copyOf(data, data.length) : null; - } - - /** - * Get current state predicate. - * - * @return state predicate - */ - public SerializablePredicate getPredicate() { - return this.predicate; - } - - /** - * Get current state data. - * - * @return state data - */ - public Optional getData() { - return this.data != null - ? Optional.of(Arrays.copyOf(this.data, this.data.length)) - : Optional.empty(); - } - - /** - * Calculate current state hash. - * - * @return state hash - */ - public DataHash calculateHash() { - return new DataHasher(HashAlgorithm.SHA256) - .update(this.toCbor()) - .digest(); - } - - /** - * Create current state from CBOR bytes. - * - * @param bytes CBOR bytes - * @return current state - */ - public static TokenState fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); - - return new TokenState( - EncodedPredicate.fromCbor(data.get(0)), - CborDeserializer.readOptional(data.get(1), CborDeserializer::readByteString) - ); - } - - /** - * Convert current state to CBOR bytes. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeArray( - CborSerializer.encodeArray( - CborSerializer.encodeUnsignedInteger(this.predicate.getEngine().ordinal()), - CborSerializer.encodeByteString(this.predicate.encode()), - CborSerializer.encodeByteString(this.predicate.encodeParameters()) - ), - CborSerializer.encodeOptional(this.data, CborSerializer::encodeByteString) - ); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof TokenState)) { - return false; - } - TokenState that = (TokenState) o; - return Arrays.equals(this.predicate.encode(), that.predicate.encode()) - && Arrays.equals(this.predicate.encodeParameters(), that.predicate.encodeParameters()) - && Objects.equals(this.predicate.getEngine(), that.predicate.getEngine()) - && Objects.deepEquals(this.data, that.data); - } - - @Override - public int hashCode() { - return Objects.hash(this.predicate, Arrays.hashCode(this.data)); - } - - @Override - public String toString() { - return String.format( - "TokenState{predicate=%s, data=%s}", - this.predicate, - this.data != null ? HexConverter.encode(this.data) : "null" - ); - } -} \ No newline at end of file diff --git a/src/main/java/org/unicitylabs/sdk/token/TokenType.java b/src/main/java/org/unicitylabs/sdk/token/TokenType.java deleted file mode 100644 index 08c8b57..0000000 --- a/src/main/java/org/unicitylabs/sdk/token/TokenType.java +++ /dev/null @@ -1,78 +0,0 @@ - -package org.unicitylabs.sdk.token; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import java.util.Arrays; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.util.HexConverter; - -/** - * Unique identifier describing the type/category of a token. - */ -@JsonSerialize(using = TokenTypeJson.Serializer.class) -@JsonDeserialize(using = TokenTypeJson.Deserializer.class) -public class TokenType { - - private final byte[] bytes; - - /** - * Token type constructor. - * - * @param bytes type bytes - */ - public TokenType(byte[] bytes) { - this.bytes = Arrays.copyOf(bytes, bytes.length); - } - - /** - * Get token type as bytes. - * - * @return type bytes - */ - public byte[] getBytes() { - return Arrays.copyOf(this.bytes, this.bytes.length); - } - - /** - * Create token type from CBOR. - * - * @param bytes CBOR bytes - * @return token type - */ - public static TokenType fromCbor(byte[] bytes) { - return new TokenType(CborDeserializer.readByteString(bytes)); - } - - /** - * Convert token type to CBOR. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeByteString(this.bytes); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - TokenType tokenType = (TokenType) o; - return Arrays.equals(this.bytes, tokenType.bytes); - } - - @Override - public int hashCode() { - return Arrays.hashCode(this.bytes); - } - - @Override - public String toString() { - return String.format("TokenType[%s]", HexConverter.encode(this.bytes)); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/token/TokenTypeJson.java b/src/main/java/org/unicitylabs/sdk/token/TokenTypeJson.java deleted file mode 100644 index 8aacde5..0000000 --- a/src/main/java/org/unicitylabs/sdk/token/TokenTypeJson.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.unicitylabs.sdk.token; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import java.io.IOException; - -/** - * Token type serializer and deserializer implementation. - */ -public class TokenTypeJson { - - private TokenTypeJson() { - } - - /** - * Token type serializer. - */ - public static class Serializer extends StdSerializer { - - /** - * Create token type serializer. - */ - public Serializer() { - super(TokenType.class); - } - - /** - * Serialize token type. - * - * @param value token type - * @param gen json generator - * @param serializers serializer provider - * @throws IOException on serialization failure - */ - @Override - public void serialize(TokenType value, JsonGenerator gen, SerializerProvider serializers) - throws IOException { - gen.writeObject(value.getBytes()); - } - } - - /** - * Token type deserializer. - */ - public static class Deserializer extends StdDeserializer { - - /** - * Create token type deserializer. - */ - public Deserializer() { - super(TokenType.class); - } - - /** - * Deserialize token type. - * - * @param p Parser used for reading JSON content - * @param ctx Context that can be used to access information about this deserialization - * activity. - * @return token type - * @throws IOException on deserialization failure - */ - @Override - public TokenType deserialize(JsonParser p, DeserializationContext ctx) - throws IOException { - return new TokenType(p.readValueAs(byte[].class)); - } - } -} - diff --git a/src/main/java/org/unicitylabs/sdk/token/fungible/CoinId.java b/src/main/java/org/unicitylabs/sdk/token/fungible/CoinId.java deleted file mode 100644 index ac32e37..0000000 --- a/src/main/java/org/unicitylabs/sdk/token/fungible/CoinId.java +++ /dev/null @@ -1,67 +0,0 @@ - -package org.unicitylabs.sdk.token.fungible; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import java.util.Arrays; -import org.unicitylabs.sdk.util.BitString; -import org.unicitylabs.sdk.util.HexConverter; - -/** - * Coin ID representation. - */ -@JsonSerialize(using = CoinIdJson.Serializer.class) -@JsonDeserialize(using = CoinIdJson.Deserializer.class) -public class CoinId { - - private final byte[] bytes; - - /** - * Create coin ID from bytes. - * - * @param bytes coin identifier bytes. - */ - public CoinId(byte[] bytes) { - this.bytes = Arrays.copyOf(bytes, bytes.length); - } - - /** - * Get coin ID bytes. - * - * @return coin id bytes - */ - public byte[] getBytes() { - return Arrays.copyOf(this.bytes, this.bytes.length); - } - - /** - * Convert coin ID to bit string. - * - * @return coin id bitstring - */ - public BitString toBitString() { - return new BitString(this.bytes); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - CoinId coinId = (CoinId) o; - return Arrays.equals(this.bytes, coinId.bytes); - } - - @Override - public int hashCode() { - return Arrays.hashCode(this.bytes); - } - - @Override - public String toString() { - return String.format("CoinId{bytes=%s}", HexConverter.encode(this.bytes)); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/token/fungible/CoinIdJson.java b/src/main/java/org/unicitylabs/sdk/token/fungible/CoinIdJson.java deleted file mode 100644 index c6d9b4c..0000000 --- a/src/main/java/org/unicitylabs/sdk/token/fungible/CoinIdJson.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.unicitylabs.sdk.token.fungible; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.exc.MismatchedInputException; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import java.io.IOException; -import java.math.BigInteger; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.util.HexConverter; - -/** - * Token coin json serializer and deserializer. - */ -public class CoinIdJson { - - private CoinIdJson() { - } - - /** - * Token coin serializer. - */ - public static class Serializer extends StdSerializer { - - /** - * Create token coin serializer. - */ - public Serializer() { - super(CoinId.class); - } - - /** - * Serialize token coin. - * - * @param value token coin. - * @param gen json generator. - * @param serializers serializer provider. - * @throws IOException on serialization failure - */ - @Override - public void serialize(CoinId value, JsonGenerator gen, SerializerProvider serializers) - throws IOException { - gen.writeObject(value.getBytes()); - } - } - - /** - * Token coin deserializer. - */ - public static class Deserializer extends StdDeserializer { - - /** - * Create token coin deserializer. - */ - public Deserializer() { - super(CoinId.class); - } - - /** - * Deserialize token coin. - * - * @param p Parser used for reading JSON content - * @param ctx Context that can be used to access information about this deserialization - * activity. - * @return token coin data - * @throws IOException on deserialization failure - */ - @Override - public CoinId deserialize(JsonParser p, DeserializationContext ctx) throws IOException { - if (p.getCurrentToken() != JsonToken.VALUE_STRING) { - throw MismatchedInputException.from( - p, - TokenId.class, - "Expected string value" - ); - } - - try { - return new CoinId(p.readValueAs(byte[].class)); - } catch (Exception e) { - throw MismatchedInputException.from(p, TokenId.class, "Expected bytes"); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/org/unicitylabs/sdk/token/fungible/TokenCoinData.java b/src/main/java/org/unicitylabs/sdk/token/fungible/TokenCoinData.java deleted file mode 100644 index 138e097..0000000 --- a/src/main/java/org/unicitylabs/sdk/token/fungible/TokenCoinData.java +++ /dev/null @@ -1,108 +0,0 @@ - -package org.unicitylabs.sdk.token.fungible; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import java.math.BigInteger; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -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.util.BigIntegerConverter; - -/** - * Token coin data representation. - */ -@JsonSerialize(using = TokenCoinDataJson.Serializer.class) -@JsonDeserialize(using = TokenCoinDataJson.Deserializer.class) -public class TokenCoinData { - - private final Map coins; - - /** - * Create token coin data from coins map. - * - * @param coins map of token coins - */ - public TokenCoinData(Map coins) { - this.coins = Map.copyOf(coins); - } - - /** - * Get token coins map. - * - * @return token coins map - */ - public Map getCoins() { - return this.coins; - } - - /** - * Create token coins data from CBOR. - * - * @param bytes CBOR bytes. - * @return token coin data - */ - public static TokenCoinData fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); - - Map coins = new LinkedHashMap<>(); - for (byte[] coinBytes : data) { - List coinData = CborDeserializer.readArray(coinBytes); - CoinId coinId = new CoinId(CborDeserializer.readByteString(coinData.get(0))); - - if (coins.containsKey(coinId)) { - throw new CborSerializationException( - String.format("Duplicate coin ID in coin data: %s", coinId) - ); - } - - BigInteger amount = BigIntegerConverter.decode( - CborDeserializer.readByteString(coinData.get(1)) - ); - coins.put(coinId, amount); - } - - return new TokenCoinData(coins); - } - - /** - * Convert token coins data to CBOR. - * - * @return token coins data as cbor - */ - public byte[] toCbor() { - return CborSerializer.encodeArray( - this.coins.entrySet().stream() - .map(entry -> CborSerializer.encodeArray( - CborSerializer.encodeByteString(entry.getKey().getBytes()), - CborSerializer.encodeByteString( - BigIntegerConverter.encode(entry.getValue()) - ) - )) - .toArray(byte[][]::new) - ); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof TokenCoinData)) { - return false; - } - TokenCoinData that = (TokenCoinData) o; - return Objects.equals(this.coins, that.coins); - } - - @Override - public int hashCode() { - return Objects.hashCode(this.coins); - } - - @Override - public String toString() { - return String.format("TokenCoinData{%s}", this.coins); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/token/fungible/TokenCoinDataJson.java b/src/main/java/org/unicitylabs/sdk/token/fungible/TokenCoinDataJson.java deleted file mode 100644 index 0143110..0000000 --- a/src/main/java/org/unicitylabs/sdk/token/fungible/TokenCoinDataJson.java +++ /dev/null @@ -1,110 +0,0 @@ -package org.unicitylabs.sdk.token.fungible; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.exc.MismatchedInputException; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import java.io.IOException; -import java.math.BigInteger; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.unicitylabs.sdk.util.HexConverter; - -/** - * Token coin data json serializer and deserializer. - */ -public class TokenCoinDataJson { - - private TokenCoinDataJson() { - } - - /** - * Token coin data serializer. - */ - public static class Serializer extends StdSerializer { - - /** - * Create token coin data serializer. - */ - public Serializer() { - super(TokenCoinData.class); - } - - /** - * Serialize token coin data. - * - * @param value token coin data. - * @param gen json generator. - * @param serializers serializer provider. - * @throws IOException on serialization failure - */ - @Override - public void serialize(TokenCoinData value, JsonGenerator gen, SerializerProvider serializers) - throws IOException { - gen.writeStartArray(); - for (Map.Entry entry : value.getCoins().entrySet()) { - gen.writeStartArray(); - gen.writeObject(HexConverter.encode(entry.getKey().getBytes())); - gen.writeObject(entry.getValue().toString()); - gen.writeEndArray(); - } - gen.writeEndArray(); - } - } - - /** - * Token coin data deserializer. - */ - public static class Deserializer extends StdDeserializer { - - /** - * Create token coin data deserializer. - */ - public Deserializer() { - super(TokenCoinData.class); - } - - /** - * Deserialize token coin data. - * - * @param p Parser used for reading JSON content - * @param ctx Context that can be used to access information about this deserialization - * activity. - * @return token coin data - * @throws IOException on deserialization failure - */ - @Override - public TokenCoinData deserialize(JsonParser p, DeserializationContext ctx) throws IOException { - List data = ctx.readValue(p, - ctx.getTypeFactory().constructCollectionType(List.class, - ctx.getTypeFactory().constructArrayType(String.class))); - - LinkedHashMap coins = new LinkedHashMap<>(); - for (String[] entry : data) { - if (entry.length != 2) { - throw MismatchedInputException.from( - p, - TokenCoinData.class, - "Each entry must be an array of two elements: [coinId, amount]" - ); - } - try { - CoinId coinId = new CoinId(HexConverter.decode(entry[0])); - if (coins.containsKey(coinId)) { - throw new IOException("Duplicate CoinId: " + coinId); - } - - coins.put(coinId, new BigInteger(entry[1])); - } catch (Exception e) { - throw MismatchedInputException.from(p, "Invalid coin data", e); - } - } - - return new TokenCoinData(coins); - } - } -} \ No newline at end of file diff --git a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java new file mode 100644 index 0000000..9c9e777 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java @@ -0,0 +1,154 @@ +package org.unicitylabs.sdk.transaction; + +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.verification.PredicateVerifierService; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +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; + +/** + * Mint transaction bundled with an inclusion proof. + */ +public class CertifiedMintTransaction implements Transaction { + + private final MintTransaction transaction; + private final InclusionProof inclusionProof; + + private CertifiedMintTransaction(MintTransaction 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(); + } + + /** + * Returns the token identifier. + * + * @return token id + */ + public TokenId getTokenId() { + return this.transaction.getTokenId(); + } + + /** + * Returns the token type. + * + * @return token type + */ + public TokenType getTokenType() { + return this.transaction.getTokenType(); + } + + public Optional getJustification() { + return this.transaction.getJustification(); + } + + @Override + public byte[] getStateMask() { + return this.transaction.getStateMask(); + } + + /** + * Returns the inclusion proof certifying this transaction. + * + * @return inclusion proof + */ + public InclusionProof getInclusionProof() { + return this.inclusionProof; + } + + /** + * Deserializes a certified mint transaction from CBOR. + * + * @param bytes CBOR-encoded certified mint transaction + * @return decoded certified mint transaction + */ + public static CertifiedMintTransaction fromCbor(byte[] bytes) { + List data = CborDeserializer.decodeArray(bytes, 2); + return new CertifiedMintTransaction(MintTransaction.fromCbor(data.get(0)), + InclusionProof.fromCbor(data.get(1))); + } + + /** + * Creates a certified mint transaction after verifying the inclusion proof. + * + * @param trustBase trust base used to verify inclusion proof signatures + * @param predicateVerifier service used for predicate verification during proof validation + * @param transaction mint transaction to certify + * @param inclusionProof inclusion proof for the transaction + * @return certified mint transaction + * @throws VerificationException if inclusion proof verification fails + */ + public static CertifiedMintTransaction fromTransaction( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + MintTransaction transaction, + InclusionProof inclusionProof + ) { + 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 CertifiedMintTransaction(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("CertifiedMintTransaction{transaction=%s, inclusionProof=%s}", + this.transaction, this.inclusionProof); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java new file mode 100644 index 0000000..16beb0e --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java @@ -0,0 +1,160 @@ +package org.unicitylabs.sdk.transaction; + +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.verification.PredicateVerifierService; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +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; + +/** + * Transfer transaction with a verified inclusion proof. + */ +public class CertifiedTransferTransaction implements Transaction { + + private final TransferTransaction transaction; + private final InclusionProof inclusionProof; + + private CertifiedTransferTransaction( + TransferTransaction 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(); + } + + /** + * Get inclusion proof for this transaction. + * + * @return inclusion proof + */ + public InclusionProof getInclusionProof() { + return this.inclusionProof; + } + + /** + * Deserialize a certified transfer transaction from CBOR bytes. + * + * @param bytes CBOR encoded certified transfer transaction + * @param token token providing the source state for the deserialized transfer + * + * @return certified transfer transaction + */ + public static CertifiedTransferTransaction fromCbor(byte[] bytes, Token token) { + List data = CborDeserializer.decodeArray(bytes, 2); + + return new CertifiedTransferTransaction( + TransferTransaction.fromCbor(data.get(0), token), + InclusionProof.fromCbor(data.get(1)) + ); + } + + /** + * Create a certified transfer transaction from a transfer transaction and inclusion proof. + * + *

The inclusion proof is verified against the transaction before creating the certified + * instance. + * + * @param trustBase trust base used for proof verification + * @param predicateVerifier predicate verifier used by verification rules + * @param transaction transfer transaction + * @param inclusionProof inclusion proof + * + * @return certified transfer transaction + * + * @throws VerificationException if inclusion proof verification fails + */ + public static CertifiedTransferTransaction fromTransaction( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + TransferTransaction transaction, + InclusionProof inclusionProof + ) { + 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 CertifiedTransferTransaction(transaction, inclusionProof); + } + + /** + * Calculate state hash of the transfer transaction. + * + * @return state hash + */ + @Override + public DataHash calculateStateHash() { + return this.transaction.calculateStateHash(); + } + + /** + * Calculate hash of the transfer transaction. + * + * @return transaction hash + */ + @Override + public DataHash calculateTransactionHash() { + return this.transaction.calculateTransactionHash(); + } + + /** + * Serialize this certified transfer transaction to CBOR bytes. + * + * @return CBOR bytes + */ + @Override + public byte[] toCbor() { + return CborSerializer.encodeArray(this.transaction.toCbor(), this.inclusionProof.toCbor()); + } + + @Override + public String toString() { + return String.format("CertifiedTransferTransaction{transaction=%s, inclusionProof=%s}", + this.transaction, this.inclusionProof); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/Commitment.java b/src/main/java/org/unicitylabs/sdk/transaction/Commitment.java deleted file mode 100644 index 8a14bef..0000000 --- a/src/main/java/org/unicitylabs/sdk/transaction/Commitment.java +++ /dev/null @@ -1,88 +0,0 @@ - -package org.unicitylabs.sdk.transaction; - -import java.util.Objects; -import org.unicitylabs.sdk.api.Authenticator; -import org.unicitylabs.sdk.api.RequestId; - -/** - * Commitment representing a submitted transaction. - * - * @param the type of transaction data - */ -public abstract class Commitment> { - - private final RequestId requestId; - private final T transactionData; - private final Authenticator authenticator; - - /** - * Create commitment. - * - * @param requestId request id - * @param transactionData transaction data - * @param authenticator authenticator - */ - protected Commitment(RequestId requestId, T transactionData, Authenticator authenticator) { - this.requestId = requestId; - this.transactionData = transactionData; - this.authenticator = authenticator; - } - - /** - * Returns the request ID associated with this commitment. - * - * @return request ID - */ - public RequestId getRequestId() { - return requestId; - } - - /** - * Returns the transaction data associated with this commitment. - * - * @return transaction data - */ - public T getTransactionData() { - return transactionData; - } - - /** - * Returns the authenticator associated with this commitment. - * - * @return authenticator - */ - public Authenticator getAuthenticator() { - return authenticator; - } - - /** - * Convert commitment to transaction. - * - * @param inclusionProof Commitment inclusion proof - * @return transaction - */ - public abstract Transaction toTransaction(InclusionProof inclusionProof); - - @Override - public boolean equals(Object o) { - if (!(o instanceof Commitment)) { - return false; - } - Commitment that = (Commitment) o; - return Objects.equals(this.requestId, that.requestId) - && Objects.equals(this.transactionData, that.transactionData) - && Objects.equals(this.authenticator, that.authenticator); - } - - @Override - public int hashCode() { - return Objects.hash(this.requestId, this.transactionData, authenticator); - } - - @Override - public String toString() { - return String.format("Commitment{requestId=%s, transactionData=%s, authenticator=%s}", - this.requestId, this.transactionData, this.authenticator); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/InclusionProof.java b/src/main/java/org/unicitylabs/sdk/transaction/InclusionProof.java deleted file mode 100644 index 9538978..0000000 --- a/src/main/java/org/unicitylabs/sdk/transaction/InclusionProof.java +++ /dev/null @@ -1,223 +0,0 @@ -package org.unicitylabs.sdk.transaction; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import org.unicitylabs.sdk.api.Authenticator; -import org.unicitylabs.sdk.api.LeafValue; -import org.unicitylabs.sdk.api.RequestId; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.bft.UnicityCertificate; -import org.unicitylabs.sdk.bft.verification.UnicityCertificateVerificationContext; -import org.unicitylabs.sdk.bft.verification.UnicityCertificateVerificationRule; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.mtree.MerkleTreePathVerificationResult; -import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreePath; -import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreePathStep; -import org.unicitylabs.sdk.serializer.UnicityObjectMapper; -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.serializer.json.JsonSerializationException; - -/** - * Represents a proof of inclusion or non-inclusion in a sparse merkle tree. - */ -public class InclusionProof { - - private final SparseMerkleTreePath merkleTreePath; - private final Authenticator authenticator; - private final DataHash transactionHash; - private final UnicityCertificate unicityCertificate; - - @JsonCreator - InclusionProof( - @JsonProperty("merkleTreePath") SparseMerkleTreePath merkleTreePath, - @JsonProperty("authenticator") Authenticator authenticator, - @JsonProperty("transactionHash") DataHash transactionHash, - @JsonProperty("unicityCertificate") UnicityCertificate unicityCertificate - ) { - Objects.requireNonNull(merkleTreePath, "Merkle tree path cannot be null."); - Objects.requireNonNull(unicityCertificate, "Unicity certificate cannot be null."); - - if ((authenticator == null) != (transactionHash == null)) { - throw new IllegalArgumentException( - "Authenticator and transaction hash must be both set or both null."); - } - this.merkleTreePath = merkleTreePath; - this.authenticator = authenticator; - this.transactionHash = transactionHash; - this.unicityCertificate = unicityCertificate; - } - - /** - * Get merkle tree path. - * - * @return merkle tree path - */ - public SparseMerkleTreePath getMerkleTreePath() { - return this.merkleTreePath; - } - - /** - * Get unicity certificate. - * - * @return unicity certificate - */ - public UnicityCertificate getUnicityCertificate() { - return this.unicityCertificate; - } - - /** - * Get authenticator on inclusion proof, null on non inclusion proof. - * - * @return authenticator - */ - public Optional getAuthenticator() { - return Optional.ofNullable(this.authenticator); - } - - /** - * Get authenticator on inclusion proof, null on non inclusion proof. - * - * @return inclusion proof - */ - public Optional getTransactionHash() { - return Optional.ofNullable(this.transactionHash); - } - - /** - * Verify inclusion proof. - * - * @param requestId request id - * @param trustBase trust base for unicity certificate anchor verification - * @return inclusion proof verification status - */ - public InclusionProofVerificationStatus verify(RequestId requestId, RootTrustBase trustBase) { - // Check if path is valid and signed by a trusted authority - if (!new UnicityCertificateVerificationRule().verify( - new UnicityCertificateVerificationContext( - this.merkleTreePath.getRootHash(), - this.unicityCertificate, - trustBase - ) - ).isSuccessful()) { - return InclusionProofVerificationStatus.NOT_AUTHENTICATED; - } - - MerkleTreePathVerificationResult result = this.merkleTreePath.verify( - requestId.toBitString().toBigInteger()); - if (!result.isPathValid()) { - return InclusionProofVerificationStatus.PATH_INVALID; - } - - if (this.authenticator != null && this.transactionHash != null) { - if (!this.authenticator.verify(this.transactionHash)) { - return InclusionProofVerificationStatus.NOT_AUTHENTICATED; - } - - try { - LeafValue leafValue = LeafValue.create(this.authenticator, this.transactionHash); - if (this.merkleTreePath.getSteps().size() == 0) { - return InclusionProofVerificationStatus.PATH_NOT_INCLUDED; - } - - SparseMerkleTreePathStep step = this.merkleTreePath.getSteps().get(0); - if (!Arrays.equals(leafValue.getBytes(), step.getData().orElse(null))) { - return InclusionProofVerificationStatus.PATH_NOT_INCLUDED; - } - } catch (CborSerializationException e) { - return InclusionProofVerificationStatus.NOT_AUTHENTICATED; - } - } - - if (!result.isPathIncluded()) { - return InclusionProofVerificationStatus.PATH_NOT_INCLUDED; - } - - return InclusionProofVerificationStatus.OK; - } - - /** - * Create inclusion proof from CBOR bytes. - * - * @param bytes CBOR bytes - * @return inclusion proof - */ - public static InclusionProof fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); - - return new InclusionProof( - SparseMerkleTreePath.fromCbor(data.get(0)), - CborDeserializer.readOptional(data.get(1), Authenticator::fromCbor), - CborDeserializer.readOptional(data.get(2), DataHash::fromCbor), - UnicityCertificate.fromCbor(data.get(3)) - ); - } - - /** - * Convert inclusion proof to CBOR bytes. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeArray( - this.merkleTreePath.toCbor(), - CborSerializer.encodeOptional(this.authenticator, Authenticator::toCbor), - CborSerializer.encodeOptional(this.transactionHash, DataHash::toCbor), - this.unicityCertificate.toCbor() - ); - } - - /** - * Get inclusion proof from JSON. - * - * @param input inclusion proof JSON string - * @return inclusion proof - */ - public static InclusionProof fromJson(String input) { - try { - return UnicityObjectMapper.JSON.readValue(input, InclusionProof.class); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(InclusionProof.class, e); - } - } - - /** - * Get inclusion proof as JSON. - * - * @return inclusion proof JSON string - */ - public String toJson() { - try { - return UnicityObjectMapper.JSON.writeValueAsString(this); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(InclusionProof.class, e); - } - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof InclusionProof)) { - return false; - } - InclusionProof that = (InclusionProof) o; - return Objects.equals(merkleTreePath, that.merkleTreePath) && Objects.equals(authenticator, - that.authenticator) && Objects.equals(transactionHash, that.transactionHash); - } - - @Override - public int hashCode() { - return Objects.hash(merkleTreePath, authenticator, transactionHash); - } - - @Override - public String toString() { - return String.format("InclusionProof{merkleTreePath=%s, authenticator=%s, transactionHash=%s}", - merkleTreePath, authenticator, transactionHash); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/InclusionProofVerificationStatus.java b/src/main/java/org/unicitylabs/sdk/transaction/InclusionProofVerificationStatus.java deleted file mode 100644 index d67bdb8..0000000 --- a/src/main/java/org/unicitylabs/sdk/transaction/InclusionProofVerificationStatus.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.unicitylabs.sdk.transaction; - -/** - * Status codes for verifying an InclusionProof. - */ -public enum InclusionProofVerificationStatus { - /** - * Inclusion proof verification failed because the proof could not be authenticated. - */ - NOT_AUTHENTICATED("NOT_AUTHENTICATED"), - /** - * Inclusion proof verification failed because the path is not included in the Merkle tree. - */ - PATH_NOT_INCLUDED("PATH_NOT_INCLUDED"), - /** - * Inclusion proof verification failed because the path is invalid. - */ - PATH_INVALID("PATH_INVALID"), - /** - * Inclusion proof verification succeeded. - */ - OK("OK"); - - private final String value; - - InclusionProofVerificationStatus(String value) { - this.value = value; - } - - /** - * Get inclusion proof verification status value. - * - * @return status value - */ - public String getValue() { - return value; - } -} \ No newline at end of file diff --git a/src/main/java/org/unicitylabs/sdk/transaction/MintCommitment.java b/src/main/java/org/unicitylabs/sdk/transaction/MintCommitment.java deleted file mode 100644 index 03e9297..0000000 --- a/src/main/java/org/unicitylabs/sdk/transaction/MintCommitment.java +++ /dev/null @@ -1,65 +0,0 @@ - -package org.unicitylabs.sdk.transaction; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Objects; -import org.unicitylabs.sdk.api.Authenticator; -import org.unicitylabs.sdk.api.RequestId; -import org.unicitylabs.sdk.signing.MintSigningService; -import org.unicitylabs.sdk.signing.SigningService; - -/** - * Commitment representing a submitted transaction. - * - * @param the type of transaction data - */ -public class MintCommitment extends - Commitment> { - @JsonCreator - private MintCommitment( - @JsonProperty("requestId") - RequestId requestId, - @JsonProperty("transactionData") - MintTransaction.Data transactionData, - @JsonProperty("authenticator") - Authenticator authenticator - ) { - super(requestId, transactionData, authenticator); - } - - /** - * Create mint transaction from commitment. - * - * @param inclusionProof Commitment inclusion proof - * @return mint transaction - */ - @Override - public MintTransaction toTransaction(InclusionProof inclusionProof) { - return new MintTransaction<>(this.getTransactionData(), inclusionProof); - } - - /** - * Create mint commitment from transaction data. - * - * @param data mint transaction data - * @param mint reason - * @return mint commitment - */ - public static MintCommitment create( - MintTransaction.Data data - ) { - Objects.requireNonNull(data, "Transaction data cannot be null"); - - SigningService signingService = MintSigningService.create(data.getTokenId()); - return new MintCommitment<>( - RequestId.create(signingService.getPublicKey(), data.getSourceState()), - data, - Authenticator.create( - signingService, - data.calculateHash(), - data.getSourceState() - ) - ); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/MintReasonType.java b/src/main/java/org/unicitylabs/sdk/transaction/MintReasonType.java deleted file mode 100644 index b88cdfc..0000000 --- a/src/main/java/org/unicitylabs/sdk/transaction/MintReasonType.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.unicitylabs.sdk.transaction; - -/** - * Mint reason type. - */ -public enum MintReasonType { - /** - * Tokens mint reason based on the split of a token. - */ - TOKEN_SPLIT, -} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java index e80e9ed..80eaf3a 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java @@ -1,434 +1,252 @@ - package org.unicitylabs.sdk.transaction; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import java.nio.charset.StandardCharsets; +import org.unicitylabs.sdk.api.InclusionProof; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.MintSigningService; +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.crypto.secp256k1.SigningService; +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.util.HexConverter; + import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Optional; -import org.unicitylabs.sdk.address.Address; -import org.unicitylabs.sdk.address.AddressFactory; -import org.unicitylabs.sdk.api.RequestId; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.serializer.UnicityObjectMapper; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.serializer.json.JsonSerializationException; -import org.unicitylabs.sdk.signing.MintSigningService; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.token.TokenType; -import org.unicitylabs.sdk.token.fungible.TokenCoinData; -import org.unicitylabs.sdk.transaction.split.SplitMintReason; -import org.unicitylabs.sdk.util.HexConverter; -import org.unicitylabs.sdk.verification.VerificationResult; /** - * Mint transaction. + * Represents a Mint Transaction. * - * @param mint reason + *

This transaction is responsible for minting new tokens with specific attributes and assigns + * it to an initial owner. */ -public class MintTransaction extends - Transaction> { +public class MintTransaction 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 byte[] justification; + private final byte[] data; + + private MintTransaction( + MintTransactionState sourceStateHash, + EncodedPredicate lockScript, + EncodedPredicate recipient, + TokenId tokenId, + TokenType tokenType, + byte[] justification, + byte[] data + ) { + this.sourceStateHash = sourceStateHash; + this.lockScript = lockScript; + this.recipient = recipient; + this.tokenId = tokenId; + this.tokenType = tokenType; + this.justification = justification; + this.data = data; + } + + public int getVersion() { + return MintTransaction.VERSION; + } + - @JsonCreator - MintTransaction( - @JsonProperty("data") - Data data, - @JsonProperty("inclusionProof") - InclusionProof inclusionProof) { - super(data, inclusionProof); + @Override + public MintTransactionState getSourceStateHash() { + return this.sourceStateHash; + } + + @Override + public EncodedPredicate getLockScript() { + return this.lockScript; + } + + @Override + public EncodedPredicate getRecipient() { + return this.recipient; } /** - * Create mint transaction from CBOR bytes. + * Retrieves the unique token identifier. * - * @param bytes CBOR bytes - * @return mint transaction + * @return the token identifier as a {@code TokenId}. */ - public static MintTransaction fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); - - return new MintTransaction<>( - Data.fromCbor(data.get(0)), - InclusionProof.fromCbor(data.get(1)) - ); + public TokenId getTokenId() { + return this.tokenId; } /** - * Create mint transaction from JSON string. + * Retrieves the type identifier of the token. * - * @param input JSON string - * @return mint transaction + * @return the token type as a {@code TokenType}. */ - public static MintTransaction fromJson(String input) { - try { - return UnicityObjectMapper.JSON.readValue(input, MintTransaction.class); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(MintTransaction.class, e); - } + public TokenType getTokenType() { + return this.tokenType; } /** - * Verify mint transaction. + * Retrieves the justification for the mint transaction, if any. * - * @param trustBase root trust base - * @return verification result + * @return optional justification bytes */ - public VerificationResult verify(RootTrustBase trustBase) { - if (!this.getInclusionProof().getAuthenticator().isPresent()) { - return VerificationResult.fail("Missing authenticator"); - } - - if (!this.getInclusionProof().getTransactionHash().isPresent()) { - return VerificationResult.fail("Missing transaction hash"); - } - - if (!this.getData().getSourceState() - .equals(MintTransactionState.create(this.getData().getTokenId()))) { - return VerificationResult.fail("Invalid source state"); - } - - SigningService signingService = MintSigningService.create(this.getData().getTokenId()); - if (!Arrays.equals(signingService.getPublicKey(), - this.getInclusionProof().getAuthenticator().get().getPublicKey())) { - return VerificationResult.fail("Authenticator public key mismatch"); - } - - if (!this.getInclusionProof().getAuthenticator().get() - .verify(this.getInclusionProof().getTransactionHash().get())) { - return VerificationResult.fail("Authenticator verification failed"); - } + public Optional getJustification() { + return Optional.ofNullable(this.justification != null ? Arrays.copyOf(this.justification, this.justification.length) : null); + } - VerificationResult reasonResult = this.getData().getReason() - .map(reason -> reason.verify(this)) - .orElse(VerificationResult.success()); + @Override + public Optional getData() { + return Optional.ofNullable(this.data != null ? Arrays.copyOf(this.data, this.data.length) : null); + } - if (!reasonResult.isSuccessful()) { - return VerificationResult.fail("Mint reason verification failed", List.of(reasonResult)); - } + @Override + public byte[] getStateMask() { + return this.tokenId.getBytes(); + } - InclusionProofVerificationStatus inclusionProofStatus = this.getInclusionProof().verify( - RequestId.create( - MintSigningService.create(this.getData().getTokenId()).getPublicKey(), - this.getData().getSourceState() - ), - trustBase + /** + * Create a mint transaction. + * + * @param recipient recipient predicate + * @param tokenId token identifier + * @param tokenType token type identifier + * @param justification mint justification bytes, may be null + * @param data payload bytes, may be null + * + * @return mint transaction + */ + public static MintTransaction create( + Predicate recipient, + TokenId tokenId, + TokenType tokenType, + byte[] justification, + byte[] data + ) { + Objects.requireNonNull(recipient, "Recipient cannot be null"); + Objects.requireNonNull(tokenId, "Token ID cannot be null"); + Objects.requireNonNull(tokenType, "Token type cannot be null"); + + SigningService signingService = MintSigningService.create(tokenId); + return new MintTransaction( + MintTransactionState.create(tokenId), + EncodedPredicate.fromPredicate(SignaturePredicate.fromSigningService(signingService)), + EncodedPredicate.fromPredicate(recipient), + tokenId, + tokenType, + justification != null ? Arrays.copyOf(justification, justification.length) : null, + data != null ? Arrays.copyOf(data, data.length) : null ); - - if (inclusionProofStatus != InclusionProofVerificationStatus.OK) { - return VerificationResult.fail( - String.format("Inclusion proof verification failed with status %s", inclusionProofStatus) - ); - } - - return VerificationResult.success(); } /** - * Mint transaction data. + * Deserialize mint transaction from CBOR bytes. * - * @param mint reason + * @param bytes CBOR bytes + * + * @return mint transaction */ - public static class Data implements - TransactionData { - - private final TokenId tokenId; - private final TokenType tokenType; - private final byte[] tokenData; - private final TokenCoinData coinData; - private final MintTransactionState sourceState; - private final Address recipient; - private final byte[] salt; - private final DataHash recipientDataHash; - private final R reason; - - /** - * Create mint transaction data. - * - * @param tokenId token id - * @param tokenType token type - * @param tokenData token immutable data - * @param coinData token coin data - * @param recipient token recipient address - * @param salt mint transaction salt - * @param recipientDataHash recipient data hash - * @param reason mint reason - */ - @JsonCreator - public Data( - @JsonProperty("tokenId") TokenId tokenId, - @JsonProperty("tokenType") TokenType tokenType, - @JsonProperty("tokenData") byte[] tokenData, - @JsonProperty("coinData") TokenCoinData coinData, - @JsonProperty("recipient") Address recipient, - @JsonProperty("salt") byte[] salt, - @JsonProperty("recipientDataHash") DataHash recipientDataHash, - @JsonProperty("reason") - @JsonDeserialize(using = MintTransactionReasonJson.Deserializer.class) - R reason - ) { - Objects.requireNonNull(tokenId, "Token ID cannot be null"); - Objects.requireNonNull(tokenType, "Token type cannot be null"); - Objects.requireNonNull(recipient, "Recipient cannot be null"); - Objects.requireNonNull(salt, "Salt cannot be null"); - - this.tokenId = tokenId; - this.tokenType = tokenType; - this.tokenData = tokenData == null ? null : Arrays.copyOf(tokenData, tokenData.length); - this.coinData = coinData; - this.sourceState = MintTransactionState.create(tokenId); - this.recipient = recipient; - this.salt = Arrays.copyOf(salt, salt.length); - this.recipientDataHash = recipientDataHash; - this.reason = reason; + public static MintTransaction fromCbor(byte[] bytes) { + CborDeserializer.CborTag tag = CborDeserializer.decodeTag(bytes); + if (tag.getTag() != MintTransaction.CBOR_TAG) { + throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } + List data = CborDeserializer.decodeArray(tag.getData(), 6); - /** - * Get token id. - * - * @return token id - */ - public TokenId getTokenId() { - return this.tokenId; + int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); + if (version != MintTransaction.VERSION) { + throw new CborSerializationException(String.format("Unsupported version: %s", version)); } - /** - * Get token type. - * - * @return token type - */ - public TokenType getTokenType() { - return this.tokenType; - } - - /** - * Get immutable token data. - * - * @return token data - */ - public Optional getTokenData() { - return Optional.ofNullable(this.tokenData); - } - - /** - * Get token coin data. - * - * @return token coin data - */ - public Optional getCoinData() { - return Optional.ofNullable(this.coinData); - } - - /** - * Get recipient data hash. - * - * @return recipient data hash - */ - public Optional getRecipientDataHash() { - return Optional.ofNullable(this.recipientDataHash); - } - - /** - * Get mint transaction salt. - * - * @return transaction salt - */ - public byte[] getSalt() { - return Arrays.copyOf(this.salt, this.salt.length); - } - - /** - * Get token recipient address. - * - * @return recipient address - */ - public Address getRecipient() { - return this.recipient; - } - - /** - * Get mint reason. - * - * @return mint reason - */ - public Optional getReason() { - return Optional.ofNullable(this.reason); - } - - /** - * Get mint transaction source state. - * - * @return source state - */ - @JsonIgnore - public MintTransactionState getSourceState() { - return this.sourceState; - } - - /** - * Calculate mint transaction hash. - * - * @return transaction hash. - */ - public DataHash calculateHash() { - return new DataHasher(HashAlgorithm.SHA256) - .update(this.toCbor()) - .digest(); - } - - /** - * Create mint transaction data from CBOR bytes. - * - * @param bytes CBOR bytes - * @return mint transaction data - */ - public static Data fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); - - return new Data<>( - TokenId.fromCbor(data.get(0)), - TokenType.fromCbor(data.get(1)), - CborDeserializer.readOptional(data.get(2), CborDeserializer::readByteString), - CborDeserializer.readOptional(data.get(3), TokenCoinData::fromCbor), - AddressFactory.createAddress(CborDeserializer.readTextString(data.get(4))), - CborDeserializer.readByteString(data.get(5)), - CborDeserializer.readOptional(data.get(6), DataHash::fromCbor), - CborDeserializer.readOptional(data.get(7), SplitMintReason::fromCbor) - ); - } - - /** - * Convert mint transaction data to CBOR bytes. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeArray( - this.tokenId.toCbor(), - this.tokenType.toCbor(), - CborSerializer.encodeOptional(this.tokenData, CborSerializer::encodeByteString), - CborSerializer.encodeOptional(this.coinData, TokenCoinData::toCbor), - CborSerializer.encodeTextString(this.recipient.getAddress()), - CborSerializer.encodeByteString(this.salt), - CborSerializer.encodeOptional(this.recipientDataHash, DataHash::toCbor), - CborSerializer.encodeOptional(this.reason, MintTransactionReason::toCbor) - ); - } - - /** - * Create mint transaction data from JSON string. - * - * @param input JSON string - * @return mint transaction data - */ - public static Data fromJson(String input) { - try { - return UnicityObjectMapper.JSON.readValue(input, Data.class); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(Data.class, e); - } - } - - /** - * Convert mint transaction data to JSON string. - * - * @return JSON string - */ - public String toJson() { - try { - return UnicityObjectMapper.JSON.writeValueAsString(this); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(Data.class, e); - } - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof Data)) { - return false; - } - Data that = (Data) o; + return MintTransaction.create( + EncodedPredicate.fromCbor(data.get(1)), + TokenId.fromCbor(data.get(2)), + TokenType.fromCbor(data.get(3)), + CborDeserializer.decodeNullable(data.get(4), CborDeserializer::decodeByteString), + CborDeserializer.decodeNullable(data.get(5), CborDeserializer::decodeByteString) + ); + } - return Objects.equals(this.tokenId, that.tokenId) - && Objects.equals(this.tokenType, that.tokenType) - && Objects.deepEquals(this.tokenData, that.tokenData) - && Objects.equals(this.coinData, that.coinData) - && Objects.equals(this.sourceState, that.sourceState) - && Objects.equals(this.recipient, that.recipient) - && Objects.deepEquals(this.salt, that.salt) - && Objects.equals(this.recipientDataHash, that.recipientDataHash) - && Objects.equals(this.reason, that.reason); - } + /** + * Calculate mint transaction state hash. + * + * @return state hash + */ + @Override + public DataHash calculateStateHash() { + return new DataHasher(HashAlgorithm.SHA256) + .update( + CborSerializer.encodeArray( + CborSerializer.encodeByteString(this.sourceStateHash.getImprint()), + CborSerializer.encodeByteString(this.getStateMask()) + ) + ) + .digest(); + } - @Override - public int hashCode() { - return Objects.hash(this.tokenId, this.tokenType, Arrays.hashCode(tokenData), this.coinData, - this.sourceState, - this.recipient, Arrays.hashCode(this.salt), this.recipientDataHash, this.reason); - } + /** + * Calculate hash of serialized mint transaction. + * + * @return transaction hash + */ + @Override + public DataHash calculateTransactionHash() { + return new DataHasher(HashAlgorithm.SHA256).update(this.toCbor()).digest(); + } - @Override - public String toString() { - return String.format( - "Data{" - + "tokenId=%s, " - + "tokenType=%s, " - + "tokenData=%s, " - + "coinData=%s, " - + "sourceState=%s, " - + "recipient=%s, " - + "salt=%s, " - + "dataHash=%s, " - + "reason=%s" - + "}", - this.tokenId, this.tokenType, - this.tokenData != null ? HexConverter.encode(this.tokenData) : null, this.coinData, - this.sourceState, this.recipient, HexConverter.encode(this.salt), this.recipientDataHash, - this.reason); - } + /** + * Serialize mint transaction to CBOR bytes. + * + * @return CBOR bytes + */ + @Override + public byte[] toCbor() { + return CborSerializer.encodeTag( + MintTransaction.CBOR_TAG, + CborSerializer.encodeArray( + CborSerializer.encodeUnsignedInteger(MintTransaction.VERSION), + this.recipient.toCbor(), + this.tokenId.toCbor(), + this.tokenType.toCbor(), + CborSerializer.encodeNullable(this.justification, CborSerializer::encodeByteString), + CborSerializer.encodeNullable(this.data, CborSerializer::encodeByteString) + ) + ); } /** - * Nametag mint data. + * Build certified mint transaction by attaching and verifying inclusion proof. + * + * @param trustBase root trust base + * @param predicateVerifier predicate verifier + * @param inclusionProof inclusion proof + * + * @return certified mint transaction */ - public static class NametagData extends Data { + public CertifiedMintTransaction toCertifiedTransaction( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + InclusionProof inclusionProof + ) { + return CertifiedMintTransaction.fromTransaction(trustBase, predicateVerifier, this, + inclusionProof); + } - /** - * Create nametag mint data. - * - * @param name nametag - * @param tokenType token type - * @param recipient recipient address - * @param salt mint salt - * @param targetAddress target address - */ - public NametagData( - String name, - TokenType tokenType, - Address recipient, - byte[] salt, - Address targetAddress - ) { - super( - TokenId.fromNameTag(name), - tokenType, - targetAddress.getAddress().getBytes(StandardCharsets.UTF_8), - null, - recipient, - salt, - null, - null - ); - } + @Override + public String toString() { + return String.format( + "MintTransaction{sourceStateHash=%s, lockScript=%s, recipient=%s, tokenId=%s, tokenType=%s, data=%s}", + this.sourceStateHash, this.lockScript, this.recipient, this.tokenId, this.tokenType, + HexConverter.encode(this.data)); } } diff --git a/src/main/java/org/unicitylabs/sdk/transaction/MintTransactionReason.java b/src/main/java/org/unicitylabs/sdk/transaction/MintTransactionReason.java deleted file mode 100644 index e7718ae..0000000 --- a/src/main/java/org/unicitylabs/sdk/transaction/MintTransactionReason.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.unicitylabs.sdk.transaction; - -import org.unicitylabs.sdk.verification.VerificationResult; - -/** - * Mint transaction reason. - */ -public interface MintTransactionReason { - - /** - * Get mint reason type. - * - * @return reason type - */ - String getType(); - - /** - * Verify mint reason for genesis. - * - * @param genesis Genesis to verify against - * @return verification result - */ - VerificationResult verify(MintTransaction genesis); - - /** - * Convert mint transaction reason to CBOR bytes. - * - * @return CBOR representation of reason - */ - byte[] toCbor(); -} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/MintTransactionReasonJson.java b/src/main/java/org/unicitylabs/sdk/transaction/MintTransactionReasonJson.java deleted file mode 100644 index 87eea9e..0000000 --- a/src/main/java/org/unicitylabs/sdk/transaction/MintTransactionReasonJson.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.unicitylabs.sdk.transaction; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import java.io.IOException; -import org.unicitylabs.sdk.transaction.split.SplitMintReason; - -/** - * Mint transaction reason deserializer implementation. - */ -public class MintTransactionReasonJson { - - private MintTransactionReasonJson() { - } - - /** - * Sparse merkle tree path step deserializer. - */ - public static class Deserializer extends StdDeserializer { - - /** - * Create deserializer. - */ - public Deserializer() { - super(MintTransactionReason.class); - } - - /** - * Deserialize mint transaction reason. - * - * @param p Parser used for reading JSON content - * @param ctx Context that can be used to access information about this deserialization - * activity. - * @return mint transaction reason - * @throws IOException on deserialization failure - */ - @Override - public MintTransactionReason deserialize(JsonParser p, DeserializationContext ctx) - throws IOException { - return p.readValueAs(SplitMintReason.class); - } - } -} - diff --git a/src/main/java/org/unicitylabs/sdk/transaction/MintTransactionState.java b/src/main/java/org/unicitylabs/sdk/transaction/MintTransactionState.java index cb11459..8626f17 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/MintTransactionState.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/MintTransactionState.java @@ -1,29 +1,44 @@ package org.unicitylabs.sdk.transaction; -import org.unicitylabs.sdk.api.RequestId; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.token.TokenId; +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.CborSerializer; import org.unicitylabs.sdk.util.HexConverter; +import java.util.Objects; + /** - * Token mint state. + * Represents the state of a mint transaction. */ public class MintTransactionState extends DataHash { private static final byte[] MINT_SUFFIX = HexConverter.decode( - "9e82002c144d7c5796c50f6db50a0c7bbd7f717ae3af6c6c71a3e9eba3022730"); + "9e82002c144d7c5796c50f6db50a0c7bbd7f717ae3af6c6c71a3e9eba3022730"); private MintTransactionState(DataHash hash) { super(hash.getAlgorithm(), hash.getData()); } /** - * Create token initial state from token id. + * Create a mint transaction state from token id. * * @param tokenId token id - * @return mint state + * @return mint transaction state */ public static MintTransactionState create(TokenId tokenId) { - return new MintTransactionState(RequestId.create(tokenId.getBytes(), MINT_SUFFIX)); + Objects.requireNonNull(tokenId, "Token ID cannot be null"); + + return new MintTransactionState( + new DataHasher(HashAlgorithm.SHA256) + .update( + CborSerializer.encodeArray( + CborSerializer.encodeByteString(tokenId.getBytes()), + CborSerializer.encodeByteString(MintTransactionState.MINT_SUFFIX) + ) + ) + .digest() + ); } + } diff --git a/src/main/java/org/unicitylabs/sdk/transaction/Token.java b/src/main/java/org/unicitylabs/sdk/transaction/Token.java new file mode 100644 index 0000000..23b4cc0 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/Token.java @@ -0,0 +1,254 @@ +package org.unicitylabs.sdk.transaction; + +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.CborSerializationException; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.transaction.verification.CertifiedMintTransactionVerificationRule; +import org.unicitylabs.sdk.transaction.verification.CertifiedTransferTransactionVerificationRule; +import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifierService; +import org.unicitylabs.sdk.util.verification.VerificationException; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Immutable token aggregate containing the certified genesis mint transaction and transfer history. + */ +public final class Token { + public static final long CBOR_TAG = 39040; + private static final int VERSION = 1; + + private final CertifiedMintTransaction genesis; + private final List transactions; + + private Token(CertifiedMintTransaction genesis, List transactions) { + this.genesis = genesis; + this.transactions = List.copyOf(transactions); + } + + private Token(CertifiedMintTransaction genesis) { + this(genesis, List.of()); + } + + public int getVersion() { + return Token.VERSION; + } + + /** + * Returns the token identifier. + * + * @return token id + */ + public TokenId getId() { + return this.genesis.getTokenId(); + } + + /** + * Returns the token type. + * + * @return token type + */ + public TokenType getType() { + return this.genesis.getTokenType(); + } + + /** + * Returns the certified genesis mint transaction. + * + * @return genesis transaction + */ + public CertifiedMintTransaction getGenesis() { + return this.genesis; + } + + /** + * Returns the most recent transaction in the token history. + * + * @return latest transfer transaction, or genesis transaction when no transfers exist + */ + public Transaction getLatestTransaction() { + if (this.transactions.isEmpty()) { + return this.genesis; + } + + return this.transactions.get(this.transactions.size() - 1); + } + + /** + * Returns the certified transfer transactions. + * + * @return immutable list of transfer transactions + */ + public List getTransactions() { + return this.transactions; + } + + /** + * Deserializes a token from CBOR. + * + * @param bytes CBOR-encoded token bytes + * @return decoded token + */ + public static Token fromCbor(byte[] bytes) { + CborDeserializer.CborTag tag = CborDeserializer.decodeTag(bytes); + if (tag.getTag() != Token.CBOR_TAG) { + throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); + } + List data = CborDeserializer.decodeArray(tag.getData(), 3); + + int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); + if (version != Token.VERSION) { + throw new CborSerializationException(String.format("Unsupported version: %s", version)); + } + + CertifiedMintTransaction genesis = CertifiedMintTransaction.fromCbor(data.get(1)); + List transactionsCbor = CborDeserializer.decodeArray(data.get(2)); + + List transactions = new ArrayList<>(); + for (byte[] transaction : transactionsCbor) { + transactions.add(CertifiedTransferTransaction.fromCbor(transaction, new Token(genesis, transactions))); + } + + return new Token(genesis, transactions); + } + + /** + * Creates a token from a certified genesis transaction and verifies it. + * + * @param trustBase trust base used for certification checks + * @param predicateVerifier predicate verifier service + * @param mintJustificationVerifier mint justification verifier service + * @param genesis certified mint transaction + * @return verified token instance + * @throws VerificationException if genesis verification fails + */ + public static Token mint( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + 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) { + throw new VerificationException("Invalid token genesis", result); + } + + return token; + } + + /** + * Returns a new token instance with an additional verified transfer transaction. + * + * @param trustBase trust base used for certification checks + * @param predicateVerifier predicate verifier service + * @param transaction certified transfer transaction to append + * @return new token instance with appended transfer + * @throws VerificationException if transfer verification fails + */ + public Token transfer( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + CertifiedTransferTransaction transaction + ) { + 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, + transaction + ); + if (result.getStatus() != VerificationStatus.OK) { + throw new VerificationException("Invalid token transfer transaction", result); + } + + ArrayList transactions = new ArrayList<>(this.transactions); + transactions.add(transaction); + return new Token(this.genesis, transactions); + } + + /** + * Verifies genesis and transfer transaction chain integrity. + * + * @param trustBase trust base used for certification checks + * @param predicateVerifier predicate verifier service + * @param mintJustificationVerifier mint justification verifier service + * @return verification result with nested per-step verification details + */ + public VerificationResult verify( + RootTrustBase trustBase, + 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, + predicateVerifier, + mintJustificationVerifier, + this.genesis + ); + results.add(result); + if (result.getStatus() != VerificationStatus.OK) { + return new VerificationResult<>("TokenVerification", VerificationStatus.FAIL, + "Genesis verification failed", results); + } + + List> transferResults = new ArrayList<>(); + for (int i = 0; i < this.transactions.size(); i++) { + CertifiedTransferTransaction transaction = this.transactions.get(i); + result = CertifiedTransferTransactionVerificationRule.verify(trustBase, predicateVerifier, transaction); + transferResults.add(result); + if (result.getStatus() != VerificationStatus.OK) { + results.add( + new VerificationResult<>("TokenTransferVerification", VerificationStatus.FAIL, "", + transferResults) + ); + + return new VerificationResult<>("TokenVerification", VerificationStatus.FAIL, + String.format("Transaction[%s] verification failed", i), results); + } + } + results.add(new VerificationResult<>("TokenTransferVerification", VerificationStatus.OK, "", + transferResults)); + + return new VerificationResult<>("TokenVerification", VerificationStatus.OK, "", results); + } + + /** + * Serializes this token to CBOR bytes. + * + * @return CBOR-encoded token bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeTag( + Token.CBOR_TAG, + CborSerializer.encodeArray( + CborSerializer.encodeUnsignedInteger(Token.VERSION), + this.genesis.toCbor(), + CborSerializer.encodeArray( + this.transactions.stream().map(Transaction::toCbor).toArray(byte[][]::new)) + ) + ); + } + + @Override + public String toString() { + return String.format("Token{genesis=%s, transactions=%s}", this.genesis, this.transactions); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/token/TokenId.java b/src/main/java/org/unicitylabs/sdk/transaction/TokenId.java similarity index 54% rename from src/main/java/org/unicitylabs/sdk/token/TokenId.java rename to src/main/java/org/unicitylabs/sdk/transaction/TokenId.java index ed49ec2..fd44370 100644 --- a/src/main/java/org/unicitylabs/sdk/token/TokenId.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/TokenId.java @@ -1,82 +1,67 @@ -package org.unicitylabs.sdk.token; +package org.unicitylabs.sdk.transaction; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Objects; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.util.BitString; import org.unicitylabs.sdk.util.HexConverter; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Objects; + + /** * Globally unique identifier of a token. */ -@JsonSerialize(using = TokenIdJson.Serializer.class) -@JsonDeserialize(using = TokenIdJson.Deserializer.class) public class TokenId { + private static final SecureRandom RANDOM = new SecureRandom(); private final byte[] bytes; /** - * Create token id from bytes. + * Create a token id from byte array. * * @param bytes token id bytes */ public TokenId(byte[] bytes) { + Objects.requireNonNull(bytes, "Token id cannot be null"); + this.bytes = Arrays.copyOf(bytes, bytes.length); } /** - * Get token id bytes. + * Generate a random token id. * - * @return token id bytes + * @return token id */ - public byte[] getBytes() { - return Arrays.copyOf(this.bytes, this.bytes.length); + public static TokenId generate() { + byte[] bytes = new byte[32]; + RANDOM.nextBytes(bytes); + return new TokenId(bytes); } /** - * Get token id as bit string. + * Get token id bytes. * - * @return token id bit string + * @return token id bytes */ - public BitString toBitString() { - return new BitString(this.bytes); + public byte[] getBytes() { + return Arrays.copyOf(this.bytes, this.bytes.length); } /** - * Create token id from nametag. + * Deserialize an token id from CBOR bytes. * - * @param name nametag - * @return token id - */ - public static TokenId fromNameTag(String name) { - Objects.requireNonNull(name, "Name cannot be null"); - - return new TokenId( - new DataHasher(HashAlgorithm.SHA256) - .update(name.getBytes(StandardCharsets.UTF_8)) - .digest() - .getImprint() - ); - } - - /** - * Create token id from CBOR bytes. + * @param bytes CBOR encoded token id bytes * - * @param bytes CBOR bytes * @return token id */ public static TokenId fromCbor(byte[] bytes) { - return new TokenId(CborDeserializer.readByteString(bytes)); + return new TokenId(CborDeserializer.decodeByteString(bytes)); } /** - * Convert token id to CBOR bytes. + * Serialize token id to CBOR bytes. * * @return CBOR bytes */ @@ -84,12 +69,18 @@ public byte[] toCbor() { return CborSerializer.encodeByteString(this.bytes); } + /** + * Convert token id to bit string. + * + * @return bit string + */ + public BitString toBitString() { + return BitString.fromBytes(this.bytes); + } + @Override public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { + if (!(o instanceof TokenId)) { return false; } TokenId tokenId = (TokenId) o; diff --git a/src/main/java/org/unicitylabs/sdk/transaction/TokenType.java b/src/main/java/org/unicitylabs/sdk/transaction/TokenType.java new file mode 100644 index 0000000..b845cdd --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/TokenType.java @@ -0,0 +1,98 @@ +package org.unicitylabs.sdk.transaction; + +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.util.BitString; +import org.unicitylabs.sdk.util.HexConverter; + +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Objects; + +/** + * Type identifier of a token. + */ +public class TokenType { + + private static final SecureRandom RANDOM = new SecureRandom(); + private final byte[] bytes; + + /** + * Create a token type from byte array. + * + * @param bytes token type bytes + */ + public TokenType(byte[] bytes) { + Objects.requireNonNull(bytes, "Token type cannot be null"); + + this.bytes = Arrays.copyOf(bytes, bytes.length); + } + + /** + * Get token type bytes. + * + * @return token type bytes + */ + public byte[] getBytes() { + return Arrays.copyOf(this.bytes, this.bytes.length); + } + + /** + * Generate a random token type. + * + * @return token type + */ + public static TokenType generate() { + byte[] bytes = new byte[32]; + RANDOM.nextBytes(bytes); + return new TokenType(bytes); + } + + /** + * Deserialize a token type from CBOR bytes. + * + * @param bytes CBOR encoded token type bytes + * + * @return token type + */ + public static TokenType fromCbor(byte[] bytes) { + return new TokenType(CborDeserializer.decodeByteString(bytes)); + } + + /** + * Serialize token type to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeByteString(this.bytes); + } + + /** + * Convert token type to bit string. + * + * @return bit string + */ + public BitString toBitString() { + return BitString.fromBytes(this.bytes); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof TokenType)) { + return false; + } + TokenType tokenId = (TokenType) o; + return Arrays.equals(this.bytes, tokenId.bytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(this.bytes); + } + + @Override + public String toString() { + return String.format("TokenType[%s]", HexConverter.encode(this.bytes)); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java b/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java index 9336960..38791ea 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java @@ -1,119 +1,68 @@ - package org.unicitylabs.sdk.transaction; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import java.util.Objects; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.serializer.UnicityObjectMapper; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.serializer.json.JsonSerializationException; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.predicate.EncodedPredicate; +import java.util.Optional; /** - * Token transaction. - * - * @param transaction data + * Common interface for token transactions. */ -public abstract class Transaction> { - - private final T data; - private final InclusionProof inclusionProof; +public interface Transaction { - @JsonCreator - Transaction( - @JsonProperty("data") T data, - @JsonProperty("inclusionProof") InclusionProof inclusionProof - ) { - Objects.requireNonNull(data, "Transaction data cannot be null"); - Objects.requireNonNull(inclusionProof, "Inclusion proof cannot be null"); - - this.data = data; - this.inclusionProof = inclusionProof; - } + /** + * Get transaction payload bytes. + * + * @return payload bytes + */ + Optional getData(); /** - * Get transaction data. + * Gets the predicate that locks this transaction. * - * @return transaction data + * @return lock script predicate */ - public T getData() { - return this.data; - } + EncodedPredicate getLockScript(); /** - * Get transaction inclusion proof. + * Gets the transaction recipient. * - * @return inclusion proof + * @return recipient predicate */ - public InclusionProof getInclusionProof() { - return this.inclusionProof; - } + EncodedPredicate getRecipient(); /** - * Verify if recipient data is added to transaction. + * Gets the source state hash. * - * @param stateData recipient data - * @return true if contains given data hash + * @return source state hash */ - public boolean containsRecipientData(byte[] stateData) { - if (this.data.getRecipientDataHash().isPresent() == (stateData == null)) { - return false; - } + DataHash getSourceStateHash(); - if (!this.data.getRecipientDataHash().isPresent()) { - return true; - } + /** + * Get transaction randomness component. + * + * @return randomness bytes + */ + byte[] getStateMask(); - DataHasher hasher = new DataHasher(HashAlgorithm.SHA256); - hasher.update(stateData); - return hasher.digest().equals(this.data.getRecipientDataHash().orElse(null)); - } + /** + * Calculates the resulting state hash. + * + * @return state hash + */ + DataHash calculateStateHash(); /** - * Convert transaction to JSON string. + * Calculates the transaction hash. * - * @return JSON string + * @return transaction hash */ - public String toJson() { - try { - return UnicityObjectMapper.JSON.writeValueAsString(this); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(Transaction.class, e); - } - } + DataHash calculateTransactionHash(); /** - * Convert transaction to CBOR bytes. + * Serializes this transaction as CBOR. * * @return CBOR bytes */ - public byte[] toCbor() { - return CborSerializer.encodeArray( - this.data.toCbor(), - this.inclusionProof.toCbor() - ); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof Transaction)) { - return false; - } - Transaction that = (Transaction) o; - return Objects.equals(this.data, that.data) && Objects.equals(this.inclusionProof, - that.inclusionProof); - } - - @Override - public int hashCode() { - return Objects.hash(this.data, this.inclusionProof); - } - - @Override - public String toString() { - return String.format("Transaction{data=%s, inclusionProof=%s}", this.data, this.inclusionProof); - } + byte[] toCbor(); } diff --git a/src/main/java/org/unicitylabs/sdk/transaction/TransactionData.java b/src/main/java/org/unicitylabs/sdk/transaction/TransactionData.java deleted file mode 100644 index 74c0028..0000000 --- a/src/main/java/org/unicitylabs/sdk/transaction/TransactionData.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.unicitylabs.sdk.transaction; - -import java.util.Optional; -import org.unicitylabs.sdk.address.Address; -import org.unicitylabs.sdk.hash.DataHash; - -/** - * Interface representing the data of a transaction. - * - * @param the type of the transaction source state - */ -public interface TransactionData { - - /** - * Gets the transaction source state. - * - * @return the source state - */ - T getSourceState(); - - /** - * Gets the recipient address of the transaction. - * - * @return the recipient address - */ - Address getRecipient(); - - /** - * Gets the optional recipient data hash. - * - * @return an Optional containing the data hash if present, otherwise empty - */ - Optional getRecipientDataHash(); - - /** - * Calculates the hash of the transaction data. - * - * @return the calculated DataHash - */ - DataHash calculateHash(); - - /** - * Convert transaction data to CBOR bytes. - * - * @return CBOR bytes - */ - byte[] toCbor(); -} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/TransferCommitment.java b/src/main/java/org/unicitylabs/sdk/transaction/TransferCommitment.java deleted file mode 100644 index 5e9396f..0000000 --- a/src/main/java/org/unicitylabs/sdk/transaction/TransferCommitment.java +++ /dev/null @@ -1,83 +0,0 @@ - -package org.unicitylabs.sdk.transaction; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Objects; -import org.unicitylabs.sdk.address.Address; -import org.unicitylabs.sdk.api.Authenticator; -import org.unicitylabs.sdk.api.RequestId; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.Token; - -/** - * Commitment representing a transfer transaction. - */ -public class TransferCommitment extends Commitment { - - @JsonCreator - private TransferCommitment( - @JsonProperty("requestId") - RequestId requestId, - @JsonProperty("transactionData") - TransferTransaction.Data transactionData, - @JsonProperty("authenticator") - Authenticator authenticator - ) { - super(requestId, transactionData, authenticator); - } - - /** - * Create transfer transaction from transfer commitment. - * - * @param inclusionProof Commitment inclusion proof - * @return transfer transaction - */ - @Override - public TransferTransaction toTransaction(InclusionProof inclusionProof) { - return new TransferTransaction(this.getTransactionData(), inclusionProof); - } - - /** - * Create transfer commitment. - * - * @param token current token - * @param recipient recipient of token - * @param salt transaction salt - * @param recipientDataHash recipient data hash - * @param message transaction message - * @param signingService signing service to unlock token - * @return transfer commitment - */ - public static TransferCommitment create( - Token token, - Address recipient, - byte[] salt, - DataHash recipientDataHash, - byte[] message, - SigningService signingService - ) { - Objects.requireNonNull(token, "Token cannot be null"); - Objects.requireNonNull(recipient, "Recipient address cannot be null"); - Objects.requireNonNull(salt, "Salt cannot be null"); - Objects.requireNonNull(signingService, "SigningService cannot be null"); - - TransferTransaction.Data data = new TransferTransaction.Data( - token.getState(), - recipient, - salt, - recipientDataHash, - message, - token.getNametags() - ); - RequestId requestId = RequestId.create(signingService.getPublicKey(), data.getSourceState()); - Authenticator authenticator = Authenticator.create( - signingService, - data.calculateHash(), - data.getSourceState().calculateHash() - ); - - return new TransferCommitment(requestId, data, authenticator); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java index d611143..9390b38 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java @@ -1,298 +1,186 @@ - package org.unicitylabs.sdk.transaction; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; -import org.unicitylabs.sdk.address.Address; -import org.unicitylabs.sdk.address.AddressFactory; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; +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.PredicateEngineService; -import org.unicitylabs.sdk.serializer.UnicityObjectMapper; +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.serializer.json.JsonSerializationException; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.token.TokenState; import org.unicitylabs.sdk.util.HexConverter; -import org.unicitylabs.sdk.verification.VerificationResult; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; /** - * Token transfer transaction. + * Transfer transaction that moves token ownership from a source state to a recipient. */ -public class TransferTransaction extends Transaction { +public class TransferTransaction implements Transaction { + public static final long CBOR_TAG = 39045; + private static final int VERSION = 1; + + private final DataHash sourceStateHash; + private final EncodedPredicate lockScript; + private final EncodedPredicate recipient; + private final byte[] stateMask; + private final byte[] data; + + private TransferTransaction( + DataHash sourceStateHash, + EncodedPredicate lockScript, + EncodedPredicate recipient, + byte[] stateMask, + byte[] data + ) { + this.sourceStateHash = sourceStateHash; + this.lockScript = lockScript; + this.recipient = recipient; + this.stateMask = stateMask; + this.data = data; + } - @JsonCreator - TransferTransaction( - @JsonProperty("data") - Data data, - @JsonProperty("inclusionProof") - InclusionProof inclusionProof) { - super(data, inclusionProof); + public int getVersion() { + return TransferTransaction.VERSION; } - /** - * Create transfer transaction from CBOR bytes. - * - * @param bytes CBOR bytes - * @return transfer transaction - */ - public static TransferTransaction fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); - return new TransferTransaction( - Data.fromCbor(data.get(0)), - InclusionProof.fromCbor(data.get(1)) - ); + @Override + public Optional getData() { + return Optional.ofNullable(this.data != null ? Arrays.copyOf(this.data, this.data.length) : null); } - /** - * Create transfer transaction from JSON string. - * - * @param input JSON string - * @return transfer transaction - */ - public static TransferTransaction fromJson(String input) { - try { - return UnicityObjectMapper.JSON.readValue(input, TransferTransaction.class); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(TransferTransaction.class, e); - } + @Override + public EncodedPredicate getLockScript() { + return this.lockScript; + } + + @Override + public EncodedPredicate getRecipient() { + return this.recipient; + } + + @Override + public DataHash getSourceStateHash() { + return this.sourceStateHash; + } + + @Override + public byte[] getStateMask() { + return Arrays.copyOf(this.stateMask, this.stateMask.length); } /** - * Verify if transaction is based off of that token state. + * Creates a transfer transaction from the latest state of the provided token. * - * @param trustBase trust base to verify against - * @param token token - * @return verification result + * @param token token whose latest transaction is used as the source + * @param recipient recipient predicate + * @param stateMask transaction randomness component + * @param data transfer payload + * @return created transfer transaction */ - public VerificationResult verify(RootTrustBase trustBase, Token token) { - Predicate predicate = PredicateEngineService.createPredicate(token.getState().getPredicate()); + public static TransferTransaction create(Token token, Predicate recipient, + byte[] stateMask, byte[] data) { + Transaction transaction = token.getLatestTransaction(); - return VerificationResult.fromChildren("Transaction verification", List.of( - token.verifyNametagTokens(trustBase), - token.verifyRecipient(), - token.verifyRecipientData(), - predicate.verify(token, this, trustBase) - ? VerificationResult.success() - : VerificationResult.fail("Predicate verification failed") - )); + return new TransferTransaction( + transaction.calculateStateHash(), + transaction.getRecipient(), + EncodedPredicate.fromPredicate(recipient), + stateMask, + data + ); } /** - * Transaction data for token state transitions. + * Deserializes a transfer transaction from CBOR bytes. + * + * @param bytes CBOR-encoded transfer transaction + * @param token token providing the source state for the deserialized transfer + * @return decoded transfer transaction */ - public static class Data implements TransactionData { - - private final TokenState sourceState; - private final Address recipient; - private final byte[] salt; - private final DataHash recipientDataHash; - private final byte[] message; - private final List> nametags; - - @JsonCreator - Data( - @JsonProperty("sourceState") TokenState sourceState, - @JsonProperty("recipient") Address recipient, - @JsonProperty("salt") byte[] salt, - @JsonProperty("recipientDataHash") DataHash recipientDataHash, - @JsonProperty("message") byte[] message, - @JsonProperty("nametags") List> nametags - ) { - Objects.requireNonNull(sourceState, "SourceState cannot be null"); - Objects.requireNonNull(recipient, "Recipient cannot be null"); - Objects.requireNonNull(salt, "Salt cannot be null"); - Objects.requireNonNull(nametags, "Nametags cannot be null"); - - this.sourceState = sourceState; - this.recipient = recipient; - this.salt = Arrays.copyOf(salt, salt.length); - this.recipientDataHash = recipientDataHash; - this.message = message != null ? Arrays.copyOf(message, message.length) : null; - this.nametags = List.copyOf(nametags); - } - - /** - * Get transaction source state. - * - * @return source state - */ - public TokenState getSourceState() { - return this.sourceState; + public static TransferTransaction fromCbor(byte[] bytes, Token token) { + CborDeserializer.CborTag tag = CborDeserializer.decodeTag(bytes); + if (tag.getTag() != TransferTransaction.CBOR_TAG) { + throw new CborSerializationException(String.format("Invalid CBOR tag: %s", tag.getTag())); } + List data = CborDeserializer.decodeArray(tag.getData(), 4); - /** - * Get transaction recipient address. - * - * @return recipient address - */ - public Address getRecipient() { - return this.recipient; + int version = CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(); + if (version != TransferTransaction.VERSION) { + throw new CborSerializationException(String.format("Unsupported version: %s", version)); } - /** - * Get transaction salt. - * - * @return transaction salt - */ - public byte[] getSalt() { - return Arrays.copyOf(this.salt, this.salt.length); - } - - /** - * Get transaction recipient data hash. - * - * @return recipient data hash - */ - public Optional getRecipientDataHash() { - return Optional.ofNullable(this.recipientDataHash); - } - - /** - * Get transaction message. - * - * @return transaction message - */ - public Optional getMessage() { - return this.message != null - ? Optional.of(Arrays.copyOf(this.message, this.message.length)) - : Optional.empty(); - } - - /** - * Get transaction nametags. - * - * @return nametags - */ - public List> getNametags() { - return this.nametags; - } - - /** - * Calculate transfer transaction data hash. - * - * @return transaction data hash - */ - public DataHash calculateHash() { - return new DataHasher(HashAlgorithm.SHA256) - .update(this.toCbor()) - .digest(); - } - - /** - * Create transfer transaction data from CBOR bytes. - * - * @param bytes CBOR bytes - * @return transfer transaction - */ - public static Data fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); - - return new Data( - TokenState.fromCbor(data.get(0)), - AddressFactory.createAddress(CborDeserializer.readTextString(data.get(1))), - CborDeserializer.readByteString(data.get(2)), - CborDeserializer.readOptional(data.get(3), DataHash::fromCbor), - CborDeserializer.readOptional(data.get(4), CborDeserializer::readByteString), - CborDeserializer.readArray(data.get(5)).stream() - .map(Token::fromCbor) - .collect(Collectors.toList()) - ); - } - - /** - * Convert transfer transaction data to CBOR bytes. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeArray( - this.sourceState.toCbor(), - CborSerializer.encodeTextString(this.recipient.getAddress()), - CborSerializer.encodeByteString(this.salt), - CborSerializer.encodeOptional(this.recipientDataHash, DataHash::toCbor), - CborSerializer.encodeOptional(this.message, CborSerializer::encodeByteString), - CborSerializer.encodeArray( - this.nametags.stream() - .map(Token::toCbor) - .toArray(byte[][]::new) - ) - ); - } - - /** - * Create transfer transaction data from JSON string. - * - * @param input JSON string - * @return transfer transaction data - */ - public static Data fromJson(String input) { - try { - return UnicityObjectMapper.JSON.readValue(input, Data.class); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(Data.class, e); - } - } + return TransferTransaction.create( + token, + EncodedPredicate.fromCbor(data.get(1)), + CborDeserializer.decodeByteString(data.get(2)), + CborDeserializer.decodeNullable(data.get(3), CborDeserializer::decodeByteString) + ); + } - /** - * Convert transfer transaction data to JSON string. - * - * @return JSON string - */ - public String toJson() { - try { - return UnicityObjectMapper.JSON.writeValueAsString(this); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(Data.class, e); - } - } + @Override + public DataHash calculateStateHash() { + return new DataHasher(HashAlgorithm.SHA256) + .update( + CborSerializer.encodeArray( + CborSerializer.encodeByteString(this.sourceStateHash.getImprint()), + CborSerializer.encodeByteString(this.stateMask) + ) + ) + .digest(); + } - @Override - public boolean equals(Object o) { - if (!(o instanceof Data)) { - return false; - } - Data that = (Data) o; - return Objects.equals(this.sourceState, that.sourceState) - && Objects.equals(this.recipient, that.recipient) - && Objects.deepEquals(this.salt, that.salt) - && Objects.equals(this.recipientDataHash, that.recipientDataHash) - && Objects.deepEquals(this.message, that.message) - && Objects.equals(this.nametags, that.nametags); - } + @Override + public DataHash calculateTransactionHash() { + return new DataHasher(HashAlgorithm.SHA256) + .update(this.toCbor()) + .digest(); + } - @Override - public int hashCode() { - return Objects.hash(this.sourceState, this.recipient, Arrays.hashCode(this.salt), - this.recipientDataHash, - Arrays.hashCode(this.message), this.nametags); - } + @Override + public byte[] toCbor() { + return CborSerializer.encodeTag( + TransferTransaction.CBOR_TAG, + CborSerializer.encodeArray( + CborSerializer.encodeUnsignedInteger(TransferTransaction.VERSION), + EncodedPredicate.fromPredicate(this.recipient).toCbor(), + CborSerializer.encodeByteString(this.stateMask), + CborSerializer.encodeNullable(this.data, CborSerializer::encodeByteString) + ) + ); + } - @Override - public String toString() { - return String.format( - "Data{" - + "sourceState=%s, " - + "recipient=%s, " - + "salt=%s, " - + "dataHash=%s, " - + "message=%s, " - + "nametags=%s" - + "}", - this.sourceState, this.recipient, HexConverter.encode(this.salt), this.recipientDataHash, - this.message != null ? HexConverter.encode(this.message) : null, this.nametags); - } + /** + * Converts this transfer transaction to a certified transfer transaction. + * + * @param trustBase trust base used for proof verification + * @param predicateVerifier predicate verifier service + * @param inclusionProof inclusion proof for this transaction + * @return certified transfer transaction + */ + public CertifiedTransferTransaction toCertifiedTransaction( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + InclusionProof inclusionProof + ) { + return CertifiedTransferTransaction.fromTransaction( + trustBase, + predicateVerifier, + this, + inclusionProof + ); } + @Override + public String toString() { + return String.format( + "TransferTransaction{sourceStateHash=%s, lockScript=%s, recipient=%s, stateMask=%s, data=%s}", + this.sourceStateHash, this.lockScript, this.recipient, HexConverter.encode(this.stateMask), + HexConverter.encode(this.data)); + } } diff --git a/src/main/java/org/unicitylabs/sdk/transaction/split/SplitMintReason.java b/src/main/java/org/unicitylabs/sdk/transaction/split/SplitMintReason.java deleted file mode 100644 index 89b5f39..0000000 --- a/src/main/java/org/unicitylabs/sdk/transaction/split/SplitMintReason.java +++ /dev/null @@ -1,174 +0,0 @@ - -package org.unicitylabs.sdk.transaction.split; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import java.math.BigInteger; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; -import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreePathStep; -import org.unicitylabs.sdk.predicate.Predicate; -import org.unicitylabs.sdk.predicate.PredicateEngineService; -import org.unicitylabs.sdk.predicate.embedded.BurnPredicate; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.token.fungible.CoinId; -import org.unicitylabs.sdk.token.fungible.TokenCoinData; -import org.unicitylabs.sdk.transaction.MintReasonType; -import org.unicitylabs.sdk.transaction.MintTransaction; -import org.unicitylabs.sdk.transaction.MintTransactionReason; -import org.unicitylabs.sdk.verification.VerificationResult; - -/** - * Mint reason for splitting a token. - */ -@JsonIgnoreProperties() -public class SplitMintReason implements MintTransactionReason { - - private final Token token; - private final List proofs; - - @JsonCreator - SplitMintReason( - @JsonProperty("token") Token token, - @JsonProperty("proofs") List proofs - ) { - Objects.requireNonNull(token, "Token cannot be null"); - Objects.requireNonNull(proofs, "Proofs cannot be null"); - - this.token = token; - this.proofs = List.copyOf(proofs); - } - - /** - * Get mint reason type. - * - * @return token split reason - */ - @JsonProperty(access = JsonProperty.Access.READ_ONLY) - public String getType() { - return MintReasonType.TOKEN_SPLIT.name(); - } - - /** - * Get token which was burnt for split. - * - * @return burnt token - */ - public Token getToken() { - return this.token; - } - - /** - * Get proofs for currently minted token coins. - * - * @return split proofs - */ - public List getProofs() { - return List.copyOf(this.proofs); - } - - /** - * Verify mint transaction against mint reason. - * - * @param transaction Genesis to verify against - * @return verification result - */ - public VerificationResult verify(MintTransaction transaction) { - if (!transaction.getData().getCoinData().isPresent()) { - return VerificationResult.fail("Coin data is missing."); - } - - Predicate predicate = PredicateEngineService.createPredicate( - this.token.getState().getPredicate()); - if (!(predicate instanceof BurnPredicate)) { - return VerificationResult.fail("Token is not burned"); - } - - Map coins = transaction.getData().getCoinData().map(TokenCoinData::getCoins) - .orElse(Map.of()); - if (coins.size() != this.proofs.size()) { - return VerificationResult.fail("Total amount of coins differ in token and proofs."); - } - - for (SplitMintReasonProof proof : this.proofs) { - if (!proof.getAggregationPath().verify(proof.getCoinId().toBitString().toBigInteger()) - .isSuccessful()) { - return VerificationResult.fail( - "Aggregation path verification failed for coin: " + proof.getCoinId()); - } - - if (!proof.getCoinTreePath() - .verify(transaction.getData().getTokenId().toBitString().toBigInteger()).isSuccessful()) { - return VerificationResult.fail( - "Coin tree path verification failed for token"); - } - - List aggregationPathSteps = proof.getAggregationPath() - .getSteps(); - if (aggregationPathSteps.size() == 0 - || !Arrays.equals(proof.getCoinTreePath().getRootHash().getImprint(), - aggregationPathSteps.get(0).getData().orElse(null)) - ) { - return VerificationResult.fail("Coin tree root does not match aggregation path leaf."); - } - - if (!proof.getCoinTreePath().getSteps().get(0).getValue().equals(coins.get(proof.getCoinId()))) { - return VerificationResult.fail("Coin amount in token does not match coin tree leaf."); - } - - if (!proof.getAggregationPath().getRootHash() - .equals(((BurnPredicate) predicate).getReason())) { - return VerificationResult.fail("Burn reason does not match aggregation root."); - } - } - - return VerificationResult.success(); - } - - /** - * Create split mint reason from CBOR bytes. - * - * @param bytes CBOR bytes - * @return mint reason - */ - public static SplitMintReason fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); - - return new SplitMintReason( - Token.fromCbor(data.get(1)), - CborDeserializer.readArray(data.get(2)).stream() - .map(SplitMintReasonProof::fromCbor) - .collect(Collectors.toList()) - ); - } - - /** - * Convert split mint reason to CBOR bytes. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeArray( - CborSerializer.encodeTextString(this.getType()), - this.token.toCbor(), - CborSerializer.encodeArray( - this.proofs.stream() - .map(SplitMintReasonProof::toCbor) - .toArray(byte[][]::new) - ) - ); - } - - @Override - public String toString() { - return String.format("SplitMintReason{token=%s, proofs=%s}", this.token, this.proofs); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/split/SplitMintReasonProof.java b/src/main/java/org/unicitylabs/sdk/transaction/split/SplitMintReasonProof.java deleted file mode 100644 index 405a83d..0000000 --- a/src/main/java/org/unicitylabs/sdk/transaction/split/SplitMintReasonProof.java +++ /dev/null @@ -1,93 +0,0 @@ - -package org.unicitylabs.sdk.transaction.split; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; -import java.util.Objects; -import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreePath; -import org.unicitylabs.sdk.mtree.sum.SparseMerkleSumTreePath; -import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.token.fungible.CoinId; - -/** - * Split mint reason proof for specific coin. - */ -public class SplitMintReasonProof { - - private final CoinId coinId; - private final SparseMerkleTreePath aggregationPath; - private final SparseMerkleSumTreePath coinTreePath; - - @JsonCreator - SplitMintReasonProof( - @JsonProperty("coinId") CoinId coinId, - @JsonProperty("aggregationPath") SparseMerkleTreePath aggregationPath, - @JsonProperty("coinTreePath") SparseMerkleSumTreePath coinTreePath - ) { - Objects.requireNonNull(coinId, "coinId cannot be null"); - Objects.requireNonNull(aggregationPath, "aggregationPath cannot be null"); - Objects.requireNonNull(coinTreePath, "coinTreePath cannot be null"); - - this.coinId = coinId; - this.aggregationPath = aggregationPath; - this.coinTreePath = coinTreePath; - } - - /** - * Get coin ID associated with proof. - * - * @return coin id - */ - public CoinId getCoinId() { - return this.coinId; - } - - /** - * Get aggregation path for current coin in coin trees. - * - * @return aggregation path - */ - public SparseMerkleTreePath getAggregationPath() { - return this.aggregationPath; - } - - /** - * Get coin tree path for current coin. - * - * @return coin tree path - */ - public SparseMerkleSumTreePath getCoinTreePath() { - return this.coinTreePath; - } - - /** - * Create split mint reason proof from CBOR bytes. - * - * @param bytes CBOR bytes - * @return split mint reason proof - */ - public static SplitMintReasonProof fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); - - return new SplitMintReasonProof( - new CoinId(CborDeserializer.readByteString(data.get(0))), - SparseMerkleTreePath.fromCbor(data.get(1)), - SparseMerkleSumTreePath.fromCbor(data.get(2)) - ); - } - - /** - * Convert split mint reason proof to CBOR bytes. - * - * @return CBOR bytes - */ - public byte[] toCbor() { - return CborSerializer.encodeArray( - CborSerializer.encodeByteString(this.coinId.getBytes()), - this.aggregationPath.toCbor(), - this.coinTreePath.toCbor() - ); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/split/TokenSplitBuilder.java b/src/main/java/org/unicitylabs/sdk/transaction/split/TokenSplitBuilder.java deleted file mode 100644 index b4c97a2..0000000 --- a/src/main/java/org/unicitylabs/sdk/transaction/split/TokenSplitBuilder.java +++ /dev/null @@ -1,275 +0,0 @@ -package org.unicitylabs.sdk.transaction.split; - -import java.math.BigInteger; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; -import org.unicitylabs.sdk.address.Address; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.mtree.BranchExistsException; -import org.unicitylabs.sdk.mtree.LeafOutOfBoundsException; -import org.unicitylabs.sdk.mtree.plain.SparseMerkleTree; -import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreeRootNode; -import org.unicitylabs.sdk.mtree.sum.SparseMerkleSumTree; -import org.unicitylabs.sdk.mtree.sum.SparseMerkleSumTree.LeafValue; -import org.unicitylabs.sdk.mtree.sum.SparseMerkleSumTreeRootNode; -import org.unicitylabs.sdk.predicate.embedded.BurnPredicate; -import org.unicitylabs.sdk.predicate.embedded.BurnPredicateReference; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.token.TokenState; -import org.unicitylabs.sdk.token.TokenType; -import org.unicitylabs.sdk.token.fungible.CoinId; -import org.unicitylabs.sdk.token.fungible.TokenCoinData; -import org.unicitylabs.sdk.transaction.MintCommitment; -import org.unicitylabs.sdk.transaction.MintTransaction; -import org.unicitylabs.sdk.transaction.TransferCommitment; -import org.unicitylabs.sdk.transaction.TransferTransaction; -import org.unicitylabs.sdk.verification.VerificationException; - -/** - * Token splitting builder. - */ -public class TokenSplitBuilder { - - private final Map tokens = new HashMap<>(); - - /** - * Create new token split builder. - */ - public TokenSplitBuilder() { - } - - /** - * Create new token which will be created from selected token. - * - * @param id new token id - * @param type new token type - * @param data new token data - * @param coinData new token coin data - * @param recipient new token recipient address - * @param salt new token salt - * @param recipientDataHash new token recipient data hash - * @return current builder - */ - public TokenSplitBuilder createToken( - TokenId id, - TokenType type, - byte[] data, - TokenCoinData coinData, - Address recipient, - byte[] salt, - DataHash recipientDataHash - ) { - this.tokens.put(id, - new TokenRequest(id, type, data, coinData, recipient, salt, recipientDataHash)); - - return this; - } - - /** - * Split old token to new tokens. - * - * @param token token to be used for split - * @return token split object for submitting info - * @throws LeafOutOfBoundsException if building aggregation tree and coin tree fail - * @throws BranchExistsException if building aggregation tree and coin tree fail - */ - public TokenSplit build(Token token) throws LeafOutOfBoundsException, BranchExistsException { - Objects.requireNonNull(token, "Token cannot be null"); - - Map trees = new HashMap<>(); - - for (TokenRequest data : this.tokens.values()) { - for (Map.Entry coin : data.coinData.getCoins().entrySet()) { - SparseMerkleSumTree tree = trees.computeIfAbsent(coin.getKey(), - k -> new SparseMerkleSumTree(HashAlgorithm.SHA256)); - tree.addLeaf(data.id.toBitString().toBigInteger(), - new LeafValue(coin.getKey().getBytes(), coin.getValue())); - } - } - - Map tokenCoins = token.getCoins().map(TokenCoinData::getCoins) - .orElse(Map.of()); - if (trees.size() != tokenCoins.size()) { - throw new IllegalArgumentException("Token has different number of coins than expected"); - } - - SparseMerkleTree aggregationTree = new SparseMerkleTree(HashAlgorithm.SHA256); - Map coinRoots = new HashMap<>(); - for (Entry tree : trees.entrySet()) { - BigInteger coinsInToken = Optional.ofNullable(tokenCoins.get(tree.getKey())) - .orElse(BigInteger.ZERO); - SparseMerkleSumTreeRootNode root = tree.getValue().calculateRoot(); - if (root.getValue().compareTo(coinsInToken) != 0) { - throw new IllegalArgumentException( - String.format("Token contained %s %s coins, but tree has %s", - coinsInToken, tree.getKey(), root.getValue())); - } - - coinRoots.put(tree.getKey(), root); - aggregationTree.addLeaf(tree.getKey().toBitString().toBigInteger(), - root.getRootHash().getImprint()); - } - - return new TokenSplit( - token, - aggregationTree.calculateRoot(), - coinRoots, - this.tokens - ); - } - - /** - * Token split request object. - */ - public static class TokenSplit { - - private final Token token; - private final SparseMerkleTreeRootNode aggregationRoot; - private final Map coinRoots; - private final Map tokens; - - private TokenSplit( - Token token, - SparseMerkleTreeRootNode aggregationRoot, - Map coinRoots, - Map tokens - ) { - this.token = token; - this.aggregationRoot = aggregationRoot; - this.coinRoots = coinRoots; - this.tokens = tokens; - } - - /** - * Create burn commitment to burn token going through split. - * - * @param salt burn commitment salt - * @param signingService signing service used to unlock token - * @return transfer commitment for sending to unicity service - */ - public TransferCommitment createBurnCommitment(byte[] salt, SigningService signingService) { - return TransferCommitment.create( - token, - BurnPredicateReference.create( - this.token.getType(), - this.aggregationRoot.getRootHash() - ).toAddress(), - salt, - null, - null, - signingService - ); - } - - /** - * Create split mint commitments after burn transaction is received. - * - * @param trustBase trust base for burn transaction verification - * @param burnTransaction burn transaction - * @return list of mint commitments for sending to unicity service - * @throws VerificationException if token verification fails - */ - public List> createSplitMintCommitments( - RootTrustBase trustBase, - TransferTransaction burnTransaction - ) throws VerificationException { - Objects.requireNonNull(burnTransaction, "Burn transaction cannot be null"); - - Token burnedToken = this.token.update( - trustBase, - new TokenState( - new BurnPredicate( - this.token.getId(), - this.token.getType(), - this.aggregationRoot.getRootHash() - ), - null - ), - burnTransaction, - List.of() - ); - - return List.copyOf( - this.tokens.values().stream() - .map(request -> MintCommitment.create( - new MintTransaction.Data<>( - request.id, - request.type, - request.data, - request.coinData, - request.recipient, - request.salt, - request.recipientDataHash, - new SplitMintReason( - burnedToken, - List.copyOf( - request.coinData.getCoins().keySet().stream() - .map(coinId -> new SplitMintReasonProof( - coinId, - this.aggregationRoot - .getPath(coinId.toBitString().toBigInteger()), - this.coinRoots.get(coinId) - .getPath(request.id.toBitString().toBigInteger()) - ) - ) - .collect(Collectors.toList()) - ) - ) - ) - ) - ) - .collect(Collectors.toList()) - ); - } - } - - /** - * New token request for generating it out of burnt token. - */ - public static class TokenRequest { - - private final TokenId id; - private final TokenType type; - private final byte[] data; - private final TokenCoinData coinData; - private final Address recipient; - private final byte[] salt; - private final DataHash recipientDataHash; - - TokenRequest( - TokenId id, - TokenType type, - byte[] data, - TokenCoinData coinData, - Address recipient, - byte[] salt, - DataHash recipientDataHash - ) { - Objects.requireNonNull(id, "Token cannot be null"); - Objects.requireNonNull(type, "Token type cannot be null"); - Objects.requireNonNull(recipient, "Recipient cannot be null"); - Objects.requireNonNull(salt, "Salt cannot be null"); - if (coinData == null || coinData.getCoins().isEmpty()) { - throw new IllegalArgumentException("Token must have at least one coin"); - } - - this.id = id; - this.type = type; - this.data = data == null ? null : Arrays.copyOf(data, data.length); - this.coinData = coinData; - this.recipient = recipient; - this.salt = Arrays.copyOf(salt, salt.length); - this.recipientDataHash = recipientDataHash; - } - } -} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedMintTransactionVerificationRule.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedMintTransactionVerificationRule.java new file mode 100644 index 0000000..4c041dc --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedMintTransactionVerificationRule.java @@ -0,0 +1,87 @@ +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.SignaturePredicate; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.transaction.CertifiedMintTransaction; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Verification rule set for certified mint transactions. + * + *

The verification checks that the lock script in certification data matches the expected + * mint lock script derived from the token id, and that the inclusion proof is valid. + */ +public class CertifiedMintTransactionVerificationRule { + + private CertifiedMintTransactionVerificationRule() { + } + + /** + * Verify a certified mint transaction. + * + * @param trustBase root trust base + * @param predicateVerifier predicate verifier + * @param mintJustificationVerifier mint justification verifier + * @param transaction certified mint transaction to verify + * + * @return verification result with child results for each validation step + */ + public static VerificationResult verify( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + MintJustificationVerifierService mintJustificationVerifier, + CertifiedMintTransaction transaction + ) { + List> results = new ArrayList<>(); + + SigningService signingService = MintSigningService.create(transaction.getTokenId()); + 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); + + results.add(result); + if (result.getStatus() != VerificationStatus.OK) { + return new VerificationResult<>("CertifiedMintTransactionVerificationRule", + VerificationStatus.FAIL, "Invalid lock script", results); + } + + result = InclusionProofVerificationRule.verify(trustBase, predicateVerifier, + transaction.getInclusionProof(), transaction); + results.add(result); + if (result.getStatus() != InclusionProofVerificationStatus.OK) { + return new VerificationResult<>("CertifiedMintTransactionVerificationRule", + VerificationStatus.FAIL, "Inclusion proof verification failed", results); + } + + result = mintJustificationVerifier.verify(transaction); + results.add(result); + if (result.getStatus() != VerificationStatus.OK) { + return new VerificationResult<>( + "CertifiedMintTransactionVerificationRule", + VerificationStatus.FAIL, + "Invalid mint justification", + results + ); + } + + return new VerificationResult<>("CertifiedMintTransactionVerificationRule", + VerificationStatus.OK, "", results); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedTransferTransactionVerificationRule.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedTransferTransactionVerificationRule.java new file mode 100644 index 0000000..37612ed --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedTransferTransactionVerificationRule.java @@ -0,0 +1,48 @@ +package org.unicitylabs.sdk.transaction.verification; + +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.transaction.CertifiedTransferTransaction; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +import java.util.ArrayList; + +/** + * Verification rule set for certified transfer transactions. + * + *

The verification checks that the certified transfer transaction's inclusion proof is valid + * against the trust base. + */ +public class CertifiedTransferTransactionVerificationRule { + + private CertifiedTransferTransactionVerificationRule() { + } + + /** + * Verify a certified transfer transaction against the previous transaction. + * + * @param trustBase root trust base used for inclusion proof verification + * @param predicateVerifier predicate verifier used by inclusion proof verification + * @param transaction certified transfer transaction to verify + * + * @return verification result with child results for each validation step + */ + public static VerificationResult verify( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + CertifiedTransferTransaction transaction) { + ArrayList> results = new ArrayList>(); + + VerificationResult result = InclusionProofVerificationRule.verify(trustBase, + predicateVerifier, transaction.getInclusionProof(), transaction); + results.add(result); + if (result.getStatus() != InclusionProofVerificationStatus.OK) { + return new VerificationResult<>("CertifiedTransferTransactionVerificationRule", + VerificationStatus.FAIL, "Inclusion proof verification failed", results); + } + + return new VerificationResult<>("CertifiedTransferTransactionVerificationRule", + VerificationStatus.OK, "", results); + } +} 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/transaction/verification/InclusionProofVerificationRule.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationRule.java new file mode 100644 index 0000000..3b8f09e --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationRule.java @@ -0,0 +1,106 @@ +package org.unicitylabs.sdk.transaction.verification; + +import org.unicitylabs.sdk.api.CertificationData; +import org.unicitylabs.sdk.api.InclusionProof; +import org.unicitylabs.sdk.api.StateId; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.api.bft.verification.UnicityCertificateVerification; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.transaction.Transaction; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +/** + * This class provides the functionality to verify an inclusion proof against a given trust base + * and transaction. It ensures that the inclusion proof is valid, authentic, and corresponds to + * the specified transaction. + * + *

The verification process involves several checks, including: + * - Validating the trust base against the inclusion proof. + * - Ensuring the Merkle tree path is valid and included in the committed tree. + * - Verifying the certification data referenced by the inclusion proof. + * - Checking that the transaction hash matches the reference in the proof. + * - Confirming the proof's leaf value aligns with the expected hash. + * - Verifies given predicate against certification data + */ +public class InclusionProofVerificationRule { + + /** + * Verifies the provided inclusion proof against the specified trust base and transaction. + * + * @param trustBase the root trust base used to validate the inclusion proof + * @param predicateVerifier the service responsible for evaluating transaction predicates + * @param inclusionProof the inclusion proof containing certification data and merkle tree path + * @param transaction the transaction that is being verified against the proof + * + * @return a {@code VerificationResult} object containing the {@code InclusionProofVerificationStatus} + * and additional details about the verification outcome + */ + public static VerificationResult verify(RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, InclusionProof inclusionProof, + Transaction transaction) { + if (inclusionProof.getInclusionCertificate() == null) { + return new VerificationResult<>( + "InclusionProofVerificationRule", + InclusionProofVerificationStatus.INCLUSION_CERTIFICATE_MISSING + ); + } + + CertificationData certificationData = inclusionProof.getCertificationData().orElse(null); + if (certificationData == null) { + return new VerificationResult<>("InclusionProofVerificationRule", + InclusionProofVerificationStatus.MISSING_CERTIFICATION_DATA); + } + + if (!certificationData.getTransactionHash().equals(transaction.calculateTransactionHash())) { + return new VerificationResult<>("InclusionProofVerificationRule", + InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH); + } + + StateId stateId = StateId.fromTransaction(transaction); + if (!inclusionProof.getInclusionCertificate().verify(stateId, certificationData.getTransactionHash(), new DataHash(HashAlgorithm.SHA256, inclusionProof.getUnicityCertificate().getInputRecord().getHash()))) { + return new VerificationResult<>("InclusionProofVerificationRule", + InclusionProofVerificationStatus.PATH_INVALID); + } + + VerificationResult result = ShardIdMatchesStateIdRule.verify( + stateId, + inclusionProof.getUnicityCertificate().getShardTreeCertificate() + ); + if (result.getStatus() != VerificationStatus.OK) { + return new VerificationResult<>( + "InclusionProofVerificationRule", + InclusionProofVerificationStatus.SHARD_ID_MISMATCH, + "", + result + ); + } + + result = UnicityCertificateVerification.verify(trustBase, inclusionProof); + if (result.getStatus() != VerificationStatus.OK) { + return new VerificationResult<>( + "InclusionProofVerificationRule", + InclusionProofVerificationStatus.INVALID_TRUSTBASE, + "", + result + ); + } + + result = predicateVerifier.verify( + transaction.getLockScript(), + transaction.getSourceStateHash(), + certificationData.getTransactionHash(), + certificationData.getUnlockScript() + ); + + if (result.getStatus() != VerificationStatus.OK) { + return new VerificationResult<>("InclusionProofVerificationRule", + InclusionProofVerificationStatus.NOT_AUTHENTICATED, "", result); + } + + return new VerificationResult<>("InclusionProofVerificationRule", + InclusionProofVerificationStatus.OK); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationStatus.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationStatus.java new file mode 100644 index 0000000..c179fdd --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationStatus.java @@ -0,0 +1,25 @@ +package org.unicitylabs.sdk.transaction.verification; + +/** + * Status codes returned by inclusion proof verification. + */ +public enum InclusionProofVerificationStatus { + /** The provided trust base is invalid or cannot be used for verification. */ + INVALID_TRUSTBASE, + /** Certification data required for verification is missing. */ + MISSING_CERTIFICATION_DATA, + /** Transaction hash does not match the value referenced by the proof. */ + TRANSACTION_HASH_MISMATCH, + /** Proof authentication failed. */ + NOT_AUTHENTICATED, + /** Proof path is not included in the committed tree state. */ + PATH_NOT_INCLUDED, + + INCLUSION_CERTIFICATE_MISSING, + /** Proof path structure or hashes are invalid. */ + PATH_INVALID, + /** Shard id of the unicity certificate does not match the transaction state id. */ + SHARD_ID_MISMATCH, + /** Inclusion proof verification succeeded. */ + OK +} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/MintJustificationVerifier.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/MintJustificationVerifier.java new file mode 100644 index 0000000..ae94df2 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/MintJustificationVerifier.java @@ -0,0 +1,33 @@ +package org.unicitylabs.sdk.transaction.verification; + +import org.unicitylabs.sdk.transaction.CertifiedMintTransaction; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +/** + * Verifier for a specific kind of certified mint transaction justification, identified by a CBOR + * tag. Implementations are registered with {@link MintJustificationVerifierService} and dispatched + * based on the tag of the bytes stored in the mint transaction's justification field. + */ +public interface MintJustificationVerifier { + + /** + * Get the CBOR tag identifying the justification kind handled by this verifier. + * + * @return CBOR tag + */ + long getTag(); + + /** + * Verify the justification of the given certified mint transaction. + * + * @param transaction certified mint transaction whose justification is being verified + * @param mintJustificationVerifierService dispatcher used to recursively verify nested mint + * justifications (for example, the burn token's mint chain) + * + * @return verification result + */ + VerificationResult verify( + CertifiedMintTransaction transaction, + MintJustificationVerifierService mintJustificationVerifierService); +} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/MintJustificationVerifierService.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/MintJustificationVerifierService.java new file mode 100644 index 0000000..2e1f34a --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/MintJustificationVerifierService.java @@ -0,0 +1,70 @@ +package org.unicitylabs.sdk.transaction.verification; + +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.transaction.CertifiedMintTransaction; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +import java.util.HashMap; +import java.util.Map; + +/** + * Dispatcher for {@link MintJustificationVerifier} implementations. Verifiers are registered + * by their CBOR tag; on {@link #verify(CertifiedMintTransaction)} the service reads the tag of + * the mint transaction's justification and routes the verification to the matching verifier. + * + *

Mint transactions with no justification are accepted as OK without further checks. + */ +public class MintJustificationVerifierService { + private final Map verifiers = new HashMap<>(); + + /** + * Register a verifier for its declared tag. Each tag may be registered only once. + * + * @param verifier verifier to register + * + * @return this service for fluent chaining + * + * @throws IllegalArgumentException if a verifier for the same tag is already registered + */ + public MintJustificationVerifierService register(MintJustificationVerifier verifier) { + if (this.verifiers.containsKey(verifier.getTag())) { + throw new IllegalArgumentException(String.format("Duplicate mint justification verifier for tag %s.", verifier.getTag())); + } + + this.verifiers.put(verifier.getTag(), verifier); + return this; + } + + /** + * Verify the mint justification carried by the given transaction. + * + * @param transaction certified mint transaction to verify + * + * @return verification result; OK if the transaction has no justification, otherwise the result + * of the verifier registered for the justification's CBOR tag + */ + public VerificationResult verify(CertifiedMintTransaction transaction) { + byte[] bytes = transaction.getJustification().orElse(null); + if (bytes == null) { + return new VerificationResult<>("MintJustificationVerification", VerificationStatus.OK); + } + + CborDeserializer.CborTag tag = CborDeserializer.decodeTag(bytes); + MintJustificationVerifier verifier = this.verifiers.get(tag.getTag()); + if (verifier == null) { + return new VerificationResult<>( + "MintJustificationVerification", + VerificationStatus.FAIL, + String.format("Unsupported mint justification tag: %s", tag.getTag()) + ); + } + + VerificationResult result = verifier.verify(transaction, this); + if (result.getStatus() != VerificationStatus.OK) { + return new VerificationResult<>("MintJustificationVerification", VerificationStatus.FAIL, String.format("Verification failed for tag %s", tag.getTag()), result); + } + + return new VerificationResult<>("MintJustificationVerification", VerificationStatus.OK, "", result); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/ShardIdMatchesStateIdRule.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/ShardIdMatchesStateIdRule.java new file mode 100644 index 0000000..2d2b36b --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/ShardIdMatchesStateIdRule.java @@ -0,0 +1,50 @@ +package org.unicitylabs.sdk.transaction.verification; + +import org.unicitylabs.sdk.api.StateId; +import org.unicitylabs.sdk.api.bft.ShardId; +import org.unicitylabs.sdk.api.bft.ShardTreeCertificate; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +/** + * Rule to verify that the shard id of the shard tree certificate is a prefix of the transaction + * state id. An empty shard id matches any state id. + */ +public class ShardIdMatchesStateIdRule { + + private ShardIdMatchesStateIdRule() { + } + + /** + * Verify that the shard id is a prefix of the state id. + * + * @param stateId state id of the transaction being verified + * @param shardTreeCertificate shard tree certificate carrying the shard id + * + * @return verification result with {@link VerificationStatus#OK} on match (or empty shard id), + * otherwise {@link VerificationStatus#FAIL} + */ + public static VerificationResult verify( + StateId stateId, + ShardTreeCertificate shardTreeCertificate + ) { + if (stateId == null) { + return new VerificationResult<>("ShardIdMatchesStateIdRule", VerificationStatus.FAIL, "State ID is missing."); + } + + if (shardTreeCertificate == null) { + return new VerificationResult<>("ShardIdMatchesStateIdRule", VerificationStatus.FAIL, "Shard tree certificate is missing."); + } + + ShardId shardId = shardTreeCertificate.getShard(); + if (shardId.getLength() == 0) { + return new VerificationResult<>("ShardIdMatchesStateIdRule", VerificationStatus.OK); + } + + if (!shardId.isPrefixOf(stateId.getData())) { + return new VerificationResult<>("ShardIdMatchesStateIdRule", VerificationStatus.FAIL); + } + + return new VerificationResult<>("ShardIdMatchesStateIdRule", VerificationStatus.OK); + } +} 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/main/java/org/unicitylabs/sdk/util/BitString.java b/src/main/java/org/unicitylabs/sdk/util/BitString.java index ef9506e..98e7bf7 100644 --- a/src/main/java/org/unicitylabs/sdk/util/BitString.java +++ b/src/main/java/org/unicitylabs/sdk/util/BitString.java @@ -1,7 +1,7 @@ package org.unicitylabs.sdk.util; import java.math.BigInteger; -import org.unicitylabs.sdk.hash.DataHash; +import java.util.Arrays; /** * Represents a bit string as a BigInteger. This class is used to ensure that leading zero bits are @@ -11,12 +11,7 @@ public class BitString { private final BigInteger value; - /** - * Creates a BitString from a byte array. - * - * @param data The input data to convert into a BitString. - */ - public BitString(byte[] data) { + private BitString(byte[] data) { byte[] dataWithPrefix = new byte[data.length + 1]; dataWithPrefix[0] = 1; System.arraycopy(data, 0, dataWithPrefix, 1, data.length); @@ -24,13 +19,53 @@ public BitString(byte[] data) { } /** - * Creates a BitString from a DataHash imprint. + * Creates a BitString from raw bytes with no bit reordering. BigInteger bit 0 is the LSB of the + * last byte. * - * @param dataHash DataHash - * @return A BitString instance + * @param data input bytes + * @return BitString */ - public static BitString fromDataHash(DataHash dataHash) { - return new BitString(dataHash.getImprint()); + public static BitString fromBytes(byte[] data) { + return new BitString(Arrays.copyOf(data, data.length)); + } + + /** + * Creates a BitString for LSB-first tree routing with reversed byte order. BigInteger bit 0 is + * bit 0 (LSB) of data[0], matching getBitAtDepth LSB convention. + * + * @param data input bytes + * @return BitString + */ + public static BitString fromBytesReversedLSB(byte[] data) { + byte[] reversed = new byte[data.length]; + for (int i = 0; i < data.length; i++) { + reversed[i] = data[data.length - 1 - i]; + } + return new BitString(reversed); + } + + /** + * Creates a BitString for MSB-first tree routing with reversed byte order. BigInteger bit 0 is + * bit 7 (MSB) of data[0], matching getBitAtDepth MSB convention. + * + * @param data input bytes + * @return BitString + */ + public static BitString fromBytesReversedMSB(byte[] data) { + byte[] reversed = new byte[data.length]; + for (int i = 0; i < data.length; i++) { + int b = data[data.length - 1 - i] & 0xFF; + int bitReversed = ((b & 0x80) >> 7) + | ((b & 0x40) >> 5) + | ((b & 0x20) >> 3) + | ((b & 0x10) >> 1) + | ((b & 0x08) << 1) + | ((b & 0x04) << 3) + | ((b & 0x02) << 5) + | ((b & 0x01) << 7); + reversed[i] = (byte) bitReversed; + } + return new BitString(reversed); } /** @@ -40,7 +75,17 @@ public static BitString fromDataHash(DataHash dataHash) { * @return The BigInteger representation of the bit string */ public BigInteger toBigInteger() { - return value; + return this.value; + } + + /** + * Converts bit string to byte array. + * + * @return The byte array representation of the bit string + */ + public byte[] toBytes() { + byte[] encoded = BigIntegerConverter.encode(this.value); + return Arrays.copyOfRange(encoded, 1, encoded.length); } /** @@ -50,11 +95,6 @@ public BigInteger toBigInteger() { */ @Override public String toString() { - String binary = value.toString(2); - // Remove the leading '1' bit we added - if (binary.length() > 1 && binary.charAt(0) == '1') { - return binary.substring(1); - } - return binary; + return this.value.toString(2).substring(1); } } \ No newline at end of file diff --git a/src/main/java/org/unicitylabs/sdk/util/HexConverter.java b/src/main/java/org/unicitylabs/sdk/util/HexConverter.java index 0f1e13f..dc3961d 100644 --- a/src/main/java/org/unicitylabs/sdk/util/HexConverter.java +++ b/src/main/java/org/unicitylabs/sdk/util/HexConverter.java @@ -7,7 +7,8 @@ public class HexConverter { private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray(); - private HexConverter() {} + private HexConverter() { + } /** * Convert byte array to hex. diff --git a/src/main/java/org/unicitylabs/sdk/util/InclusionProofUtils.java b/src/main/java/org/unicitylabs/sdk/util/InclusionProofUtils.java index fb40735..4388068 100644 --- a/src/main/java/org/unicitylabs/sdk/util/InclusionProofUtils.java +++ b/src/main/java/org/unicitylabs/sdk/util/InclusionProofUtils.java @@ -1,16 +1,22 @@ package org.unicitylabs.sdk.util; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.unicitylabs.sdk.StateTransitionClient; +import org.unicitylabs.sdk.api.InclusionProof; +import org.unicitylabs.sdk.api.StateId; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +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.time.Duration; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.unicitylabs.sdk.StateTransitionClient; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.transaction.Commitment; -import org.unicitylabs.sdk.transaction.InclusionProof; -import org.unicitylabs.sdk.transaction.InclusionProofVerificationStatus; /** * Utility class for working with inclusion proofs. @@ -19,7 +25,7 @@ public class InclusionProofUtils { private static final Logger logger = LoggerFactory.getLogger(InclusionProofUtils.class); private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds( - 30); // 30 seconds should be enough for direct leader + 30); // 30 seconds should be enough for direct leader private static final Duration DEFAULT_INTERVAL = Duration.ofMillis(1000); private InclusionProofUtils() { @@ -28,35 +34,40 @@ private InclusionProofUtils() { /** * Wait for an inclusion proof to be available and verified. * - * @param client State transition client - * @param trustBase Root trust base - * @param commitment Inclusion proof commitment to wait for + * @param client State transition client + * @param trustBase Root trust base + * @param predicateVerifier Predicate verifier service + * @param transaction Transaction to wait Inclusion proof for * @return Completable future with inclusion proof */ public static CompletableFuture waitInclusionProof( - StateTransitionClient client, - RootTrustBase trustBase, - Commitment commitment + StateTransitionClient client, + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + Transaction transaction ) { - return waitInclusionProof(client, trustBase, commitment, DEFAULT_TIMEOUT, DEFAULT_INTERVAL); + return waitInclusionProof(client, trustBase, predicateVerifier, transaction, DEFAULT_TIMEOUT, + DEFAULT_INTERVAL); } /** * Wait for an inclusion proof to be available and verified with custom timeout. * - * @param client State transition client - * @param trustBase Root trust base - * @param commitment Inclusion proof commitment to wait for - * @param timeout Maximum duration to wait for the inclusion proof - * @param interval Interval between checks for the inclusion proof + * @param client State transition client + * @param trustBase Root trust base + * @param predicateVerifier Predicate verifier service + * @param transaction Transaction to wait Inclusion proof for + * @param timeout Maximum duration to wait for the inclusion proof + * @param interval Interval between checks for the inclusion proof * @return Completable future with inclusion proof */ public static CompletableFuture waitInclusionProof( - StateTransitionClient client, - RootTrustBase trustBase, - Commitment commitment, - Duration timeout, - Duration interval + StateTransitionClient client, + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + Transaction transaction, + Duration timeout, + Duration interval ) { CompletableFuture future = new CompletableFuture<>(); @@ -64,41 +75,44 @@ public static CompletableFuture waitInclusionProof( long startTime = System.currentTimeMillis(); long timeoutMillis = timeout.toMillis(); - checkInclusionProof(client, trustBase, commitment, future, startTime, timeoutMillis, - interval.toMillis()); + checkInclusionProof(client, trustBase, predicateVerifier, transaction, future, startTime, + timeoutMillis, + interval.toMillis()); return future; } private static void checkInclusionProof( - StateTransitionClient client, - RootTrustBase trustBase, - Commitment commitment, - CompletableFuture future, - long startTime, - long timeoutMillis, - long intervalMillis) { - + StateTransitionClient client, + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + Transaction transaction, + CompletableFuture future, + long startTime, + long timeoutMillis, + long intervalMillis) { if (System.currentTimeMillis() - startTime > timeoutMillis) { future.completeExceptionally(new TimeoutException("Timeout waiting for inclusion proof")); } - client.getInclusionProof(commitment.getRequestId()).thenAccept(response -> { - InclusionProofVerificationStatus status = response.getInclusionProof() - .verify(commitment.getRequestId(), trustBase); - switch (status) { + StateId stateId = StateId.fromTransaction(transaction); + client.getInclusionProof(stateId).thenAccept(response -> { + VerificationResult result = InclusionProofVerificationRule.verify( + trustBase, predicateVerifier, response.getInclusionProof(), transaction); + switch (result.getStatus()) { case OK: future.complete(response.getInclusionProof()); break; - case PATH_NOT_INCLUDED: + case INCLUSION_CERTIFICATE_MISSING: CompletableFuture.delayedExecutor(intervalMillis, TimeUnit.MILLISECONDS) - .execute(() -> checkInclusionProof(client, trustBase, commitment, future, startTime, - timeoutMillis, - intervalMillis)); + .execute(() -> checkInclusionProof(client, trustBase, predicateVerifier, transaction, + future, startTime, + timeoutMillis, + intervalMillis)); break; default: future.completeExceptionally( - new RuntimeException(String.format("Inclusion proof verification failed: %s", status))); + new VerificationException("Inclusion proof verification failed", result)); } }).exceptionally(e -> { future.completeExceptionally(e); diff --git a/src/main/java/org/unicitylabs/sdk/util/LongConverter.java b/src/main/java/org/unicitylabs/sdk/util/LongConverter.java new file mode 100644 index 0000000..f0d5293 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/util/LongConverter.java @@ -0,0 +1,33 @@ +package org.unicitylabs.sdk.util; + +/** + * Long converter to bytes. + */ +public class LongConverter { + + private LongConverter() { + } + + /** + * Encode a non-negative long as minimum-length unsigned big-endian bytes, with a minimum length + * of one byte. Zero is encoded as {@code [0x00]}, not the empty array. + * + * @param value non-negative long + * @return bytes + */ + public static byte[] encode(long value) { + if (value < 0) { + throw new IllegalArgumentException("value must be non-negative"); + } + if (value == 0) { + return new byte[]{0x00}; + } + int length = (64 - Long.numberOfLeadingZeros(value) + 7) / 8; + byte[] result = new byte[length]; + for (int i = length - 1; i >= 0; i--) { + result[i] = (byte) (value & 0xffL); + value >>>= 8; + } + return result; + } +} diff --git a/src/main/java/org/unicitylabs/sdk/util/verification/VerificationException.java b/src/main/java/org/unicitylabs/sdk/util/verification/VerificationException.java new file mode 100644 index 0000000..8990742 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/util/verification/VerificationException.java @@ -0,0 +1,32 @@ +package org.unicitylabs.sdk.util.verification; + +/** + * Exception thrown when a verification flow returns a failing result. + */ +public class VerificationException extends RuntimeException { + /** + * Verification result associated with this exception. + */ + private final VerificationResult result; + + /** + * Creates a verification exception with message and failing verification result. + * + * @param message verification failure message + * @param result verification result associated with the failure + */ + public VerificationException(String message, VerificationResult result) { + super(String.format("Verification exception { message: '%s', result: %s", message, result.toString())); + + this.result = result; + } + + /** + * Returns the verification result associated with this exception. + * + * @return verification result + */ + public VerificationResult getVerificationResult() { + return this.result; + } +} diff --git a/src/main/java/org/unicitylabs/sdk/util/verification/VerificationResult.java b/src/main/java/org/unicitylabs/sdk/util/verification/VerificationResult.java new file mode 100644 index 0000000..40f1d64 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/util/verification/VerificationResult.java @@ -0,0 +1,135 @@ +package org.unicitylabs.sdk.util.verification; + +import java.util.List; +import java.util.Objects; + +/** + * Generic verification result containing status, message and optional nested rule results. + * + * @param status enum/type used by the verification rule + */ +public class VerificationResult { + + private final String rule; + private final S status; + private final String message; + private final List> results; + + /** + * Create verification result with no nested results. + * + * @param rule verification rule name + * @param status verification status + * @param message descriptive message + */ + public VerificationResult( + String rule, + S status, + String message + ) { + this(rule, status, message, List.of()); + } + + /** + * Create verification result with empty message and no nested results. + * + * @param rule verification rule name + * @param status verification status + */ + public VerificationResult( + String rule, + S status + ) { + this(rule, status, "", List.of()); + } + + /** + * Create verification result with nested results as varargs. + * + * @param rule verification rule name + * @param status verification status + * @param message descriptive message + * @param results nested verification results + */ + public VerificationResult( + String rule, + S status, + String message, + VerificationResult... results + ) { + this(rule, status, message, List.of(results)); + } + + /** + * Create verification result. + * + * @param rule verification rule name + * @param status verification status + * @param message descriptive message + * @param results nested verification results + */ + public VerificationResult( + String rule, + S status, + String message, + List> results + ) { + Objects.requireNonNull(rule, "Rule cannot be null"); + Objects.requireNonNull(status, "Status cannot be null"); + Objects.requireNonNull(message, "Message cannot be null"); + Objects.requireNonNull(results, "Results cannot be null"); + + this.rule = rule; + this.status = status; + this.message = message; + this.results = List.copyOf(results); + } + + /** + * Get verification rule name. + * + * @return rule name + */ + public String getRule() { + return this.rule; + } + + /** + * Get verification status. + * + * @return verification status + */ + public S getStatus() { + return this.status; + } + + /** + * Get verification message. + * + * @return verification message + */ + public String getMessage() { + return this.message; + } + + /** + * Get nested verification results. + * + * @return nested results + */ + public List> getResults() { + return this.results; + } + + + @Override + public String toString() { + return String.format( + "VerificationResult{rule=%s, status=%s, message=%s, results=%s}", + this.rule, + this.status, + this.message, + this.results + ); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/util/verification/VerificationStatus.java b/src/main/java/org/unicitylabs/sdk/util/verification/VerificationStatus.java new file mode 100644 index 0000000..1ce6656 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/util/verification/VerificationStatus.java @@ -0,0 +1,11 @@ +package org.unicitylabs.sdk.util.verification; + +/** + * Outcome status of a verification step. + */ +public enum VerificationStatus { + /** Verification succeeded. */ + OK, + /** Verification failed. */ + FAIL +} diff --git a/src/main/java/org/unicitylabs/sdk/verification/CompositeVerificationRule.java b/src/main/java/org/unicitylabs/sdk/verification/CompositeVerificationRule.java deleted file mode 100644 index 7ce7880..0000000 --- a/src/main/java/org/unicitylabs/sdk/verification/CompositeVerificationRule.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.unicitylabs.sdk.verification; - -import java.util.ArrayList; -import java.util.List; - -/** - * A composite verification rule that chains multiple verification rules together. - * - *

This class allows you to create a sequence of verification rules where each rule can lead to - * another rule based on the result of the verification. The first rule in the chain is provided at - * construction, and subsequent rules can be determined dynamically based on the outcome of each - * verification step. - * - *

When the {@code verify} method is called, it starts with the first rule and continues to - * execute subsequent rules based on whether the previous rule was successful or not. The final - * result is a composite {@code VerificationResult} that includes the results of all executed - * rules. - * - * @param the type of context used for verification - */ -public abstract class CompositeVerificationRule - extends VerificationRule { - - private final VerificationRule firstRule; - private final String message; - - /** - * Constructs a {@code CompositeVerificationRule} with the specified message and the first rule in - * the chain. - * - * @param message a descriptive message for the composite rule - * @param firstRule the first verification rule to execute in the chain - */ - public CompositeVerificationRule( - String message, - VerificationRule firstRule - ) { - super(message); - - this.firstRule = firstRule; - this.message = message; - } - - @Override - public VerificationResult verify(C context) { - VerificationRule rule = this.firstRule; - List results = new ArrayList<>(); - - while (rule != null) { - VerificationResult result = rule.verify(context); - results.add(result); - rule = rule.getNextRule(result.isSuccessful() - ? VerificationResultCode.OK - : VerificationResultCode.FAIL); - } - - return VerificationResult.fromChildren(this.message, results); - } -} \ No newline at end of file diff --git a/src/main/java/org/unicitylabs/sdk/verification/VerificationContext.java b/src/main/java/org/unicitylabs/sdk/verification/VerificationContext.java deleted file mode 100644 index 34505be..0000000 --- a/src/main/java/org/unicitylabs/sdk/verification/VerificationContext.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.unicitylabs.sdk.verification; - -/** - * Verification context interface. - */ -public interface VerificationContext { - -} diff --git a/src/main/java/org/unicitylabs/sdk/verification/VerificationException.java b/src/main/java/org/unicitylabs/sdk/verification/VerificationException.java deleted file mode 100644 index 871944a..0000000 --- a/src/main/java/org/unicitylabs/sdk/verification/VerificationException.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.unicitylabs.sdk.verification; - -import java.util.Objects; - -/** - * Exception thrown when a verification fails. - */ -public class VerificationException extends Exception { - - /** - * Verification result. - */ - private final VerificationResult verificationResult; - - /** - * Create exception with message and verification result. - * - * @param message message - * @param verificationResult verification result - */ - public VerificationException(String message, VerificationResult verificationResult) { - super(message); - Objects.requireNonNull(verificationResult, "verificationResult cannot be null"); - - this.verificationResult = verificationResult; - } - - /** - * Get verification result. - * - * @return verification result - */ - public VerificationResult getVerificationResult() { - return verificationResult; - } -} diff --git a/src/main/java/org/unicitylabs/sdk/verification/VerificationResult.java b/src/main/java/org/unicitylabs/sdk/verification/VerificationResult.java deleted file mode 100644 index c2739f5..0000000 --- a/src/main/java/org/unicitylabs/sdk/verification/VerificationResult.java +++ /dev/null @@ -1,97 +0,0 @@ -package org.unicitylabs.sdk.verification; - -import java.util.List; - -/** - * Verification result implementation. - */ -public class VerificationResult { - - private final VerificationResultCode status; - private final List results; - private final String message; - - private VerificationResult(VerificationResultCode status, String message, - List results) { - this.message = message; - this.results = List.copyOf(results); - this.status = status; - } - - /** - * Return successful verification result. - * - * @return verification result - */ - public static VerificationResult success() { - return new VerificationResult(VerificationResultCode.OK, "Verification successful", List.of()); - } - - /** - * Return successful verification result with child results. - * - * @param results child results - * @return verification result - */ - public static VerificationResult success(List results) { - return new VerificationResult(VerificationResultCode.OK, "Verification successful", results); - } - - /** - * Return failed verification result. - * - * @param error error message - * @return verification result - */ - public static VerificationResult fail(String error) { - return new VerificationResult(VerificationResultCode.FAIL, error, List.of()); - } - - /** - * Return failed verification result with child results. - * - * @param error error message - * @param results child results - * @return verification result - */ - public static VerificationResult fail(String error, List results) { - return new VerificationResult(VerificationResultCode.FAIL, error, results); - } - - /** - * Create verification result from child results, all has to succeed. - * - * @param message message for the verification result - * @param children child results - * @return verification result - */ - public static VerificationResult fromChildren( - String message, - List children - ) { - return new VerificationResult( - children.stream().allMatch(VerificationResult::isSuccessful) - ? VerificationResultCode.OK - : VerificationResultCode.FAIL, - message, - children - ); - } - - /** - * Is verification successful. - * - * @return success if verification status is ok - */ - public boolean isSuccessful() { - return this.status == VerificationResultCode.OK; - } - - @Override - public String toString() { - return String.format( - "TokenVerificationResult{isSuccessful=%s, message='%s', results=%s}", - this.status, this.message, this.results - ); - } -} diff --git a/src/main/java/org/unicitylabs/sdk/verification/VerificationResultCode.java b/src/main/java/org/unicitylabs/sdk/verification/VerificationResultCode.java deleted file mode 100644 index d3bec04..0000000 --- a/src/main/java/org/unicitylabs/sdk/verification/VerificationResultCode.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.unicitylabs.sdk.verification; - -/** - * Result code for verification. - */ -public enum VerificationResultCode { - /** - * Verification succeeded. - */ - OK, - /** - * Verification failed. - */ - FAIL -} diff --git a/src/main/java/org/unicitylabs/sdk/verification/VerificationRule.java b/src/main/java/org/unicitylabs/sdk/verification/VerificationRule.java deleted file mode 100644 index 13681b4..0000000 --- a/src/main/java/org/unicitylabs/sdk/verification/VerificationRule.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.unicitylabs.sdk.verification; - -import java.util.Objects; - -/** - * Verification rule base class. - * - * @param verification context - */ -public abstract class VerificationRule { - - private final String message; - private final VerificationRule onSuccessRule; - private final VerificationRule onFailureRule; - - /** - * Create the rule without any subsequent rules. - * - * @param message rule message - */ - protected VerificationRule(String message) { - this(message, null, null); - } - - /** - * Create the rule with subsequent rules for success and failure. - * - * @param message rule message - * @param onSuccessRule rule to execute on success - * @param onFailureRule rule to execute on failure - */ - protected VerificationRule( - String message, - VerificationRule onSuccessRule, - VerificationRule onFailureRule - ) { - Objects.requireNonNull(message, "Message cannot be null"); - - this.message = message; - this.onSuccessRule = onSuccessRule; - this.onFailureRule = onFailureRule; - } - - /** - * Get verification rule message. - * - * @return message - */ - public String getMessage() { - return this.message; - } - - /** - * Get next verification rule based on verification result. - * - * @param resultCode result of current verification rule - * @return next rule or null if no rule exists for given result - */ - public VerificationRule getNextRule(VerificationResultCode resultCode) { - switch (resultCode) { - case OK: - return this.onSuccessRule; - case FAIL: - return this.onFailureRule; - default: - return null; - } - } - - /** - * Verify context against current rule. - * - * @param context verification context - * @return verification result - */ - public abstract VerificationResult verify(C context); -} \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/AndroidCompatibilityTest.java b/src/test/java/org/unicitylabs/sdk/AndroidCompatibilityTest.java index 9d00a69..a8002ef 100644 --- a/src/test/java/org/unicitylabs/sdk/AndroidCompatibilityTest.java +++ b/src/test/java/org/unicitylabs/sdk/AndroidCompatibilityTest.java @@ -1,10 +1,11 @@ package org.unicitylabs.sdk; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.TokenType; import org.junit.jupiter.api.Test; +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.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.transaction.TokenType; import java.nio.charset.StandardCharsets; @@ -15,28 +16,26 @@ * These tests ensure we don't use any Java APIs that are not available on Android. */ public class AndroidCompatibilityTest { - - @Test - void testCoreSDKFeaturesWorkOnAndroid() throws Exception { - // Test 1: Hashing (uses Bouncy Castle, not Java crypto) - byte[] data = "test data".getBytes(StandardCharsets.UTF_8); - var hash = new DataHasher(HashAlgorithm.SHA256).update(data).digest(); - assertNotNull(hash); - assertEquals(HashAlgorithm.SHA256, hash.getAlgorithm()); - - // Test 2: Signing Service (uses Bouncy Castle) - byte[] secret = "test secret".getBytes(StandardCharsets.UTF_8); - byte[] nonce = new byte[32]; - var signingService = SigningService.createFromMaskedSecret(secret, nonce); - assertNotNull(signingService.getPublicKey()); - - // Test 3: Token IDs and Types + + @Test + void testCoreSDKFeaturesWorkOnAndroid() throws Exception { + // Test 1: Hashing (uses Bouncy Castle, not Java crypto) + byte[] data = "test data".getBytes(StandardCharsets.UTF_8); + DataHash hash = new DataHasher(HashAlgorithm.SHA256).update(data).digest(); + assertNotNull(hash); + assertEquals(HashAlgorithm.SHA256, hash.getAlgorithm()); + + // Test 2: Signing Service (uses Bouncy Castle) + SigningService signingService = SigningService.generate(); + assertNotNull(signingService.getPublicKey()); + + // Test 3: Token IDs and Types // TokenId tokenId = TokenId.create(new byte[32]); - TokenType tokenType = new TokenType(new byte[32]); + TokenType tokenType = new TokenType(new byte[32]); // assertNotNull(tokenId); - assertNotNull(tokenType); - - // Test 4: Predicates + assertNotNull(tokenType); + + // Test 4: Predicates // var predicate = MaskedPredicate.create( // tokenId, // tokenType, @@ -45,25 +44,25 @@ void testCoreSDKFeaturesWorkOnAndroid() throws Exception { // nonce // ).get(); // assertNotNull(predicate); - - // Test 5: Addresses + + // Test 5: Addresses // var address = DirectAddress.create(predicate.getReference()).get(); // assertNotNull(address.toString()); - - // Test 6: Verify we're not using Java 11+ specific APIs - // This is enforced by Animal Sniffer during build - } - - @Test - void testNoJava11SpecificAPIs() { - // This test documents that we avoid Java 11+ specific APIs: - // - No java.net.http.HttpClient (using OkHttp instead) - // - No var keyword in public APIs - // - No List.of(), Map.of(), Set.of() (using traditional constructors) - // - No Files.readString/writeString - // - Target Java 11 instead of Java 8 (Android 12+ supports Java 11) - - // Animal Sniffer plugin verifies this at build time - assertTrue(true, "Animal Sniffer validates Android compatibility"); - } + + // Test 6: Verify we're not using Java 11+ specific APIs + // This is enforced by Animal Sniffer during build + } + + @Test + void testNoJava11SpecificAPIs() { + // This test documents that we avoid Java 11+ specific APIs: + // - No java.net.http.HttpClient (using OkHttp instead) + // - No var keyword in public APIs + // - No List.of(), Map.of(), Set.of() (using traditional constructors) + // - No Files.readString/writeString + // - Target Java 11 instead of Java 8 (Android 12+ supports Java 11) + + // Animal Sniffer plugin verifies this at build time + assertTrue(true, "Animal Sniffer validates Android compatibility"); + } } \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/MockAggregatorServer.java b/src/test/java/org/unicitylabs/sdk/MockAggregatorServer.java index fc63d44..b81d366 100644 --- a/src/test/java/org/unicitylabs/sdk/MockAggregatorServer.java +++ b/src/test/java/org/unicitylabs/sdk/MockAggregatorServer.java @@ -1,157 +1,157 @@ package org.unicitylabs.sdk; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.JsonNode; import org.jetbrains.annotations.Nullable; import java.io.IOException; -import java.util.Set; import java.util.HashSet; +import java.util.Set; import java.util.UUID; public class MockAggregatorServer { - - private final MockWebServer server; - private final ObjectMapper objectMapper; - private final Set protectedMethods; - private volatile boolean simulateRateLimit = false; - private volatile int rateLimitRetryAfter = 0; - private volatile String expectedApiKey = null; - - public MockAggregatorServer() { - this.server = new MockWebServer(); - this.objectMapper = new ObjectMapper(); - this.protectedMethods = new HashSet<>(); - this.protectedMethods.add("submit_commitment"); - - server.setDispatcher(new Dispatcher() { - @Override - public MockResponse dispatch(RecordedRequest request) { - return handleRequest(request); - } - }); - } - - public void start() throws IOException { - server.start(); - } - - public void shutdown() throws IOException { - server.shutdown(); - } - - public String getUrl() { - return server.url("/").toString(); - } - - public RecordedRequest takeRequest() throws InterruptedException { - return server.takeRequest(); - } - - public void simulateRateLimitForNextRequest(int retryAfterSeconds) { - this.simulateRateLimit = true; - this.rateLimitRetryAfter = retryAfterSeconds; - } - - public void setExpectedApiKey(String apiKey) { - this.expectedApiKey = apiKey; - } - - private MockResponse handleRequest(RecordedRequest request) { + + private final MockWebServer server; + private final ObjectMapper objectMapper; + private final Set protectedMethods; + private volatile boolean simulateRateLimit = false; + private volatile int rateLimitRetryAfter = 0; + private volatile String expectedApiKey = null; + + public MockAggregatorServer() { + this.server = new MockWebServer(); + this.objectMapper = new ObjectMapper(); + this.protectedMethods = new HashSet<>(); + this.protectedMethods.add("certification_request"); + + server.setDispatcher(new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + return handleRequest(request); + } + }); + } + + public void start() throws IOException { + server.start(); + } + + public void shutdown() throws IOException { + server.shutdown(); + } + + public String getUrl() { + return server.url("/").toString(); + } + + public RecordedRequest takeRequest() throws InterruptedException { + return server.takeRequest(); + } + + public void simulateRateLimitForNextRequest(int retryAfterSeconds) { + this.simulateRateLimit = true; + this.rateLimitRetryAfter = retryAfterSeconds; + } + + public void setExpectedApiKey(String apiKey) { + this.expectedApiKey = apiKey; + } + + private MockResponse handleRequest(RecordedRequest request) { + try { + if (simulateRateLimit) { try { - if (simulateRateLimit) { - try { - return new MockResponse() - .setResponseCode(429) - .setHeader("Retry-After", String.valueOf(rateLimitRetryAfter)) - .setBody("Too Many Requests"); - } finally { - // Reset for next request - simulateRateLimit = false; - rateLimitRetryAfter = 0; - } - } - - String method = extractJsonRpcMethod(request); - - if (protectedMethods.contains(method) && expectedApiKey != null && !hasValidApiKey(request)) { - return new MockResponse() - .setResponseCode(401) - .setHeader("WWW-Authenticate", "Bearer") - .setBody("Unauthorized"); - } - - return generateSuccessResponse(method); - - } catch (Exception e) { - return new MockResponse() - .setResponseCode(400) - .setBody("Bad Request"); + return new MockResponse() + .setResponseCode(429) + .setHeader("Retry-After", String.valueOf(rateLimitRetryAfter)) + .setBody("Too Many Requests"); + } finally { + // Reset for next request + simulateRateLimit = false; + rateLimitRetryAfter = 0; } + } + + String method = extractJsonRpcMethod(request); + + if (protectedMethods.contains(method) && expectedApiKey != null && !hasValidApiKey(request)) { + return new MockResponse() + .setResponseCode(401) + .setHeader("WWW-Authenticate", "Bearer") + .setBody("Unauthorized"); + } + + return generateSuccessResponse(method); + + } catch (Exception e) { + return new MockResponse() + .setResponseCode(400) + .setBody("Bad Request"); } + } - private boolean hasValidApiKey(RecordedRequest request) { - String authHeader = request.getHeader("Authorization"); - if (authHeader != null && authHeader.startsWith("Bearer ")) { - String providedKey = authHeader.substring(7); - return expectedApiKey.equals(providedKey); - } - return false; + private boolean hasValidApiKey(RecordedRequest request) { + String authHeader = request.getHeader("Authorization"); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + String providedKey = authHeader.substring(7); + return expectedApiKey.equals(providedKey); } + return false; + } - private @Nullable String extractJsonRpcMethod(RecordedRequest request) throws JsonProcessingException { - if (!"POST".equals(request.getMethod())) { - return null; - } - JsonNode jsonRequest = objectMapper.readTree(request.getBody().readUtf8()); - return jsonRequest.has("method") ? jsonRequest.get("method").asText() : null; + private @Nullable String extractJsonRpcMethod(RecordedRequest request) throws JsonProcessingException { + if (!"POST".equals(request.getMethod())) { + return null; } + JsonNode jsonRequest = objectMapper.readTree(request.getBody().readUtf8()); + return jsonRequest.has("method") ? jsonRequest.get("method").asText() : null; + } - private MockResponse generateSuccessResponse(String method) { - String responseBody; - String id = UUID.randomUUID().toString(); - - switch (method != null ? method : "") { - case "submit_commitment": - responseBody = String.format( - "{\n" + - " \"jsonrpc\": \"2.0\",\n" + - " \"result\": {\n" + - " \"status\": \"SUCCESS\"\n" + - " },\n" + - " \"id\": \"%s\"\n" + - "}", id); - break; - - case "get_block_height": - responseBody = String.format( - "{\n" + - " \"jsonrpc\": \"2.0\",\n" + - " \"result\": {\n" + - " \"blockNumber\": \"67890\"\n" + - " },\n" + - " \"id\": \"%s\"\n" + - "}", id); - break; - - default: - responseBody = String.format( - "{\n" + - " \"jsonrpc\": \"2.0\",\n" + - " \"result\": \"OK\",\n" + - " \"id\": \"%s\"\n" + - "}", id); - break; - } - - return new MockResponse() + private MockResponse generateSuccessResponse(String method) { + String responseBody; + String id = UUID.randomUUID().toString(); + + switch (method != null ? method : "") { + case "certification_request": + responseBody = String.format( + "{\n" + + " \"jsonrpc\": \"2.0\",\n" + + " \"result\": {\n" + + " \"status\": \"SUCCESS\"\n" + + " },\n" + + " \"id\": \"%s\"\n" + + "}", id); + break; + + case "get_block_height": + responseBody = String.format( + "{\n" + + " \"jsonrpc\": \"2.0\",\n" + + " \"result\": {\n" + + " \"blockNumber\": \"67890\"\n" + + " },\n" + + " \"id\": \"%s\"\n" + + "}", id); + break; + + default: + responseBody = String.format( + "{\n" + + " \"jsonrpc\": \"2.0\",\n" + + " \"result\": \"OK\",\n" + + " \"id\": \"%s\"\n" + + "}", id); + break; + } + + return new MockResponse() .setResponseCode(200) .setHeader("Content-Type", "application/json") .setBody(responseBody); - } + } } \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java b/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java index 3fca876..93be061 100644 --- a/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java +++ b/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java @@ -1,73 +1,104 @@ package org.unicitylabs.sdk; -import java.util.AbstractMap; +import org.unicitylabs.sdk.api.*; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.api.bft.RootTrustBaseUtils; +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.verification.PredicateVerifierService; +import org.unicitylabs.sdk.smt.radix.FinalizedNodeBranch; +import org.unicitylabs.sdk.smt.radix.SparseMerkleTree; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; import java.util.concurrent.CompletableFuture; -import org.unicitylabs.sdk.api.Authenticator; -import org.unicitylabs.sdk.api.AggregatorClient; -import org.unicitylabs.sdk.api.InclusionProofResponse; -import org.unicitylabs.sdk.api.LeafValue; -import org.unicitylabs.sdk.api.RequestId; -import org.unicitylabs.sdk.api.SubmitCommitmentResponse; -import org.unicitylabs.sdk.api.SubmitCommitmentStatus; -import org.unicitylabs.sdk.bft.UnicityCertificateUtils; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.mtree.plain.SparseMerkleTree; -import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreeRootNode; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.transaction.InclusionProofFixture; public class TestAggregatorClient implements AggregatorClient { - - private final SparseMerkleTree tree = new SparseMerkleTree(HashAlgorithm.SHA256); - private final HashMap> requests = new HashMap<>(); + private final RootTrustBase trustBase; + private final PredicateVerifierService predicateVerifier; + private final SparseMerkleTree sparseMerkleTree; + private final HashMap requests = new HashMap<>(); private final SigningService signingService; - public TestAggregatorClient(SigningService signingService) { - Objects.requireNonNull(signingService, "Signing service cannot be null"); + private TestAggregatorClient(SparseMerkleTree smt, SigningService signingService) { + this.sparseMerkleTree = smt; this.signingService = signingService; + this.trustBase = RootTrustBaseUtils.generateRootTrustBase(this.signingService.getPublicKey()); + this.predicateVerifier = PredicateVerifierService.create(); + } + + public RootTrustBase getTrustBase() { + return this.trustBase; + } + + /** + * Creates a new TestAggregatorClient instance with generated private key. If no private key is provided, a new one is + * generated. + */ + public static TestAggregatorClient create() { + return TestAggregatorClient.create(SigningService.generatePrivateKey()); + } + + + /** + * Creates a new TestAggregatorClient instance with private key. If no private key is provided, a new one is + * generated. + */ + public static TestAggregatorClient create(byte[] privateKey) { + return new TestAggregatorClient( + new SparseMerkleTree(HashAlgorithm.SHA256), + new SigningService(privateKey) + ); } @Override - public CompletableFuture submitCommitment( - RequestId requestId, - DataHash transactionHash, - Authenticator authenticator - ) { + public CompletableFuture submitCertificationRequest(CertificationData certificationData) { try { - tree.addLeaf( - requestId.toBitString().toBigInteger(), - LeafValue.create(authenticator, transactionHash).getBytes() + StateId stateId = StateId.fromCertificationData(certificationData); + + VerificationResult result = this.predicateVerifier.verify( + certificationData.getLockScript(), + certificationData.getSourceStateHash(), + certificationData.getTransactionHash(), + certificationData.getUnlockScript() ); - requests.put(requestId, new AbstractMap.SimpleEntry<>(authenticator, transactionHash)); + if (result.getStatus() != VerificationStatus.OK) { + return CompletableFuture.completedFuture(CertificationResponse.create(CertificationStatus.SIGNATURE_VERIFICATION_FAILED)); + } - return CompletableFuture.completedFuture( - new SubmitCommitmentResponse(SubmitCommitmentStatus.SUCCESS) - ); + if (!this.requests.containsKey(stateId)) { + DataHash leafValue = certificationData.getTransactionHash(); + this.sparseMerkleTree.addLeaf(stateId.getData(), leafValue.getData()); + this.requests.put(stateId, certificationData); + } + + return CompletableFuture.completedFuture(CertificationResponse.create(CertificationStatus.SUCCESS)); } catch (Exception e) { throw new RuntimeException("Aggregator commitment failed", e); } } @Override - public CompletableFuture getInclusionProof(RequestId requestId) { - Entry entry = requests.get(requestId); - SparseMerkleTreeRootNode root = tree.calculateRoot(); + public CompletableFuture getInclusionProof(StateId stateId) { + FinalizedNodeBranch root = this.sparseMerkleTree.calculateRoot(); + + if (!requests.containsKey(stateId)) { + return CompletableFuture.completedFuture(InclusionProofFixture.createResponse(null, null, root.getHash(), this.signingService)); + } + + CertificationData certificationData = requests.get(stateId); + return CompletableFuture.completedFuture( - new InclusionProofResponse( - InclusionProofFixture.create( - root.getPath(requestId.toBitString().toBigInteger()), - entry != null ? entry.getKey() : null, - entry != null ? entry.getValue() : null, - UnicityCertificateUtils.generateCertificate(signingService, root.getRootHash()) + InclusionProofFixture.createResponse( + certificationData, + InclusionCertificate.create(root, stateId.getData()), + root.getHash(), + this.signingService ) - ) ); } diff --git a/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java b/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java index e254820..c3bc5c2 100644 --- a/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java +++ b/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java @@ -4,16 +4,13 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.unicitylabs.sdk.api.AggregatorClient; -import org.unicitylabs.sdk.api.Authenticator; -import org.unicitylabs.sdk.api.JsonRpcAggregatorClient; -import org.unicitylabs.sdk.api.RequestId; -import org.unicitylabs.sdk.api.SubmitCommitmentResponse; -import org.unicitylabs.sdk.api.SubmitCommitmentStatus; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.jsonrpc.JsonRpcNetworkException; -import org.unicitylabs.sdk.signing.SigningService; +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.SignaturePredicate; +import org.unicitylabs.sdk.transaction.MintTransaction; +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.transaction.TokenType; import org.unicitylabs.sdk.util.HexConverter; import java.util.concurrent.CompletableFuture; @@ -24,123 +21,129 @@ public class TestApiKeyIntegration { - private static final String TEST_API_KEY = "test-api-key-12345"; - - private MockAggregatorServer mockServer; - private AggregatorClient clientWithApiKey; - private AggregatorClient clientWithoutApiKey; - - private DataHash transactionHash; - private RequestId requestId; - private Authenticator authenticator; - - @BeforeEach - void setUp() throws Exception { - mockServer = new MockAggregatorServer(); - mockServer.setExpectedApiKey(TEST_API_KEY); - mockServer.start(); - - clientWithApiKey = new JsonRpcAggregatorClient( + private static final String TEST_API_KEY = "test-api-key-12345"; + + private MockAggregatorServer mockServer; + private AggregatorClient clientWithApiKey; + private AggregatorClient clientWithoutApiKey; + + private CertificationData certificationData; + + @BeforeEach + void setUp() throws Exception { + mockServer = new MockAggregatorServer(); + mockServer.setExpectedApiKey(TEST_API_KEY); + mockServer.start(); + + clientWithApiKey = new JsonRpcAggregatorClient( mockServer.getUrl(), TEST_API_KEY); - clientWithoutApiKey = new JsonRpcAggregatorClient(mockServer.getUrl()); + clientWithoutApiKey = new JsonRpcAggregatorClient(mockServer.getUrl()); - SigningService signingService = new SigningService( - HexConverter.decode("0000000000000000000000000000000000000000000000000000000000000001")); + SigningService signingService = new SigningService( + HexConverter.decode("0000000000000000000000000000000000000000000000000000000000000001")); - DataHash stateHash = new DataHash(HashAlgorithm.SHA256, HexConverter.decode("fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321")); - requestId = RequestId.create(signingService.getPublicKey(), stateHash); - transactionHash = new DataHash(HashAlgorithm.SHA256, HexConverter.decode("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890")); + MintTransaction transaction = MintTransaction.create( + SignaturePredicate.fromSigningService(signingService), + TokenId.generate(), + TokenType.generate(), + null, + null + ); + certificationData = CertificationData.fromMintTransaction(transaction); + } - authenticator = Authenticator.create(signingService, transactionHash, stateHash); - } - - @AfterEach - void tearDown() throws Exception { - mockServer.shutdown(); - } - - @Test - public void testSubmitCommitmentWithApiKey() throws Exception { - CompletableFuture future = - clientWithApiKey.submitCommitment(requestId, transactionHash, authenticator); - - SubmitCommitmentResponse response = future.get(5, TimeUnit.SECONDS); - assertEquals(SubmitCommitmentStatus.SUCCESS, response.getStatus()); - - RecordedRequest request = mockServer.takeRequest(); - assertEquals("Bearer " + TEST_API_KEY, request.getHeader("Authorization")); - } - - @Test - public void testSubmitCommitmentWithoutApiKeyThrowsUnauthorized() throws Exception { - CompletableFuture future = - clientWithoutApiKey.submitCommitment(requestId, transactionHash, authenticator); - - try { - future.get(5, TimeUnit.SECONDS); - fail("Expected UnauthorizedException to be thrown"); - } catch (Exception e) { - assertInstanceOf(ExecutionException.class, e); - assertInstanceOf(JsonRpcNetworkException.class, e.getCause()); - assertEquals("Network error [401] occurred: Unauthorized", e.getCause().getMessage()); - } - - RecordedRequest request = mockServer.takeRequest(); - assertNull(request.getHeader("Authorization")); - } - - @Test - public void testSubmitCommitmentWithWrongApiKeyThrowsUnauthorized() throws Exception { - mockServer.setExpectedApiKey("different-api-key"); - - CompletableFuture future = - clientWithApiKey.submitCommitment(requestId, transactionHash, authenticator); - - try { - future.get(5, TimeUnit.SECONDS); - fail("Expected UnauthorizedException to be thrown"); - } catch (Exception e) { - assertInstanceOf(ExecutionException.class, e); - assertInstanceOf(JsonRpcNetworkException.class, e.getCause()); - assertEquals("Network error [401] occurred: Unauthorized", e.getCause().getMessage()); - } - - RecordedRequest request = mockServer.takeRequest(); - assertEquals("Bearer " + TEST_API_KEY, request.getHeader("Authorization")); - } - - @Test - public void testRateLimitExceeded() throws Exception { - mockServer.simulateRateLimitForNextRequest(30); - - CompletableFuture future = - clientWithApiKey.submitCommitment(requestId, transactionHash, authenticator); - - try { - future.get(5, TimeUnit.SECONDS); - fail("Expected RateLimitExceededException to be thrown"); - } catch (Exception e) { - assertInstanceOf(ExecutionException.class, e); - assertInstanceOf(JsonRpcNetworkException.class, e.getCause()); - assertTrue(e.getCause().getMessage().contains("Network error [429] occurred: Too Many Requests"), e.getCause().getMessage()); - } + @AfterEach + void tearDown() throws Exception { + mockServer.shutdown(); + } + + @Test + public void testSubmitCommitmentWithApiKey() throws Exception { + CompletableFuture future = clientWithApiKey.submitCertificationRequest( + certificationData + ); + + CertificationResponse response = future.get(5, TimeUnit.SECONDS); + assertEquals(CertificationStatus.SUCCESS, response.getStatus()); + + RecordedRequest request = mockServer.takeRequest(); + assertEquals("Bearer " + TEST_API_KEY, request.getHeader("Authorization")); + } + + @Test + public void testSubmitCommitmentWithoutApiKeyThrowsUnauthorized() throws Exception { + CompletableFuture future = clientWithoutApiKey.submitCertificationRequest( + certificationData + ); + + try { + future.get(5, TimeUnit.SECONDS); + fail("Expected UnauthorizedException to be thrown"); + } catch (Exception e) { + assertInstanceOf(ExecutionException.class, e); + assertInstanceOf(JsonRpcNetworkException.class, e.getCause()); + assertEquals("Network error [401] occurred: Unauthorized", e.getCause().getMessage()); } - - @Test - public void testGetBlockHeightWorksWithoutApiKey() throws Exception { - CompletableFuture future = clientWithoutApiKey.getBlockHeight(); - - Long blockHeight = future.get(5, TimeUnit.SECONDS); - assertNotNull(blockHeight); - assertEquals(67890L, blockHeight); + + RecordedRequest request = mockServer.takeRequest(); + assertNull(request.getHeader("Authorization")); + } + + @Test + public void testSubmitCommitmentWithWrongApiKeyThrowsUnauthorized() throws Exception { + mockServer.setExpectedApiKey("different-api-key"); + + CompletableFuture future = clientWithApiKey.submitCertificationRequest( + certificationData + ); + + try { + future.get(5, TimeUnit.SECONDS); + fail("Expected UnauthorizedException to be thrown"); + } catch (Exception e) { + assertInstanceOf(ExecutionException.class, e); + assertInstanceOf(JsonRpcNetworkException.class, e.getCause()); + assertEquals("Network error [401] occurred: Unauthorized", e.getCause().getMessage()); } - - @Test - public void testGetBlockHeightAlsoWorksWithApiKey() throws Exception { - CompletableFuture future = clientWithApiKey.getBlockHeight(); - - Long blockHeight = future.get(5, TimeUnit.SECONDS); - assertNotNull(blockHeight); - assertEquals(67890L, blockHeight); + + RecordedRequest request = mockServer.takeRequest(); + assertEquals("Bearer " + TEST_API_KEY, request.getHeader("Authorization")); + } + + @Test + public void testRateLimitExceeded() { + mockServer.simulateRateLimitForNextRequest(30); + + CompletableFuture future = clientWithApiKey.submitCertificationRequest( + certificationData + ); + + try { + future.get(5, TimeUnit.SECONDS); + fail("Expected RateLimitExceededException to be thrown"); + } catch (Exception e) { + assertInstanceOf(ExecutionException.class, e); + assertInstanceOf(JsonRpcNetworkException.class, e.getCause()); + assertTrue(e.getCause().getMessage().contains("Network error [429] occurred: Too Many Requests"), + e.getCause().getMessage()); } + } + + @Test + public void testGetBlockHeightWorksWithoutApiKey() throws Exception { + CompletableFuture future = clientWithoutApiKey.getBlockHeight(); + + Long blockHeight = future.get(5, TimeUnit.SECONDS); + assertNotNull(blockHeight); + assertEquals(67890L, blockHeight); + } + + @Test + public void testGetBlockHeightAlsoWorksWithApiKey() throws Exception { + CompletableFuture future = clientWithApiKey.getBlockHeight(); + + Long blockHeight = future.get(5, TimeUnit.SECONDS); + assertNotNull(blockHeight); + assertEquals(67890L, blockHeight); + } } \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/api/AuthenticatorTest.java b/src/test/java/org/unicitylabs/sdk/api/AuthenticatorTest.java deleted file mode 100644 index 2aaed3a..0000000 --- a/src/test/java/org/unicitylabs/sdk/api/AuthenticatorTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.unicitylabs.sdk.api; - -import com.fasterxml.jackson.core.JsonProcessingException; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.util.HexConverter; - -public class AuthenticatorTest { - - @Test - public void testJsonSerialization() throws JsonProcessingException { - SigningService signingService = new SigningService( - HexConverter.decode("0000000000000000000000000000000000000000000000000000000000000001")); - Authenticator authenticator = Authenticator.create( - signingService, - DataHash.fromImprint(new byte[34]), - DataHash.fromImprint(new byte[34]) - ); - - Assertions.assertEquals( - "8469736563703235366b3158210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817985841a0b37f8fba683cc68f6574cd43b39f0343a50008bf6ccea9d13231d9e7e2e1e411edc8d307254296264aebfc3dc76cd8b668373a072fd64665b50000e9fcce5201582200000000000000000000000000000000000000000000000000000000000000000000", - HexConverter.encode(authenticator.toCbor())); - - Authenticator.fromJson("{\"algorithm\":\"secp256k1\",\"publicKey\":\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\",\"signature\":\"a0b37f8fba683cc68f6574cd43b39f0343a50008bf6ccea9d13231d9e7e2e1e411edc8d307254296264aebfc3dc76cd8b668373a072fd64665b50000e9fcce5201\",\"stateHash\":\"00000000000000000000000000000000000000000000000000000000000000000000\"}"); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/api/InclusionProofFixture.java b/src/test/java/org/unicitylabs/sdk/api/InclusionProofFixture.java new file mode 100644 index 0000000..38c5ce3 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/api/InclusionProofFixture.java @@ -0,0 +1,18 @@ +package org.unicitylabs.sdk.api; + +import org.unicitylabs.sdk.api.bft.UnicityCertificateUtils; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.secp256k1.SigningService; + +public class InclusionProofFixture { + public static InclusionProofResponse createResponse(CertificationData certificationData, InclusionCertificate inclusionCertificate, DataHash root, SigningService signingService) { + return new InclusionProofResponse( + 1L, + new InclusionProof( + certificationData, + inclusionCertificate, + UnicityCertificateUtils.generateCertificate(signingService, root) + ) + ); + } +} diff --git a/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java b/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java new file mode 100644 index 0000000..faa0064 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java @@ -0,0 +1,223 @@ +package org.unicitylabs.sdk.api; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.api.bft.RootTrustBaseUtils; +import org.unicitylabs.sdk.api.bft.ShardId; +import org.unicitylabs.sdk.api.bft.UnicityCertificate; +import org.unicitylabs.sdk.api.bft.UnicityCertificateUtils; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +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; +import org.unicitylabs.sdk.transaction.MintTransaction; +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.transaction.TokenType; +import org.unicitylabs.sdk.transaction.verification.InclusionProofVerificationRule; +import org.unicitylabs.sdk.transaction.verification.InclusionProofVerificationStatus; +import org.unicitylabs.sdk.util.HexConverter; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class InclusionProofTest { + + MintTransaction transaction; + PredicateVerifierService predicateVerifier; + StateId stateId; + InclusionCertificate inclusionCertificate; + CertificationData certificationData; + RootTrustBase trustBase; + UnicityCertificate unicityCertificate; + + @BeforeAll + public void createMerkleTreePath() throws Exception { + SigningService signingService = new SigningService( + HexConverter.decode("0000000000000000000000000000000000000000000000000000000000000001")); + + + transaction = MintTransaction.create( + SignaturePredicate.fromSigningService(signingService), + TokenId.generate(), + TokenType.generate(), + null, + null + ); + + certificationData = CertificationData.fromMintTransaction(transaction); + stateId = StateId.fromCertificationData(certificationData); + + SparseMerkleTree smt = new SparseMerkleTree(HashAlgorithm.SHA256); + smt.addLeaf(stateId.getData(), certificationData.getTransactionHash().getData()); + + FinalizedNodeBranch root = smt.calculateRoot(); + inclusionCertificate = InclusionCertificate.create(root, stateId.getData()); + // Reuse user signing service as unicity certificate signing service. + trustBase = RootTrustBaseUtils.generateRootTrustBase(signingService.getPublicKey()); + unicityCertificate = UnicityCertificateUtils.generateCertificate(signingService, root.getHash()); + predicateVerifier = PredicateVerifierService.create(); + } + + @Test + public void testCborSerialization() { + InclusionProof inclusionProof = new InclusionProof( + certificationData, + inclusionCertificate, + unicityCertificate + ); + + Assertions.assertEquals(inclusionProof, InclusionProof.fromCbor(inclusionProof.toCbor())); + } + + @Test + public void testStructure() { + Assertions.assertThrows(NullPointerException.class, + () -> new InclusionProof( + this.certificationData, + this.inclusionCertificate, + null + ) + ); + Assertions.assertInstanceOf(InclusionProof.class, + new InclusionProof( + this.certificationData, + this.inclusionCertificate, + this.unicityCertificate + ) + ); + Assertions.assertInstanceOf(InclusionProof.class, + new InclusionProof( + null, + this.inclusionCertificate, + this.unicityCertificate + ) + ); + } + + @Test + public void testItVerifies() { + InclusionProof inclusionProof = new InclusionProof( + this.certificationData, + this.inclusionCertificate, + this.unicityCertificate + ); + Assertions.assertEquals( + InclusionProofVerificationStatus.OK, + InclusionProofVerificationRule.verify( + this.trustBase, + this.predicateVerifier, + inclusionProof, + this.transaction + ).getStatus() + ); + + InclusionProof invalidTransactionHashInclusionProof = new InclusionProof( + new CertificationData( + this.certificationData.getLockScript(), + this.certificationData.getSourceStateHash(), + DataHash.fromImprint( + HexConverter.decode("00000000000000000000000000000000000000000000000000000000000000000001") + ), + this.certificationData.getUnlockScript() + ), + this.inclusionCertificate, + this.unicityCertificate + ); + + Assertions.assertEquals( + InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH, + InclusionProofVerificationRule.verify( + this.trustBase, + this.predicateVerifier, + invalidTransactionHashInclusionProof, + this.transaction + ).getStatus() + ); + } + + @Test + public void testItNotAuthenticated() { + InclusionProof invalidInclusionProof = new InclusionProof( + new CertificationData( + this.certificationData.getLockScript(), + this.certificationData.getSourceStateHash(), + this.certificationData.getTransactionHash(), + SignaturePredicateUnlockScript.create( + this.transaction, + new SigningService(SigningService.generatePrivateKey()) + ).encode() + ), + this.inclusionCertificate, + this.unicityCertificate + ); + + Assertions.assertEquals( + InclusionProofVerificationStatus.NOT_AUTHENTICATED, + InclusionProofVerificationRule.verify( + this.trustBase, + this.predicateVerifier, + invalidInclusionProof, + this.transaction + ).getStatus() + ); + } + + @Test + public void testItFailsWithShardIdMismatch() { + // 1-byte shard id whose first byte doesn't match the state id's first byte. The shard check + // runs before the trust base check, so the signing service used for the new certificate's seal + // is irrelevant — reuse the test's fixed key. + byte mismatchingByte = (byte) (this.stateId.getData()[0] ^ 0xFF); + ShardId mismatchingShardId = ShardId.decode(new byte[]{mismatchingByte, (byte) 0x80}); + DataHash rootHash = new DataHash(HashAlgorithm.SHA256, + this.unicityCertificate.getInputRecord().getHash()); + SigningService signingService = SigningService.generate(); + UnicityCertificate mismatchingCertificate = UnicityCertificateUtils.generateCertificate( + signingService, + rootHash, + mismatchingShardId + ); + + InclusionProof inclusionProof = new InclusionProof( + this.certificationData, + this.inclusionCertificate, + mismatchingCertificate + ); + + Assertions.assertEquals( + InclusionProofVerificationStatus.SHARD_ID_MISMATCH, + InclusionProofVerificationRule.verify( + RootTrustBaseUtils.generateRootTrustBase(signingService.getPublicKey()), + this.predicateVerifier, + inclusionProof, + this.transaction + ).getStatus() + ); + } + + @Test + public void testVerificationFailsWithInvalidTrustbase() { + InclusionProof inclusionProof = new InclusionProof( + this.certificationData, + this.inclusionCertificate, + this.unicityCertificate + ); + + Assertions.assertEquals( + InclusionProofVerificationStatus.INVALID_TRUSTBASE, + InclusionProofVerificationRule.verify( + RootTrustBaseUtils.generateRootTrustBase( + HexConverter.decode("020000000000000000000000000000000000000000000000000000000000000001") + ), + this.predicateVerifier, + inclusionProof, + this.transaction + ).getStatus() + ); + } +} diff --git a/src/test/java/org/unicitylabs/sdk/api/RequestIdTest.java b/src/test/java/org/unicitylabs/sdk/api/RequestIdTest.java deleted file mode 100644 index 993484d..0000000 --- a/src/test/java/org/unicitylabs/sdk/api/RequestIdTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.unicitylabs.sdk.api; - -import com.fasterxml.jackson.core.JsonProcessingException; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import java.math.BigInteger; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class RequestIdTest { - - @Test - public void shouldResolveToBigInteger() { - RequestId requestId = RequestId.create(new byte[5], - new DataHash(HashAlgorithm.SHA256, new byte[32])); - Assertions.assertEquals( - new BigInteger( - "7588617643772589565921291111125869131233840654380505021472016115258380142349673042" - ), - requestId.toBitString().toBigInteger()); - } - - @Test - public void testJsonSerialization() throws JsonProcessingException { - RequestId requestId = RequestId.create( - new byte[5], - new DataHash(HashAlgorithm.SHA256, new byte[32]) - ); - - Assertions.assertEquals( - requestId, - RequestId.fromJson(requestId.toJson()) - ); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/api/bft/RootTrustBaseTest.java b/src/test/java/org/unicitylabs/sdk/api/bft/RootTrustBaseTest.java new file mode 100644 index 0000000..8487d82 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/api/bft/RootTrustBaseTest.java @@ -0,0 +1,26 @@ +package org.unicitylabs.sdk.api.bft; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class RootTrustBaseTest { + + @Test + public void testRootTrustBaseDeserializationFromJson() { + RootTrustBase trustBase = RootTrustBase.fromJson( + "{\"version\":1,\"networkId\":3,\"epoch\":1,\"epochStartRound\":1,\"rootNodes\":[{\"nodeId\":\"16Uiu2HAm3PaA9z8jonZzfvuT1WJgTxCpbFkV4Wq4PSSBk7VctkmG\",\"sigKey\":\"0x03982564bf661da9c048397114fab9dcfbfedb0ad7c1b1c83e13c0f9fa633f7aa6\",\"stake\":1},{\"nodeId\":\"16Uiu2HAm8918Ds2nPiVLXg55kypyoXoiweokpQxtnguZjgxz3pNE\",\"sigKey\":\"0x039a2f7f41c5583d339f31490757152b947ccb19944634a40a16a762a32a4855d4\",\"stake\":1},{\"nodeId\":\"16Uiu2HAmEEEGyvYZno7hm2Gs8FwfPejWdpvWC3HLivQD5hXFbNUh\",\"sigKey\":\"0x038cabc84fa86076277879554c277f9a0a19955fa4c3b37871fca81d5f709777f1\",\"stake\":1},{\"nodeId\":\"16Uiu2HAmNwgru7QSsVRacGqXdtfeaf1oqtznvXA6rSzKU1822kuW\",\"sigKey\":\"0x03044c0309fd0a713440da958f8c510a40a4347aa82622481655d162c227d771e3\",\"stake\":1}],\"quorumThreshold\":3,\"stateHash\":\"\",\"changeRecordHash\":\"\",\"previousEntryHash\":\"\",\"signatures\":{\"16Uiu2HAm3PaA9z8jonZzfvuT1WJgTxCpbFkV4Wq4PSSBk7VctkmG\":\"0xfe672d56ddd60e4b028b52999b4e43bcbdac9413d9e8da6f969d46c249da8f492cd719017510af8b199b94c7605b79707da56950a4888320f8cf7e07329e92da01\",\"16Uiu2HAm8918Ds2nPiVLXg55kypyoXoiweokpQxtnguZjgxz3pNE\":\"0x8d1b178f6617a6aff9e9d4a71febac6837bd2a5088f3e3b81c766065e6c7a7ad718d1e0a1c7f7e12954514e663b888337cbaa6e7c8bd5e721f4ae5520ca6f09e00\",\"16Uiu2HAmEEEGyvYZno7hm2Gs8FwfPejWdpvWC3HLivQD5hXFbNUh\":\"0x28ef1e0279fb2962149011177c173aabb7e1fad102f07c898b9de4fe71b424390dd0cbc59f75453a7573c4853218eab800431e42fd0d4a6000ef73d50170d03101\",\"16Uiu2HAmNwgru7QSsVRacGqXdtfeaf1oqtznvXA6rSzKU1822kuW\":\"0xf563d04beb3eb5bd5967cb53e6cc1d1b331cd37c03d7a34ba7f11bc0d2c4994818168e1f5caf88956b34384dd3d3685c432d7a487b5c0bee2da012fd70891bab01\"}}" + ); + + Assertions.assertEquals(1, trustBase.getVersion()); + Assertions.assertEquals(3, trustBase.getNetworkId()); + Assertions.assertEquals(1, trustBase.getEpoch()); + Assertions.assertEquals(1, trustBase.getEpochStartRound()); + Assertions.assertEquals(4, trustBase.getRootNodes().size()); + Assertions.assertEquals(3, trustBase.getQuorumThreshold()); + Assertions.assertEquals(0, trustBase.getStateHash().length); + Assertions.assertEquals(0, trustBase.getChangeRecordHash().length); + Assertions.assertEquals(0, trustBase.getPreviousEntryHash().length); + Assertions.assertEquals(4, trustBase.getSignatures().size()); + } + +} diff --git a/src/test/java/org/unicitylabs/sdk/api/bft/RootTrustBaseUtils.java b/src/test/java/org/unicitylabs/sdk/api/bft/RootTrustBaseUtils.java new file mode 100644 index 0000000..3ad587a --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/api/bft/RootTrustBaseUtils.java @@ -0,0 +1,27 @@ +package org.unicitylabs.sdk.api.bft; + +import java.util.Map; +import java.util.Set; + +public class RootTrustBaseUtils { + public static RootTrustBase generateRootTrustBase(byte[] publicKey) { + return new RootTrustBase( + 0, + 0, + 0, + 0, + Set.of( + new RootTrustBase.NodeInfo( + "NODE", + publicKey, + 1 + ) + ), + 1, + new byte[0], + new byte[0], + null, + Map.of() + ); + } +} diff --git a/src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateTest.java b/src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateTest.java new file mode 100644 index 0000000..dd5d36d --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateTest.java @@ -0,0 +1,21 @@ +package org.unicitylabs.sdk.api.bft; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.unicitylabs.sdk.util.HexConverter; + +public class UnicityCertificateTest { + + @Test + public void testUnicityCertificateDeserializationFromCbor() { + byte[] data = HexConverter.decode( + "d998598701d9985a8a010000f65820747276e851904305a7457d31edd9d374cdedbc7f72c4f5674da410cddb23b6c14a00000000000000000000004a00000000000000000000004a000000000000000000005820000000000000000000000000000000000000000000000000000000000000000058200000000000000000000000000000000000000000000000000000000000000000d9985b8301418080d9985c83010080d9985d880100000000f65820bd991b4e822f177252fa79aebdec0a24546519b813fdd54e6e48fc67d12c454da1644e4f44455841a3ed5d3e5ea04d39297d5db9cc69bbd97eabf2351e6f96660b8fc3e8d106daf06e0f6336c0862a8068ebdd326474b5a0f52f7942ccf010219a619c650ea4a2a301"); + UnicityCertificate unicityCertificate = UnicityCertificate.fromCbor(data); + + Assertions.assertEquals( + "d998598701d9985a8a010000f65820747276e851904305a7457d31edd9d374cdedbc7f72c4f5674da410cddb23b6c14a00000000000000000000004a00000000000000000000004a000000000000000000005820000000000000000000000000000000000000000000000000000000000000000058200000000000000000000000000000000000000000000000000000000000000000d9985b8301418080d9985c83010080d9985d880100000000f65820bd991b4e822f177252fa79aebdec0a24546519b813fdd54e6e48fc67d12c454da1644e4f44455841a3ed5d3e5ea04d39297d5db9cc69bbd97eabf2351e6f96660b8fc3e8d106daf06e0f6336c0862a8068ebdd326474b5a0f52f7942ccf010219a619c650ea4a2a301", + HexConverter.encode(unicityCertificate.toCbor()) + ); + } + +} diff --git a/src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateUtils.java b/src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateUtils.java new file mode 100644 index 0000000..5a92481 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateUtils.java @@ -0,0 +1,103 @@ +package org.unicitylabs.sdk.api.bft; + +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.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.List; +import java.util.Set; + +public class UnicityCertificateUtils { + + public static UnicityCertificate generateCertificate( + SigningService signingService, + DataHash rootHash + ) { + return generateCertificate(signingService, rootHash, + ShardId.decode(new byte[]{(byte) 0b10000000})); + } + + public static UnicityCertificate generateCertificate( + SigningService signingService, + DataHash rootHash, + ShardId shardId + ) { + InputRecord inputRecord = new InputRecord( + 0, + 0, + null, + rootHash.getData(), + new byte[10], + 0, + new byte[10], + 0, + new byte[10] + ); + UnicityTreeCertificate unicityTreeCertificate = new UnicityTreeCertificate(0, List.of()); + byte[] technicalRecordHash = new byte[32]; + byte[] shardConfigurationHash = new byte[32]; + ShardTreeCertificate shardTreeCertificate = new ShardTreeCertificate(shardId, List.of()); + + DataHash shardTreeCertificateRootHash = UnicityCertificate.calculateShardTreeCertificateRootHash( + inputRecord, + technicalRecordHash, + shardConfigurationHash, + shardTreeCertificate + ); + + byte[] key = ByteBuffer.allocate(4) + .order(ByteOrder.BIG_ENDIAN) + .putInt(unicityTreeCertificate.getPartitionIdentifier()) + .array(); + + DataHash unicitySealHash = new DataHasher(HashAlgorithm.SHA256) + .update(CborSerializer.encodeByteString(new byte[]{(byte) 0x01})) // LEAF + .update(CborSerializer.encodeByteString(key)) + .update( + CborSerializer.encodeByteString( + new DataHasher(HashAlgorithm.SHA256) + .update( + CborSerializer.encodeByteString( + shardTreeCertificateRootHash.getData() + ) + ) + .digest() + .getData() + ) + ) + .digest(); + + UnicitySeal seal = new UnicitySeal( + (short) 0, + 0L, + 0L, + 0L, + null, + unicitySealHash.getData(), + null + ); + + return new UnicityCertificate( + new InputRecord(0, 0, null, rootHash.getData(), new byte[10], 0, + new byte[10], 0, new byte[10]), + technicalRecordHash, + shardConfigurationHash, + shardTreeCertificate, + new UnicityTreeCertificate(0, List.of()), + seal.withSignatures( + Set.of( + new UnicitySeal.SignatureEntry( + "NODE", + signingService.sign( + new DataHasher(HashAlgorithm.SHA256).update(seal.toCbor()).digest() + ).encode() + ) + ) + ) + ); + } +} diff --git a/src/test/java/org/unicitylabs/sdk/bft/RootTrustBaseTest.java b/src/test/java/org/unicitylabs/sdk/bft/RootTrustBaseTest.java deleted file mode 100644 index 6c856f4..0000000 --- a/src/test/java/org/unicitylabs/sdk/bft/RootTrustBaseTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.unicitylabs.sdk.bft; - -import java.io.IOException; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class RootTrustBaseTest { - - @Test - public void testRootTrustBaseDeserializationFromJson() throws IOException { - RootTrustBase trustBase = RootTrustBase.fromJson( - "{\"version\":1,\"networkId\":3,\"epoch\":1,\"epochStartRound\":1,\"rootNodes\":[{\"nodeId\":\"16Uiu2HAm3PaA9z8jonZzfvuT1WJgTxCpbFkV4Wq4PSSBk7VctkmG\",\"sigKey\":\"0x03982564bf661da9c048397114fab9dcfbfedb0ad7c1b1c83e13c0f9fa633f7aa6\",\"stake\":1},{\"nodeId\":\"16Uiu2HAm8918Ds2nPiVLXg55kypyoXoiweokpQxtnguZjgxz3pNE\",\"sigKey\":\"0x039a2f7f41c5583d339f31490757152b947ccb19944634a40a16a762a32a4855d4\",\"stake\":1},{\"nodeId\":\"16Uiu2HAmEEEGyvYZno7hm2Gs8FwfPejWdpvWC3HLivQD5hXFbNUh\",\"sigKey\":\"0x038cabc84fa86076277879554c277f9a0a19955fa4c3b37871fca81d5f709777f1\",\"stake\":1},{\"nodeId\":\"16Uiu2HAmNwgru7QSsVRacGqXdtfeaf1oqtznvXA6rSzKU1822kuW\",\"sigKey\":\"0x03044c0309fd0a713440da958f8c510a40a4347aa82622481655d162c227d771e3\",\"stake\":1}],\"quorumThreshold\":3,\"stateHash\":\"\",\"changeRecordHash\":\"\",\"previousEntryHash\":\"\",\"signatures\":{\"16Uiu2HAm3PaA9z8jonZzfvuT1WJgTxCpbFkV4Wq4PSSBk7VctkmG\":\"0xfe672d56ddd60e4b028b52999b4e43bcbdac9413d9e8da6f969d46c249da8f492cd719017510af8b199b94c7605b79707da56950a4888320f8cf7e07329e92da01\",\"16Uiu2HAm8918Ds2nPiVLXg55kypyoXoiweokpQxtnguZjgxz3pNE\":\"0x8d1b178f6617a6aff9e9d4a71febac6837bd2a5088f3e3b81c766065e6c7a7ad718d1e0a1c7f7e12954514e663b888337cbaa6e7c8bd5e721f4ae5520ca6f09e00\",\"16Uiu2HAmEEEGyvYZno7hm2Gs8FwfPejWdpvWC3HLivQD5hXFbNUh\":\"0x28ef1e0279fb2962149011177c173aabb7e1fad102f07c898b9de4fe71b424390dd0cbc59f75453a7573c4853218eab800431e42fd0d4a6000ef73d50170d03101\",\"16Uiu2HAmNwgru7QSsVRacGqXdtfeaf1oqtznvXA6rSzKU1822kuW\":\"0xf563d04beb3eb5bd5967cb53e6cc1d1b331cd37c03d7a34ba7f11bc0d2c4994818168e1f5caf88956b34384dd3d3685c432d7a487b5c0bee2da012fd70891bab01\"}}" - ); - - Assertions.assertEquals(1, trustBase.getVersion()); - Assertions.assertEquals(3, trustBase.getNetworkId()); - Assertions.assertEquals(1, trustBase.getEpoch()); - Assertions.assertEquals(1, trustBase.getEpochStartRound()); - Assertions.assertEquals(4, trustBase.getRootNodes().size()); - Assertions.assertEquals(3, trustBase.getQuorumThreshold()); - Assertions.assertEquals(0, trustBase.getStateHash().length); - Assertions.assertEquals(0, trustBase.getChangeRecordHash().length); - Assertions.assertEquals(0, trustBase.getPreviousEntryHash().length); - Assertions.assertEquals(4, trustBase.getSignatures().size()); - } - -} diff --git a/src/test/java/org/unicitylabs/sdk/bft/RootTrustBaseUtils.java b/src/test/java/org/unicitylabs/sdk/bft/RootTrustBaseUtils.java deleted file mode 100644 index ddc75ad..0000000 --- a/src/test/java/org/unicitylabs/sdk/bft/RootTrustBaseUtils.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.unicitylabs.sdk.bft; - -import java.util.Map; -import java.util.Set; - -public class RootTrustBaseUtils { - public static RootTrustBase generateRootTrustBase(byte[] publicKey) { - return new RootTrustBase( - 0, - 0, - 0, - 0, - Set.of( - new RootTrustBase.NodeInfo( - "NODE", - publicKey, - 1 - ) - ), - 1, - new byte[0], - new byte[0], - null, - Map.of() - ); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/bft/UnicityCertificateTest.java b/src/test/java/org/unicitylabs/sdk/bft/UnicityCertificateTest.java deleted file mode 100644 index 3f2e5a1..0000000 --- a/src/test/java/org/unicitylabs/sdk/bft/UnicityCertificateTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.unicitylabs.sdk.bft; - -import java.io.IOException; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.unicitylabs.sdk.util.HexConverter; - -public class UnicityCertificateTest { - - @Test - public void testUnicityCertificateDeserializationFromCbor() throws IOException { - byte[] data = HexConverter.decode( - "d903ef8701d903f08a0118f70058220000ea2fc549e86b1f8666bd7a6d5dd0721b446856ad80c67ea958c4045dfb0b627c58220000ea2fc549e86b1f8666bd7a6d5dd0721b446856ad80c67ea958c4045dfb0b627c401a68d27af6f600f6582024bf5304778618efe3303e1ac36a5c23b7d9d64bbd2c2ae92ea1488ad59e9cfa58206a362e77353752942e9c8101511861ee7fac83ba0873ee88f34416535ae17c5c82418080d903f68301078182055820b206f60d312bf4422815c861e9330ddc3a29b0c744864d7bea41f94d1d34c669d903e988010319073b001a68d27af958204b3f7f036452271a1245ca79f6c2e35adc0b192e6eeb001dff35d70d1be3b2d55820380451b471673eb68cf750ceedb09755c08e3b0b7dea32d0101465970ca3a661a4783531365569753248416d33506141397a386a6f6e5a7a6676755431574a675478437062466b5634577134505353426b375663746b6d475841c5103160b79e7691cec53448949bbd93a870a38c89ad4c929aa1c8e77f2a1d9053d3f8f04e96bcb38ba234bf3dee52baad4b1df94be0b8a5e569918fb94e18ec00783531365569753248416d383931384473326e5069564c586735356b7970796f586f6977656f6b705178746e67755a6a67787a33704e455841fa3736ebdc729b31d6e3c964787dfcefcbd39286a1619665eac1313e6d35049b4385156f652ad9ac87a769d3bf7b8cc819bb5a6f838e595d2e2092df2592849400783531365569753248416d454545477976595a6e6f37686d3247733846776650656a57647076574333484c6976514435685846624e55685841e49ea9e0ca1bb21a37a203fef7bc64efc660ff208f4e572ad3d58ae1d5f1e86e7af1c030c66b3325f99886b1a1d0836487c16112c43c07248b490121d473287701783531365569753248416d4e776772753751537356526163477158647466656166316f71747a6e7658413672537a4b55313832326b75575841ecb36e24df58876277643117344d5329fc1bb22b51e9e6835ac587c894db06a11110e966fd97acd91659c778494d9b4dcdc1bd22ffb47a90e3bec38db8a7310f01"); - UnicityCertificate unicityCertificate = UnicityCertificate.fromCbor(data); - - Assertions.assertEquals( - "d903ef8701d903f08a0118f70058220000ea2fc549e86b1f8666bd7a6d5dd0721b446856ad80c67ea958c4045dfb0b627c58220000ea2fc549e86b1f8666bd7a6d5dd0721b446856ad80c67ea958c4045dfb0b627c401a68d27af6f600f6582024bf5304778618efe3303e1ac36a5c23b7d9d64bbd2c2ae92ea1488ad59e9cfa58206a362e77353752942e9c8101511861ee7fac83ba0873ee88f34416535ae17c5c82418080d903f68301078182055820b206f60d312bf4422815c861e9330ddc3a29b0c744864d7bea41f94d1d34c669d903e988010319073b001a68d27af958204b3f7f036452271a1245ca79f6c2e35adc0b192e6eeb001dff35d70d1be3b2d55820380451b471673eb68cf750ceedb09755c08e3b0b7dea32d0101465970ca3a661a4783531365569753248416d33506141397a386a6f6e5a7a6676755431574a675478437062466b5634577134505353426b375663746b6d475841c5103160b79e7691cec53448949bbd93a870a38c89ad4c929aa1c8e77f2a1d9053d3f8f04e96bcb38ba234bf3dee52baad4b1df94be0b8a5e569918fb94e18ec00783531365569753248416d383931384473326e5069564c586735356b7970796f586f6977656f6b705178746e67755a6a67787a33704e455841fa3736ebdc729b31d6e3c964787dfcefcbd39286a1619665eac1313e6d35049b4385156f652ad9ac87a769d3bf7b8cc819bb5a6f838e595d2e2092df2592849400783531365569753248416d454545477976595a6e6f37686d3247733846776650656a57647076574333484c6976514435685846624e55685841e49ea9e0ca1bb21a37a203fef7bc64efc660ff208f4e572ad3d58ae1d5f1e86e7af1c030c66b3325f99886b1a1d0836487c16112c43c07248b490121d473287701783531365569753248416d4e776772753751537356526163477158647466656166316f71747a6e7658413672537a4b55313832326b75575841ecb36e24df58876277643117344d5329fc1bb22b51e9e6835ac587c894db06a11110e966fd97acd91659c778494d9b4dcdc1bd22ffb47a90e3bec38db8a7310f01", - HexConverter.encode(unicityCertificate.toCbor()) - ); - } - -} diff --git a/src/test/java/org/unicitylabs/sdk/bft/UnicityCertificateUtils.java b/src/test/java/org/unicitylabs/sdk/bft/UnicityCertificateUtils.java deleted file mode 100644 index 5d8fe61..0000000 --- a/src/test/java/org/unicitylabs/sdk/bft/UnicityCertificateUtils.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.unicitylabs.sdk.bft; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.List; -import java.util.Map; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.serializer.cbor.CborSerializer; -import org.unicitylabs.sdk.signing.SigningService; - -public class UnicityCertificateUtils { - - public static UnicityCertificate generateCertificate( - SigningService signingService, - DataHash rootHash - ) { - InputRecord inputRecord = new InputRecord(0, 0, 0, null, rootHash.getImprint(), new byte[10], - 0, - new byte[10], 0, new byte[10]); - UnicityTreeCertificate unicityTreeCertificate = new UnicityTreeCertificate(0, 0, List.of()); - byte[] technicalRecordHash = new byte[32]; - byte[] shardConfigurationHash = new byte[32]; - ShardTreeCertificate shardTreeCertificate = new ShardTreeCertificate( - new byte[32], List.of() - ); - - DataHash shardTreeCertificateRootHash = UnicityCertificate.calculateShardTreeCertificateRootHash( - inputRecord, - technicalRecordHash, - shardConfigurationHash, - shardTreeCertificate - ); - - byte[] key = ByteBuffer.allocate(4) - .order(ByteOrder.BIG_ENDIAN) - .putInt(unicityTreeCertificate.getPartitionIdentifier()) - .array(); - - DataHash unicitySealHash = new DataHasher(HashAlgorithm.SHA256) - .update(CborSerializer.encodeByteString(new byte[]{(byte) 0x01})) // LEAF - .update(CborSerializer.encodeByteString(key)) - .update( - CborSerializer.encodeByteString( - new DataHasher(HashAlgorithm.SHA256) - .update( - CborSerializer.encodeByteString( - shardTreeCertificateRootHash.getData() - ) - ) - .digest() - .getData() - ) - ) - .digest(); - - UnicitySeal seal = new UnicitySeal( - 0, - (short) 0, - 0L, - 0L, - 0L, - null, - unicitySealHash.getData(), - null - ); - - return new UnicityCertificate( - 0, - new InputRecord(0, 0, 0, null, rootHash.getImprint(), new byte[10], 0, - new byte[10], 0, new byte[10]), - technicalRecordHash, - shardConfigurationHash, - shardTreeCertificate, - new UnicityTreeCertificate(0, 0, List.of()), - seal.withSignatures( - Map.of( - "NODE", - signingService.sign( - new DataHasher(HashAlgorithm.SHA256).update(seal.toCbor()).digest() - ).encode() - ) - ) - ); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/common/BaseEscrowSwapTest.java b/src/test/java/org/unicitylabs/sdk/common/BaseEscrowSwapTest.java deleted file mode 100644 index 62fd818..0000000 --- a/src/test/java/org/unicitylabs/sdk/common/BaseEscrowSwapTest.java +++ /dev/null @@ -1,220 +0,0 @@ -package org.unicitylabs.sdk.common; - -import static org.unicitylabs.sdk.utils.TestUtils.randomBytes; - -import java.nio.charset.StandardCharsets; -import java.util.List; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.unicitylabs.sdk.StateTransitionClient; -import org.unicitylabs.sdk.address.ProxyAddress; -import org.unicitylabs.sdk.api.SubmitCommitmentResponse; -import org.unicitylabs.sdk.api.SubmitCommitmentStatus; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.predicate.embedded.MaskedPredicate; -import org.unicitylabs.sdk.predicate.embedded.UnmaskedPredicate; -import org.unicitylabs.sdk.predicate.embedded.UnmaskedPredicateReference; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.token.TokenState; -import org.unicitylabs.sdk.token.TokenType; -import org.unicitylabs.sdk.transaction.TransferCommitment; -import org.unicitylabs.sdk.transaction.TransferTransaction; -import org.unicitylabs.sdk.util.HexConverter; -import org.unicitylabs.sdk.util.InclusionProofUtils; -import org.unicitylabs.sdk.utils.TokenUtils; - -/** - * Alice has a nametag and acts as an escrow for the swap Bob transfers token to Alice Carol - * transfers token to Alice - *

- * Alice transfers Bob's token to Carol Alice transfers Carol's token to Bob - *

- * Everyone's happy :) - */ -public abstract class BaseEscrowSwapTest { - - protected StateTransitionClient client; - protected RootTrustBase trustBase; - private final TokenType tokenType = new TokenType(HexConverter.decode( - "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509")); - - - private final byte[] ALICE_SECRET = "ALICE_SECRET".getBytes(StandardCharsets.UTF_8); - private final byte[] BOB_SECRET = "BOB_SECRET".getBytes(StandardCharsets.UTF_8); - private final byte[] CAROL_SECRET = "CAROL_SECRET".getBytes(StandardCharsets.UTF_8); - - private final String ALICE_NAMETAG = String.format("ALICE_%s", System.currentTimeMillis()); - private final String BOB_NAMETAG = String.format("BOB_%s", System.currentTimeMillis()); - private final String CAROL_NAMETAG = String.format("CAROL_%s", System.currentTimeMillis()); - - private String[] transferToken(Token token, SigningService signingService, String nametag) - throws Exception { - TransferCommitment commitment = TransferCommitment.create( - token, - ProxyAddress.create(nametag), - randomBytes(32), - null, - null, - signingService - ); - - SubmitCommitmentResponse response = this.client.submitCommitment(commitment).get(); - if (response.getStatus() != SubmitCommitmentStatus.SUCCESS) { - throw new RuntimeException("Failed to submit transfer commitment: " + response); - } - - return new String[]{ - token.toJson(), - commitment.toTransaction( - InclusionProofUtils.waitInclusionProof( - this.client, - this.trustBase, - commitment - ).get() - ).toJson() - }; - } - - private Token mintToken(byte[] secret) throws Exception { - return TokenUtils.mintToken( - this.client, - this.trustBase, - secret, - new TokenId(randomBytes(32)), - this.tokenType, - randomBytes(32), - null, - randomBytes(32), - randomBytes(32), - null - ); - } - - private Token receiveToken(String[] tokenInfo, SigningService signingService, - Token nametagToken) throws Exception { - Token token = Token.fromJson(tokenInfo[0]); - TransferTransaction transaction = TransferTransaction.fromJson(tokenInfo[1]); - - TokenState state = new TokenState( - UnmaskedPredicate.create( - token.getId(), - token.getType(), - signingService, - HashAlgorithm.SHA256, - transaction.getData().getSalt() - ), - null - ); - - return this.client.finalizeTransaction( - this.trustBase, - token, - state, - transaction, - List.of(nametagToken) - ); - } - - @Test - void testEscrow() throws Exception { - // Make nametags unique for each test run - Token bobToken = mintToken(BOB_SECRET); - String[] bobSerializedData = this.transferToken( - bobToken, - SigningService.createFromMaskedSecret( - BOB_SECRET, - ((MaskedPredicate) bobToken.getState().getPredicate()).getNonce() - ), - ALICE_NAMETAG - ); - - Token carolToken = mintToken(CAROL_SECRET); - String[] carolSerializedData = this.transferToken( - carolToken, - SigningService.createFromMaskedSecret( - CAROL_SECRET, - ((MaskedPredicate) carolToken.getState().getPredicate()).getNonce() - ), - ALICE_NAMETAG - ); - - Token aliceNametagToken = TokenUtils.mintNametagToken( - this.client, - this.trustBase, - ALICE_SECRET, - this.tokenType, - ALICE_NAMETAG, - UnmaskedPredicateReference.create( - this.tokenType, - SigningService.createFromSecret(ALICE_SECRET), - HashAlgorithm.SHA256 - ).toAddress(), - randomBytes(32), - randomBytes(32) - ); - - Token aliceBobToken = receiveToken( - bobSerializedData, - SigningService.createFromSecret(ALICE_SECRET), - aliceNametagToken - ); - Assertions.assertTrue(aliceBobToken.verify(this.trustBase).isSuccessful()); - Token aliceCarolToken = receiveToken( - carolSerializedData, - SigningService.createFromSecret(ALICE_SECRET), - aliceNametagToken - ); - Assertions.assertTrue(aliceCarolToken.verify(this.trustBase).isSuccessful()); - - Token aliceToCarolToken = receiveToken( - transferToken( - aliceBobToken, - SigningService.createFromSecret(ALICE_SECRET), - CAROL_NAMETAG - ), - SigningService.createFromSecret(CAROL_SECRET), - TokenUtils.mintNametagToken( - this.client, - this.trustBase, - CAROL_SECRET, - this.tokenType, - CAROL_NAMETAG, - UnmaskedPredicateReference.create( - this.tokenType, - SigningService.createFromSecret(CAROL_SECRET), - HashAlgorithm.SHA256 - ).toAddress(), - randomBytes(32), - randomBytes(32) - ) - ); - Assertions.assertTrue(aliceToCarolToken.verify(this.trustBase).isSuccessful()); - - Token aliceToBobToken = receiveToken( - transferToken( - aliceCarolToken, - SigningService.createFromSecret(ALICE_SECRET), - BOB_NAMETAG - ), - SigningService.createFromSecret(BOB_SECRET), - TokenUtils.mintNametagToken( - this.client, - this.trustBase, - BOB_SECRET, - this.tokenType, - BOB_NAMETAG, - UnmaskedPredicateReference.create( - this.tokenType, - SigningService.createFromSecret(BOB_SECRET), - HashAlgorithm.SHA256 - ).toAddress(), - randomBytes(32), - randomBytes(32) - ) - ); - Assertions.assertTrue(aliceToBobToken.verify(this.trustBase).isSuccessful()); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java b/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java index 56c2b2b..25859d7 100644 --- a/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java +++ b/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java @@ -1,50 +1,25 @@ package org.unicitylabs.sdk.common; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.unicitylabs.sdk.utils.TestUtils.randomBytes; - -import java.math.BigInteger; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.UUID; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.unicitylabs.sdk.StateTransitionClient; -import org.unicitylabs.sdk.address.DirectAddress; -import org.unicitylabs.sdk.address.ProxyAddress; -import org.unicitylabs.sdk.api.RequestId; -import org.unicitylabs.sdk.api.SubmitCommitmentResponse; -import org.unicitylabs.sdk.api.SubmitCommitmentStatus; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.predicate.PredicateEngineService; -import org.unicitylabs.sdk.predicate.embedded.MaskedPredicate; -import org.unicitylabs.sdk.predicate.embedded.MaskedPredicateReference; -import org.unicitylabs.sdk.predicate.embedded.UnmaskedPredicate; -import org.unicitylabs.sdk.predicate.embedded.UnmaskedPredicateReference; -import org.unicitylabs.sdk.signing.MintSigningService; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.token.TokenState; -import org.unicitylabs.sdk.token.TokenType; -import org.unicitylabs.sdk.token.fungible.CoinId; -import org.unicitylabs.sdk.token.fungible.TokenCoinData; -import org.unicitylabs.sdk.transaction.InclusionProof; -import org.unicitylabs.sdk.transaction.InclusionProofVerificationStatus; -import org.unicitylabs.sdk.transaction.MintCommitment; -import org.unicitylabs.sdk.transaction.MintTransaction; -import org.unicitylabs.sdk.transaction.TransferCommitment; -import org.unicitylabs.sdk.transaction.TransferTransaction; -import org.unicitylabs.sdk.transaction.split.SplitMintReason; -import org.unicitylabs.sdk.transaction.split.TokenSplitBuilder; -import org.unicitylabs.sdk.transaction.split.TokenSplitBuilder.TokenSplit; +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.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; /** @@ -54,307 +29,110 @@ public abstract class CommonTestFlow { protected StateTransitionClient client; protected RootTrustBase trustBase; + protected PredicateVerifierService predicateVerifier; + protected MintJustificationVerifierService mintJustificationVerifier; - private static final byte[] ALICE_SECRET = "Alice".getBytes(StandardCharsets.UTF_8); - private static final byte[] BOB_SECRET = "Bob".getBytes(StandardCharsets.UTF_8); - private static final byte[] CAROL_SECRET = "Carol".getBytes(StandardCharsets.UTF_8); + private static final SigningService ALICE_SIGNING_SERVICE = SigningService.generate(); + private static final SigningService BOB_SIGNING_SERVICE = SigningService.generate(); + private static final SigningService CAROL_SIGNING_SERVICE = SigningService.generate(); /** * Test basic token transfer flow: Alice -> Bob -> Carol */ @Test public void testTransferFlow() throws Exception { - Token aliceToken = TokenUtils.mintToken( - this.client, - this.trustBase, - ALICE_SECRET - ); - - assertTrue(this.client.isMinted(aliceToken.getId(), this.trustBase).get()); - assertTrue(aliceToken.verify(this.trustBase).isSuccessful()); - - String bobNameTag = UUID.randomUUID().toString(); - - // Alice transfers to Bob - String bobCustomData = "Bob's custom data"; - byte[] bobStateData = bobCustomData.getBytes(StandardCharsets.UTF_8); - DataHash bobDataHash = new DataHasher(HashAlgorithm.SHA256).update(bobStateData).digest(); - - // Submit transfer transaction - SigningService aliceSigningService = SigningService.createFromMaskedSecret( - ALICE_SECRET, - ((MaskedPredicate) aliceToken.getState().getPredicate()).getNonce() - ); - TransferCommitment aliceToBobTransferCommitment = TransferCommitment.create( - aliceToken, - ProxyAddress.create(bobNameTag), - randomBytes(32), - bobDataHash, - null, - aliceSigningService - ); - SubmitCommitmentResponse aliceToBobTransferSubmitResponse = this.client.submitCommitment( - aliceToBobTransferCommitment - ).get(); - - if (aliceToBobTransferSubmitResponse.getStatus() != SubmitCommitmentStatus.SUCCESS) { - throw new Exception(String.format("Failed to submit transaction commitment: %s", - aliceToBobTransferSubmitResponse.getStatus())); - } - - // Wait for inclusion proof - InclusionProof aliceToBobTransferInclusionProof = InclusionProofUtils.waitInclusionProof( - this.client, - this.trustBase, - aliceToBobTransferCommitment - ).get(); - - // Create transfer transaction - TransferTransaction aliceToBobTransferTransaction = aliceToBobTransferCommitment.toTransaction( - aliceToBobTransferInclusionProof - ); - - // Check if alice token is spent - assertTrue(this.client.isStateSpent(aliceToken, aliceSigningService.getPublicKey(), this.trustBase).get()); - - // Bob prepares to receive the token - DirectAddress bobAddress = UnmaskedPredicateReference.create( - aliceToken.getType(), - SigningService.createFromSecret(BOB_SECRET), - HashAlgorithm.SHA256 - ).toAddress(); - - // Bob mints a name tag tokens - Token bobNametagToken = TokenUtils.mintNametagToken( - this.client, - this.trustBase, - BOB_SECRET, - bobNameTag, - bobAddress - ); - - // Bob finalizes the token - Token bobToken = client.finalizeTransaction( - this.trustBase, - aliceToken, - new TokenState( - UnmaskedPredicate.create( - aliceToken.getId(), - aliceToken.getType(), - SigningService.createFromSecret(BOB_SECRET), - HashAlgorithm.SHA256, - aliceToBobTransferTransaction.getData().getSalt() - ), - bobStateData - ), - aliceToBobTransferTransaction, - List.of(bobNametagToken) - ); - - // Verify Bob is now the owner - assertTrue(bobToken.verify(this.trustBase).isSuccessful()); - assertTrue(PredicateEngineService - .createPredicate(bobToken.getState().getPredicate()) - .isOwner(SigningService.createFromSecret(BOB_SECRET).getPublicKey()) - ); - assertEquals(aliceToken.getId(), bobToken.getId()); - assertEquals(aliceToken.getType(), bobToken.getType()); - - // Transfer to Carol with UnmaskedPredicate - DirectAddress carolAddress = UnmaskedPredicateReference.create( - bobToken.getType(), - SigningService.createFromSecret(CAROL_SECRET), - HashAlgorithm.SHA256).toAddress(); - - // Bob transfers to Carol (no custom data) - // Submit transfer transaction - TransferCommitment bobToCarolTransferCommitment = TransferCommitment.create( - bobToken, - carolAddress, - randomBytes(32), - null, - null, - SigningService.createFromSecret(BOB_SECRET) - ); - SubmitCommitmentResponse bobToCarolTransferSubmitResponse = client.submitCommitment( - bobToCarolTransferCommitment - ).get(); - - if (bobToCarolTransferSubmitResponse.getStatus() != SubmitCommitmentStatus.SUCCESS) { - throw new Exception(String.format("Failed to submit transaction commitment: %s", - bobToCarolTransferSubmitResponse.getStatus())); - } - - InclusionProof bobToCarolInclusionProof = InclusionProofUtils.waitInclusionProof( - this.client, - this.trustBase, - bobToCarolTransferCommitment - ).get(); - TransferTransaction bobToCarolTransaction = bobToCarolTransferCommitment.toTransaction( - bobToCarolInclusionProof - ); - - // Carol creates UnmaskedPredicate and finalizes - UnmaskedPredicate carolPredicate = UnmaskedPredicate.create( - bobToken.getId(), - bobToken.getType(), - SigningService.createFromSecret(CAROL_SECRET), - HashAlgorithm.SHA256, - bobToCarolTransaction.getData().getSalt() - ); - - Token carolToken = this.client.finalizeTransaction( - this.trustBase, - bobToken, - new TokenState(carolPredicate, null), - bobToCarolTransaction - ); - - assertTrue(carolToken.verify(this.trustBase).isSuccessful()); - assertEquals(2, carolToken.getTransactions().size()); - - // Bob receives carol token with nametag - TransferCommitment carolToBobTransferCommitment = TransferCommitment.create( - carolToken, - ProxyAddress.create(bobNameTag), - randomBytes(32), - null, - null, - SigningService.createFromSecret(CAROL_SECRET) + Token aliceToken = TokenUtils.mintToken( + this.client, + this.trustBase, + this.predicateVerifier, + this.mintJustificationVerifier, + SignaturePredicate.create(ALICE_SIGNING_SERVICE.getPublicKey()) ); - SubmitCommitmentResponse carolToBobTransferSubmitResponse = this.client.submitCommitment( - carolToBobTransferCommitment - ).get(); - if (carolToBobTransferSubmitResponse.getStatus() != SubmitCommitmentStatus.SUCCESS) { - throw new Exception(String.format("Failed to submit transaction commitment: %s", - carolToBobTransferSubmitResponse.getStatus())); - } - - InclusionProof carolToBobInclusionProof = InclusionProofUtils.waitInclusionProof( - this.client, - this.trustBase, - carolToBobTransferCommitment - ).get(); - - TransferTransaction carolToBobTransaction = carolToBobTransferCommitment.toTransaction( - carolToBobInclusionProof + Token bobToken = TokenUtils.transferToken( + this.client, + this.trustBase, + this.predicateVerifier, + this.mintJustificationVerifier, + aliceToken.toCbor(), + SignaturePredicate.create(BOB_SIGNING_SERVICE.getPublicKey()), + ALICE_SIGNING_SERVICE ); - Token carolToBobToken = client.finalizeTransaction( - this.trustBase, - carolToken, - new TokenState( - UnmaskedPredicate.create( - carolToken.getId(), - carolToken.getType(), - SigningService.createFromSecret(BOB_SECRET), - HashAlgorithm.SHA256, - carolToBobTransaction.getData().getSalt() - ), - null - ), - carolToBobTransaction, - List.of(bobNametagToken) + Token carolToken = TokenUtils.transferToken( + this.client, + this.trustBase, + this.predicateVerifier, + this.mintJustificationVerifier, + bobToken.toCbor(), + SignaturePredicate.create(CAROL_SIGNING_SERVICE.getPublicKey()), + BOB_SIGNING_SERVICE ); - assertTrue(carolToBobToken.verify(this.trustBase).isSuccessful()); - - // SPLIT - List> splitCoins = carolToken.getCoins() - .map(data -> List.copyOf(data.getCoins().entrySet())) - .orElse(List.of()); - - TokenType splitTokenType = new TokenType(randomBytes(32)); - byte[] splitTokenNonce = randomBytes(32); + Assertions.assertEquals(VerificationStatus.OK, + carolToken.verify(this.trustBase, this.predicateVerifier, this.mintJustificationVerifier).getStatus()); + } - TokenSplit split = new TokenSplitBuilder() - .createToken( - new TokenId(randomBytes(32)), - splitTokenType, - null, - new TokenCoinData(Map.ofEntries(splitCoins.get(0))), - MaskedPredicateReference.create( - splitTokenType, - SigningService.createFromMaskedSecret(BOB_SECRET, splitTokenNonce), - HashAlgorithm.SHA256, - splitTokenNonce - ).toAddress(), - randomBytes(32), - null - ) - .createToken( - new TokenId(randomBytes(32)), - splitTokenType, - null, - new TokenCoinData(Map.ofEntries(splitCoins.get(1))), - MaskedPredicateReference.create( - splitTokenType, - SigningService.createFromMaskedSecret(BOB_SECRET, splitTokenNonce), - HashAlgorithm.SHA256, - splitTokenNonce - ).toAddress(), - randomBytes(32), - null - ) - .build(carolToBobToken); + /** + * 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()); - TransferCommitment burnCommitment = split.createBurnCommitment( - randomBytes(32), - SigningService.createFromSecret(BOB_SECRET) + UnicityId unicityId = new UnicityId("testuser", "unicity-labs/test"); + UnicityIdMintTransaction unicityIdMintTransaction = UnicityIdMintTransaction.create( + SignaturePredicate.fromSigningService(unicityIdSigningService), + targetPredicate, + unicityId, + TokenType.generate(), + targetPredicate ); - if (client.submitCommitment(burnCommitment).get().getStatus() - != SubmitCommitmentStatus.SUCCESS) { - throw new Exception("Failed to submit burn commitment"); - } - - List> splitCommitments = split.createSplitMintCommitments( - this.trustBase, - burnCommitment.toTransaction( - InclusionProofUtils.waitInclusionProof( - this.client, - this.trustBase, - burnCommitment - ).get() - ) + CertificationData unicityIdCertificationData = CertificationData.fromTransaction( + unicityIdMintTransaction, + SignaturePredicateUnlockScript.create(unicityIdMintTransaction, unicityIdSigningService) ); - List> splitTransactions = new ArrayList<>(); - for (MintCommitment commitment : splitCommitments) { - if (client.submitCommitment(commitment).get().getStatus() != SubmitCommitmentStatus.SUCCESS) { - throw new Exception("Failed to submit split mint commitment"); - } + CertificationResponse unicityIdResponse = this.client + .submitCertificationRequest(unicityIdCertificationData).get(); + Assertions.assertEquals(CertificationStatus.SUCCESS, unicityIdResponse.getStatus()); - splitTransactions.add(commitment.toTransaction( - InclusionProofUtils.waitInclusionProof(this.client, this.trustBase, commitment).get())); - } - Assertions.assertEquals( - 2, - splitTransactions.stream() - .map(transaction -> transaction.getData() - .getReason() - .map(reason -> reason.verify(transaction).isSuccessful()) - .orElse(false) + UnicityIdToken aliceUnicityIdToken = UnicityIdToken.mint( + this.trustBase, + this.predicateVerifier, + unicityIdMintTransaction.toCertifiedTransaction( + this.trustBase, + this.predicateVerifier, + InclusionProofUtils.waitInclusionProof(this.client, this.trustBase, + this.predicateVerifier, unicityIdMintTransaction).get() ) - .filter(Boolean::booleanValue) - .count() ); - MaskedPredicate splitTokenPredicate = MaskedPredicate.create( - splitTransactions.get(0).getData().getTokenId(), - splitTransactions.get(0).getData().getTokenType(), - SigningService.createFromMaskedSecret(BOB_SECRET, splitTokenNonce), - HashAlgorithm.SHA256, - splitTokenNonce - ); + 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()); - Assertions.assertDoesNotThrow(() -> - Token.create( + Token aliceToken = TokenUtils.mintToken( + this.client, this.trustBase, - new TokenState(splitTokenPredicate, null), - splitTransactions.get(0) - ) + 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/common/split/BaseTokenSplitTest.java b/src/test/java/org/unicitylabs/sdk/common/split/BaseTokenSplitTest.java deleted file mode 100644 index fa0d29d..0000000 --- a/src/test/java/org/unicitylabs/sdk/common/split/BaseTokenSplitTest.java +++ /dev/null @@ -1,175 +0,0 @@ -package org.unicitylabs.sdk.common.split; - -import static org.unicitylabs.sdk.utils.TestUtils.randomBytes; - -import java.math.BigInteger; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.unicitylabs.sdk.StateTransitionClient; -import org.unicitylabs.sdk.address.ProxyAddress; -import org.unicitylabs.sdk.api.SubmitCommitmentResponse; -import org.unicitylabs.sdk.api.SubmitCommitmentStatus; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.predicate.embedded.MaskedPredicate; -import org.unicitylabs.sdk.predicate.embedded.UnmaskedPredicate; -import org.unicitylabs.sdk.predicate.embedded.UnmaskedPredicateReference; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.token.TokenState; -import org.unicitylabs.sdk.token.TokenType; -import org.unicitylabs.sdk.token.fungible.CoinId; -import org.unicitylabs.sdk.token.fungible.TokenCoinData; -import org.unicitylabs.sdk.transaction.MintCommitment; -import org.unicitylabs.sdk.transaction.TransferCommitment; -import org.unicitylabs.sdk.transaction.split.SplitMintReason; -import org.unicitylabs.sdk.transaction.split.TokenSplitBuilder; -import org.unicitylabs.sdk.transaction.split.TokenSplitBuilder.TokenSplit; -import org.unicitylabs.sdk.util.HexConverter; -import org.unicitylabs.sdk.util.InclusionProofUtils; -import org.unicitylabs.sdk.utils.TokenUtils; - -public abstract class BaseTokenSplitTest { - - protected StateTransitionClient client; - protected RootTrustBase trustBase; - - @Test - void testTokenSplitFullAmounts() throws Exception { - TokenType tokenType = new TokenType(HexConverter.decode( - "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509")); - byte[] secret = "SECRET".getBytes(StandardCharsets.UTF_8); - - Token token = TokenUtils.mintToken( - this.client, - this.trustBase, - secret, - new TokenId(randomBytes(32)), - tokenType, - randomBytes(32), - new TokenCoinData( - Map.of( - new CoinId("test_eur".getBytes(StandardCharsets.UTF_8)), - BigInteger.valueOf(100), - new CoinId("test_usd".getBytes(StandardCharsets.UTF_8)), - BigInteger.valueOf(100) - ) - ), - randomBytes(32), - randomBytes(32), - null - ); - - String nametag = UUID.randomUUID().toString(); - - Token nametagToken = TokenUtils.mintNametagToken( - this.client, - this.trustBase, - secret, - nametag, - UnmaskedPredicateReference.create( - tokenType, - SigningService.createFromSecret(secret), - HashAlgorithm.SHA256 - ).toAddress() - ); - - TokenSplitBuilder builder = new TokenSplitBuilder(); - TokenSplit split = builder - .createToken( - new TokenId(randomBytes(32)), - tokenType, - null, - new TokenCoinData(Map.of( - new CoinId("test_eur".getBytes(StandardCharsets.UTF_8)), - BigInteger.valueOf(100) - )), - ProxyAddress.create(nametag), - randomBytes(32), - null - ) - .createToken( - new TokenId(randomBytes(32)), - tokenType, - null, - new TokenCoinData(Map.of( - new CoinId("test_usd".getBytes(StandardCharsets.UTF_8)), - BigInteger.valueOf(100) - )), - ProxyAddress.create(nametag), - randomBytes(32), - null - ) - .build(token); - - TransferCommitment burnCommitment = split.createBurnCommitment( - randomBytes(32), - SigningService.createFromMaskedSecret( - secret, - ((MaskedPredicate) token.getState().getPredicate()).getNonce() - ) - ); - - SubmitCommitmentResponse burnCommitmentResponse = this.client - .submitCommitment(burnCommitment) - .get(); - - if (burnCommitmentResponse.getStatus() != SubmitCommitmentStatus.SUCCESS) { - throw new Exception(String.format("Failed to submit burn commitment: %s", - burnCommitmentResponse.getStatus())); - } - - List> mintCommitments = split.createSplitMintCommitments( - this.trustBase, - burnCommitment.toTransaction( - InclusionProofUtils.waitInclusionProof( - this.client, - this.trustBase, - burnCommitment - ).get() - ) - ); - - for (MintCommitment commitment : mintCommitments) { - SubmitCommitmentResponse response = this.client - .submitCommitment(commitment) - .get(); - - if (response.getStatus() != SubmitCommitmentStatus.SUCCESS) { - throw new Exception(String.format("Failed to submit burn commitment: %s", - response.getStatus())); - } - - TokenState state = new TokenState( - UnmaskedPredicate.create( - commitment.getTransactionData().getTokenId(), - commitment.getTransactionData().getTokenType(), - SigningService.createFromSecret(secret), - HashAlgorithm.SHA256, - commitment.getTransactionData().getSalt() - ), - null - ); - - Token splitToken = Token.create( - this.trustBase, - state, - commitment.toTransaction( - InclusionProofUtils.waitInclusionProof(this.client, this.trustBase, commitment).get() - ), - List.of(nametagToken) - ); - - Assertions.assertTrue(splitToken.verify(this.trustBase).isSuccessful()); - } - - - } - - -} diff --git a/src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SignatureRecoveryTest.java b/src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SignatureRecoveryTest.java new file mode 100644 index 0000000..99dacf6 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SignatureRecoveryTest.java @@ -0,0 +1,88 @@ +package org.unicitylabs.sdk.crypto.secp256k1; + +import org.junit.jupiter.api.Test; +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.util.HexConverter; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test signature recovery functionality + */ +public class SignatureRecoveryTest { + + @Test + void testSignatureRecoveryId() { + // Create a signing service with a known private key + byte[] privateKey = HexConverter.decode("c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4"); + SigningService signingService = new SigningService(privateKey); + + // Create test data and hash it + byte[] testData = "Hello, Unicity!".getBytes(); + DataHasher hasher = new DataHasher(HashAlgorithm.SHA256); + hasher.update(testData); + DataHash hash = hasher.digest(); + + // Sign the hash + Signature signature = signingService.sign(hash); + + // Verify recovery ID is 0 or 1 + assertTrue(signature.getRecovery() == 0 || signature.getRecovery() == 1, + "Recovery ID should be 0 or 1, got: " + signature.getRecovery()); + + // Verify signature with known public key + byte[] publicKey = signingService.getPublicKey(); + assertTrue(SigningService.verifyWithPublicKey(hash, signature.getBytes(), publicKey)); + } + + @Test + void testPublicKeyRecovery() { + // Create a signing service with a known private key + byte[] privateKey = HexConverter.decode("c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4"); + SigningService signingService = new SigningService(privateKey); + byte[] expectedPublicKey = signingService.getPublicKey(); + + // Create test data and hash it + byte[] testData = "Test public key recovery".getBytes(); + DataHasher hasher = new DataHasher(HashAlgorithm.SHA256); + hasher.update(testData); + DataHash hash = hasher.digest(); + + // Sign the hash + Signature signature = signingService.sign(hash); + + // Verify signature using recovered public key + assertTrue(SigningService.verifySignatureWithRecoveredPublicKey(hash, signature), + "Signature verification with recovered public key should succeed"); + } + + @Test + void testSignatureFormatCompliance() { + // Test with the exact values from TypeScript test + String transactionHashHex = "0000d6035b65700f0af73cc62a580eb833c20f40aaee460087f5fb43ebb3c047f1d4"; + String signatureHex = "301c7f19d5e0a7e350012ab7bbaf26a0152a751eec06d18563f96bcf06d2380e7de7ce6cebb8c11479d1bd9c463c3ba47396b5f815c552b344d430b0d011a2e701"; + String expectedPublicKeyHex = "02bf8d9e7687f66c7fce1e98edbc05566f7db740030722cf6cf62aca035c5035ea"; + + // Parse the signature + byte[] sigBytes = HexConverter.decode(signatureHex); + assertEquals(65, sigBytes.length, "Signature should be 65 bytes"); + + // Extract components + int recoveryId = sigBytes[64] & 0xFF; + + // Create signature object + byte[] sigOnly = new byte[64]; + System.arraycopy(sigBytes, 0, sigOnly, 0, 64); + Signature signature = new Signature(sigOnly, recoveryId); + + // Parse hash + DataHash transactionHash = DataHash.fromImprint(HexConverter.decode(transactionHashHex)); + + // Verify using recovered public key + assertTrue(SigningService.verifySignatureWithRecoveredPublicKey(transactionHash, signature), + "Should verify with recovered public key"); + } +} \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SigningServiceTest.java b/src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SigningServiceTest.java new file mode 100644 index 0000000..8ed558f --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SigningServiceTest.java @@ -0,0 +1,79 @@ +package org.unicitylabs.sdk.crypto.secp256k1; + +import org.junit.jupiter.api.Test; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; + +import static org.junit.jupiter.api.Assertions.*; + +public class SigningServiceTest { + + @Test + public void testGeneratePrivateKey() { + byte[] privateKey = SigningService.generatePrivateKey(); + + assertNotNull(privateKey); + assertEquals(32, privateKey.length); + + // Test that we can create a signing service with it + SigningService service = new SigningService(privateKey); + assertNotNull(service.getPublicKey()); + assertEquals(33, service.getPublicKey().length); // Compressed public key + } + + @Test + public void testSignAndVerify() { + byte[] privateKey = SigningService.generatePrivateKey(); + SigningService service = new SigningService(privateKey); + + // Create a test hash + DataHash hash = new DataHash(HashAlgorithm.SHA256, new byte[32]); + + // Sign the hash + Signature signature = service.sign(hash); + + assertNotNull(signature); + assertEquals(64, signature.getBytes().length); + + // Verify the signature + boolean isValid = service.verify(hash, signature); + + assertTrue(isValid); + } + + @Test + public void testVerifyWithPublicKey() { + byte[] privateKey = SigningService.generatePrivateKey(); + SigningService service = new SigningService(privateKey); + byte[] publicKey = service.getPublicKey(); + + // Create a test hash + DataHash hash = new DataHash(HashAlgorithm.SHA256, new byte[32]); + + // Sign the hash + Signature signature = service.sign(hash); + + // Verify with public key + boolean isValid = SigningService.verifyWithPublicKey(hash, signature.getBytes(), publicKey); + + assertTrue(isValid); + } + + @Test + public void testInvalidSignature() { + byte[] privateKey = SigningService.generatePrivateKey(); + SigningService service = new SigningService(privateKey); + + // Create a test hash + DataHash hash = new DataHash(HashAlgorithm.SHA256, new byte[32]); + + // Create an invalid signature + byte[] invalidSig = new byte[64]; + Signature signature = new Signature(invalidSig, 0); + + // Verify the signature + boolean isValid = service.verify(hash, signature); + + assertFalse(isValid); + } +} \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/e2e/BasicE2ETest.java b/src/test/java/org/unicitylabs/sdk/e2e/BasicE2ETest.java deleted file mode 100644 index 2469706..0000000 --- a/src/test/java/org/unicitylabs/sdk/e2e/BasicE2ETest.java +++ /dev/null @@ -1,117 +0,0 @@ -package org.unicitylabs.sdk.e2e; - -import org.junit.jupiter.api.Disabled; -import org.unicitylabs.sdk.api.JsonRpcAggregatorClient; -import org.unicitylabs.sdk.api.Authenticator; -import org.unicitylabs.sdk.api.RequestId; -import org.unicitylabs.sdk.api.SubmitCommitmentResponse; -import org.unicitylabs.sdk.api.SubmitCommitmentStatus; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.signing.SigningService; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; - -import java.security.SecureRandom; -import java.util.concurrent.ExecutorService; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Basic end-to-end test to verify connectivity with aggregator. - */ -@Tag("integration") -@EnabledIfEnvironmentVariable(named = "AGGREGATOR_URL", matches = ".+") -@Disabled("Skip performance tests") -public class BasicE2ETest { - - @Test - void testCommitmentPerformance() throws Exception { - String aggregatorUrl = System.getenv("AGGREGATOR_URL"); - assertNotNull(aggregatorUrl, "AGGREGATOR_URL environment variable must be set"); - - JsonRpcAggregatorClient aggregatorClient = new JsonRpcAggregatorClient(aggregatorUrl); - - long startTime = System.currentTimeMillis(); - SecureRandom sr = new SecureRandom(); - byte[] randomSecret = new byte[32]; - sr.nextBytes(randomSecret); - byte[] stateBytes = new byte[32]; - sr.nextBytes(stateBytes); - DataHash stateHash = new DataHasher(HashAlgorithm.SHA256).update(stateBytes).digest(); - DataHash txDataHash = new DataHasher(HashAlgorithm.SHA256).update("test commitment performance".getBytes()).digest(); - SigningService signingService = SigningService.createFromSecret(randomSecret); - RequestId requestId = RequestId.create(signingService.getPublicKey(), stateHash.getImprint()); - Authenticator auth = Authenticator.create(signingService, txDataHash, stateHash); - SubmitCommitmentResponse response = aggregatorClient.submitCommitment(requestId, txDataHash, auth).get(); - - if (response.getStatus() != SubmitCommitmentStatus.SUCCESS) { - System.err.println("Commitment submission failed with status: " + response.getStatus()); - } - long endTime = System.currentTimeMillis(); - - long duration = endTime - startTime; - System.out.println("Commitment submission took: " + duration + " ms"); - - assertTrue(duration < 5000, "Commitment submission should take less than 5 seconds"); - } - - @Test - void testCommitmentPerformanceMultiThreaded() throws Exception { - int threadCount = 100; // configure as needed - int commitmentsPerThread = 10; // configure as needed - - String aggregatorUrl = System.getenv("AGGREGATOR_URL"); - assertNotNull(aggregatorUrl, "AGGREGATOR_URL environment variable must be set"); - - JsonRpcAggregatorClient aggregatorClient = new JsonRpcAggregatorClient(aggregatorUrl); - ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(threadCount); - java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(threadCount * commitmentsPerThread); - - long startTime = System.currentTimeMillis(); - java.util.List> results = new java.util.ArrayList<>(); - - for (int t = 0; t < threadCount; t++) { - for (int c = 0; c < commitmentsPerThread; c++) { - results.add(executor.submit(() -> { - try { - var sr = new SecureRandom(); - byte[] randomSecret = new byte[32]; - sr.nextBytes(randomSecret); - byte[] stateBytes = new byte[32]; - sr.nextBytes(stateBytes); - byte[] txData = new byte[32]; - sr.nextBytes(txData); - - DataHash stateHash = new DataHasher(HashAlgorithm.SHA256).update(stateBytes).digest(); - DataHash txDataHash = new DataHasher(HashAlgorithm.SHA256).update(txData).digest(); - SigningService signingService = SigningService.createFromSecret(randomSecret); - RequestId requestId = RequestId.create(signingService.getPublicKey(), stateHash.getImprint()); - Authenticator auth = Authenticator.create(signingService, txDataHash, stateHash); - SubmitCommitmentResponse response = aggregatorClient.submitCommitment(requestId, txDataHash, auth).get(); - return response.getStatus() == SubmitCommitmentStatus.SUCCESS; - } finally { - latch.countDown(); - } - })); - } - } - - latch.await(); - long endTime = System.currentTimeMillis(); - executor.shutdown(); - - long duration = endTime - startTime; - long successCount = results.stream().filter(f -> { - try { return f.get(); } catch (Exception e) { return false; } - }).count(); - - System.out.println("Total commitments: " + (threadCount * commitmentsPerThread)); - System.out.println("Successful: " + successCount); - System.out.println("Commitment submission took: " + duration + " ms"); - - assertEquals(threadCount * commitmentsPerThread, successCount, "All commitments should succeed"); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/e2e/CucumberTestRunner.java b/src/test/java/org/unicitylabs/sdk/e2e/CucumberTestRunner.java deleted file mode 100644 index 4029145..0000000 --- a/src/test/java/org/unicitylabs/sdk/e2e/CucumberTestRunner.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.unicitylabs.sdk.e2e; - -import io.cucumber.junit.platform.engine.Constants; -import org.junit.platform.suite.api.*; - -/** - * Updated Cucumber test runner configuration for E2E tests. - * This class configures the test execution environment and feature discovery - * with the new shared step definitions approach. - */ -//@Suite -//@IncludeEngines("cucumber") -//@SelectPackages("org.unicitylabs.sdk.features") -//@ConfigurationParameter(key = Constants.GLUE_PROPERTY_NAME, value = "org.unicitylabs.sdk.e2e.steps,org.unicitylabs.sdk.e2e.steps.shared,org.unicitylabs.sdk.e2e.config") -//@ConfigurationParameter(key = Constants.PLUGIN_PROPERTY_NAME, value = "pretty,html:build/cucumber-reports/cucumber.html,json:build/cucumber-reports/cucumber.json") -//@ConfigurationParameter(key = Constants.EXECUTION_DRY_RUN_PROPERTY_NAME, value = "false") -//@ConfigurationParameter(key = Constants.PLUGIN_PUBLISH_QUIET_PROPERTY_NAME, value = "true") -//public class CucumberTestRunner { -// static { -// // Only set default tags if no tags are specified -// if (System.getProperty("cucumber.filter.tags") == null) { -// System.setProperty("cucumber.filter.tags", "not @ignore"); -// } -// } -//} \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/e2e/E2EEscrowSwapTest.java b/src/test/java/org/unicitylabs/sdk/e2e/E2EEscrowSwapTest.java deleted file mode 100644 index 8fc13e1..0000000 --- a/src/test/java/org/unicitylabs/sdk/e2e/E2EEscrowSwapTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.unicitylabs.sdk.e2e; - -import java.io.IOException; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.unicitylabs.sdk.StateTransitionClient; -import org.unicitylabs.sdk.api.JsonRpcAggregatorClient; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.common.BaseEscrowSwapTest; - - -@Tag("integration") -@EnabledIfEnvironmentVariable(named = "AGGREGATOR_URL", matches = ".+") -public class E2EEscrowSwapTest extends BaseEscrowSwapTest { - @BeforeEach - void setUp() throws IOException { - String aggregatorUrl = System.getenv("AGGREGATOR_URL"); - Assertions.assertNotNull(aggregatorUrl, "AGGREGATOR_URL environment variable must be set"); - - this.client = new StateTransitionClient(new JsonRpcAggregatorClient(aggregatorUrl)); - // TODO: Close the stream - this.trustBase = RootTrustBase.fromJson( - new String(getClass().getResourceAsStream("/trust-base.json").readAllBytes()) - ); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/e2e/TokenE2ETest.java b/src/test/java/org/unicitylabs/sdk/e2e/TokenE2ETest.java index c55c0a6..d829eb0 100644 --- a/src/test/java/org/unicitylabs/sdk/e2e/TokenE2ETest.java +++ b/src/test/java/org/unicitylabs/sdk/e2e/TokenE2ETest.java @@ -1,14 +1,18 @@ package org.unicitylabs.sdk.e2e; -import java.io.IOException; -import org.unicitylabs.sdk.StateTransitionClient; -import org.unicitylabs.sdk.api.JsonRpcAggregatorClient; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.unicitylabs.sdk.bft.RootTrustBase; +import org.unicitylabs.sdk.StateTransitionClient; +import org.unicitylabs.sdk.api.JsonRpcAggregatorClient; +import org.unicitylabs.sdk.api.bft.RootTrustBase; import org.unicitylabs.sdk.common.CommonTestFlow; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifierService; + +import java.io.IOException; +import java.io.InputStream; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -31,9 +35,12 @@ void setUp() throws IOException { this.aggregatorClient = new JsonRpcAggregatorClient(aggregatorUrl); this.client = new StateTransitionClient(this.aggregatorClient); - this.trustBase = RootTrustBase.fromJson( - new String(getClass().getResourceAsStream("/trust-base.json").readAllBytes()) - ); + 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.mintJustificationVerifier = new MintJustificationVerifierService(); + } } @Test diff --git a/src/test/java/org/unicitylabs/sdk/e2e/config/CucumberConfiguration.java b/src/test/java/org/unicitylabs/sdk/e2e/config/CucumberConfiguration.java deleted file mode 100644 index ddb7f9c..0000000 --- a/src/test/java/org/unicitylabs/sdk/e2e/config/CucumberConfiguration.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.unicitylabs.sdk.e2e.config; - -import org.unicitylabs.sdk.e2e.context.TestContext; -import io.cucumber.java.Before; -import io.cucumber.java.After; - -/** - * Cucumber configuration for dependency injection and test lifecycle management. - * This ensures that TestContext is properly shared across all step definition classes. - */ -public class CucumberConfiguration { - - private static TestContext testContext = new TestContext(); - - /** - * Provides a shared TestContext instance for all step definition classes. - * This method will be called by step definition classes to get - * the shared TestContext instance. - */ - public static TestContext getTestContext() { - return testContext; - } - - /** - * Hook that runs before each scenario to reset the test context. - * This ensures each scenario starts with a clean state. - */ - @Before - public void setUp() { - testContext.clearTestState(); // Clear test state but keep clients if they exist - System.out.println("Test context cleared for new scenario"); - } - - /** - * Hook that runs after each scenario for cleanup. - * This can be used for any additional cleanup if needed. - */ - @After - public void tearDown() { - // Optional: Add any cleanup logic here - // For now, we keep the context alive for potential debugging - System.out.println("Scenario completed"); - } - - /** - * Hook that runs after scenarios tagged with @reset to completely reset the context. - */ - @After("@reset") - public void fullReset() { - testContext.reset(); - System.out.println("Full context reset performed"); - } -} \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/e2e/context/TestContext.java b/src/test/java/org/unicitylabs/sdk/e2e/context/TestContext.java deleted file mode 100644 index e5500f2..0000000 --- a/src/test/java/org/unicitylabs/sdk/e2e/context/TestContext.java +++ /dev/null @@ -1,303 +0,0 @@ -package org.unicitylabs.sdk.e2e.context; - -import org.unicitylabs.sdk.StateTransitionClient; -import org.unicitylabs.sdk.TestAggregatorClient; -import org.unicitylabs.sdk.api.AggregatorClient; -import org.unicitylabs.sdk.api.SubmitCommitmentResponse; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.predicate.Predicate; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.transaction.Transaction; -import org.unicitylabs.sdk.transaction.TransferTransaction; -import org.unicitylabs.sdk.utils.TestUtils; -import org.unicitylabs.sdk.utils.helpers.CommitmentResult; -import org.unicitylabs.sdk.utils.helpers.PendingTransfer; - -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; - -/** - * Shared test context that maintains state across all step definition classes. - * This allows different step definition classes to share data and avoid duplication. - */ -public class TestContext { - - // Core clients - private AggregatorClient aggregatorClient; - private TestAggregatorClient testAggregatorClient; - private StateTransitionClient client; - private RootTrustBase trustBase; - - - // User management - private Map userSigningServices = new HashMap<>(); - private Map userNonces = new HashMap<>(); - private Map userSecrets = new HashMap<>(); - private Map userPredicate = new HashMap<>(); - private Map> userTokens = new HashMap<>(); - private Map> nameTagTokens = new HashMap<>(); - private final Map> pendingTransfers = new HashMap<>(); - private final Map> userNametagRelations = new HashMap<>(); - - - - // Test execution state - private Long blockHeight; - private byte[] randomSecret; - private byte[] stateBytes; - private DataHash stateHash; - private DataHash txDataHash; - private SubmitCommitmentResponse commitmentResponse; - private long submissionDuration; - private Exception lastError; - private boolean operationSucceeded; - - // Performance testing - private int configuredThreadCount; - private int configuredCommitmentsPerThread; - private List aggregatorClients; - private List> concurrentResults = new ArrayList<>(); - private long concurrentSubmissionDuration; - private List bulkResults = new ArrayList<>(); - private long bulkOperationDuration; - - // Transfer chain tracking - private List transferChain = new ArrayList<>(); - private Token chainToken; - private Map transferCustomData = new HashMap<>(); - - // Current operation context - private String currentUser; - private String expectedErrorType; - private int expectedSplitCount; - private int configuredUserCount; - private int configuredTokensPerUser; - - - // Getters and Setters - public AggregatorClient getAggregatorClient() { return aggregatorClient; } - public void setAggregatorClient(AggregatorClient aggregatorClient) { this.aggregatorClient = aggregatorClient; } - - public RootTrustBase getTrustBase() { return trustBase; } - public void setTrustBase(RootTrustBase trustBase) { this.trustBase = trustBase; } - - public TestAggregatorClient getTestAggregatorClient() { return testAggregatorClient; } - public void setTestAggregatorClient(TestAggregatorClient testAggregatorClient) { this.testAggregatorClient = testAggregatorClient; } - - public StateTransitionClient getClient() { return client; } - public void setClient(StateTransitionClient client) { this.client = client; } - - public Map getUserSigningServices() { return userSigningServices; } - public void setUserSigningServices(Map userSigningServices) { this.userSigningServices = userSigningServices; } - - public Map getUserNonces() { return userNonces; } - public void setUserNonces(Map userNonces) { this.userNonces = userNonces; } - - public Map getUserSecret() { return userSecrets; } - public void setUserSecret(Map userSecrets) { this.userSecrets = userSecrets; } - - public Map getUserPredicate() { - return userPredicate; - } - - public void setUserPredicate(Map userPredicate) { - this.userPredicate = userPredicate; - } - - public List getAggregatorClients() { - return aggregatorClients; - } - - public void setAggregatorClients(List aggregatorClients) { - this.aggregatorClients = aggregatorClients; - } - - public Map> getUserTokens() { return userTokens; } - public void setUserTokens(Map> userTokens) { this.userTokens = userTokens; } - - public Map> getNameTagTokens() { return nameTagTokens; } - public void setNameTagTokens(Map> nameTagTokens) { this.nameTagTokens = nameTagTokens; } - - public Long getBlockHeight() { return blockHeight; } - public void setBlockHeight(Long blockHeight) { this.blockHeight = blockHeight; } - - public byte[] getRandomSecret() { return randomSecret; } - public void setRandomSecret(byte[] randomSecret) { this.randomSecret = randomSecret; } - - public byte[] getStateBytes() { return stateBytes; } - public void setStateBytes(byte[] stateBytes) { this.stateBytes = stateBytes; } - - public DataHash getStateHash() { return stateHash; } - public void setStateHash(DataHash stateHash) { this.stateHash = stateHash; } - - public DataHash getTxDataHash() { return txDataHash; } - public void setTxDataHash(DataHash txDataHash) { this.txDataHash = txDataHash; } - - public SubmitCommitmentResponse getCommitmentResponse() { return commitmentResponse; } - public void setCommitmentResponse(SubmitCommitmentResponse commitmentResponse) { this.commitmentResponse = commitmentResponse; } - - public long getSubmissionDuration() { return submissionDuration; } - public void setSubmissionDuration(long submissionDuration) { this.submissionDuration = submissionDuration; } - - public Exception getLastError() { return lastError; } - public void setLastError(Exception lastError) { this.lastError = lastError; } - - public boolean isOperationSucceeded() { return operationSucceeded; } - public void setOperationSucceeded(boolean operationSucceeded) { this.operationSucceeded = operationSucceeded; } - - public int getConfiguredThreadCount() { return configuredThreadCount; } - public void setConfiguredThreadCount(int configuredThreadCount) { this.configuredThreadCount = configuredThreadCount; } - - public int getConfiguredCommitmentsPerThread() { return configuredCommitmentsPerThread; } - public void setConfiguredCommitmentsPerThread(int configuredCommitmentsPerThread) { this.configuredCommitmentsPerThread = configuredCommitmentsPerThread; } - - public List> getConcurrentResults() { return concurrentResults; } - public void setConcurrentResults(List> concurrentResults) { this.concurrentResults = concurrentResults; } - - public long getConcurrentSubmissionDuration() { return concurrentSubmissionDuration; } - public void setConcurrentSubmissionDuration(long concurrentSubmissionDuration) { this.concurrentSubmissionDuration = concurrentSubmissionDuration; } - - public List getBulkResults() { return bulkResults; } - public void setBulkResults(List bulkResults) { this.bulkResults = bulkResults; } - - public long getBulkOperationDuration() { return bulkOperationDuration; } - public void setBulkOperationDuration(long bulkOperationDuration) { this.bulkOperationDuration = bulkOperationDuration; } - - public List getTransferChain() { return transferChain; } - public void setTransferChain(List transferChain) { this.transferChain = transferChain; } - - public Token getChainToken() { return chainToken; } - public void setChainToken(Token chainToken) { this.chainToken = chainToken; } - - public Map getTransferCustomData() { return transferCustomData; } - public void setTransferCustomData(Map transferCustomData) { this.transferCustomData = transferCustomData; } - - public String getCurrentUser() { return currentUser; } - public void setCurrentUser(String currentUser) { this.currentUser = currentUser; } - - public String getExpectedErrorType() { return expectedErrorType; } - public void setExpectedErrorType(String expectedErrorType) { this.expectedErrorType = expectedErrorType; } - - public int getExpectedSplitCount() { return expectedSplitCount; } - public void setExpectedSplitCount(int expectedSplitCount) { this.expectedSplitCount = expectedSplitCount; } - - public int getConfiguredUserCount() { return configuredUserCount; } - public void setConfiguredUserCount(int configuredUserCount) { this.configuredUserCount = configuredUserCount; } - - public int getConfiguredTokensPerUser() { return configuredTokensPerUser; } - public void setConfiguredTokensPerUser(int configuredTokensPerUser) { this.configuredTokensPerUser = configuredTokensPerUser; } - - public void savePendingTransfer(String user, Token token, TransferTransaction tx) { - pendingTransfers.computeIfAbsent(user, k -> new ArrayList<>()) - .add(new PendingTransfer(token, tx)); - } - - public List getPendingTransfers(String user) { - return pendingTransfers.getOrDefault(user, List.of()); - } - - public void clearPendingTransfers(String user) { - pendingTransfers.remove(user); - } - - - // Utility methods - public void addUserToken(String userName, Token token) { - userTokens.computeIfAbsent(userName, k -> new ArrayList<>()).add(token); - } - - public Token getUserToken(String userName) { - List tokens = userTokens.get(userName); - return (tokens != null && !tokens.isEmpty()) ? tokens.get(0) : null; - } - - public Token getUserToken(String userName, int index) { - List tokens = userTokens.get(userName); - return (tokens != null && tokens.size() > index) ? tokens.get(index) : null; - } - - public void addNameTagToken(String userName, Token nameTagToken) { - nameTagTokens.computeIfAbsent(userName, k -> new ArrayList<>()).add(nameTagToken); - } - - public Token getNameTagToken(String userName) { - List tokens = nameTagTokens.get(userName); - return (tokens != null && !tokens.isEmpty()) ? tokens.get(0) : null; - } - - public void addNametagRelation(String username, TokenId nametagId, TokenId originalTokenId) { - userNametagRelations - .computeIfAbsent(username, k -> new HashMap<>()) - .put(nametagId, originalTokenId); - } - - public TokenId getOriginalTokenIdForNametag(String username, TokenId nametagId) { - Map relations = userNametagRelations.get(username); - if (relations == null) return null; - return relations.get(nametagId); - } - - public Map getNametagRelationsForUser(String username) { - return userNametagRelations.getOrDefault(username, new HashMap<>()); - } - - public void setNametagRelationsForUser(String username, Map relations) { - userNametagRelations.put(username, relations); - } - - public Map> getAllNametagRelations() { - return userNametagRelations; - } - - public void clearUserData() { - userSigningServices.clear(); - userNonces.clear(); - userSecrets.clear(); - userTokens.clear(); - nameTagTokens.clear(); - userNametagRelations.clear(); - } - - public void clearTestState() { - configuredUserCount = 0; - blockHeight = null; - randomSecret = null; - stateBytes = null; - stateHash = null; - txDataHash = null; - commitmentResponse = null; - submissionDuration = 0; - lastError = null; - operationSucceeded = false; - concurrentResults.clear(); - bulkResults.clear(); - transferChain.clear(); - chainToken = null; - transferCustomData.clear(); - currentUser = null; - expectedErrorType = null; - } - - public void reset() { - clearUserData(); - clearTestState(); - aggregatorClient = null; - testAggregatorClient = null; - client = null; - trustBase = null; - } - - private List> commitmentFutures = new ArrayList<>(); - - public void setCommitmentFutures(List> futures) { - this.commitmentFutures = futures; - } - - public List> getCommitmentFutures() { - return commitmentFutures; - } -} \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/e2e/steps/AdvancedStepDefinitions.java b/src/test/java/org/unicitylabs/sdk/e2e/steps/AdvancedStepDefinitions.java deleted file mode 100644 index 2eec822..0000000 --- a/src/test/java/org/unicitylabs/sdk/e2e/steps/AdvancedStepDefinitions.java +++ /dev/null @@ -1,298 +0,0 @@ -package org.unicitylabs.sdk.e2e.steps; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import io.cucumber.java.en.And; -import io.cucumber.java.en.Given; -import io.cucumber.java.en.Then; -import io.cucumber.java.en.When; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.unicitylabs.sdk.address.ProxyAddress; -import org.unicitylabs.sdk.e2e.config.CucumberConfiguration; -import org.unicitylabs.sdk.e2e.context.TestContext; -import org.unicitylabs.sdk.e2e.steps.shared.StepHelper; -import org.unicitylabs.sdk.predicate.embedded.MaskedPredicate; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.token.TokenType; -import org.unicitylabs.sdk.token.fungible.TokenCoinData; -import org.unicitylabs.sdk.transaction.Transaction; -import org.unicitylabs.sdk.transaction.TransferTransaction; -import org.unicitylabs.sdk.utils.TestUtils; -import org.unicitylabs.sdk.utils.helpers.PendingTransfer; - -/** - * Advanced step definitions for complex scenarios and edge cases. - */ -public class AdvancedStepDefinitions { - - private final TestContext context; - - public AdvancedStepDefinitions() { - this.context = CucumberConfiguration.getTestContext(); - } - - StepHelper helper = new StepHelper(); - - @When("the token is transferred through the chain of existing users") - public void theTokenIsTransferredThroughTheChain() throws Exception { - List users = new ArrayList<>(context.getUserSigningServices().keySet()); - - // remove the one that should go first (if it’s already inside) - users.removeAll(context.getTransferChain()); - - // prepend the transfer chain at the beginning - List orderedUsers = new ArrayList<>(); - orderedUsers.addAll(context.getTransferChain()); // first element(s) - orderedUsers.addAll(users); - - - Token currentToken = context.getChainToken(); - - for (int i = 0; i < orderedUsers.size() - 1; i++) { - String fromUser = orderedUsers.get(i); - String toUser = orderedUsers.get(i + 1); - - SigningService fromSigningService = context.getUserSigningServices().get(fromUser); - SigningService toSigningService = context.getUserSigningServices().get(toUser); - byte[] toNonce = context.getUserNonces().get(toUser); - - // Create a simple direct address for transfer - var toPredicate = MaskedPredicate.create( - currentToken.getId(), - currentToken.getType(), - toSigningService, - org.unicitylabs.sdk.hash.HashAlgorithm.SHA256, - toNonce - ); - var toAddress = toPredicate.getReference().toAddress(); - - String customData = "Transfer from " + fromUser + " to " + toUser; - System.out.println(customData); - context.getTransferCustomData().put(toUser, customData); - - currentToken = TestUtils.transferToken( - context.getClient(), - currentToken, - fromSigningService, - toSigningService, - toNonce, - toAddress, - customData.getBytes(StandardCharsets.UTF_8), - List.of(), - context.getTrustBase() - ); - - context.getTransferChain().add(toUser); - } - - context.setChainToken(currentToken); - } - - @Then("the final token should maintain original properties") - public void theFinalTokenShouldMaintainOriginalProperties() { - assertNotNull(context.getChainToken(), "Final token should exist"); - assertTrue(context.getChainToken().verify(context.getTrustBase()).isSuccessful(), "Final token should be valid"); - - // Additional property validation can be added based on requirements - } - - @And("all intermediate transfers should be recorded correctly") - public void allIntermediateTransfersShouldBeRecordedCorrectly() { - assertEquals(4, context.getTransferChain().size(), "Transfer chain should have 4 users"); - assertEquals("Alice", context.getTransferChain().get(0), "Chain should start with Alice"); - assertEquals("Bob", context.getTransferChain().get(3), "Chain should end with Dave"); - } - - @And("the token should have transfers in history") - public void theTokenShouldHaveTransfersInHistory(int expectedTransfers) { - int actualTransfers = context.getChainToken().getTransactions().size() - 1; // Subtract mint transaction - assertEquals(expectedTransfers, actualTransfers, "Token should have expected number of transfers"); - } - - // Name Tag Scenarios Steps - @Given("{string} creates nametags for each token") - public void createsNameTagTokensWithDifferentAddresses(String username) throws Exception { - List nametags = new ArrayList<>(); - List ownerTokens = context.getUserTokens().get(context.getCurrentUser()); - - Map relations = new HashMap<>(); - - for (Token token : ownerTokens) { - String nameTagIdentifier = TestUtils.generateRandomString(10); - Token nametagToken = helper.createNameTagTokenForUser( - username, - token, - nameTagIdentifier, - TestUtils.generateRandomString(10) - ); - nametags.add(nametagToken); - - // store relation: nametag -> original token - relations.put(nametagToken.getId(), token.getId()); - } - - context.getNameTagTokens().put(username, nametags); - context.setNametagRelationsForUser(username, relations); - } - - @When("{string} transfers tokens to each of {string} nametags") - public void userTransfersTokensToEachOfNameTags(String fromUser, String toUser) throws Exception { - List nametagTokens = context.getNameTagTokens().get(toUser); - List tokens = context.getUserTokens().get(fromUser); - - for (Token nametagToken : nametagTokens) { - TokenId originalTokenId = context.getOriginalTokenIdForNametag(toUser, nametagToken.getId()); - - Token tokenToTransfer = tokens.stream() - .filter(t -> t.getId().equals(originalTokenId)) - .findFirst() - .orElseThrow(() -> new RuntimeException("Token not found for transfer: " + originalTokenId)); - - ProxyAddress proxyAddress = ProxyAddress.create(nametagToken.getId()); - - helper.transferToken( - fromUser, - toUser, - tokenToTransfer, - proxyAddress, - null - ); - } - } - - @And("{string} consolidates all received tokens") - public void userConsolidatesAllReceivedTokens(String username) { - // Consolidation logic would depend on specific requirements - List tokens = context.getUserTokens().getOrDefault(username, new ArrayList<>()); - // Verify user has received tokens - assertFalse(tokens.isEmpty(), username + " should have received tokens"); - } - - @Then("{string} should own {int} tokens") - public void userShouldOwnTokens(String username, int expectedTokenCount) { - List tokens = context.getUserTokens().getOrDefault(username, new ArrayList<>()); - assertEquals(expectedTokenCount, tokens.size(), username +" should own expected number of tokens"); - - // Verify ownership - for (Token token : tokens) { - SigningService signingService = SigningService.createFromSecret( - context.getUserSecret().get(username) - ); - assertTrue(token.verify(context.getTrustBase()).isSuccessful(), "Token should be valid"); - assertTrue(TestUtils - .validateTokenOwnership( - token, - signingService, - context.getTrustBase() - ), - username + " should own all tokens"); - } - } - - @And("all {string} nametag tokens should remain valid") - public void allNameTagTokensShouldRemainValid(String username) { - List nametags = context.getNameTagTokens().get(username); - for (Token nametag : nametags) { - assertTrue(nametag.verify(context.getTrustBase()).isSuccessful(), "All name tag tokens should remain valid"); - } - } - - @And("proxy addressing should work for all {string} name tags") - public void proxyAddressingShouldWorkForAllNameTags(String username) { - List nametags = context.getNameTagTokens().get(username); - - for (Token nametag : nametags) { - var proxyAddress = org.unicitylabs.sdk.address.ProxyAddress.create(nametag.getId()); - assertNotNull(proxyAddress, "Proxy address should be creatable for all name tags"); - } - } - - // Large Data Handling Steps - @Given("a token with custom data of size {int} bytes") - public void aTokenWithCustomDataOfSizeBytes(int dataSize) throws Exception { - String alice = "Alice"; - byte[] largeData = new byte[dataSize]; - Arrays.fill(largeData, (byte) 'A'); // Fill with 'A' characters - - TokenId tokenId = TestUtils.generateRandomTokenId(); - TokenType tokenType = TestUtils.generateRandomTokenType(); - TokenCoinData coinData = TestUtils.createRandomCoinData(1); - - // Create token with large custom data - MaskedPredicate predicate = MaskedPredicate.create( - tokenId, - tokenType, - context.getUserSigningServices().get(alice), - org.unicitylabs.sdk.hash.HashAlgorithm.SHA256, - context.getUserNonces().get(alice) - ); - - var tokenState = new org.unicitylabs.sdk.token.TokenState(predicate, largeData); - - // Store for later use in transfer - context.setChainToken(TestUtils.mintTokenForUser( - context.getClient(), - context.getUserSigningServices().get(alice), - context.getUserNonces().get(alice), - tokenId, - tokenType, - coinData, - context.getTrustBase() - )); - } - - @And("{string} finalizes all received tokens") - public void finalizesAllReceivedTokens(String username) throws Exception { - List pendingTransfers = context.getPendingTransfers(username); - - for (PendingTransfer pending : pendingTransfers) { - Token token = pending.getSourceToken(); - TransferTransaction tx = pending.getTransaction(); - helper.finalizeTransfer( - username, - token, - tx - ); - } - context.clearPendingTransfers(username); - } - - @Given("{string} creates {int} tokens") - public void createsNametagCountTokens(String username, int quantity) throws Exception { - for (int i = 0; i < quantity; i++) { - TokenId tokenId = TestUtils.generateRandomTokenId(); - TokenType tokenType = TestUtils.generateRandomTokenType(); - TokenCoinData coinData = TestUtils.createRandomCoinData(1); - - Token token = TestUtils.mintTokenForUser( - context.getClient(), - context.getUserSigningServices().get(username), - context.getUserNonces().get(username), - tokenId, - tokenType, - coinData, - context.getTrustBase() - ); - - // do post-processing here (still in parallel) - if (TestUtils.validateTokenOwnership( - token, - context.getUserSigningServices().get(username), - context.getTrustBase() - )) { - context.addUserToken(username, token); - } - context.setCurrentUser(username); - } - } -} \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/e2e/steps/StepDefinitions.java b/src/test/java/org/unicitylabs/sdk/e2e/steps/StepDefinitions.java deleted file mode 100644 index e799d47..0000000 --- a/src/test/java/org/unicitylabs/sdk/e2e/steps/StepDefinitions.java +++ /dev/null @@ -1,366 +0,0 @@ -package org.unicitylabs.sdk.e2e.steps; - -import org.unicitylabs.sdk.address.ProxyAddress; -import org.unicitylabs.sdk.e2e.config.CucumberConfiguration; -import org.unicitylabs.sdk.e2e.context.TestContext; -import org.unicitylabs.sdk.e2e.steps.shared.StepHelper; -import org.unicitylabs.sdk.token.fungible.CoinId; -import org.unicitylabs.sdk.utils.TestUtils; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.token.TokenType; -import org.unicitylabs.sdk.token.fungible.TokenCoinData; -import io.cucumber.java.en.And; -import io.cucumber.java.en.Given; -import io.cucumber.java.en.Then; -import io.cucumber.java.en.When; - -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.*; -import java.util.stream.Collectors; - - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Refactored step definitions that use TestContext and SharedStepDefinitions. - * These steps handle specific scenarios that aren't covered by the shared steps. - */ -public class StepDefinitions { - - private final TestContext context; - - public StepDefinitions() { - this.context = CucumberConfiguration.getTestContext(); - } - - StepHelper helper = new StepHelper(); - - // Token Properties Validation - @And("the token should maintain its original ID and type") - public void theTokenShouldMaintainItsOriginalIdAndType() { - String currentUser = context.getCurrentUser(); - if (currentUser == null) currentUser = "Alice"; // fallback - - // Get the first user's token (original) and current user's token - List userNames = new ArrayList<>(context.getUserTokens().keySet()); - if (userNames.size() >= 2) { - Token originalToken = context.getUserToken(userNames.get(0)); - Token currentToken = context.getUserToken(currentUser); - - if (originalToken != null && currentToken != null) { - assertEquals(originalToken.getId(), currentToken.getId(), "Token ID should remain the same"); - assertEquals(originalToken.getType(), currentToken.getType(), "Token type should remain the same"); - } - } - } - - @And("the token should have {int} transactions in its history") - public void theTokenShouldHaveTransactionsInItsHistory(int expectedTransactionCount) { - String currentUser = context.getCurrentUser(); - if (currentUser == null) { - // Find the last user who received a token - List userNames = new ArrayList<>(context.getUserTokens().keySet()); - currentUser = userNames.get(userNames.size() - 1); - } - - Token token = context.getUserToken(currentUser); - assertNotNull(token, "Token should exist for validation"); - assertEquals(expectedTransactionCount, token.getTransactions().size(), // Subtract mint transaction - "Token should have the expected number of transactions"); - } - - // Minting with Parameters - @Given("user {string} with nonce of {int} bytes") - public void userWithNonceOfBytes(String userName, int nonceLength) { - byte[] nonce = TestUtils.generateRandomBytes(nonceLength); - SigningService signingService = TestUtils.createSigningServiceForUser(userName, nonce); - - context.getUserSigningServices().put(userName, signingService); - context.getUserNonces().put(userName, nonce); - context.getUserTokens().put(userName, new ArrayList<>()); - context.setCurrentUser(userName); - } - - @When("the user mints a token of type {string} with coin data containing {int} coins") - public void theUserMintsATokenOfTypeWithCoinDataContainingCoins(String tokenType, int coinCount) throws Exception { - String user = context.getCurrentUser(); - TokenId tokenId = TestUtils.generateRandomTokenId(); - TokenType type = TestUtils.createTokenTypeFromString(tokenType); - TokenCoinData coinData = TestUtils.createRandomCoinData(coinCount); - - Token token = TestUtils.mintTokenForUser( - context.getClient(), - context.getUserSigningServices().get(user), - context.getUserNonces().get(user), - tokenId, - type, - coinData, - context.getTrustBase() - ); - context.addUserToken(user, token); - } - - @Then("the token should be minted successfully") - public void theTokenShouldBeMintedSuccessfully() { - String user = context.getCurrentUser(); - Token token = context.getUserToken(user); - assertNotNull(token, "Token should be minted"); - } - - @And("the token should be verified successfully") - public void theTokenShouldBeVerifiedSuccessfully() { - String user = context.getCurrentUser(); - Token token = context.getUserToken(user); - assertTrue(token.verify(context.getTrustBase()).isSuccessful(), "Token should be verified successfully"); - } - - @And("the token should belong to the user") - public void theTokenShouldBelongToTheUser() { - String user = context.getCurrentUser(); - Token token = context.getUserToken(user); - SigningService signingService = context.getUserSigningServices().get(user); - - assertTrue(TestUtils - .validateTokenOwnership( - token, - signingService, - context.getTrustBase() - ), - "Token should belong to the user"); - } - - @Then("the name tag token should be created successfully") - public void theNameTagTokenShouldBeCreatedSuccessfully() { - String user = context.getCurrentUser(); - Token nametagToken = context.getNameTagToken(user); - assertNotNull(nametagToken, "Name tag token should be created"); - assertTrue(nametagToken.verify(context.getTrustBase()).isSuccessful(), "Name tag token should be valid"); - } - - @And("the name tag should be usable for proxy addressing") - public void theNameTagShouldBeUsableForProxyAddressing() { - String user = context.getCurrentUser(); - Token nametagToken = context.getNameTagToken(user); - ProxyAddress proxyAddress = ProxyAddress.create(nametagToken.getId()); - assertNotNull(proxyAddress, "Proxy address should be creatable from name tag"); - } - - // Bulk Operations - @Given("{int} users are configured for bulk operations") - public void usersAreConfiguredForBulkOperations(int userCount) { - context.setConfiguredUserCount(userCount); - - // Setup additional users if needed - for (int i = 0; i < userCount; i++) { - String userName = "BulkUser" + i; - TestUtils.setupUser(userName, context.getUserSigningServices(), context.getUserNonces(), context.getUserSecret()); - context.getUserTokens().put(userName, new ArrayList<>()); - } - } - - @When("each user mints {int} tokens simultaneously") - public void eachUserMintsTokensSimultaneously(int tokensPerUser) throws Exception { - context.setConfiguredTokensPerUser(tokensPerUser); - - //Lower the thread pool size to avoid overload - int poolSize = Math.min(500, context.getConfiguredUserCount() * 2); - ExecutorService executor = Executors.newFixedThreadPool(poolSize); - - //ExecutorService executor = Executors.newFixedThreadPool(context.getConfiguredUserCount() * 2); - List> futures = new ArrayList<>(); - Map, String> futureOwners = new ConcurrentHashMap<>(); - - long startTime = System.currentTimeMillis(); - - // Create minting tasks for each user - for (int userIndex = 0; userIndex < context.getConfiguredUserCount(); userIndex++) { - String userName = "BulkUser" + userIndex; - SigningService signingService = context.getUserSigningServices().get(userName); - byte[] nonce = context.getUserNonces().get(userName); - - for (int tokenIndex = 0; tokenIndex < tokensPerUser; tokenIndex++) { - String requestId = userName + "-token" + tokenIndex; // helpful identifier - - CompletableFuture future = CompletableFuture.supplyAsync(() -> { - try { - TokenId tokenId = TestUtils.generateRandomTokenId(); - TokenType tokenType = TestUtils.generateRandomTokenType(); - TokenCoinData coinData = TestUtils.createRandomCoinData(2); - - Token token = TestUtils - .mintTokenForUser( - context.getClient(), - signingService, - nonce, - tokenId, - tokenType, - coinData, - context.getTrustBase() - ); - System.out.println(token.getGenesis().getData().getSourceState()); - // do post-processing here (still in parallel) - for (var entry : context.getUserSigningServices().entrySet()) { - if (TestUtils - .validateTokenOwnership( - token, - entry.getValue(), - context.getTrustBase() - ) - ) { - context.addUserToken(entry.getKey(), token); - break; - } - } - System.out.println("[Collector] Got result from " + requestId + - " on thread " + Thread.currentThread().getName()); - return TestUtils.TokenOperationResult.success("Token minted successfully (" + requestId + ")", token); - } catch (Exception e) { - e.printStackTrace(); - System.out.println("[Collector] Failed " + requestId + " on thread " + Thread.currentThread().getName() + - " with " + e.getMessage()); - return TestUtils.TokenOperationResult.failure("Failed to mint token (" + requestId + ")", e); - } - }, executor).orTimeout(40, TimeUnit.SECONDS) - .exceptionally(ex -> TestUtils.TokenOperationResult.failure("Timeout (" + requestId + ")", (Exception) ex));; - - futures.add(future); - futureOwners.put(future, requestId); - } - } - - // Start monitoring thread - ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor(); - monitor.scheduleAtFixedRate(() -> { - long doneCount = futures.stream().filter(CompletableFuture::isDone).count(); - long total = futures.size(); - long pending = total - doneCount; - - System.out.println("[Monitor] " + doneCount + "/" + total + " completed, " + pending + " still pending"); - - if (pending == 0) { - System.out.println("[Monitor] All requests completed. Stopping monitor."); - monitor.shutdown(); // ✅ stop the monitor here - } - - // After 15s, dump details of stuck ones - if (System.currentTimeMillis() - startTime > 15_000 && pending > 0) { - List pendingRequests = futures.stream() - .filter(f -> !f.isDone()) - .map(futureOwners::get) - .collect(Collectors.toList()); - System.out.println("[Monitor] Still waiting for requests: " + pendingRequests); - } - }, 5, 5, TimeUnit.SECONDS); - - // Wait for all operations to complete and collect results - List results = futures.stream() - .map(CompletableFuture::join) - .collect(Collectors.toList()); - - long successes = results.stream().filter(TestUtils.TokenOperationResult::isSuccess).count(); - long failures = results.size() - successes; - - System.out.println("[Summary] Successes: " + successes + ", Failures: " + failures); - - long endTime = System.currentTimeMillis(); - context.setBulkResults(results); - context.setBulkOperationDuration(endTime - startTime); - - executor.shutdown(); - } - - @And("all tokens are verified in parallel") - public void allTokensAreVerifiedInParallel() { - // Verification is already done during token creation - long successfulTokens = context.getBulkResults().stream() - .mapToLong(result -> result.isSuccess() ? 1 : 0) - .sum(); - - System.out.println("Successfully created tokens: " + successfulTokens); - System.out.println("Total operation time: " + context.getBulkOperationDuration() + " ms"); - } - - @Then("all {int} tokens should be created successfully") - public void allTokensShouldBeCreatedSuccessfully(int expectedTotalTokens) { - long successfulTokens = context.getBulkResults().stream() - .mapToLong(result -> result.isSuccess() ? 1 : 0) - .sum(); - - assertEquals(expectedTotalTokens, successfulTokens, - "All tokens should be created successfully"); - } - - @And("the operation should complete within {int} seconds") - public void theOperationShouldCompleteWithinSeconds(int maxSeconds) { - long maxMilliseconds = maxSeconds * 1000L; - TestUtils.PerformanceValidator.validateDuration( - context.getBulkOperationDuration(), - maxMilliseconds, - "Bulk token creation" - ); - } - - @And("the success rate should be at least {int}%") - public void theSuccessRateShouldBeAtLeast(int minSuccessRate) { - long successful = context.getBulkResults().stream() - .mapToLong(result -> result.isSuccess() ? 1 : 0) - .sum(); - long total = context.getBulkResults().size(); - - TestUtils.PerformanceValidator.validateSuccessRate( - successful, - total, - minSuccessRate / 100.0, - "Bulk operations" - ); - } - - // Transfer Chain Operations - @Given("{string} mints a token with {int} coin value") - public void userMintsATokenWithCoinValue(String userName, int coinValue) throws Exception { - TokenId tokenId = TestUtils.generateRandomTokenId(); - TokenType tokenType = TestUtils.generateRandomTokenType(); - - // Create coin data with specified value - TokenCoinData coinData = createCoinDataWithValue(BigInteger.valueOf(coinValue)); - - Token token = TestUtils.mintTokenForUser( - context.getClient(), - context.getUserSigningServices().get(userName), - context.getUserNonces().get(userName), - tokenId, - tokenType, - coinData, - context.getTrustBase() - ); - - context.setChainToken(token); - context.getTransferChain().add(userName); - context.setCurrentUser(userName); - } - - @And("each transfer includes custom data validation") - public void eachTransferIncludesCustomDataValidation() { - // Validation is included in the transfer process - for (Map.Entry entry : context.getTransferCustomData().entrySet()) { - assertNotNull(entry.getValue(), "Custom data should be present for " + entry.getKey()); - assertTrue(entry.getValue().contains("Transfer from"), "Custom data should have expected format"); - } - } - - @And("the token should have {int} transfers in history") - public void theTokenShouldHaveTransfersInHistory(int expectedTransfers) { - int actualTransfers = context.getChainToken().getTransactions().size(); // Subtract mint transaction - assertEquals(expectedTransfers, actualTransfers, "Token should have expected number of transfers"); - } - - private TokenCoinData createCoinDataWithValue(BigInteger totalValue) { - CoinId coinId = new CoinId(TestUtils.generateRandomBytes(32)); - return new TokenCoinData(java.util.Map.of(coinId, totalValue)); - } -} \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/e2e/steps/shared/SharedStepDefinitions.java b/src/test/java/org/unicitylabs/sdk/e2e/steps/shared/SharedStepDefinitions.java deleted file mode 100644 index 3e336d6..0000000 --- a/src/test/java/org/unicitylabs/sdk/e2e/steps/shared/SharedStepDefinitions.java +++ /dev/null @@ -1,648 +0,0 @@ -package org.unicitylabs.sdk.e2e.steps.shared; - -import org.unicitylabs.sdk.StateTransitionClient; -import org.unicitylabs.sdk.address.DirectAddress; -import org.unicitylabs.sdk.address.ProxyAddress; -import org.unicitylabs.sdk.api.*; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.e2e.config.CucumberConfiguration; -import org.unicitylabs.sdk.e2e.context.TestContext; -import org.unicitylabs.sdk.predicate.PredicateEngineService; -import org.unicitylabs.sdk.predicate.embedded.UnmaskedPredicate; -import org.unicitylabs.sdk.serializer.UnicityObjectMapper; -import org.unicitylabs.sdk.transaction.*; -import org.unicitylabs.sdk.utils.TestUtils; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.token.TokenType; -import org.unicitylabs.sdk.token.fungible.TokenCoinData; -import io.cucumber.datatable.DataTable; -import io.cucumber.java.en.And; -import io.cucumber.java.en.Given; -import io.cucumber.java.en.Then; -import io.cucumber.java.en.When; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.concurrent.*; -import java.util.stream.Collectors; - -import static org.unicitylabs.sdk.utils.TestUtils.randomCoinData; -import static org.junit.jupiter.api.Assertions.*; - -import org.unicitylabs.sdk.utils.helpers.CommitmentResult; -import org.unicitylabs.sdk.verification.VerificationResult; - - -/** - * Shared step definitions that can be reused across multiple feature files. - * These steps use TestContext to maintain state and avoid duplication. - */ -public class SharedStepDefinitions { - - private final TestContext context; - - public SharedStepDefinitions() { // ✅ Public zero-argument constructor - this.context = CucumberConfiguration.getTestContext(); - } - - StepHelper helper = new StepHelper(); - - // Setup Steps - @Given("the aggregator URL is configured") - public void theAggregatorUrlIsConfigured() { -// String aggregatorUrl = System.getenv("AGGREGATOR_URL"); - String aggregatorUrl = "http://localhost:3000"; - - - assertNotNull(aggregatorUrl, "AGGREGATOR_URL environment variable must be set"); - context.setAggregatorClient(new JsonRpcAggregatorClient(aggregatorUrl)); - } - - @And("the aggregator client is initialized") - public void theAggregatorClientIsInitialized() { - assertNotNull(context.getAggregatorClient(), "Aggregator client should be initialized"); - } - - @And("the state transition client is initialized") - public void theStateTransitionClientIsInitialized() { - context.setClient(new StateTransitionClient(context.getAggregatorClient())); - assertNotNull(context.getClient(), "State transition client should be initialized"); - } - - @And("the following users are set up with their signing services") - public void usersAreSetUpWithTheirSigningServices(DataTable dataTable) { - List users = dataTable.asList(); - for (String user : users) { - TestUtils.setupUser(user, context.getUserSigningServices(), context.getUserNonces(), context.getUserSecret()); - context.getUserTokens().put(user, new ArrayList<>()); - } - } - - // Aggregator Operations - @When("I request the current block height") - public void iRequestTheCurrentBlockHeight() throws Exception { - Long blockHeight = context.getAggregatorClient().getBlockHeight().get(); - context.setBlockHeight(blockHeight); - } - - @Then("the block height should be returned") - public void theBlockHeightShouldBeReturned() { - assertNotNull(context.getBlockHeight(), "Block height should not be null"); - } - - @And("the block height should be greater than {int}") - public void theBlockHeightShouldBeGreaterThan(int minHeight) { - assertTrue(context.getBlockHeight() > minHeight, "Block height should be greater than " + minHeight); - } - - // Commitment Operations - @Given("a random secret of {int} bytes") - public void aRandomSecretOfBytes(int secretLength) { - byte[] randomSecret = TestUtils.generateRandomBytes(secretLength); - context.setRandomSecret(randomSecret); - assertNotNull(randomSecret); - assertEquals(secretLength, randomSecret.length); - } - - @And("a state hash from {int} bytes of random data") - public void aStateHashFromBytesOfRandomData(int stateLength) { - byte[] stateBytes = TestUtils.generateRandomBytes(stateLength); - DataHash stateHash = TestUtils.hashData(stateBytes); - context.setStateBytes(stateBytes); - context.setStateHash(stateHash); - assertNotNull(stateHash); - } - - @And("transaction data {string}") - public void transactionData(String txData) { - DataHash txDataHash = TestUtils.hashData(txData.getBytes(StandardCharsets.UTF_8)); - context.setTxDataHash(txDataHash); - assertNotNull(txDataHash); - } - - @When("I submit a commitment with the generated data") - public void iSubmitACommitmentWithTheGeneratedData() throws Exception { - long startTime = System.currentTimeMillis(); - - SigningService signingService = SigningService.createFromSecret(context.getRandomSecret()); - var requestId = TestUtils.createRequestId(signingService, context.getStateHash()); - var authenticator = TestUtils.createAuthenticator(signingService, context.getTxDataHash(), context.getStateHash()); - - SubmitCommitmentResponse response = context.getAggregatorClient() - .submitCommitment(requestId, context.getTxDataHash(), authenticator).get(); - context.setCommitmentResponse(response); - - long endTime = System.currentTimeMillis(); - context.setSubmissionDuration(endTime - startTime); - } - - @Then("the commitment should be submitted successfully") - public void theCommitmentShouldBeSubmittedSuccessfully() { - assertNotNull(context.getCommitmentResponse(), "Commitment response should not be null"); - assertEquals(SubmitCommitmentStatus.SUCCESS, context.getCommitmentResponse().getStatus(), - "Commitment should be submitted successfully"); - } - - @And("the submission should complete in less than {int} milliseconds") - public void theSubmissionShouldCompleteInLessThanMilliseconds(int maxDuration) { - assertTrue(context.getSubmissionDuration() < maxDuration, - String.format("Submission took %d ms, should be less than %d ms", - context.getSubmissionDuration(), maxDuration)); - } - - // Multi-threaded Operations - @Given("I configure {int} threads with {int} commitments each") - public void iConfigureThreadsWithCommitmentsEach(int threadCount, int commitmentsPerThread) { - context.setConfiguredThreadCount(threadCount); - context.setConfiguredCommitmentsPerThread(commitmentsPerThread); - - // Reuse existing user setup to create users - context.setConfiguredUserCount(threadCount); - - // Setup additional users if needed - for (int i = 0; i < threadCount; i++) { - String userName = "BulkUser" + i; - TestUtils.setupUser(userName, context.getUserSigningServices(), context.getUserNonces(), context.getUserSecret()); - context.getUserTokens().put(userName, new ArrayList<>()); - } - } - - @When("I submit all mint commitments concurrently") - public void iSubmitAllMintCommitmentsConcurrently() throws Exception { - int threadsCount = context.getConfiguredThreadCount(); - int commitmentsPerThread = context.getConfiguredCommitmentsPerThread(); - - Map userSigningServices = context.getUserSigningServices(); - ExecutorService executor = Executors.newFixedThreadPool(threadsCount); - - List> futures = new ArrayList<>(); - - for (Map.Entry entry : userSigningServices.entrySet()) { - String userName = entry.getKey(); - SigningService signingService = entry.getValue(); - - for (int i = 0; i < commitmentsPerThread; i++) { - CompletableFuture future = CompletableFuture.supplyAsync(() -> { - long start = System.nanoTime(); - byte[] stateBytes = TestUtils.generateRandomBytes(32); - byte[] txData = TestUtils.generateRandomBytes(32); - - DataHash stateHash = TestUtils.hashData(stateBytes); - DataHash txDataHash = TestUtils.hashData(txData); - RequestId requestId = TestUtils.createRequestId(signingService, stateHash); - - try { - Authenticator authenticator = TestUtils.createAuthenticator(signingService, txDataHash, stateHash); - - SubmitCommitmentResponse response = context.getAggregatorClient() - .submitCommitment(requestId, txDataHash, authenticator).get(); - - boolean success = response.getStatus() == SubmitCommitmentStatus.SUCCESS; - long end = System.nanoTime(); - - return new CommitmentResult(userName, Thread.currentThread().getName(), - requestId, success, start, end); - } catch (Exception e) { - long end = System.nanoTime(); - return new CommitmentResult(userName, Thread.currentThread().getName(), - requestId, false, start, end); - } - }, executor); - - futures.add(future); - } - } - - context.setCommitmentFutures(futures); - - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); - executor.shutdown(); - } - - // Token Operations - @Given("{string} mints a token with random coin data") - public void userMintsATokenWithRandomCoinData(String username) throws Exception { - TokenId tokenId = TestUtils.generateRandomTokenId(); - TokenType tokenType = TestUtils.generateRandomTokenType();; - TokenCoinData coinData = TestUtils.createRandomCoinData(2); - - Token token = TestUtils.mintTokenForUser( - context.getClient(), - context.getUserSigningServices().get(username), - context.getUserNonces().get(username), - tokenId, - tokenType, - coinData, - context.getTrustBase() - ); - - // do post-processing here (still in parallel) - if (TestUtils.validateTokenOwnership( - token, - context.getUserSigningServices().get(username), - context.getTrustBase() - )) { - context.addUserToken(username, token); - } - context.setCurrentUser(username); - } - - @When("{string} transfers the token to {string} using a proxy address") - public void userTransfersTheTokenToUserUsingAProxyAddress(String fromUser, String toUser) throws Exception { - Token sourceToken = context.getUserToken(fromUser); - ProxyAddress proxyAddress = ProxyAddress.create(context.getNameTagToken(toUser).getId()); - helper.transferToken(fromUser, toUser, sourceToken, proxyAddress, null); - } - - @When("{string} transfers the token to {string} using an unmasked predicate") - public void userTransfersTheTokenToUserUsingAnUnmaskedPredicate(String fromUser, String toUser) throws Exception { - Token sourceToken = context.getUserToken(fromUser); - SigningService toSigningService = context.getUserSigningServices().get(toUser); - - UnmaskedPredicate userPredicate = UnmaskedPredicate.create( - sourceToken.getId(), - sourceToken.getType(), - toSigningService, - HashAlgorithm.SHA256, - context.getUserNonces().get(toUser) - ); - context.getUserPredicate().put(toUser, userPredicate); - - DirectAddress toAddress = userPredicate.getReference().toAddress(); - - helper.transferToken(fromUser, toUser, sourceToken, toAddress, null); - } - - @Then("{string} should own the token successfully") - public void userShouldOwnTheTokenSuccessfully(String username) { - Token token = context.getUserToken(username); - context.setCurrentUser(username); - SigningService signingService = context.getUserSigningServices().get(username); - VerificationResult result = token.verify(context.getTrustBase()); - assertTrue(result.isSuccessful(), "Token should be valid"); -// assertTrue(token.getState().getPredicate().getEngine().equals(signingService.getPublicKey()), username + " should own the token"); - assertTrue(PredicateEngineService.createPredicate(token.getState().getPredicate()).isOwner(signingService.getPublicKey()), username + " should own the token"); - } - - @Then("all mint commitments should receive inclusion proofs within {int} seconds") - public void allMintCommitmentsShouldReceiveInclusionProofs(int timeoutSeconds) throws Exception { - List results = helper.collectCommitmentResults(); - helper.verifyAllInclusionProofsInParallel(timeoutSeconds); - - long verifiedCount = results.stream() - .filter(CommitmentResult::isVerified) - .count(); - - System.out.println("Verified commitments: " + verifiedCount + " / " + results.size()); - // Print failed ones (not verified) - results.stream() - .filter(r -> !r.isVerified()) - .forEach(r -> System.out.println( - "❌ Commitment failed: requestId=" + r.getRequestId().toString() + ", status=" + r.getStatus() - )); - - assertEquals(results.size(), verifiedCount, "All commitments should be verified"); - } - - @Given("user {string} create a nametag token with custom data {string}") - public void userCreateANametagTokenWithCustomData(String username, String customData) throws Exception { - Token token = context.getUserToken(context.getCurrentUser()); - context.setCurrentUser(username); - String nameTagIdentifier = TestUtils.generateRandomString(10); - Token nametagToken = helper.createNameTagTokenForUser( - username, - token, - nameTagIdentifier, - customData - ); - assertNotNull(nametagToken, "Name tag token should be created"); - assertTrue(nametagToken.verify(context.getTrustBase()).isSuccessful(), "Name tag token should be valid"); - context.addNameTagToken(username, nametagToken); - } - - @Given("the aggregator URLs are configured") - public void theAggregatorURLsAreConfigured() { - // You can either use environment variables or hardcode the URLs - List aggregatorUrls = Arrays.asList( - System.getenv("AGGREGATOR_URL") - ); - - assertNotNull(aggregatorUrls, "Aggregator URLs must be configured"); - assertFalse(aggregatorUrls.isEmpty(), "At least one aggregator URL must be provided"); - - List clients = new ArrayList<>(); - for (String url : aggregatorUrls) { - clients.add(new JsonRpcAggregatorClient(url.trim())); - } - - context.setAggregatorClients(clients); - } - - @And("the aggregator clients are initialized") - public void theAggregatorClientsAreInitialized() { - List clients = context.getAggregatorClients(); - assertNotNull(clients, "Aggregator clients should be initialized"); - assertFalse(clients.isEmpty(), "At least one aggregator client should be initialized"); - } - - @When("I submit all mint commitments concurrently to all aggregators") - public void iSubmitAllMintCommitmentsConcurrentlyToAllAggregators() { - int threadsCount = context.getConfiguredThreadCount(); - int commitmentsPerThread = context.getConfiguredCommitmentsPerThread(); - List aggregatorClients = context.getAggregatorClients(); - - Map userSigningServices = context.getUserSigningServices(); - - // Calculate total thread pool size: threads * aggregators - int totalThreadPoolSize = threadsCount * aggregatorClients.size(); - ExecutorService executor = Executors.newFixedThreadPool(totalThreadPoolSize); - - List> futures = new ArrayList<>(); - - for (Map.Entry entry : userSigningServices.entrySet()) { - String userName = entry.getKey(); - SigningService signingService = entry.getValue(); - - for (int i = 0; i < commitmentsPerThread; i++) { - // Generate the commitment data once for this iteration - byte[] stateBytes = TestUtils.generateRandomBytes(32); - byte[] txData = TestUtils.generateRandomBytes(32); - DataHash stateHash = TestUtils.hashData(stateBytes); - DataHash txDataHash = TestUtils.hashData(txData); - RequestId requestId = TestUtils.createRequestId(signingService, stateHash); - - // Submit the same commitment to all aggregators concurrently - for (int aggIndex = 0; aggIndex < aggregatorClients.size(); aggIndex++) { - AggregatorClient aggregatorClient = aggregatorClients.get(aggIndex); - String aggregatorId = "Aggregator" + aggIndex; - - CompletableFuture future = CompletableFuture.supplyAsync(() -> { - long start = System.nanoTime(); - - try { - Authenticator authenticator = TestUtils.createAuthenticator(signingService, txDataHash, stateHash); - - SubmitCommitmentResponse response = aggregatorClient - .submitCommitment(requestId, txDataHash, authenticator).get(); - - boolean success = response.getStatus() == SubmitCommitmentStatus.SUCCESS; - long end = System.nanoTime(); - - return new CommitmentResult(userName + "-" + aggregatorId, - Thread.currentThread().getName(), - requestId, success, start, end); - } catch (Exception e) { - long end = System.nanoTime(); - return new CommitmentResult(userName + "-" + aggregatorId, - Thread.currentThread().getName(), - requestId, false, start, end); - } - }, executor); - - futures.add(future); - } - } - } - - context.setCommitmentFutures(futures); - - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); - executor.shutdown(); - } - - @Then("all commitments should be processed successfully") - public void allCommitmentsShouldBeProcessedSuccessfully() { - int threadsCount = context.getConfiguredThreadCount(); - int commitmentsPerThread = context.getConfiguredCommitmentsPerThread(); - List aggregatorClients = context.getAggregatorClients(); - - Map userSigningServices = context.getUserSigningServices(); - ExecutorService executor = Executors.newFixedThreadPool(threadsCount); - - List>> futures = new ArrayList<>(); - - for (Map.Entry entry : userSigningServices.entrySet()) { - String userName = entry.getKey(); - SigningService signingService = entry.getValue(); - - for (int i = 0; i < commitmentsPerThread; i++) { - CompletableFuture> future = CompletableFuture.supplyAsync(() -> { - List results = new ArrayList<>(); - - // Generate commitment data once - byte[] stateBytes = TestUtils.generateRandomBytes(32); - byte[] txData = TestUtils.generateRandomBytes(32); - DataHash stateHash = TestUtils.hashData(stateBytes); - DataHash txDataHash = TestUtils.hashData(txData); - RequestId requestId = TestUtils.createRequestId(signingService, stateHash); - - // Submit to all aggregators with the same data - for (int aggIndex = 0; aggIndex < aggregatorClients.size(); aggIndex++) { - AggregatorClient aggregatorClient = aggregatorClients.get(aggIndex); - String aggregatorId = "Aggregator" + aggIndex; - - long start = System.nanoTime(); - try { - Authenticator authenticator = TestUtils.createAuthenticator(signingService, txDataHash, stateHash); - - SubmitCommitmentResponse response = aggregatorClient - .submitCommitment(requestId, txDataHash, authenticator).get(); - - boolean success = response.getStatus() == SubmitCommitmentStatus.SUCCESS; - long end = System.nanoTime(); - - results.add(new CommitmentResult(userName + "-" + aggregatorId, - Thread.currentThread().getName(), - requestId, success, start, end)); - } catch (Exception e) { - long end = System.nanoTime(); - results.add(new CommitmentResult(userName + "-" + aggregatorId, - Thread.currentThread().getName(), - requestId, false, start, end)); - } - } - - return results; - }, executor); - - futures.add(future); - } - } - - // Flatten the results - List> flattenedFutures = new ArrayList<>(); - for (CompletableFuture> future : futures) { - CompletableFuture flattened = future.thenCompose(results -> { - // Return the first result (or you could return all) - return CompletableFuture.completedFuture(results.get(0)); - }); - flattenedFutures.add(flattened); - } - - context.setCommitmentFutures(flattenedFutures); - - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); - executor.shutdown(); - } - - @Then("all mint commitments should receive inclusion proofs from all aggregators within {int} seconds") - public void allMintCommitmentsShouldReceiveInclusionProofsFromAllAggregatorsWithinSeconds(int timeoutSeconds) throws Exception { - List results = helper.collectCommitmentResults(); - - // Verify inclusion proofs for all aggregators in parallel - helper.verifyAllInclusionProofsInParallelForMultipleAggregators(timeoutSeconds, context.getAggregatorClients()); - - long verifiedCount = results.stream() - .filter(CommitmentResult::isVerified) - .count(); - - System.out.println("=== Inclusion Proof Verification Results ==="); - System.out.println("Total commitments: " + results.size()); - System.out.println("Verified commitments: " + verifiedCount + " / " + results.size()); - - // Group results by aggregator for detailed reporting - Map> resultsByAggregator = results.stream() - .collect(Collectors.groupingBy(r -> helper.extractAggregatorFromUserName(r.getUserName()))); - - for (Map.Entry> entry : resultsByAggregator.entrySet()) { - String aggregatorId = entry.getKey(); - List aggregatorResults = entry.getValue(); - - long aggregatorVerifiedCount = aggregatorResults.stream() - .filter(CommitmentResult::isVerified) - .count(); - - System.out.println("\n" + aggregatorId + ":"); - System.out.println(" Verified: " + aggregatorVerifiedCount + " / " + aggregatorResults.size()); - - // Print failed ones for this aggregator - aggregatorResults.stream() - .filter(r -> !r.isVerified()) - .forEach(r -> System.out.println( - " ❌ Failed: requestId=" + r.getRequestId().toString() + - ", status=" + (r.getStatus() != null ? r.getStatus() : "Unknown") + - ", user=" + r.getUserName() - )); - - // Print successful ones (optional, for debugging) - if (aggregatorVerifiedCount > 0) { - System.out.println(" ✅ Successfully verified " + aggregatorVerifiedCount + " commitments"); - } - } - - assertEquals(results.size(), verifiedCount, "All commitments should be verified"); - } - - @Then("all mint commitments should receive inclusion proofs within {int} seconds with {int}% success rate") - public void allMintCommitmentsShouldReceiveInclusionProofsWithSuccessRate(int timeoutSeconds, int expectedSuccessRate) throws Exception { - List results = helper.collectCommitmentResults(); - - // Verify inclusion proofs for all aggregators in parallel - helper.verifyAllInclusionProofsInParallelForMultipleAggregators(timeoutSeconds, context.getAggregatorClients()); - - long verifiedCount = results.stream() - .filter(CommitmentResult::isVerified) - .count(); - - double actualSuccessRate = (double) verifiedCount / results.size() * 100; - - System.out.println("=== Inclusion Proof Verification Results ==="); - System.out.println("Total commitments: " + results.size()); - System.out.println("Verified commitments: " + verifiedCount + " / " + results.size()); - System.out.println("Actual success rate: " + String.format("%.2f%%", actualSuccessRate)); - System.out.println("Expected success rate: " + expectedSuccessRate + "%"); - - // Detailed reporting by aggregator - Map> resultsByAggregator = results.stream() - .collect(Collectors.groupingBy(r -> helper.extractAggregatorFromUserName(r.getUserName()))); - - for (Map.Entry> entry : resultsByAggregator.entrySet()) { - String aggregatorId = entry.getKey(); - List aggregatorResults = entry.getValue(); - - long aggregatorVerifiedCount = aggregatorResults.stream() - .filter(CommitmentResult::isVerified) - .count(); - - double aggregatorSuccessRate = (double) aggregatorVerifiedCount / aggregatorResults.size() * 100; - - System.out.println("\n" + aggregatorId + ":"); - System.out.println(" Success rate: " + String.format("%.2f%%", aggregatorSuccessRate) + - " (" + aggregatorVerifiedCount + " / " + aggregatorResults.size() + ")"); - } - - assertTrue(actualSuccessRate >= expectedSuccessRate, - String.format("Expected success rate of at least %d%%, but got %.2f%%", - expectedSuccessRate, actualSuccessRate)); - } - - @Then("I should see performance metrics for each aggregator") - public void iShouldSeePerformanceMetricsForEachAggregator() { - List results = helper.collectCommitmentResults(); - List aggregatorClients = context.getAggregatorClients(); - - System.out.println("\n=== 📊 AGGREGATOR PERFORMANCE COMPARISON ==="); - - // Print detailed breakdown - helper.printDetailedResultsByAggregator(results, aggregatorClients.size()); - - // Additional performance analysis - helper.printPerformanceComparison(results, aggregatorClients.size()); - } - - @Then("aggregator performance should meet minimum thresholds") - public void aggregatorPerformanceShouldMeetMinimumThresholds() { - List results = helper.collectCommitmentResults(); - List aggregatorClients = context.getAggregatorClients(); - - Map> resultsByAggregator = results.stream() - .collect(Collectors.groupingBy(r -> helper.extractAggregatorFromUserName(r.getUserName()))); - - for (int i = 0; i < aggregatorClients.size(); i++) { - String aggregatorId = "-Aggregator" + i; - List aggregatorResults = resultsByAggregator.getOrDefault(aggregatorId, new ArrayList<>()); - - if (aggregatorResults.isEmpty()) continue; - - long verifiedCount = aggregatorResults.stream() - .filter(CommitmentResult::isVerified) - .count(); - - double successRate = (double) verifiedCount / aggregatorResults.size() * 100; - - // Assert minimum success rate (configurable) - assertTrue(successRate >= 90.0, - String.format("Aggregator%d success rate (%.2f%%) should be at least 90%%", i, successRate)); - - // Assert reasonable average inclusion time - OptionalDouble avgInclusionTime = aggregatorResults.stream() - .filter(CommitmentResult::isVerified) - .mapToDouble(CommitmentResult::getInclusionDurationMillis) - .average(); - - if (avgInclusionTime.isPresent()) { - assertTrue(avgInclusionTime.getAsDouble() <= 30000, // 30 seconds max - String.format("Aggregator%d average inclusion time (%.2fms) should be under 30 seconds", - i, avgInclusionTime.getAsDouble())); - } - - System.out.println("✅ Aggregator" + i + " meets performance thresholds"); - } - } - - @And("trust-base.json is set") - public void trustBaseIsSet() throws IOException { - // Write code here that turns the phrase above into concrete actions - context.setTrustBase(UnicityObjectMapper.JSON.readValue( - getClass().getResourceAsStream("/trust-base.json"), - RootTrustBase.class - ) - ); - assertNotNull(context.getTrustBase(), "trust-base.json must be set"); - } -} \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/e2e/steps/shared/StepHelper.java b/src/test/java/org/unicitylabs/sdk/e2e/steps/shared/StepHelper.java deleted file mode 100644 index 09c5c9b..0000000 --- a/src/test/java/org/unicitylabs/sdk/e2e/steps/shared/StepHelper.java +++ /dev/null @@ -1,487 +0,0 @@ -package org.unicitylabs.sdk.e2e.steps.shared; - -import org.unicitylabs.sdk.address.Address; -import org.unicitylabs.sdk.address.DirectAddress; -import org.unicitylabs.sdk.address.ProxyAddress; -import org.unicitylabs.sdk.api.AggregatorClient; -import org.unicitylabs.sdk.api.SubmitCommitmentResponse; -import org.unicitylabs.sdk.api.SubmitCommitmentStatus; -import org.unicitylabs.sdk.e2e.config.CucumberConfiguration; -import org.unicitylabs.sdk.e2e.context.TestContext; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.predicate.embedded.MaskedPredicate; -import org.unicitylabs.sdk.predicate.Predicate; -import org.unicitylabs.sdk.predicate.embedded.UnmaskedPredicate; -import org.unicitylabs.sdk.predicate.embedded.UnmaskedPredicateReference; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.token.TokenState; -import org.unicitylabs.sdk.token.TokenType; -import org.unicitylabs.sdk.transaction.*; -import org.unicitylabs.sdk.utils.TestUtils; -import org.unicitylabs.sdk.utils.helpers.CommitmentResult; - -import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.concurrent.*; -import java.util.stream.Collectors; - -import static org.unicitylabs.sdk.util.InclusionProofUtils.waitInclusionProof; -import static org.unicitylabs.sdk.utils.TestUtils.randomBytes; - - -public class StepHelper { - - private final TestContext context; - - public StepHelper() { // ✅ Public zero-argument constructor - this.context = CucumberConfiguration.getTestContext(); - } - - public Token createNameTagTokenForUser(String username, Token token, String nameTagIdentifier, String nametagData) throws Exception { - byte[] nametagNonce = TestUtils.generateRandomBytes(32); - - TokenType nametagTokenType = TestUtils.generateRandomTokenType(); - - MaskedPredicate nametagPredicate = MaskedPredicate.create( - TokenId.fromNameTag(nameTagIdentifier),//person name actually should go in here from contacts (unique thing from contacts) - nametagTokenType, - SigningService.createFromMaskedSecret(context.getUserSecret().get(username), nametagNonce), - HashAlgorithm.SHA256, - nametagNonce - ); - - DirectAddress nametagAddress = nametagPredicate.getReference().toAddress(); - - DirectAddress userAddress = UnmaskedPredicateReference.create( - token.getType(), - SigningService.createFromSecret(context.getUserSecret().get(username)), - HashAlgorithm.SHA256 - ).toAddress(); - - var nametagMintCommitment = MintCommitment.create( - new MintTransaction.NametagData( - nameTagIdentifier, - nametagTokenType, - nametagAddress, - TestUtils.generateRandomBytes(32), - userAddress - ) - ); - - SubmitCommitmentResponse response = context.getClient().submitCommitment(nametagMintCommitment).get(); - if (response.getStatus() != SubmitCommitmentStatus.SUCCESS) { - throw new Exception("Failed to submit nametag mint commitment: " + response.getStatus()); - } - - InclusionProof inclusionProof = waitInclusionProof( - context.getClient(), - context.getTrustBase(), - nametagMintCommitment - ).get(); - MintTransaction nametagGenesis = nametagMintCommitment.toTransaction(inclusionProof); - - return Token.create( - context.getTrustBase(), - new TokenState(nametagPredicate, null), - nametagGenesis - ); - } - - public void transferToken(String fromUser, String toUser, Token token, Address toAddress, String customData) throws Exception { - SigningService fromSigningService = context.getUserSigningServices().get(fromUser); - - // Create data hash and state data if custom data provided - DataHash dataHash = null; - byte[] stateData = null; - if (customData != null && !customData.isEmpty()) { - stateData = customData.getBytes(StandardCharsets.UTF_8); - dataHash = TestUtils.hashData(stateData); - } - - // Submit transfer commitment - TransferCommitment transferCommitment = TransferCommitment.create( - token, - toAddress, - randomBytes(32), - dataHash, - null, - fromSigningService - ); - - SubmitCommitmentResponse response = context.getClient().submitCommitment(transferCommitment).get(); - if (response.getStatus() != SubmitCommitmentStatus.SUCCESS) { - throw new Exception("Failed to submit transfer commitment: " + response.getStatus()); - } - - // Wait for inclusion proof - InclusionProof inclusionProof = waitInclusionProof( - context.getClient(), - context.getTrustBase(), - transferCommitment - ).get(); - TransferTransaction transferTransaction = transferCommitment.toTransaction( - inclusionProof - ); - - context.savePendingTransfer(toUser, token, transferTransaction); - } - - public void finalizeTransfer(String username, Token token, TransferTransaction tx) throws Exception { - - byte[] secret = context.getUserSecret().get(username); - - Token currentNameTagToken = context.getNameTagToken(username); - List nametagTokens = context.getNameTagTokens().get(username); - if (nametagTokens != null && !nametagTokens.isEmpty()) { - for (Token t : nametagTokens) { - String actualNametagAddress = tx.getData().getRecipient().getAddress(); - String expectedProxyAddress = ProxyAddress.create(t.getId()).getAddress(); - - if (actualNametagAddress.equalsIgnoreCase(expectedProxyAddress)) { - currentNameTagToken = t; - break; - } - } - } - - List> additionalTokens = new ArrayList<>(); - if (currentNameTagToken != null) { - additionalTokens.add(currentNameTagToken); - } - - Predicate unlockPredicate = context.getUserPredicate().get(username); - if (unlockPredicate == null){ - context.getUserSigningServices().put(username, SigningService.createFromSecret(secret)); - unlockPredicate = UnmaskedPredicate.create( - token.getId(), - token.getType(), - context.getUserSigningServices().get(username), - HashAlgorithm.SHA256, - tx.getData().getSalt() - ); - } - - TokenState recipientState = new TokenState( - unlockPredicate, - null - ); - - Token finalizedToken = context.getClient().finalizeTransaction( - context.getTrustBase(), - token, - recipientState, - tx, - additionalTokens - ); - - context.addUserToken(username, finalizedToken); - } - - public boolean submitSingleCommitment() { - try { - byte[] randomSecret = TestUtils.generateRandomBytes(32); - byte[] stateBytes = TestUtils.generateRandomBytes(32); - byte[] txData = TestUtils.generateRandomBytes(32); - - DataHash stateHash = TestUtils.hashData(stateBytes); - DataHash txDataHash = TestUtils.hashData(txData); - SigningService signingService = SigningService.createFromSecret(randomSecret); - var requestId = TestUtils.createRequestId(signingService, stateHash); - var authenticator = TestUtils.createAuthenticator(signingService, txDataHash, stateHash); - - SubmitCommitmentResponse response = context.getAggregatorClient() - .submitCommitment(requestId, txDataHash, authenticator).get(); - return response.getStatus() == SubmitCommitmentStatus.SUCCESS; - } catch (Exception e) { - return false; - } - } - - public void verifyAllInclusionProofsInParallel(int timeoutSeconds) - throws InterruptedException { - List results = collectCommitmentResults(); - ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); - CountDownLatch latch = new CountDownLatch(results.size()); - - long startAll = System.nanoTime(); - long globalTimeout = startAll + TimeUnit.SECONDS.toNanos(timeoutSeconds); - - for (CommitmentResult result : results) { - executor.submit(() -> { - long inclStart = System.nanoTime(); - boolean verified = false; - String errorMessage = "Global timeout reached"; - - try { - while (System.nanoTime() < globalTimeout && !verified) { - try { - InclusionProof proof = context.getAggregatorClient() - .getInclusionProof(result.getRequestId()) - .get(calculateRemainingTimeout(globalTimeout), TimeUnit.MILLISECONDS).getInclusionProof(); - - if (proof != null && proof.verify(result.getRequestId(), context.getTrustBase()) - == InclusionProofVerificationStatus.OK) { - result.markVerified(inclStart, System.nanoTime()); - verified = true; - } else { - InclusionProofVerificationStatus status = proof.verify(result.getRequestId(), context.getTrustBase()); - errorMessage = status.toString(); - Thread.sleep(1000); // Небольшая пауза перед повторной попыткой - } - } catch (TimeoutException e) { - errorMessage = "Individual operation timeout: " + e.getMessage(); - } catch (ExecutionException e) { - errorMessage = "Execution error: " + e.getMessage(); - Thread.sleep(1000); // Пауза перед повторной попыткой - } - } - - if (!verified) { - result.markFailedVerification(inclStart, System.nanoTime(), errorMessage); - } - - } catch (Exception e) { - result.markFailedVerification(inclStart, System.nanoTime(), - "Unexpected error: " + e.getMessage()); - } finally { - latch.countDown(); - } - }); - } - - // Wait for all tasks to complete or timeout - boolean finished = latch.await(timeoutSeconds, TimeUnit.SECONDS); - executor.shutdownNow(); - - long endAll = System.nanoTime(); - System.out.println("All inclusion proofs completed in: " + ((endAll - startAll) / 1_000_000) + " ms"); - - if (!finished) { - System.err.println("Timeout reached before all inclusion proofs were verified"); - } - } - - private long calculateRemainingTimeout(long globalTimeoutNanos) { - long remaining = globalTimeoutNanos - System.nanoTime(); - return TimeUnit.NANOSECONDS.toMillis(Math.max(remaining, 100)); // Минимум 100мс - } - - public List collectCommitmentResults() { - return context.getCommitmentFutures().stream() - .map(f -> { - try { - return f.get(); // wait for completion - } catch (Exception e) { - e.printStackTrace(); - return null; - } - }) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - } - - // Helper method to extract aggregator info from username - public String extractAggregatorFromUserName(String userName) { - if (userName.contains("-Aggregator")) { - return userName.substring(userName.indexOf("-Aggregator")); - } - return "Unknown-Aggregator"; - } - - // Updated helper method for your existing CommitmentResult class - public void verifyAllInclusionProofsInParallelForMultipleAggregators(int timeoutSeconds, List aggregatorClients) throws Exception { - List results = collectCommitmentResults(); - - // Group results by aggregator - Map> resultsByAggregator = results.stream() - .collect(Collectors.groupingBy(r -> extractAggregatorFromUserName(r.getUserName()))); - - ExecutorService executor = Executors.newFixedThreadPool(aggregatorClients.size()); - List> verificationFutures = new ArrayList<>(); - - for (int i = 0; i < aggregatorClients.size(); i++) { - AggregatorClient aggregatorClient = aggregatorClients.get(i); - String aggregatorId = "Aggregator" + i; - List aggregatorResults = resultsByAggregator.getOrDefault("-" + aggregatorId, new ArrayList<>()); - - CompletableFuture future = CompletableFuture.runAsync(() -> { - try { - verifyInclusionProofsForAggregator(aggregatorClient, aggregatorResults, timeoutSeconds); - } catch (Exception e) { - throw new RuntimeException("Failed to verify inclusion proofs for " + aggregatorId, e); - } - }, executor); - - verificationFutures.add(future); - } - - try { - CompletableFuture.allOf(verificationFutures.toArray(new CompletableFuture[0])) - .get(timeoutSeconds + 10, TimeUnit.SECONDS); // Add buffer time for processing - } finally { - executor.shutdown(); - } - } - - private void verifyInclusionProofsForAggregator(AggregatorClient aggregatorClient, - List results, - int timeoutSeconds) throws Exception { - long globalStartTime = System.currentTimeMillis(); - long timeoutMillis = timeoutSeconds * 1000L; - - for (CommitmentResult result : results) { - long inclusionStartTime = System.nanoTime(); - - if (!result.isSuccess()) { - long inclusionEndTime = System.nanoTime(); - result.markFailedVerification(inclusionStartTime, inclusionEndTime, "Commitment submission failed"); - continue; - } - - boolean verified = false; - String statusMessage = "Timeout waiting for inclusion proof"; - - // Poll for inclusion proof with timeout - while (System.currentTimeMillis() - globalStartTime < timeoutMillis) { - try { - // Check if inclusion proof is available - InclusionProof proofResponse = aggregatorClient - .getInclusionProof(result.getRequestId()).get(5, TimeUnit.SECONDS).getInclusionProof(); - if (proofResponse != null && proofResponse.verify(result.getRequestId(), context.getTrustBase()) - == InclusionProofVerificationStatus.OK) { - System.out.println("InclusionProofVerificationStatus.OK"); - result.markVerified(inclusionStartTime, System.nanoTime()); - verified = true; - break; - } else { - InclusionProofVerificationStatus status = proofResponse.verify(result.getRequestId(), context.getTrustBase()); - System.out.println(status.toString()); - statusMessage = status.toString(); - } - Thread.sleep(1000); - } catch (TimeoutException e) { - // Continue polling - statusMessage = "Timeout during proof retrieval"; - } catch (Exception e) { - statusMessage = "Error retrieving proof: " + e.getMessage(); - break; - } - } - - long inclusionEndTime = System.nanoTime(); - - // Use your existing methods to mark verification result - if (verified) { - result.markVerified(inclusionStartTime, inclusionEndTime); - } else { - result.markFailedVerification(inclusionStartTime, inclusionEndTime, statusMessage); - } - } - } - - // Method to print detailed results by aggregator - public void printDetailedResultsByAggregator(List results, int aggregatorCount) { - System.out.println("\n=== Detailed Results by Aggregator ==="); - - Map> resultsByAggregator = results.stream() - .collect(Collectors.groupingBy(r -> extractAggregatorFromUserName(r.getUserName()))); - - for (int i = 0; i < aggregatorCount; i++) { - String aggregatorId = "-Aggregator" + i; - List aggregatorResults = resultsByAggregator.getOrDefault(aggregatorId, new ArrayList<>()); - - long verifiedCount = aggregatorResults.stream() - .filter(CommitmentResult::isVerified) - .count(); - - double successRate = aggregatorResults.isEmpty() ? 0 : - (double) verifiedCount / aggregatorResults.size() * 100; - - // Calculate average inclusion proof time for verified commitments - OptionalDouble avgInclusionTime = aggregatorResults.stream() - .filter(CommitmentResult::isVerified) - .mapToDouble(CommitmentResult::getInclusionDurationMillis) - .average(); - - System.out.println("Aggregator" + i + " (localhost:" + (3000 + i * 5080) + "):"); - System.out.println(" Total commitments: " + aggregatorResults.size()); - System.out.println(" Verified: " + verifiedCount + " / " + aggregatorResults.size()); - System.out.println(" Success rate: " + String.format("%.2f%%", successRate)); - - if (avgInclusionTime.isPresent()) { - System.out.println(" Average inclusion time: " + String.format("%.2f ms", avgInclusionTime.getAsDouble())); - } - - // Print failed verifications - List failed = aggregatorResults.stream() - .filter(r -> !r.isVerified()) - .collect(Collectors.toList()); - - if (!failed.isEmpty()) { - System.out.println(" Failed verifications (" + failed.size() + "):"); - failed.forEach(r -> System.out.println(" ❌ " + r.getRequestId() + - " - " + (r.getStatus() != null ? r.getStatus() : "Unknown error"))); - } else { - System.out.println(" ✅ All commitments verified successfully!"); - } - - System.out.println(); - } - } - - public void printPerformanceComparison(List results, int aggregatorCount) { - Map> resultsByAggregator = results.stream() - .collect(Collectors.groupingBy(r -> extractAggregatorFromUserName(r.getUserName()))); - - System.out.println("=== 🏆 PERFORMANCE WINNER ANALYSIS ==="); - - // Find best success rate - double bestSuccessRate = 0; - String bestSuccessAggregator = ""; - - // Find fastest average inclusion time - double fastestAvgTime = Double.MAX_VALUE; - String fastestAggregator = ""; - - for (int i = 0; i < aggregatorCount; i++) { - String aggregatorId = "-Aggregator" + i; - List aggregatorResults = resultsByAggregator.getOrDefault(aggregatorId, new ArrayList<>()); - - if (aggregatorResults.isEmpty()) continue; - - long verifiedCount = aggregatorResults.stream() - .filter(CommitmentResult::isVerified) - .count(); - - double successRate = (double) verifiedCount / aggregatorResults.size() * 100; - - if (successRate > bestSuccessRate) { - bestSuccessRate = successRate; - bestSuccessAggregator = "Aggregator" + i; - } - - OptionalDouble avgInclusionTime = aggregatorResults.stream() - .filter(CommitmentResult::isVerified) - .mapToDouble(CommitmentResult::getInclusionDurationMillis) - .average(); - - if (avgInclusionTime.isPresent() && avgInclusionTime.getAsDouble() < fastestAvgTime) { - fastestAvgTime = avgInclusionTime.getAsDouble(); - fastestAggregator = "Aggregator" + i; - } - } - - System.out.println("🥇 Highest Success Rate: " + bestSuccessAggregator + - " (" + String.format("%.2f%%", bestSuccessRate) + ")"); - - if (fastestAvgTime != Double.MAX_VALUE) { - System.out.println("⚡ Fastest Inclusion Time: " + fastestAggregator + - " (" + String.format("%.2f ms", fastestAvgTime) + ")"); - } - - System.out.println("=====================================\n"); - } -} \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/functional/FunctionalCommonFlowTest.java b/src/test/java/org/unicitylabs/sdk/functional/FunctionalCommonFlowTest.java index 8a1b9fc..89b3df9 100644 --- a/src/test/java/org/unicitylabs/sdk/functional/FunctionalCommonFlowTest.java +++ b/src/test/java/org/unicitylabs/sdk/functional/FunctionalCommonFlowTest.java @@ -4,15 +4,17 @@ import org.unicitylabs.sdk.StateTransitionClient; import org.unicitylabs.sdk.TestAggregatorClient; import org.unicitylabs.sdk.common.CommonTestFlow; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.bft.RootTrustBaseUtils; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifierService; public class FunctionalCommonFlowTest extends CommonTestFlow { @BeforeEach void setUp() { - SigningService signingService = new SigningService(SigningService.generatePrivateKey()); - this.client = new StateTransitionClient(new TestAggregatorClient(signingService)); - this.trustBase = RootTrustBaseUtils.generateRootTrustBase(signingService.getPublicKey()); + TestAggregatorClient aggregatorClient = TestAggregatorClient.create(); + this.client = new StateTransitionClient(aggregatorClient); + this.trustBase = aggregatorClient.getTrustBase(); + this.predicateVerifier = PredicateVerifierService.create(); + this.mintJustificationVerifier = new MintJustificationVerifierService(); } } \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/functional/FunctionalEscrowSwapTest.java b/src/test/java/org/unicitylabs/sdk/functional/FunctionalEscrowSwapTest.java deleted file mode 100644 index 4940240..0000000 --- a/src/test/java/org/unicitylabs/sdk/functional/FunctionalEscrowSwapTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.unicitylabs.sdk.functional; - -import org.junit.jupiter.api.BeforeEach; -import org.unicitylabs.sdk.StateTransitionClient; -import org.unicitylabs.sdk.TestAggregatorClient; -import org.unicitylabs.sdk.common.BaseEscrowSwapTest; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.bft.RootTrustBaseUtils; - -public class FunctionalEscrowSwapTest extends BaseEscrowSwapTest { - @BeforeEach - void setUp() { - SigningService signingService = new SigningService(SigningService.generatePrivateKey()); - this.client = new StateTransitionClient(new TestAggregatorClient(signingService)); - this.trustBase = RootTrustBaseUtils.generateRootTrustBase(signingService.getPublicKey()); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/functional/FunctionalTokenSplitTest.java b/src/test/java/org/unicitylabs/sdk/functional/FunctionalTokenSplitTest.java deleted file mode 100644 index 0d4c7e8..0000000 --- a/src/test/java/org/unicitylabs/sdk/functional/FunctionalTokenSplitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.unicitylabs.sdk.functional; - -import org.unicitylabs.sdk.StateTransitionClient; -import org.unicitylabs.sdk.TestAggregatorClient; -import org.unicitylabs.sdk.common.split.BaseTokenSplitTest; -import org.junit.jupiter.api.BeforeEach; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.bft.RootTrustBaseUtils; - -public class FunctionalTokenSplitTest extends BaseTokenSplitTest { - @BeforeEach - void setUp() { - SigningService signingService = new SigningService(SigningService.generatePrivateKey()); - this.client = new StateTransitionClient(new TestAggregatorClient(signingService)); - this.trustBase = RootTrustBaseUtils.generateRootTrustBase(signingService.getPublicKey()); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/functional/FunctionalUnsignedPredicateDoubleSpendPreventionTest.java b/src/test/java/org/unicitylabs/sdk/functional/FunctionalUnsignedPredicateDoubleSpendPreventionTest.java deleted file mode 100644 index 3d2cee9..0000000 --- a/src/test/java/org/unicitylabs/sdk/functional/FunctionalUnsignedPredicateDoubleSpendPreventionTest.java +++ /dev/null @@ -1,139 +0,0 @@ -package org.unicitylabs.sdk.functional; - -import static org.unicitylabs.sdk.utils.TestUtils.randomBytes; - -import java.nio.charset.StandardCharsets; -import java.util.List; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.unicitylabs.sdk.StateTransitionClient; -import org.unicitylabs.sdk.TestAggregatorClient; -import org.unicitylabs.sdk.address.Address; -import org.unicitylabs.sdk.api.SubmitCommitmentResponse; -import org.unicitylabs.sdk.api.SubmitCommitmentStatus; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.mtree.BranchExistsException; -import org.unicitylabs.sdk.predicate.embedded.MaskedPredicate; -import org.unicitylabs.sdk.predicate.embedded.UnmaskedPredicate; -import org.unicitylabs.sdk.predicate.embedded.UnmaskedPredicateReference; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.token.TokenState; -import org.unicitylabs.sdk.token.TokenType; -import org.unicitylabs.sdk.transaction.TransferCommitment; -import org.unicitylabs.sdk.transaction.TransferTransaction; -import org.unicitylabs.sdk.util.HexConverter; -import org.unicitylabs.sdk.util.InclusionProofUtils; -import org.unicitylabs.sdk.bft.RootTrustBaseUtils; -import org.unicitylabs.sdk.utils.TokenUtils; - -public class FunctionalUnsignedPredicateDoubleSpendPreventionTest { - - protected StateTransitionClient client; - protected RootTrustBase trustBase; - - private final byte[] BOB_SECRET = "BOB_SECRET".getBytes(StandardCharsets.UTF_8); - - private String[] transferToken(Token token, byte[] secret, Address address) throws Exception { - TransferCommitment commitment = TransferCommitment.create( - token, - address, - randomBytes(32), - null, - null, - SigningService.createFromMaskedSecret( - secret, - ((MaskedPredicate) token.getState().getPredicate()).getNonce() - ) - ); - - SubmitCommitmentResponse response = this.client.submitCommitment(commitment).get(); - if (response.getStatus() != SubmitCommitmentStatus.SUCCESS) { - throw new RuntimeException("Failed to submit transfer commitment: " + response); - } - - return new String[]{ - token.toJson(), - commitment.toTransaction( - InclusionProofUtils.waitInclusionProof(this.client, this.trustBase, commitment) - .get() - ).toJson() - }; - } - - private Token mintToken(byte[] secret) throws Exception { - return TokenUtils.mintToken( - this.client, - this.trustBase, - secret, - new TokenId(randomBytes(32)), - new TokenType(HexConverter.decode( - "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509")), - randomBytes(32), - null, - randomBytes(32), - randomBytes(32), - null - ); - } - - private Token receiveToken(String[] tokenInfo, byte[] secret) throws Exception { - Token token = Token.fromJson(tokenInfo[0]); - TransferTransaction transaction = TransferTransaction.fromJson(tokenInfo[1]); - - TokenState state = new TokenState( - UnmaskedPredicate.create( - token.getId(), - token.getType(), - SigningService.createFromSecret(secret), - HashAlgorithm.SHA256, - transaction.getData().getSalt() - ), - null - ); - - return this.client.finalizeTransaction( - this.trustBase, - token, - state, - transaction, - List.of() - ); - } - - @BeforeEach - void setUp() { - SigningService signingService = new SigningService(SigningService.generatePrivateKey()); - this.client = new StateTransitionClient(new TestAggregatorClient(signingService)); - this.trustBase = RootTrustBaseUtils.generateRootTrustBase(signingService.getPublicKey()); - } - - @Test - void testDoubleSpend() throws Exception { - Token token = mintToken(BOB_SECRET); - - UnmaskedPredicateReference reference = UnmaskedPredicateReference.create( - token.getType(), - SigningService.createFromSecret(BOB_SECRET), - HashAlgorithm.SHA256 - ); - - Assertions.assertTrue( - receiveToken( - transferToken(token, BOB_SECRET, reference.toAddress()), - BOB_SECRET - ).verify(trustBase).isSuccessful()); - RuntimeException ex = Assertions.assertThrows( - RuntimeException.class, - () -> receiveToken( - transferToken(token, BOB_SECRET, reference.toAddress()), - BOB_SECRET - ).verify(trustBase) - ); - - Assertions.assertInstanceOf(BranchExistsException.class, ex.getCause()); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java new file mode 100644 index 0000000..bad56fb --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java @@ -0,0 +1,105 @@ +package org.unicitylabs.sdk.functional.payment; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.unicitylabs.sdk.StateTransitionClient; +import org.unicitylabs.sdk.TestAggregatorClient; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.payment.SplitMintJustification; +import org.unicitylabs.sdk.payment.SplitMintJustificationVerifier; +import org.unicitylabs.sdk.payment.SplitResult; +import org.unicitylabs.sdk.payment.TokenSplit; +import org.unicitylabs.sdk.payment.asset.Asset; +import org.unicitylabs.sdk.payment.asset.AssetId; +import org.unicitylabs.sdk.predicate.builtin.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; +import org.unicitylabs.sdk.transaction.TokenType; +import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifierService; +import org.unicitylabs.sdk.util.verification.VerificationStatus; +import org.unicitylabs.sdk.utils.TokenUtils; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * End-to-end functional test for the token split flow: mint a source token, split it, burn the + * source, mint the split output token with the resulting justification, and verify the split + * output through {@link Token#verify}. + */ +public class SplitBuilderTest { + + @Test + public void buildAndVerifySplitToken() throws Exception { + TestAggregatorClient aggregatorClient = TestAggregatorClient.create(); + RootTrustBase trustBase = aggregatorClient.getTrustBase(); + StateTransitionClient client = new StateTransitionClient(aggregatorClient); + PredicateVerifierService predicateVerifier = PredicateVerifierService.create(); + + MintJustificationVerifierService mintJustificationVerifier = new MintJustificationVerifierService(); + mintJustificationVerifier.register(new SplitMintJustificationVerifier( + trustBase, predicateVerifier, TestPaymentData::decode)); + + SigningService signingService = SigningService.generate(); + SignaturePredicate ownerPredicate = SignaturePredicate.fromSigningService(signingService); + + Set assets = Set.of( + new Asset(new AssetId("ASSET_1".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)), + new Asset(new AssetId("ASSET_2".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)) + ); + + Token sourceToken = TokenUtils.mintToken( + client, + trustBase, + predicateVerifier, + mintJustificationVerifier, + ownerPredicate, + null, + new TestPaymentData(assets).encode() + ); + + TokenId outputTokenId = TokenId.generate(); + SplitResult split = TokenSplit.split( + sourceToken, + TestPaymentData::decode, + Map.of(outputTokenId, assets) + ); + + Token burnToken = TokenUtils.transferToken( + client, + trustBase, + predicateVerifier, + sourceToken, + split.getBurnTransaction(), + SignaturePredicateUnlockScript.create(split.getBurnTransaction(), signingService) + ); + + SplitMintJustification justification = SplitMintJustification.create( + burnToken, + new LinkedHashSet<>(split.getProofs().get(outputTokenId)) + ); + + Token splitToken = TokenUtils.mintToken( + client, + trustBase, + predicateVerifier, + mintJustificationVerifier, + outputTokenId, + TokenType.generate(), + ownerPredicate, + justification.toCbor(), + new TestPaymentData(assets).encode() + ); + + Assertions.assertEquals( + VerificationStatus.OK, + splitToken.verify(trustBase, predicateVerifier, mintJustificationVerifier).getStatus() + ); + } +} diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/SplitMintJustificationVerifierTest.java b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitMintJustificationVerifierTest.java new file mode 100644 index 0000000..1cae00d --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitMintJustificationVerifierTest.java @@ -0,0 +1,474 @@ +package org.unicitylabs.sdk.functional.payment; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.unicitylabs.sdk.StateTransitionClient; +import org.unicitylabs.sdk.TestAggregatorClient; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.payment.PaymentData; +import org.unicitylabs.sdk.payment.SplitMintJustification; +import org.unicitylabs.sdk.payment.SplitMintJustificationVerifier; +import org.unicitylabs.sdk.payment.SplitAssetProof; +import org.unicitylabs.sdk.payment.SplitResult; +import org.unicitylabs.sdk.payment.TokenSplit; +import org.unicitylabs.sdk.payment.asset.Asset; +import org.unicitylabs.sdk.payment.asset.AssetId; +import org.unicitylabs.sdk.predicate.builtin.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; +import org.unicitylabs.sdk.smt.plain.SparseMerkleTree; +import org.unicitylabs.sdk.smt.plain.SparseMerkleTreeRootNode; +import org.unicitylabs.sdk.smt.sum.SparseMerkleSumTree; +import org.unicitylabs.sdk.smt.sum.SparseMerkleSumTreeRootNode; +import org.unicitylabs.sdk.transaction.Token; +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.transaction.TokenType; +import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifierService; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; +import org.unicitylabs.sdk.utils.TokenUtils; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Unit tests for the failure branches of {@link SplitMintJustificationVerifier}. Each test drives + * one specific reject path inside the verifier by handing it a corrupted or mismatched fixture. + * The verifier is invoked directly; integration through the dispatcher and {@link Token#verify} + * is covered by {@link SplitBuilderTest}. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class SplitMintJustificationVerifierTest { + + private RootTrustBase trustBase; + private PredicateVerifierService predicateVerifier; + private MintJustificationVerifierService mintJustificationVerifier; + private SplitMintJustificationVerifier splitMintJustificationVerifier; + private Asset asset1; + private Asset asset2; + private Token splitToken; + private SplitMintJustification splitJustification; + + @BeforeAll + public void setupFixture() throws Exception { + TestAggregatorClient aggregatorClient = TestAggregatorClient.create(); + this.trustBase = aggregatorClient.getTrustBase(); + + StateTransitionClient client = new StateTransitionClient(aggregatorClient); + this.predicateVerifier = PredicateVerifierService.create(); + + this.splitMintJustificationVerifier = new SplitMintJustificationVerifier( + this.trustBase, this.predicateVerifier, TestPaymentData::decode); + this.mintJustificationVerifier = new MintJustificationVerifierService(); + this.mintJustificationVerifier.register(this.splitMintJustificationVerifier); + + SigningService signingService = SigningService.generate(); + 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)); + + Set assets = Set.of(this.asset1, this.asset2); + + Token sourceToken = TokenUtils.mintToken( + client, + this.trustBase, + this.predicateVerifier, + this.mintJustificationVerifier, + ownerPredicate, + null, + new TestPaymentData(assets).encode() + ); + + TokenId outputTokenId = TokenId.generate(); + SplitResult split = TokenSplit.split( + sourceToken, + TestPaymentData::decode, + Map.of(outputTokenId, assets) + ); + + Token burnToken = TokenUtils.transferToken( + client, + this.trustBase, + this.predicateVerifier, + sourceToken, + split.getBurnTransaction(), + SignaturePredicateUnlockScript.create(split.getBurnTransaction(), signingService) + ); + + this.splitJustification = SplitMintJustification.create( + burnToken, + new LinkedHashSet<>(split.getProofs().get(outputTokenId)) + ); + + this.splitToken = TokenUtils.mintToken( + client, + this.trustBase, + this.predicateVerifier, + this.mintJustificationVerifier, + outputTokenId, + TokenType.generate(), + ownerPredicate, + this.splitJustification.toCbor(), + new TestPaymentData(assets).encode() + ); + } + + @Test + public void verifyFailsWhenTransactionIsNull() { + assertNpe("transaction cannot be null", + () -> this.splitMintJustificationVerifier.verify(null, this.mintJustificationVerifier)); + } + + @Test + public void verifyFailsWhenDeserializerIsNull() { + assertNpe("decodePaymentData cannot be null", + () -> new SplitMintJustificationVerifier(this.trustBase, this.predicateVerifier, null)); + } + + @Test + public void verifyFailsWhenTrustBaseIsNull() { + assertNpe("trustBase cannot be null", + () -> new SplitMintJustificationVerifier(null, this.predicateVerifier, TestPaymentData::decode)); + } + + @Test + public void verifyFailsWhenPredicateVerifierIsNull() { + assertNpe("predicateVerifier cannot be null", + () -> new SplitMintJustificationVerifier(this.trustBase, null, TestPaymentData::decode)); + } + + @Test + public void verifyFailsWhenJustificationIsMissing() { + VerificationResult result = verifyWith(null, originalDataBytes()); + assertFailWithMessage(result, "Transaction has no justification."); + } + + @Test + public void verifyFailsWhenAssetsAreMissing() { + VerificationResult result = verifyWithPaymentData( + this.splitJustification.toCbor(), paymentDataOf(null)); + assertFailWithMessage(result, "Assets data is missing."); + } + + @Test + public void verifyFailsWhenBurnTokenVerificationFails() { + byte[] corruptedJustification = corruptBurnTokenInJustification(this.splitJustification.toCbor()); + + VerificationResult result = verifyWith(corruptedJustification, originalDataBytes()); + assertFailWithMessage(result, "Burn token verification failed."); + Assertions.assertFalse(result.getResults().isEmpty()); + } + + @Test + public void verifyFailsWhenAssetAndProofCountsDiffer() { + byte[] data = new TestPaymentData(Set.of(this.asset1)).encode(); + + VerificationResult result = verifyWith(this.splitJustification.toCbor(), data); + assertFailWithMessage(result, "Total amount of assets differ in token and proofs."); + } + + @Test + public void verifyFailsWhenAssetEntryIsNull() { + Set invalidAssets = new NonUniqueAssetSet(Arrays.asList(null, this.asset1)); + + VerificationResult result = verifyWithPaymentData( + this.splitJustification.toCbor(), paymentDataOf(invalidAssets)); + assertFailWithMessage(result, "Asset data is missing."); + } + + @Test + public void verifyFailsWhenAssetIdsAreDuplicated() { + Asset duplicate = new Asset(this.asset1.getId(), this.asset1.getValue().add(BigInteger.ONE)); + Set duplicatedAssets = new NonUniqueAssetSet(List.of(this.asset1, duplicate)); + + VerificationResult result = verifyWithPaymentData( + this.splitJustification.toCbor(), paymentDataOf(duplicatedAssets)); + assertFailWithMessage(result, + String.format("Duplicate asset id %s found in asset data.", this.asset1.getId())); + } + + @Test + public void verifyFailsWhenAggregationPathVerificationFails() throws Exception { + List proofs = new ArrayList<>(this.splitJustification.getProofs()); + SplitAssetProof proof = proofs.get(0); + SparseMerkleTreeRootNode aggregationRoot = new SparseMerkleTree(HashAlgorithm.SHA256).calculateRoot(); + + proofs.set( + 0, + SplitAssetProof.create( + proof.getAssetId(), + aggregationRoot.getPath(proof.getAssetId().toBitString().toBigInteger()), + proof.getAssetTreePath() + ) + ); + + SplitMintJustification mutated = SplitMintJustification.create( + this.splitJustification.getToken(), new LinkedHashSet<>(proofs)); + + VerificationResult result = verifyWith(mutated.toCbor(), originalDataBytes()); + assertFailWithMessage(result, + String.format("Aggregation path verification failed for asset: %s", proof.getAssetId())); + } + + @Test + public void verifyFailsWhenAssetTreePathVerificationFails() throws Exception { + List proofs = new ArrayList<>(this.splitJustification.getProofs()); + SplitAssetProof proof = proofs.get(0); + + SparseMerkleSumTreeRootNode assetTreeRoot = new SparseMerkleSumTree(HashAlgorithm.SHA256).calculateRoot(); + + SplitAssetProof mutatedProof = SplitAssetProof.create( + proof.getAssetId(), + proof.getAggregationPath(), + assetTreeRoot.getPath(this.splitToken.getId().toBitString().toBigInteger()) + ); + proofs.set(0, mutatedProof); + + SplitMintJustification mutated = SplitMintJustification.create( + this.splitJustification.getToken(), new LinkedHashSet<>(proofs)); + + VerificationResult result = verifyWith(mutated.toCbor(), originalDataBytes()); + assertFailWithMessage(result, + String.format("Asset tree path verification failed for token: %s", this.splitToken.getId())); + } + + @Test + public void verifyFailsWhenProofsUseDifferentAssetTrees() throws Exception { + List proofs = new ArrayList<>( + SplitMintJustification.fromCbor(this.splitJustification.toCbor()).getProofs()); + SplitAssetProof lastProof = proofs.get(proofs.size() - 1); + + SparseMerkleTree aggregationTree = new SparseMerkleTree(HashAlgorithm.SHA256); + aggregationTree.addLeaf( + lastProof.getAssetId().toBitString().toBigInteger(), + lastProof.getAssetTreePath().getRootHash().getImprint() + ); + SparseMerkleTreeRootNode otherAggregationRoot = aggregationTree.calculateRoot(); + + proofs.set(proofs.size() - 1, SplitAssetProof.create( + lastProof.getAssetId(), + otherAggregationRoot.getPath(lastProof.getAssetId().toBitString().toBigInteger()), + lastProof.getAssetTreePath() + )); + + SplitMintJustification mutated = SplitMintJustification.create( + this.splitJustification.getToken(), new LinkedHashSet<>(proofs)); + + VerificationResult result = verifyWith(mutated.toCbor(), originalDataBytes()); + assertFailWithMessage(result, "Current proof is not derived from the same asset tree as other proofs."); + } + + @Test + public void verifyFailsWhenAssetTreeRootDoesNotMatchAggregationLeaf() throws Exception { + List proofs = new ArrayList<>(this.splitJustification.getProofs()); + SplitAssetProof proof = proofs.get(0); + + SparseMerkleSumTree assetTree = new SparseMerkleSumTree(HashAlgorithm.SHA256); + assetTree.addLeaf( + this.splitToken.getId().toBitString().toBigInteger(), + new SparseMerkleSumTree.LeafValue( + proof.getAssetId().getBytes(), + proof.getAssetTreePath().getSteps().get(0).getValue().add(BigInteger.ONE) + ) + ); + + SplitAssetProof mutatedProof = SplitAssetProof.create( + proof.getAssetId(), + proof.getAggregationPath(), + assetTree.calculateRoot().getPath(this.splitToken.getId().toBitString().toBigInteger()) + ); + proofs.set(0, mutatedProof); + + SplitMintJustification mutated = SplitMintJustification.create( + this.splitJustification.getToken(), new LinkedHashSet<>(proofs)); + + VerificationResult result = verifyWith(mutated.toCbor(), originalDataBytes()); + assertFailWithMessage(result, "Asset tree root does not match aggregation path leaf."); + } + + @Test + public void verifyFailsWhenProofAssetIdIsMissingFromAssetData() { + List proofs = List.of(this.splitJustification.getProofs().get(0)); + PaymentData originalPaymentData = TestPaymentData.decode(originalDataBytes()); + Set assets = originalPaymentData.getAssets().stream() + .filter(asset -> !asset.getId().equals(proofs.get(0).getAssetId())) + .collect(Collectors.toSet()); + + SplitMintJustification mutated = SplitMintJustification.create( + this.splitJustification.getToken(), new LinkedHashSet<>(proofs)); + byte[] data = new TestPaymentData(assets).encode(); + + VerificationResult result = verifyWith(mutated.toCbor(), data); + assertFailWithMessage(result, + String.format("Asset id %s not found in asset data.", proofs.get(0).getAssetId())); + } + + @Test + public void verifyFailsWhenAssetAmountDoesNotMatchLeafAmount() { + PaymentData originalPaymentData = TestPaymentData.decode(originalDataBytes()); + List assets = new ArrayList<>(originalPaymentData.getAssets()); + Asset asset = assets.get(0); + Asset modified = new Asset(asset.getId(), asset.getValue().add(BigInteger.ONE)); + assets.set(0, modified); + + byte[] data = new TestPaymentData(Set.copyOf(assets)).encode(); + + VerificationResult result = verifyWith(this.splitJustification.toCbor(), data); + assertFailWithMessage(result, + String.format("Asset amount for asset id %s does not match asset tree leaf.", asset.getId())); + } + + @Test + public void verifyFailsWhenAggregationRootDoesNotMatchBurnPredicate() throws Exception { + List originalProofs = new ArrayList<>(this.splitJustification.getProofs()); + + SparseMerkleTree aggregationTree = new SparseMerkleTree(HashAlgorithm.SHA256); + for (SplitAssetProof proof : originalProofs) { + aggregationTree.addLeaf( + proof.getAssetId().toBitString().toBigInteger(), + proof.getAssetTreePath().getRootHash().getImprint() + ); + } + aggregationTree.addLeaf( + new BigInteger(1, "extra-leaf-marker".getBytes(StandardCharsets.UTF_8)), + new byte[]{0x01} + ); + SparseMerkleTreeRootNode aggregationRoot = aggregationTree.calculateRoot(); + + List mutatedProofs = new ArrayList<>(); + for (SplitAssetProof proof : originalProofs) { + mutatedProofs.add(SplitAssetProof.create( + proof.getAssetId(), + aggregationRoot.getPath(proof.getAssetId().toBitString().toBigInteger()), + proof.getAssetTreePath() + )); + } + + SplitMintJustification mutated = SplitMintJustification.create( + this.splitJustification.getToken(), new LinkedHashSet<>(mutatedProofs)); + + VerificationResult result = verifyWith(mutated.toCbor(), originalDataBytes()); + assertFailWithMessage(result, "Aggregation path root does not match burn predicate."); + } + + private byte[] originalDataBytes() { + return this.splitToken.getGenesis().getData().orElseThrow(); + } + + private VerificationResult verifyWith(byte[] justification, byte[] data) { + Token modified = withJustificationAndData(this.splitToken, justification, data); + return this.splitMintJustificationVerifier.verify(modified.getGenesis(), this.mintJustificationVerifier); + } + + private VerificationResult verifyWithPaymentData(byte[] justification, + PaymentData paymentData) { + Token modified = withJustificationAndData(this.splitToken, justification, originalDataBytes()); + SplitMintJustificationVerifier verifier = new SplitMintJustificationVerifier( + this.trustBase, this.predicateVerifier, ignored -> paymentData); + return verifier.verify(modified.getGenesis(), this.mintJustificationVerifier); + } + + private Token withJustificationAndData(Token token, byte[] justification, byte[] data) { + CborDeserializer.CborTag tokenTag = CborDeserializer.decodeTag(token.toCbor()); + List tokenData = CborDeserializer.decodeArray(tokenTag.getData()); + + List certifiedGenesis = CborDeserializer.decodeArray(tokenData.get(1)); + + CborDeserializer.CborTag mintTag = CborDeserializer.decodeTag(certifiedGenesis.get(0)); + List mint = CborDeserializer.decodeArray(mintTag.getData()); + + mint.set(4, CborSerializer.encodeNullable(justification, CborSerializer::encodeByteString)); + mint.set(5, CborSerializer.encodeNullable(data, CborSerializer::encodeByteString)); + + certifiedGenesis.set(0, CborSerializer.encodeTag(mintTag.getTag(), encodeArray(mint))); + tokenData.set(1, encodeArray(certifiedGenesis)); + + return Token.fromCbor(CborSerializer.encodeTag(tokenTag.getTag(), encodeArray(tokenData))); + } + + private byte[] corruptBurnTokenInJustification(byte[] justificationBytes) { + CborDeserializer.CborTag justificationTag = CborDeserializer.decodeTag(justificationBytes); + List reasonData = CborDeserializer.decodeArray(justificationTag.getData()); + + CborDeserializer.CborTag tokenTag = CborDeserializer.decodeTag(reasonData.get(0)); + List tokenData = CborDeserializer.decodeArray(tokenTag.getData()); + List transactions = CborDeserializer.decodeArray(tokenData.get(2)); + List certifiedTransfer = CborDeserializer.decodeArray(transactions.get(0)); + + CborDeserializer.CborTag transferTag = CborDeserializer.decodeTag(certifiedTransfer.get(0)); + List transfer = CborDeserializer.decodeArray(transferTag.getData()); + + byte[] differentStateMask = new byte[32]; + differentStateMask[0] = 1; + transfer.set(2, CborSerializer.encodeByteString(differentStateMask)); + + certifiedTransfer.set(0, CborSerializer.encodeTag(transferTag.getTag(), encodeArray(transfer))); + transactions.set(0, encodeArray(certifiedTransfer)); + tokenData.set(2, encodeArray(transactions)); + reasonData.set(0, CborSerializer.encodeTag(tokenTag.getTag(), encodeArray(tokenData))); + return CborSerializer.encodeTag(justificationTag.getTag(), encodeArray(reasonData)); + } + + private static PaymentData paymentDataOf(Set assets) { + return new PaymentData() { + @Override + public Set getAssets() { + return assets; + } + + @Override + public byte[] encode() { + return new byte[0]; + } + }; + } + + private void assertFailWithMessage(VerificationResult result, String message) { + Assertions.assertEquals(VerificationStatus.FAIL, result.getStatus()); + Assertions.assertEquals(message, result.getMessage()); + } + + private void assertNpe(String message, Runnable callback) { + NullPointerException error = Assertions.assertThrows(NullPointerException.class, callback::run); + Assertions.assertEquals(message, error.getMessage()); + } + + private byte[] encodeArray(List data) { + return CborSerializer.encodeArray(data.toArray(new byte[0][])); + } + + static final class NonUniqueAssetSet extends AbstractSet { + + private final List items; + + NonUniqueAssetSet(List items) { + this.items = new ArrayList<>(items); + } + + @Override + public Iterator iterator() { + return this.items.iterator(); + } + + @Override + public int size() { + return this.items.size(); + } + } +} diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/TestPaymentData.java b/src/test/java/org/unicitylabs/sdk/functional/payment/TestPaymentData.java new file mode 100644 index 0000000..2e670a7 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/TestPaymentData.java @@ -0,0 +1,40 @@ +package org.unicitylabs.sdk.functional.payment; + +import org.unicitylabs.sdk.payment.PaymentData; +import org.unicitylabs.sdk.payment.asset.Asset; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; + +import java.util.Set; +import java.util.stream.Collectors; + +public class TestPaymentData implements PaymentData { + + private final Set assets; + + public TestPaymentData(Set assets) { + this.assets = Set.copyOf(assets); + } + + @Override + public Set getAssets() { + return this.assets; + } + + public static TestPaymentData decode(byte[] bytes) { + Set assets = CborDeserializer.decodeArray(bytes).stream() + .map(Asset::fromCbor) + .collect(Collectors.toSet()); + + return new TestPaymentData(assets); + } + + @Override + public byte[] encode() { + return CborSerializer.encodeArray( + this.assets.stream() + .map(Asset::toCbor) + .toArray(byte[][]::new) + ); + } +} \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/TokenSplitTest.java b/src/test/java/org/unicitylabs/sdk/functional/payment/TokenSplitTest.java new file mode 100644 index 0000000..cf56c85 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/TokenSplitTest.java @@ -0,0 +1,111 @@ +package org.unicitylabs.sdk.functional.payment; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.unicitylabs.sdk.StateTransitionClient; +import org.unicitylabs.sdk.TestAggregatorClient; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.payment.SplitMintJustificationVerifier; +import org.unicitylabs.sdk.payment.TokenSplit; +import org.unicitylabs.sdk.payment.asset.Asset; +import org.unicitylabs.sdk.payment.asset.AssetId; +import org.unicitylabs.sdk.predicate.builtin.SignaturePredicate; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.transaction.Token; +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifierService; +import org.unicitylabs.sdk.utils.TokenUtils; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Set; + +/** + * Unit tests for the precondition (IAE) branches of {@link TokenSplit#split}. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class TokenSplitTest { + + private Asset asset1; + private Asset asset2; + private Token sourceToken; + + @BeforeAll + public void setupFixture() throws Exception { + TestAggregatorClient aggregatorClient = TestAggregatorClient.create(); + RootTrustBase trustBase = aggregatorClient.getTrustBase(); + StateTransitionClient client = new StateTransitionClient(aggregatorClient); + PredicateVerifierService predicateVerifier = PredicateVerifierService.create(); + + MintJustificationVerifierService mintJustificationVerifier = new MintJustificationVerifierService(); + mintJustificationVerifier.register(new SplitMintJustificationVerifier( + trustBase, predicateVerifier, TestPaymentData::decode)); + + SigningService signingService = SigningService.generate(); + 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)); + + this.sourceToken = TokenUtils.mintToken( + client, + trustBase, + predicateVerifier, + mintJustificationVerifier, + ownerPredicate, + null, + new TestPaymentData(Set.of(this.asset1, this.asset2)).encode() + ); + } + + @Test + public void splitFailsWhenAssetCountsDiffer() { + IllegalArgumentException exception = Assertions.assertThrows( + IllegalArgumentException.class, + () -> TokenSplit.split( + this.sourceToken, + TestPaymentData::decode, + Map.of(TokenId.generate(), Set.of(this.asset1)) + ) + ); + Assertions.assertEquals("Token and split tokens asset counts differ.", exception.getMessage()); + } + + @Test + public void splitFailsWhenAssetTreeAmountIsLess() { + IllegalArgumentException exception = Assertions.assertThrows( + IllegalArgumentException.class, + () -> TokenSplit.split( + this.sourceToken, + TestPaymentData::decode, + Map.of( + TokenId.generate(), + Set.of(this.asset1, new Asset(this.asset2.getId(), BigInteger.valueOf(400))) + ) + ) + ); + Assertions.assertEquals("Token contained 500 AssetId{bytes=41535345545f32} assets, but tree has 400", + exception.getMessage()); + } + + @Test + public void splitFailsWhenAssetTreeAmountIsMore() { + IllegalArgumentException exception = Assertions.assertThrows( + IllegalArgumentException.class, + () -> TokenSplit.split( + this.sourceToken, + TestPaymentData::decode, + Map.of( + TokenId.generate(), + Set.of(this.asset1, new Asset(this.asset2.getId(), BigInteger.valueOf(1500))) + ) + ) + ); + Assertions.assertEquals("Token contained 500 AssetId{bytes=41535345545f32} assets, but tree has 1500", + exception.getMessage()); + } +} diff --git a/src/test/java/org/unicitylabs/sdk/hash/DataHashTest.java b/src/test/java/org/unicitylabs/sdk/hash/DataHashTest.java index 2737000..31769ae 100644 --- a/src/test/java/org/unicitylabs/sdk/hash/DataHashTest.java +++ b/src/test/java/org/unicitylabs/sdk/hash/DataHashTest.java @@ -1,39 +1,38 @@ package org.unicitylabs.sdk.hash; -import com.fasterxml.jackson.core.JsonProcessingException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.unicitylabs.sdk.serializer.json.JsonSerializationException; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.util.HexConverter; public class DataHashTest { @Test public void testInvalidDataHashArguments() { NullPointerException exception = Assertions.assertThrows(NullPointerException.class, - () -> new DataHash(null, new byte[32])); + () -> new DataHash(null, new byte[32])); Assertions.assertEquals("algorithm cannot be null", exception.getMessage()); exception = Assertions.assertThrows(NullPointerException.class, - () -> new DataHash(HashAlgorithm.SHA256, null)); + () -> new DataHash(HashAlgorithm.SHA256, null)); Assertions.assertEquals("data cannot be null", exception.getMessage()); } @Test - public void testDataHashJsonSerialization() throws JsonProcessingException { - Assertions.assertEquals( - "\"00000000000000000000000000000000000000000000000000000000000000000000\"", - - new DataHash(HashAlgorithm.SHA256, new byte[32]).toJson() + public void testDataHashCborSerialization() { + Assertions.assertArrayEquals( + HexConverter.decode("582200000000000000000000000000000000000000000000000000000000000000000000"), + new DataHash(HashAlgorithm.SHA256, new byte[32]).toCbor() ); - Assertions.assertEquals( - "\"000200000000000000000000000000000000\"", - new DataHash(HashAlgorithm.SHA384, new byte[16]).toJson() + + Assertions.assertArrayEquals( + HexConverter.decode("58320002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), + new DataHash(HashAlgorithm.SHA384, new byte[48]).toCbor() ); Assertions.assertEquals( - new DataHash(HashAlgorithm.SHA256, new byte[32]), - DataHash.fromJson("\"00000000000000000000000000000000000000000000000000000000000000000000\"") + new DataHash(HashAlgorithm.SHA256, new byte[32]), + DataHash.fromCbor(HexConverter.decode("582200000000000000000000000000000000000000000000000000000000000000000000")) ); - Assertions.assertThrows(JsonSerializationException.class, () -> DataHash.fromJson("[]")); - Assertions.assertThrows(JsonSerializationException.class, () -> DataHash.fromJson("\"AABBGG\"")); } } diff --git a/src/test/java/org/unicitylabs/sdk/hash/DataHasherTest.java b/src/test/java/org/unicitylabs/sdk/hash/DataHasherTest.java index 6224b94..e77bcd9 100644 --- a/src/test/java/org/unicitylabs/sdk/hash/DataHasherTest.java +++ b/src/test/java/org/unicitylabs/sdk/hash/DataHasherTest.java @@ -1,10 +1,14 @@ package org.unicitylabs.sdk.hash; import org.junit.jupiter.api.Test; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.DataHasher; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import java.nio.charset.StandardCharsets; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class DataHasherTest { @@ -18,17 +22,17 @@ public void testSha256WithUpdate() { assertEquals(HashAlgorithm.SHA256, hash.getAlgorithm()); assertArrayEquals( - new byte[]{ - (byte) 0x2c, (byte) 0xf2, (byte) 0x4d, (byte) 0xba, (byte) 0x5f, (byte) 0xb0, - (byte) 0xa3, (byte) 0x0e, - (byte) 0x26, (byte) 0xe8, (byte) 0x3b, (byte) 0x2a, (byte) 0xc5, (byte) 0xb9, - (byte) 0xe2, (byte) 0x9e, - (byte) 0x1b, (byte) 0x16, (byte) 0x1e, (byte) 0x5c, (byte) 0x1f, (byte) 0xa7, - (byte) 0x42, (byte) 0x5e, - (byte) 0x73, (byte) 0x04, (byte) 0x33, (byte) 0x62, (byte) 0x93, (byte) 0x8b, - (byte) 0x98, (byte) 0x24 - }, - hash.getData() + new byte[]{ + (byte) 0x2c, (byte) 0xf2, (byte) 0x4d, (byte) 0xba, (byte) 0x5f, (byte) 0xb0, + (byte) 0xa3, (byte) 0x0e, + (byte) 0x26, (byte) 0xe8, (byte) 0x3b, (byte) 0x2a, (byte) 0xc5, (byte) 0xb9, + (byte) 0xe2, (byte) 0x9e, + (byte) 0x1b, (byte) 0x16, (byte) 0x1e, (byte) 0x5c, (byte) 0x1f, (byte) 0xa7, + (byte) 0x42, (byte) 0x5e, + (byte) 0x73, (byte) 0x04, (byte) 0x33, (byte) 0x62, (byte) 0x93, (byte) 0x8b, + (byte) 0x98, (byte) 0x24 + }, + hash.getData() ); } @@ -43,17 +47,17 @@ public void testMultipleUpdates() { // Should produce same hash as "hello" assertArrayEquals( - new byte[]{ - (byte) 0x2c, (byte) 0xf2, (byte) 0x4d, (byte) 0xba, (byte) 0x5f, (byte) 0xb0, - (byte) 0xa3, (byte) 0x0e, - (byte) 0x26, (byte) 0xe8, (byte) 0x3b, (byte) 0x2a, (byte) 0xc5, (byte) 0xb9, - (byte) 0xe2, (byte) 0x9e, - (byte) 0x1b, (byte) 0x16, (byte) 0x1e, (byte) 0x5c, (byte) 0x1f, (byte) 0xa7, - (byte) 0x42, (byte) 0x5e, - (byte) 0x73, (byte) 0x04, (byte) 0x33, (byte) 0x62, (byte) 0x93, (byte) 0x8b, - (byte) 0x98, (byte) 0x24 - }, - hash.getData() + new byte[]{ + (byte) 0x2c, (byte) 0xf2, (byte) 0x4d, (byte) 0xba, (byte) 0x5f, (byte) 0xb0, + (byte) 0xa3, (byte) 0x0e, + (byte) 0x26, (byte) 0xe8, (byte) 0x3b, (byte) 0x2a, (byte) 0xc5, (byte) 0xb9, + (byte) 0xe2, (byte) 0x9e, + (byte) 0x1b, (byte) 0x16, (byte) 0x1e, (byte) 0x5c, (byte) 0x1f, (byte) 0xa7, + (byte) 0x42, (byte) 0x5e, + (byte) 0x73, (byte) 0x04, (byte) 0x33, (byte) 0x62, (byte) 0x93, (byte) 0x8b, + (byte) 0x98, (byte) 0x24 + }, + hash.getData() ); } } \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/integration/TokenIntegrationTest.java b/src/test/java/org/unicitylabs/sdk/integration/TokenIntegrationTest.java deleted file mode 100644 index 2775a63..0000000 --- a/src/test/java/org/unicitylabs/sdk/integration/TokenIntegrationTest.java +++ /dev/null @@ -1,130 +0,0 @@ -package org.unicitylabs.sdk.integration; - -import org.unicitylabs.sdk.StateTransitionClient; -import org.unicitylabs.sdk.api.JsonRpcAggregatorClient; -import org.junit.jupiter.api.*; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.Network; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.utility.DockerImageName; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Integration tests for token state transitions using Testcontainers. - * Tests the Java SDK against the aggregator running in Docker. - * - * Run with: ./gradlew integrationTest - */ -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -@Tag("integration") -public class TokenIntegrationTest { - - private Network network; - private GenericContainer mongo1; - private GenericContainer mongo2; - private GenericContainer mongo3; - private GenericContainer mongoSetup; - private GenericContainer aggregator; - - private JsonRpcAggregatorClient aggregatorClient; - private StateTransitionClient client; - - @BeforeAll - void setUp() throws Exception { - network = Network.newNetwork(); - - // Start MongoDB replica set - mongo1 = new GenericContainer<>(DockerImageName.parse("mongo:7.0")) - .withNetwork(network) - .withNetworkAliases("mongo1") - .withCommand("--replSet", "rs0", "--bind_ip_all") - .waitingFor(Wait.forListeningPort().withStartupTimeout(java.time.Duration.ofMinutes(2))); - mongo2 = new GenericContainer<>(DockerImageName.parse("mongo:7.0")) - .withNetwork(network) - .withNetworkAliases("mongo2") - .withCommand("--replSet", "rs0", "--bind_ip_all") - .waitingFor(Wait.forListeningPort().withStartupTimeout(java.time.Duration.ofMinutes(2))); - mongo3 = new GenericContainer<>(DockerImageName.parse("mongo:7.0")) - .withNetwork(network) - .withNetworkAliases("mongo3") - .withCommand("--replSet", "rs0", "--bind_ip_all") - .waitingFor(Wait.forListeningPort().withStartupTimeout(java.time.Duration.ofMinutes(2))); - - mongo1.start(); - mongo2.start(); - mongo3.start(); - - Thread.sleep(5000); - - // Initialize replica set - mongoSetup = new GenericContainer<>(DockerImageName.parse("mongo:7.0")) - .withNetwork(network) - .withCopyFileToContainer( - org.testcontainers.utility.MountableFile.forClasspathResource("docker/aggregator/mongo-init.js"), - "/mongo-init.js") - .withCommand("mongosh", "--host", "mongo1:27017", "--file", "/mongo-init.js"); - - mongoSetup.start(); - - // Start aggregator - aggregator = new GenericContainer<>(DockerImageName.parse("ghcr.io/unicitynetwork/aggregators_net:bbabb5f093e829fa789ed6e83f57af98df3f1752")) - .withNetwork(network) - .withNetworkAliases("aggregator-test") - .withExposedPorts(3000) - .withEnv("MONGODB_URI", "mongodb://mongo1:27017") - .withEnv("USE_MOCK_ALPHABILL", "true") - .withEnv("ALPHABILL_PRIVATE_KEY", "FF00000000000000000000000000000000000000000000000000000000000000") - .withEnv("DISABLE_HIGH_AVAILABILITY", "true") - .withEnv("PORT", "3000") - .waitingFor(Wait.forListeningPort().withStartupTimeout(java.time.Duration.ofMinutes(2))); - aggregator.start(); - - - initializeClient(); - } - - private void initializeClient() { - String aggregatorUrl = String.format("http://localhost:%d", aggregator.getMappedPort(3000)); - aggregatorClient = new JsonRpcAggregatorClient(aggregatorUrl); - client = new StateTransitionClient(aggregatorClient); - } - - @AfterAll - void tearDown() { - if (aggregator != null) aggregator.stop(); - if (mongoSetup != null) mongoSetup.stop(); - if (mongo1 != null) mongo1.stop(); - if (mongo2 != null) mongo2.stop(); - if (mongo3 != null) mongo3.stop(); - if (network != null) network.close(); - } - - @Test - @Order(1) - void testAggregatorIsRunning() { - assertTrue(aggregator.isRunning()); - } - - @Test - @Order(2) - void testGetBlockHeight() throws Exception { - Long blockHeight = aggregatorClient.getBlockHeight().get(); - assertNotNull(blockHeight); - assertTrue(blockHeight >= 0); - } - -// @Test -// @Order(3) -// void testTransferFlow() throws Exception { -// CommonTestFlow.testTransferFlow(client); -// } -// -// @Test -// @Order(4) -// void testOfflineTransferFlow() throws Exception { -// CommonTestFlow.testOfflineTransferFlow(client); -// } -} \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/jsonrpc/JsonRpcRequestTest.java b/src/test/java/org/unicitylabs/sdk/jsonrpc/JsonRpcRequestTest.java index 72723ab..41b5771 100644 --- a/src/test/java/org/unicitylabs/sdk/jsonrpc/JsonRpcRequestTest.java +++ b/src/test/java/org/unicitylabs/sdk/jsonrpc/JsonRpcRequestTest.java @@ -1,24 +1,26 @@ package org.unicitylabs.sdk.jsonrpc; import com.fasterxml.jackson.core.JsonProcessingException; -import java.util.List; -import java.util.UUID; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.unicitylabs.sdk.api.jsonrpc.JsonRpcRequest; import org.unicitylabs.sdk.serializer.UnicityObjectMapper; +import java.util.List; +import java.util.UUID; + public class JsonRpcRequestTest { @Test public void testJsonSerialization() throws JsonProcessingException { JsonRpcRequest request = new JsonRpcRequest( - UUID.fromString("42b9d83f-f984-4c81-9a53-fa1bdbd30b99"), - "testMethod", - List.of("param1", "param2") + UUID.fromString("42b9d83f-f984-4c81-9a53-fa1bdbd30b99"), + "testMethod", + List.of("param1", "param2") ); Assertions.assertEquals( - "{\"id\":\"42b9d83f-f984-4c81-9a53-fa1bdbd30b99\",\"method\":\"testMethod\",\"params\":[\"param1\",\"param2\"],\"jsonrpc\":\"2.0\"}", - UnicityObjectMapper.JSON.writeValueAsString(request)); + "{\"id\":\"42b9d83f-f984-4c81-9a53-fa1bdbd30b99\",\"method\":\"testMethod\",\"params\":[\"param1\",\"param2\"],\"jsonrpc\":\"2.0\"}", + UnicityObjectMapper.JSON.writeValueAsString(request)); } } diff --git a/src/test/java/org/unicitylabs/sdk/jsonrpc/JsonRpcResponseTest.java b/src/test/java/org/unicitylabs/sdk/jsonrpc/JsonRpcResponseTest.java index a989eed..c2af3ea 100644 --- a/src/test/java/org/unicitylabs/sdk/jsonrpc/JsonRpcResponseTest.java +++ b/src/test/java/org/unicitylabs/sdk/jsonrpc/JsonRpcResponseTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.unicitylabs.sdk.api.BlockHeightResponse; +import org.unicitylabs.sdk.api.jsonrpc.JsonRpcResponse; import org.unicitylabs.sdk.serializer.UnicityObjectMapper; public class JsonRpcResponseTest { @@ -11,9 +12,9 @@ public class JsonRpcResponseTest { @Test public void testJsonSerialization() throws JsonProcessingException { JsonRpcResponse data = UnicityObjectMapper.JSON.readValue( - "{\"jsonrpc\":\"2.0\",\"result\":{\"blockNumber\":\"846973\"},\"id\":\"60ce8f4d-4c78-4690-a330-a92d3cf497a9\"}", - UnicityObjectMapper.JSON.getTypeFactory() - .constructParametricType(JsonRpcResponse.class, BlockHeightResponse.class)); + "{\"jsonrpc\":\"2.0\",\"result\":{\"blockNumber\":\"846973\"},\"id\":\"60ce8f4d-4c78-4690-a330-a92d3cf497a9\"}", + UnicityObjectMapper.JSON.getTypeFactory() + .constructParametricType(JsonRpcResponse.class, BlockHeightResponse.class)); Assertions.assertEquals("60ce8f4d-4c78-4690-a330-a92d3cf497a9", data.getId().toString()); Assertions.assertEquals("2.0", data.getVersion()); diff --git a/src/test/java/org/unicitylabs/sdk/mtree/plain/MerkleTreePathStepTest.java b/src/test/java/org/unicitylabs/sdk/mtree/plain/MerkleTreePathStepTest.java deleted file mode 100644 index 1de203a..0000000 --- a/src/test/java/org/unicitylabs/sdk/mtree/plain/MerkleTreePathStepTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.unicitylabs.sdk.mtree.plain; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import java.math.BigInteger; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.unicitylabs.sdk.serializer.UnicityObjectMapper; - -public class MerkleTreePathStepTest { - - @Test - public void testConstructorThrowsOnNullArguments() { - Exception exception = assertThrows(NullPointerException.class, - () -> new SparseMerkleTreePathStep(null, null)); - assertEquals("path cannot be null", exception.getMessage()); - } - - @Test - public void testJsonSerialization() throws JsonProcessingException { - ObjectMapper objectMapper = UnicityObjectMapper.JSON; - - Assertions.assertThrows(JsonMappingException.class, - () -> objectMapper.readValue("{\"path\":\"asd\",\"sibling\":null,\"branch\":null}", - SparseMerkleTreePathStep.class)); - Assertions.assertThrows(JsonMappingException.class, - () -> objectMapper.readValue("{\"path\":[],\"sibling\":null,\"branch\":null}", - SparseMerkleTreePathStep.class)); - Assertions.assertThrows(JsonMappingException.class, - () -> objectMapper.readValue("{\"sibling\":null,\"branch\":null}", - SparseMerkleTreePathStep.class)); - Assertions.assertThrows(JsonMappingException.class, - () -> objectMapper.readValue("{\"path\":\"5\",\"sibling\":null,\"branch\":\"asd\"}", - SparseMerkleTreePathStep.class)); - Assertions.assertThrows(JsonMappingException.class, - () -> objectMapper.readValue("{\"path\":5,\"sibling\":null,\"branch\":\"null\"}", - SparseMerkleTreePathStep.class)); - - SparseMerkleTreePathStep step = new SparseMerkleTreePathStep( - BigInteger.ONE, - new DataHash(HashAlgorithm.SHA384, new byte[5]).getImprint() - ); - Assertions.assertEquals(step, - objectMapper.readValue(objectMapper.writeValueAsString(step), - SparseMerkleTreePathStep.class)); - - } -} diff --git a/src/test/java/org/unicitylabs/sdk/mtree/plain/MerkleTreePathTest.java b/src/test/java/org/unicitylabs/sdk/mtree/plain/MerkleTreePathTest.java deleted file mode 100644 index de9210c..0000000 --- a/src/test/java/org/unicitylabs/sdk/mtree/plain/MerkleTreePathTest.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.unicitylabs.sdk.mtree.plain; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.exc.ValueInstantiationException; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.mtree.MerkleTreePathVerificationResult; -import org.unicitylabs.sdk.serializer.UnicityObjectMapper; -import org.unicitylabs.sdk.util.HexConverter; -import java.math.BigInteger; -import java.util.List; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class MerkleTreePathTest { - - @Test - public void testConstructorThrowsOnNullArguments() { - Exception exception = assertThrows(NullPointerException.class, - () -> new SparseMerkleTreePath(null, null) - ); - assertEquals("rootHash cannot be null", exception.getMessage()); - exception = assertThrows(NullPointerException.class, - () -> new SparseMerkleTreePath(new DataHash(HashAlgorithm.SHA256, new byte[32]), null) - ); - assertEquals("steps cannot be null", exception.getMessage()); - } - - @Test - public void testJsonSerialization() throws JsonProcessingException { - ObjectMapper objectMapper = UnicityObjectMapper.JSON; - - Assertions.assertThrows(JsonMappingException.class, - () -> objectMapper.readValue("{\"root\":\"00000000\"}", SparseMerkleTreePath.class)); - Assertions.assertThrows(JsonMappingException.class, - () -> objectMapper.readValue("{\"steps\":[]}", SparseMerkleTreePath.class)); - Assertions.assertThrows(ValueInstantiationException.class, - () -> objectMapper.readValue("{\"root\": null, \"steps\":[]}", SparseMerkleTreePath.class)); - Assertions.assertThrows(JsonMappingException.class, - () -> objectMapper.readValue("{\"root\": \"asd\", \"steps\":[]}", - SparseMerkleTreePath.class)); - Assertions.assertThrows(JsonMappingException.class, () -> objectMapper.readValue( - "{\"root\": \"000001\", \"steps\":[{\"sibling\": null, \"branch\": [\"asd\"], \"path\": \"5\"}]}", - SparseMerkleTreePath.class)); - - SparseMerkleTreePath path = new SparseMerkleTreePath( - new DataHash(HashAlgorithm.SHA256, new byte[32]), - List.of( - new SparseMerkleTreePathStep( - BigInteger.ONE, - new DataHash(HashAlgorithm.SHA384, new byte[5]).getImprint() - ) - )); - - Assertions.assertEquals(path, - objectMapper.readValue(objectMapper.writeValueAsString(path), SparseMerkleTreePath.class)); - } - - @Test - public void testShouldVerifyInclusionProof() { - SparseMerkleTreePath path = new SparseMerkleTreePath( - DataHash.fromImprint( - HexConverter.decode( - "0000e9748bbd0c45fc357ffe7c221c7db1ef02f589680d8b0a370b48a669435bde13" - ) - ), - List.of( - new SparseMerkleTreePathStep( - BigInteger.valueOf(69), - HexConverter.decode("76616c756535") - ), - new SparseMerkleTreePathStep( - BigInteger.valueOf(4), - HexConverter.decode( - "8471f8ea3c9a0e50627df4c72d9bd5affbdc12050ee7f4250974ed64949f3b0f" - ) - ), - new SparseMerkleTreePathStep( - BigInteger.valueOf(1), - HexConverter.decode( - "66507538ce0fae31018cfc7b01841b5308e7e44306445710acee947ec4a4b2cd" - ) - ) - ) - ); - - Assertions.assertEquals(new MerkleTreePathVerificationResult(true, true), - path.verify(BigInteger.valueOf(0b100010100))); - Assertions.assertEquals(new MerkleTreePathVerificationResult(true, false), - path.verify(BigInteger.valueOf(0b111))); - } - - @Test - public void testEmptyPathVerification() throws JsonProcessingException { - SparseMerkleTreePath path = UnicityObjectMapper.JSON.readValue( - "{\"root\":\"00001e54402898172f2948615fb17627733abbd120a85381c624ad060d28321be672\",\"steps\":[{\"path\":\"1\",\"data\":null},{\"path\":\"1\",\"data\":null}]}", - SparseMerkleTreePath.class); - - MerkleTreePathVerificationResult result = path.verify(BigInteger.valueOf(101)); - Assertions.assertTrue(result.isPathValid()); - Assertions.assertFalse(result.isPathIncluded()); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePathFixture.java b/src/test/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePathFixture.java deleted file mode 100644 index 1b65309..0000000 --- a/src/test/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePathFixture.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.unicitylabs.sdk.mtree.plain; - -import java.util.List; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.util.BigIntegerConverter; - -public class SparseMerkleTreePathFixture { - - public static SparseMerkleTreePath create() { - return new SparseMerkleTreePath( - new DataHasher(HashAlgorithm.SHA256) - .update(new byte[]{0}) - .update(new byte[]{0}) - .digest(), - List.of() - ); - } - -} diff --git a/src/test/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreeTest.java b/src/test/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreeTest.java deleted file mode 100644 index ffa2b73..0000000 --- a/src/test/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreeTest.java +++ /dev/null @@ -1,182 +0,0 @@ -package org.unicitylabs.sdk.mtree.plain; - -import java.lang.reflect.Field; -import java.math.BigInteger; -import java.nio.charset.StandardCharsets; -import java.util.Map; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.mtree.BranchExistsException; -import org.unicitylabs.sdk.mtree.LeafOutOfBoundsException; -import org.unicitylabs.sdk.mtree.MerkleTreePathVerificationResult; -import org.unicitylabs.sdk.util.HexConverter; - -public class SparseMerkleTreeTest { - - private final SparseMerkleTreeRootNode root = SparseMerkleTreeRootNode.create( - new PendingNodeBranch( - BigInteger.valueOf(0b10), - new PendingNodeBranch( - BigInteger.valueOf(0b10), - new PendingNodeBranch( - BigInteger.valueOf(0b100), - new PendingLeafBranch( - BigInteger.valueOf(0b10000), - HexConverter.decode("76616c75653030303030303030") - ), - new PendingNodeBranch( - BigInteger.valueOf(0b1001), - new PendingLeafBranch( - BigInteger.valueOf(0b10), - HexConverter.decode("76616c75653030303130303030") - ), - new PendingLeafBranch( - BigInteger.valueOf(0b11), - HexConverter.decode("76616c75653030303130303030") - ) - ) - ), - new PendingLeafBranch( - BigInteger.valueOf(0b11), - HexConverter.decode("76616c7565313030") - ) - ), - new PendingLeafBranch( - BigInteger.valueOf(0b1000101), - HexConverter.decode("76616c756530303031303130") - ) - ).finalize(HashAlgorithm.SHA256), - new PendingNodeBranch( - BigInteger.valueOf(0b11), - new PendingNodeBranch( - BigInteger.valueOf(0b1010), - new PendingLeafBranch( - BigInteger.valueOf(0b11110), - HexConverter.decode("76616c75653131313030313031") - ), - new PendingLeafBranch( - BigInteger.valueOf(0b1101), - HexConverter.decode("76616c756531303130313031") - ) - ), - new PendingNodeBranch( - BigInteger.valueOf(0b11), - new PendingLeafBranch( - BigInteger.valueOf(0b10), - HexConverter.decode("76616c7565303131") - ), - new PendingLeafBranch( - BigInteger.valueOf(0b1111011), - HexConverter.decode("76616c75653131313031313131") - ) - ) - ).finalize(HashAlgorithm.SHA256), - HashAlgorithm.SHA256 - ); - - @Test - public void treeShouldBeHalfCalculated() throws Exception { - SparseMerkleTree smt = new SparseMerkleTree(HashAlgorithm.SHA256); - - smt.addLeaf(BigInteger.valueOf(0b10), new byte[]{1, 2, 3}); - smt.calculateRoot(); - smt.addLeaf(BigInteger.valueOf(0b11), new byte[]{1, 2, 3, 4}); - - FinalizedLeafBranch left = new PendingLeafBranch(BigInteger.valueOf(2), - new byte[]{1, 2, 3}).finalize(HashAlgorithm.SHA256); - PendingLeafBranch right = new PendingLeafBranch(BigInteger.valueOf(3), new byte[]{1, 2, 3, 4}); - - Field leftField = SparseMerkleTree.class.getDeclaredField("left"); - leftField.setAccessible(true); - Field rightField = SparseMerkleTree.class.getDeclaredField("right"); - rightField.setAccessible(true); - - Assertions.assertEquals(left, leftField.get(smt)); - Assertions.assertEquals(right, rightField.get(smt)); - } - - @Test - public void shouldVerifyTheTree() throws Exception { - SparseMerkleTree smt = new SparseMerkleTree(HashAlgorithm.SHA256); - Map leaves = Map.ofEntries( - Map.entry(0b110010000, "value00010000"), - Map.entry(0b100000000, "value00000000"), - Map.entry(0b100010000, "value00010000"), - Map.entry(0b111100101, "value11100101"), - Map.entry(0b1100, "value100"), - Map.entry(0b1011, "value011"), - Map.entry(0b111101111, "value11101111"), - Map.entry(0b10001010, "value0001010"), - Map.entry(0b11010101, "value1010101") - ); - for (Map.Entry leaf : leaves.entrySet()) { - smt.addLeaf(BigInteger.valueOf(leaf.getKey()), - leaf.getValue().getBytes(StandardCharsets.UTF_8)); - } - - Assertions.assertThrows(BranchExistsException.class, () -> - smt.addLeaf(BigInteger.valueOf(0b10000000), "OnPath".getBytes(StandardCharsets.UTF_8)) - ); - - Assertions.assertThrows(LeafOutOfBoundsException.class, () -> - smt.addLeaf(BigInteger.valueOf(0b1000000000), - "ThroughLeaf".getBytes(StandardCharsets.UTF_8)) - ); - - Assertions.assertEquals(smt.calculateRoot(), this.root); - } - - @Test - public void shouldGetWorkingPath() throws Exception { - SparseMerkleTree smt = new SparseMerkleTree(HashAlgorithm.SHA256); - Map leaves = Map.ofEntries( - Map.entry(0b110010000, "value00010000"), - Map.entry(0b100000000, "value00000000"), - Map.entry(0b100010000, "value00010000"), - Map.entry(0b111100101, "value11100101"), - Map.entry(0b1100, "value100"), - Map.entry(0b1011, "value011"), - Map.entry(0b111101111, "value11101111"), - Map.entry(0b10001010, "value0001010"), - Map.entry(0b11010101, "value1010101") - ); - for (Map.Entry leaf : leaves.entrySet()) { - smt.addLeaf(BigInteger.valueOf(leaf.getKey()), - leaf.getValue().getBytes(StandardCharsets.UTF_8)); - } - SparseMerkleTreeRootNode root = smt.calculateRoot(); - - SparseMerkleTreePath path = root.getPath(BigInteger.valueOf(0b11010)); - MerkleTreePathVerificationResult result = path.verify(BigInteger.valueOf(0b11010)); - Assertions.assertFalse(result.isPathIncluded()); - Assertions.assertTrue(result.isPathValid()); - Assertions.assertFalse(result.isSuccessful()); - - path = root.getPath(BigInteger.valueOf(0b110010000)); - result = path.verify(BigInteger.valueOf(0b110010000)); - Assertions.assertTrue(result.isPathIncluded()); - Assertions.assertTrue(result.isPathValid()); - Assertions.assertTrue(result.isSuccessful()); - - path = root.getPath(BigInteger.valueOf(0b110010000)); - result = path.verify(BigInteger.valueOf(0b11010)); - Assertions.assertFalse(result.isPathIncluded()); - Assertions.assertTrue(result.isPathValid()); - Assertions.assertFalse(result.isSuccessful()); - - path = root.getPath(BigInteger.valueOf(0b111100101)); - result = path.verify(BigInteger.valueOf(0b111100101)); - Assertions.assertTrue(result.isPathIncluded()); - Assertions.assertTrue(result.isPathValid()); - Assertions.assertTrue(result.isSuccessful()); - - SparseMerkleTree emptyTree = new SparseMerkleTree(HashAlgorithm.SHA256); - SparseMerkleTreeRootNode emptyRoot = emptyTree.calculateRoot(); - path = emptyRoot.getPath(BigInteger.valueOf(0b100)); - result = path.verify(BigInteger.valueOf(0b10)); - Assertions.assertFalse(result.isPathIncluded()); - Assertions.assertTrue(result.isPathValid()); - Assertions.assertFalse(result.isSuccessful()); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreeTest.java b/src/test/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreeTest.java deleted file mode 100644 index 4e805bf..0000000 --- a/src/test/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreeTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.unicitylabs.sdk.mtree.sum; - - -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.mtree.sum.SparseMerkleSumTree.LeafValue; -import java.math.BigInteger; -import java.util.Map; -import java.util.Map.Entry; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -class SparseMerkleSumTreeTest { - - @Test - void shouldAgreeWithSpecExamples() throws Exception { - var treeLeftOnly = new SparseMerkleSumTree(HashAlgorithm.SHA256); - treeLeftOnly.addLeaf(new BigInteger("100", 2), new LeafValue("a".getBytes(), BigInteger.valueOf(1))); - var rootLeftOnly = treeLeftOnly.calculateRoot(); - Assertions.assertEquals(BigInteger.valueOf(1), rootLeftOnly.getValue()); - Assertions.assertEquals('"' + "0000" + "34e0cf342d70c0d10e3ba481f72db532ecfd723afa3c25812a4bef61b5198d0b" + '"', rootLeftOnly.getRootHash().toJson()); - - var treeRightOnly = new SparseMerkleSumTree(HashAlgorithm.SHA256); - treeRightOnly.addLeaf(new BigInteger("111", 2), new LeafValue("b".getBytes(), BigInteger.valueOf(2))); - var rootRightOnly = treeRightOnly.calculateRoot(); - Assertions.assertEquals(BigInteger.valueOf(2), rootRightOnly.getValue()); - Assertions.assertEquals('"' + "0000" + "da47d1cda8dab5159b2bed1ea27c3d24ed990989fac3c62ace05273fea51f958" + '"', rootRightOnly.getRootHash().toJson()); - - var treeFourLeaves = new SparseMerkleSumTree(HashAlgorithm.SHA256); - treeFourLeaves.addLeaf(new BigInteger("1000", 2), new LeafValue("a".getBytes(), BigInteger.valueOf(1))); - treeFourLeaves.addLeaf(new BigInteger("1100", 2), new LeafValue("b".getBytes(), BigInteger.valueOf(2))); - treeFourLeaves.addLeaf(new BigInteger("1011", 2), new LeafValue("c".getBytes(), BigInteger.valueOf(3))); - treeFourLeaves.addLeaf(new BigInteger("1111", 2), new LeafValue("d".getBytes(), BigInteger.valueOf(4))); - var rootFourLeaves = treeFourLeaves.calculateRoot(); - Assertions.assertEquals(BigInteger.valueOf(10), rootFourLeaves.getValue()); - Assertions.assertEquals('"' + "0000" + "adfefa7c86b18d1216eece9fe0ce82ca58fd8cf482305c3c4e1a0a1361dc9d15" + '"', rootFourLeaves.getRootHash().toJson()); - } - - @Test - void shouldBuildTreeWithNumericValues() throws Exception { - var leaves = Map.of( - new BigInteger("1000", 2), new LeafValue("left-1".getBytes(), BigInteger.valueOf(10)), - new BigInteger("1001", 2), new LeafValue("right-1".getBytes(), BigInteger.valueOf(20)), - new BigInteger("1010", 2), new LeafValue("left-2".getBytes(), BigInteger.valueOf(30)), - new BigInteger("1011", 2), new LeafValue("right-2".getBytes(), BigInteger.valueOf(40)) - ); - - SparseMerkleSumTree tree = new SparseMerkleSumTree(HashAlgorithm.SHA256); - for (Entry entry : leaves.entrySet()) { - tree.addLeaf(entry.getKey(), entry.getValue()); - } - - var root = tree.calculateRoot(); - Assertions.assertEquals(BigInteger.valueOf(100), root.getValue()); - - for (var entry : leaves.entrySet()) { - var path = root.getPath(entry.getKey()); - var verificationResult = path.verify(entry.getKey()); - Assertions.assertTrue(verificationResult.isPathIncluded()); - Assertions.assertTrue(verificationResult.isPathValid()); - Assertions.assertTrue(verificationResult.isSuccessful()); - - Assertions.assertEquals(root.getRootHash(), path.getRootHash()); - Assertions.assertArrayEquals( - entry.getValue().getValue(), - path.getSteps().get(0).getData().orElse(null) - ); - Assertions.assertEquals( - entry.getValue().getCounter(), - path.getSteps().get(0).getValue() - ); - } - - tree.addLeaf(new BigInteger("1110", 2), new LeafValue(new byte[32], BigInteger.valueOf(100))); - root = tree.calculateRoot(); - Assertions.assertEquals(BigInteger.valueOf(200), root.getValue()); - } - - @Test - void shouldThrowErrorOnNonPositivePathOrSum() { - var tree = new SparseMerkleSumTree(HashAlgorithm.SHA256); - Assertions.assertThrows(IllegalArgumentException.class, - () -> tree.addLeaf(BigInteger.valueOf(-1), - new LeafValue(new byte[32], BigInteger.valueOf(100)))); - Assertions.assertThrows(IllegalArgumentException.class, - () -> tree.addLeaf(BigInteger.ONE, new LeafValue(new byte[32], BigInteger.valueOf(-1)))); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/predicate/MaskedPredicateReferenceTest.java b/src/test/java/org/unicitylabs/sdk/predicate/MaskedPredicateReferenceTest.java deleted file mode 100644 index 49116a5..0000000 --- a/src/test/java/org/unicitylabs/sdk/predicate/MaskedPredicateReferenceTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.unicitylabs.sdk.predicate; - -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.predicate.embedded.MaskedPredicateReference; -import org.unicitylabs.sdk.token.TokenType; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -class MaskedPredicateReferenceTest { - - @Test - void testReferenceAddress() { - Assertions.assertEquals( - "DIRECT://000056787e7ec9ef8e70cc715f061bd83981d552c6f813f9a319153e24321ccf5195f0f78200", - MaskedPredicateReference.create( - new TokenType(new byte[32]), - "my_algorithm", - new byte[32], - HashAlgorithm.SHA256, - new byte[3] - ) - .toAddress() - .getAddress()); - } -} \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializerTest.java b/src/test/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializerTest.java index 1cf553a..95f6d72 100644 --- a/src/test/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializerTest.java +++ b/src/test/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializerTest.java @@ -1,8 +1,5 @@ package org.unicitylabs.sdk.serializer.cbor; -import java.util.Iterator; -import java.util.List; -import java.util.Set; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer.CborTag; @@ -10,160 +7,164 @@ import org.unicitylabs.sdk.serializer.cbor.CborSerializer.CborMap.Entry; import org.unicitylabs.sdk.util.HexConverter; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + public class CborDeserializerTest { @Test void testReadUnsignedInteger() { Assertions.assertEquals( - 5, - CborDeserializer.readUnsignedInteger(HexConverter.decode("05")).asLong() + 5, + CborDeserializer.decodeUnsignedInteger(HexConverter.decode("05")).asLong() ); Assertions.assertEquals( - 100, - CborDeserializer.readUnsignedInteger(HexConverter.decode("1864")).asLong() + 100, + CborDeserializer.decodeUnsignedInteger(HexConverter.decode("1864")).asLong() ); Assertions.assertEquals( - 10000, - CborDeserializer.readUnsignedInteger(HexConverter.decode("192710")).asLong() + 10000, + CborDeserializer.decodeUnsignedInteger(HexConverter.decode("192710")).asLong() ); Assertions.assertEquals( - 66000, - CborDeserializer.readUnsignedInteger(HexConverter.decode("1a000101d0")).asLong() + 66000, + CborDeserializer.decodeUnsignedInteger(HexConverter.decode("1a000101d0")).asLong() ); Assertions.assertEquals( - 8147483647L, - CborDeserializer.readUnsignedInteger(HexConverter.decode("1b00000001e5a0bbff")).asLong() + 8147483647L, + CborDeserializer.decodeUnsignedInteger(HexConverter.decode("1b00000001e5a0bbff")).asLong() ); Assertions.assertEquals( - -5, - CborDeserializer.readUnsignedInteger(HexConverter.decode("1bfffffffffffffffb")).asLong() + -5, + CborDeserializer.decodeUnsignedInteger(HexConverter.decode("1bfffffffffffffffb")).asLong() ); } @Test void testReadByteString() { Assertions.assertArrayEquals( - new byte[5], - CborDeserializer.readByteString(HexConverter.decode("450000000000")) + new byte[5], + CborDeserializer.decodeByteString(HexConverter.decode("450000000000")) ); Assertions.assertArrayEquals( - new byte[25], - CborDeserializer.readByteString( - HexConverter.decode("581900000000000000000000000000000000000000000000000000")) + new byte[25], + CborDeserializer.decodeByteString( + HexConverter.decode("581900000000000000000000000000000000000000000000000000")) ); } @Test void testReadTextString() { Assertions.assertEquals( - "Hello, world!", - CborDeserializer.readTextString(HexConverter.decode("6d48656c6c6f2c20776f726c6421")) + "Hello, world!", + CborDeserializer.decodeTextString(HexConverter.decode("6d48656c6c6f2c20776f726c6421")) ); Assertions.assertEquals( - new String(new byte[25]), - CborDeserializer.readTextString( - HexConverter.decode("781900000000000000000000000000000000000000000000000000")) + new String(new byte[25]), + CborDeserializer.decodeTextString( + HexConverter.decode("781900000000000000000000000000000000000000000000000000")) ); } @Test void testReadArray() { - List data = CborDeserializer.readArray( - HexConverter.decode( - "98196d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c6421") + List data = CborDeserializer.decodeArray( + HexConverter.decode( + "98196d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c6421") ); for (byte[] item : data) { - Assertions.assertEquals("Hello, world!", CborDeserializer.readTextString(item)); + Assertions.assertEquals("Hello, world!", CborDeserializer.decodeTextString(item)); } } @Test void testReadMap() { - Set data = CborDeserializer.readMap( - HexConverter.decode( - "a4430000006d48656c6c6f2c20776f726c6421430000016d48656c6c6f2c20776f726c64216454657374f66d48656c6c6f2c20776f726c6421581900000000000000000000000000000000000000000000000000") + Set data = CborDeserializer.decodeMap( + HexConverter.decode( + "a4430000006d48656c6c6f2c20776f726c6421430000016d48656c6c6f2c20776f726c64216454657374f66d48656c6c6f2c20776f726c6421581900000000000000000000000000000000000000000000000000") ); Iterator iterator = data.iterator(); Entry entry = iterator.next(); Assertions.assertArrayEquals( - CborSerializer.encodeByteString(HexConverter.decode("000000")), - entry.getKey() + CborSerializer.encodeByteString(HexConverter.decode("000000")), + entry.getKey() ); Assertions.assertArrayEquals( - CborSerializer.encodeTextString("Hello, world!"), - entry.getValue() + CborSerializer.encodeTextString("Hello, world!"), + entry.getValue() ); entry = iterator.next(); Assertions.assertArrayEquals( - CborSerializer.encodeByteString(HexConverter.decode("000001")), - entry.getKey() + CborSerializer.encodeByteString(HexConverter.decode("000001")), + entry.getKey() ); Assertions.assertArrayEquals( - CborSerializer.encodeTextString("Hello, world!"), - entry.getValue() + CborSerializer.encodeTextString("Hello, world!"), + entry.getValue() ); entry = iterator.next(); Assertions.assertArrayEquals( - CborSerializer.encodeTextString("Test"), - entry.getKey() + CborSerializer.encodeTextString("Test"), + entry.getKey() ); Assertions.assertArrayEquals( - CborSerializer.encodeNull(), - entry.getValue() + CborSerializer.encodeNull(), + entry.getValue() ); entry = iterator.next(); Assertions.assertArrayEquals( - CborSerializer.encodeTextString("Hello, world!"), - entry.getKey() + CborSerializer.encodeTextString("Hello, world!"), + entry.getKey() ); Assertions.assertArrayEquals( - CborSerializer.encodeByteString(new byte[25]), - entry.getValue() + CborSerializer.encodeByteString(new byte[25]), + entry.getValue() ); } @Test void testReadBoolean() { - Assertions.assertTrue(CborDeserializer.readBoolean(HexConverter.decode("f5"))); + Assertions.assertTrue(CborDeserializer.decodeBoolean(HexConverter.decode("f5"))); - Assertions.assertFalse(CborDeserializer.readBoolean(HexConverter.decode("f4"))); + Assertions.assertFalse(CborDeserializer.decodeBoolean(HexConverter.decode("f4"))); } @Test void testReadOptional() { Assertions.assertNull( - CborDeserializer.readOptional( - HexConverter.decode("f6"), - CborDeserializer::readUnsignedInteger - ) + CborDeserializer.decodeNullable( + HexConverter.decode("f6"), + CborDeserializer::decodeUnsignedInteger + ) ); } @Test void testEncodeTag() { - CborTag tag = CborDeserializer.readTag( - HexConverter.decode("d4781a746167206e756d62657220736d616c6c6572207468616e203234") + CborTag tag = CborDeserializer.decodeTag( + HexConverter.decode("d4781a746167206e756d62657220736d616c6c6572207468616e203234") ); Assertions.assertEquals( - 20, - tag.getTag() + 20, + tag.getTag() ); Assertions.assertArrayEquals( - CborSerializer.encodeTextString("tag number smaller than 24"), - tag.getData() + CborSerializer.encodeTextString("tag number smaller than 24"), + tag.getData() ); } } diff --git a/src/test/java/org/unicitylabs/sdk/serializer/cbor/CborSerializerTest.java b/src/test/java/org/unicitylabs/sdk/serializer/cbor/CborSerializerTest.java index 78a84cc..0658267 100644 --- a/src/test/java/org/unicitylabs/sdk/serializer/cbor/CborSerializerTest.java +++ b/src/test/java/org/unicitylabs/sdk/serializer/cbor/CborSerializerTest.java @@ -1,100 +1,101 @@ package org.unicitylabs.sdk.serializer.cbor; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.unicitylabs.sdk.serializer.cbor.CborSerializer.CborMap; import org.unicitylabs.sdk.util.HexConverter; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + public class CborSerializerTest { @Test void testCborMap() { // Check that key cannot be null on entry Assertions.assertThrows(NullPointerException.class, - () -> new CborMap.Entry(null, new byte[5])); + () -> new CborMap.Entry(null, new byte[5])); // Check that value cannot be null on entry Assertions.assertThrows(NullPointerException.class, - () -> new CborMap.Entry(new byte[5], null)); + () -> new CborMap.Entry(new byte[5], null)); // Do not allow null entries Assertions.assertThrows(NullPointerException.class, () -> new CborMap(null)); // Check if duplicate keys are detected Assertions.assertThrows(IllegalArgumentException.class, () -> Set.of( - new CborMap.Entry(new byte[5], new byte[5]), - new CborMap.Entry(new byte[5], new byte[5]) + new CborMap.Entry(new byte[5], new byte[5]), + new CborMap.Entry(new byte[5], new byte[5]) )); } @Test void testEncodeUnsignedInteger() { Assertions.assertArrayEquals( - HexConverter.decode("05"), - CborSerializer.encodeUnsignedInteger(5) + HexConverter.decode("05"), + CborSerializer.encodeUnsignedInteger(5) ); Assertions.assertArrayEquals( - HexConverter.decode("1864"), - CborSerializer.encodeUnsignedInteger(100) + HexConverter.decode("1864"), + CborSerializer.encodeUnsignedInteger(100) ); Assertions.assertArrayEquals( - HexConverter.decode("192710"), - CborSerializer.encodeUnsignedInteger(10000) + HexConverter.decode("192710"), + CborSerializer.encodeUnsignedInteger(10000) ); Assertions.assertArrayEquals( - HexConverter.decode("1a000101d0"), - CborSerializer.encodeUnsignedInteger(66000) + HexConverter.decode("1a000101d0"), + CborSerializer.encodeUnsignedInteger(66000) ); Assertions.assertArrayEquals( - HexConverter.decode("1b00000001e5a0bbff"), - CborSerializer.encodeUnsignedInteger(8147483647L) + HexConverter.decode("1b00000001e5a0bbff"), + CborSerializer.encodeUnsignedInteger(8147483647L) ); Assertions.assertArrayEquals( - HexConverter.decode("1bfffffffffffffffb"), - CborSerializer.encodeUnsignedInteger(-5) + HexConverter.decode("1bfffffffffffffffb"), + CborSerializer.encodeUnsignedInteger(-5) ); } @Test void testEncodeByteString() { Assertions.assertArrayEquals( - HexConverter.decode("450000000000"), - CborSerializer.encodeByteString(new byte[5]) + HexConverter.decode("450000000000"), + CborSerializer.encodeByteString(new byte[5]) ); Assertions.assertArrayEquals( - HexConverter.decode("581900000000000000000000000000000000000000000000000000"), - CborSerializer.encodeByteString(new byte[25]) + HexConverter.decode("581900000000000000000000000000000000000000000000000000"), + CborSerializer.encodeByteString(new byte[25]) ); } @Test void testEncodeTextString() { Assertions.assertArrayEquals( - HexConverter.decode("6d48656c6c6f2c20776f726c6421"), - CborSerializer.encodeTextString("Hello, world!") + HexConverter.decode("6d48656c6c6f2c20776f726c6421"), + CborSerializer.encodeTextString("Hello, world!") ); Assertions.assertArrayEquals( - HexConverter.decode("781900000000000000000000000000000000000000000000000000"), - CborSerializer.encodeTextString(new String(new byte[25])) + HexConverter.decode("781900000000000000000000000000000000000000000000000000"), + CborSerializer.encodeTextString(new String(new byte[25])) ); } @Test void testEncodeArray() { Assertions.assertArrayEquals( - HexConverter.decode( - "826d48656c6c6f2c20776f726c6421581900000000000000000000000000000000000000000000000000"), - CborSerializer.encodeArray( - CborSerializer.encodeTextString("Hello, world!"), - CborSerializer.encodeByteString(new byte[25]) - ) + HexConverter.decode( + "826d48656c6c6f2c20776f726c6421581900000000000000000000000000000000000000000000000000"), + CborSerializer.encodeArray( + CborSerializer.encodeTextString("Hello, world!"), + CborSerializer.encodeByteString(new byte[25]) + ) ); List list = new ArrayList<>(); @@ -103,73 +104,73 @@ void testEncodeArray() { } Assertions.assertArrayEquals( - HexConverter.decode( - "98196d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c6421"), - CborSerializer.encodeArray(list.toArray(byte[][]::new)) + HexConverter.decode( + "98196d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c64216d48656c6c6f2c20776f726c6421"), + CborSerializer.encodeArray(list.toArray(byte[][]::new)) ); } @Test void testEncodeMap() { Assertions.assertArrayEquals( - HexConverter.decode( - "a4430000006d48656c6c6f2c20776f726c6421430000016d48656c6c6f2c20776f726c64216454657374f66d48656c6c6f2c20776f726c6421581900000000000000000000000000000000000000000000000000"), - CborSerializer.encodeMap( - new CborMap( - Set.of( - new CborMap.Entry( - CborSerializer.encodeByteString(HexConverter.decode("000001")), - CborSerializer.encodeTextString("Hello, world!") - ), - new CborMap.Entry( - CborSerializer.encodeByteString(HexConverter.decode("000000")), - CborSerializer.encodeTextString("Hello, world!") - ), - new CborMap.Entry( - CborSerializer.encodeTextString("Hello, world!"), - CborSerializer.encodeByteString(new byte[25]) - ), - new CborMap.Entry( - CborSerializer.encodeTextString("Test"), - CborSerializer.encodeNull() + HexConverter.decode( + "a4430000006d48656c6c6f2c20776f726c6421430000016d48656c6c6f2c20776f726c64216454657374f66d48656c6c6f2c20776f726c6421581900000000000000000000000000000000000000000000000000"), + CborSerializer.encodeMap( + new CborMap( + Set.of( + new CborMap.Entry( + CborSerializer.encodeByteString(HexConverter.decode("000001")), + CborSerializer.encodeTextString("Hello, world!") + ), + new CborMap.Entry( + CborSerializer.encodeByteString(HexConverter.decode("000000")), + CborSerializer.encodeTextString("Hello, world!") + ), + new CborMap.Entry( + CborSerializer.encodeTextString("Hello, world!"), + CborSerializer.encodeByteString(new byte[25]) + ), + new CborMap.Entry( + CborSerializer.encodeTextString("Test"), + CborSerializer.encodeNull() + ) + ) ) - ) ) - ) ); } @Test void testEncodeBoolean() { Assertions.assertArrayEquals( - HexConverter.decode("f5"), - CborSerializer.encodeBoolean(true) + HexConverter.decode("f5"), + CborSerializer.encodeBoolean(true) ); Assertions.assertArrayEquals( - HexConverter.decode("f4"), - CborSerializer.encodeBoolean(false) + HexConverter.decode("f4"), + CborSerializer.encodeBoolean(false) ); } @Test void testEncodeNull() { Assertions.assertArrayEquals( - HexConverter.decode("f6"), - CborSerializer.encodeNull() + HexConverter.decode("f6"), + CborSerializer.encodeNull() ); } @Test void testEncodeTag() { Assertions.assertArrayEquals( - HexConverter.decode("d4781a746167206e756d62657220736d616c6c6572207468616e203234"), - CborSerializer.encodeTag(20, CborSerializer.encodeTextString("tag number smaller than 24")) + HexConverter.decode("d4781a746167206e756d62657220736d616c6c6572207468616e203234"), + CborSerializer.encodeTag(20, CborSerializer.encodeTextString("tag number smaller than 24")) ); Assertions.assertArrayEquals( - HexConverter.decode("d874706c6172676520746167206e756d626572"), - CborSerializer.encodeTag(116, CborSerializer.encodeTextString("large tag number")) + HexConverter.decode("d874706c6172676520746167206e756d626572"), + CborSerializer.encodeTag(116, CborSerializer.encodeTextString("large tag number")) ); } diff --git a/src/test/java/org/unicitylabs/sdk/signing/SignatureRecoveryTest.java b/src/test/java/org/unicitylabs/sdk/signing/SignatureRecoveryTest.java deleted file mode 100644 index 21dd8e8..0000000 --- a/src/test/java/org/unicitylabs/sdk/signing/SignatureRecoveryTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.unicitylabs.sdk.signing; - -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.util.HexConverter; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Test signature recovery functionality - */ -public class SignatureRecoveryTest { - - @Test - void testSignatureRecoveryId() { - // Create a signing service with a known private key - byte[] privateKey = HexConverter.decode("c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4"); - SigningService signingService = new SigningService(privateKey); - - // Create test data and hash it - byte[] testData = "Hello, Unicity!".getBytes(); - DataHasher hasher = new DataHasher(HashAlgorithm.SHA256); - hasher.update(testData); - DataHash hash = hasher.digest(); - - // Sign the hash - Signature signature = signingService.sign(hash); - - // Verify recovery ID is 0 or 1 - assertTrue(signature.getRecovery() == 0 || signature.getRecovery() == 1, - "Recovery ID should be 0 or 1, got: " + signature.getRecovery()); - - // Verify signature with known public key - byte[] publicKey = signingService.getPublicKey(); - assertTrue(SigningService.verifyWithPublicKey(hash, signature.getBytes(), publicKey)); - } - - @Test - void testPublicKeyRecovery() { - // Create a signing service with a known private key - byte[] privateKey = HexConverter.decode("c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4"); - SigningService signingService = new SigningService(privateKey); - byte[] expectedPublicKey = signingService.getPublicKey(); - - // Create test data and hash it - byte[] testData = "Test public key recovery".getBytes(); - DataHasher hasher = new DataHasher(HashAlgorithm.SHA256); - hasher.update(testData); - DataHash hash = hasher.digest(); - - // Sign the hash - Signature signature = signingService.sign(hash); - - // Verify signature using recovered public key - assertTrue(SigningService.verifySignatureWithRecoveredPublicKey(hash, signature), - "Signature verification with recovered public key should succeed"); - } - - @Test - void testSignatureFormatCompliance() { - // Test with the exact values from TypeScript test - String transactionHashHex = "0000d6035b65700f0af73cc62a580eb833c20f40aaee460087f5fb43ebb3c047f1d4"; - String signatureHex = "301c7f19d5e0a7e350012ab7bbaf26a0152a751eec06d18563f96bcf06d2380e7de7ce6cebb8c11479d1bd9c463c3ba47396b5f815c552b344d430b0d011a2e701"; - String expectedPublicKeyHex = "02bf8d9e7687f66c7fce1e98edbc05566f7db740030722cf6cf62aca035c5035ea"; - - // Parse the signature - byte[] sigBytes = HexConverter.decode(signatureHex); - assertEquals(65, sigBytes.length, "Signature should be 65 bytes"); - - // Extract components - int recoveryId = sigBytes[64] & 0xFF; - - // Create signature object - byte[] sigOnly = new byte[64]; - System.arraycopy(sigBytes, 0, sigOnly, 0, 64); - Signature signature = new Signature(sigOnly, recoveryId); - - // Parse hash - DataHash transactionHash = DataHash.fromImprint(HexConverter.decode(transactionHashHex)); - - // Verify using recovered public key - assertTrue(SigningService.verifySignatureWithRecoveredPublicKey(transactionHash, signature), - "Should verify with recovered public key"); - } -} \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/signing/SigningServiceTest.java b/src/test/java/org/unicitylabs/sdk/signing/SigningServiceTest.java deleted file mode 100644 index 2994166..0000000 --- a/src/test/java/org/unicitylabs/sdk/signing/SigningServiceTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.unicitylabs.sdk.signing; - -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.junit.jupiter.api.Test; - -import java.nio.charset.StandardCharsets; - -import static org.junit.jupiter.api.Assertions.*; - -public class SigningServiceTest { - - @Test - public void testGeneratePrivateKey() { - byte[] privateKey = SigningService.generatePrivateKey(); - - assertNotNull(privateKey); - assertEquals(32, privateKey.length); - - // Test that we can create a signing service with it - SigningService service = new SigningService(privateKey); - assertNotNull(service.getPublicKey()); - assertEquals(33, service.getPublicKey().length); // Compressed public key - } - - @Test - public void testCreateFromSecret() { - byte[] secret = "test secret".getBytes(StandardCharsets.UTF_8); - byte[] nonce = "test nonce".getBytes(StandardCharsets.UTF_8); - - SigningService signingService = SigningService.createFromMaskedSecret(secret, nonce); - - assertNotNull(signingService); - assertNotNull(signingService.getPublicKey()); - assertEquals("secp256k1", signingService.getAlgorithm()); - } - - @Test - public void testSignAndVerify() { - byte[] privateKey = SigningService.generatePrivateKey(); - SigningService service = new SigningService(privateKey); - - // Create a test hash - byte[] testData = "test data".getBytes(StandardCharsets.UTF_8); - DataHash hash = new DataHash(HashAlgorithm.SHA256, testData); - - // Sign the hash - Signature signature = service.sign(hash); - - assertNotNull(signature); - assertEquals(64, signature.getBytes().length); - - // Verify the signature - boolean isValid = service.verify(hash, signature); - - assertTrue(isValid); - } - - @Test - public void testVerifyWithPublicKey() { - byte[] privateKey = SigningService.generatePrivateKey(); - SigningService service = new SigningService(privateKey); - byte[] publicKey = service.getPublicKey(); - - // Create a test hash - byte[] testData = "test data".getBytes(StandardCharsets.UTF_8); - DataHash hash = new DataHash(HashAlgorithm.SHA256, testData); - - // Sign the hash - Signature signature = service.sign(hash); - - // Verify with public key - boolean isValid = SigningService.verifyWithPublicKey(hash, signature.getBytes(), publicKey); - - assertTrue(isValid); - } - - @Test - public void testInvalidSignature() { - byte[] privateKey = SigningService.generatePrivateKey(); - SigningService service = new SigningService(privateKey); - - // Create a test hash - byte[] testData = "test data".getBytes(StandardCharsets.UTF_8); - DataHash hash = new DataHash(HashAlgorithm.SHA256, testData); - - // Create an invalid signature - byte[] invalidSig = new byte[64]; - Signature signature = new Signature(invalidSig, 0); - - // Verify the signature - boolean isValid = service.verify(hash, signature); - - assertFalse(isValid); - } -} \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/mtree/CommonPathTest.java b/src/test/java/org/unicitylabs/sdk/smt/CommonPathTest.java similarity index 58% rename from src/test/java/org/unicitylabs/sdk/mtree/CommonPathTest.java rename to src/test/java/org/unicitylabs/sdk/smt/CommonPathTest.java index 47134a9..650352a 100644 --- a/src/test/java/org/unicitylabs/sdk/mtree/CommonPathTest.java +++ b/src/test/java/org/unicitylabs/sdk/smt/CommonPathTest.java @@ -1,4 +1,4 @@ -package org.unicitylabs.sdk.mtree; +package org.unicitylabs.sdk.smt; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -8,18 +8,18 @@ public class CommonPathTest { @Test - public void shouldCalculateCommonPath() throws Exception { + public void shouldCalculateCommonPath() { Assertions.assertEquals(CommonPath.create( - BigInteger.valueOf(0b11), - BigInteger.valueOf(0b111101111) + BigInteger.valueOf(0b11), + BigInteger.valueOf(0b111101111) ), new CommonPath(BigInteger.valueOf(0b11), 1)); Assertions.assertEquals(CommonPath.create( - BigInteger.valueOf(0b111101111), - BigInteger.valueOf(0b11) + BigInteger.valueOf(0b111101111), + BigInteger.valueOf(0b11) ), new CommonPath(BigInteger.valueOf(0b11), 1)); Assertions.assertEquals(CommonPath.create( - BigInteger.valueOf(0b110010000), - BigInteger.valueOf(0b100010000) + BigInteger.valueOf(0b110010000), + BigInteger.valueOf(0b100010000) ), new CommonPath(BigInteger.valueOf(0b10010000), 7)); } } diff --git a/src/test/java/org/unicitylabs/sdk/smt/plain/MerkleTreePathTest.java b/src/test/java/org/unicitylabs/sdk/smt/plain/MerkleTreePathTest.java new file mode 100644 index 0000000..76ed7c8 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/smt/plain/MerkleTreePathTest.java @@ -0,0 +1,90 @@ +package org.unicitylabs.sdk.smt.plain; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.smt.MerkleTreePathVerificationResult; +import org.unicitylabs.sdk.util.HexConverter; + +import java.math.BigInteger; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class MerkleTreePathTest { + + @Test + public void testConstructorThrowsOnNullArguments() { + Exception exception = assertThrows(NullPointerException.class, + () -> new SparseMerkleTreePath(null, null) + ); + assertEquals("rootHash cannot be null", exception.getMessage()); + exception = assertThrows(NullPointerException.class, + () -> new SparseMerkleTreePath(new DataHash(HashAlgorithm.SHA256, new byte[32]), null) + ); + assertEquals("steps cannot be null", exception.getMessage()); + } + + @Test + public void testShouldVerifyInclusionProof() { + SparseMerkleTreePath path = new SparseMerkleTreePath( + DataHash.fromImprint( + HexConverter.decode( + "0000e9748bbd0c45fc357ffe7c221c7db1ef02f589680d8b0a370b48a669435bde13" + ) + ), + List.of( + new SparseMerkleTreePathStep( + BigInteger.valueOf(69), + HexConverter.decode("76616c756535") + ), + new SparseMerkleTreePathStep( + BigInteger.valueOf(4), + HexConverter.decode( + "8471f8ea3c9a0e50627df4c72d9bd5affbdc12050ee7f4250974ed64949f3b0f" + ) + ), + new SparseMerkleTreePathStep( + BigInteger.valueOf(1), + HexConverter.decode( + "66507538ce0fae31018cfc7b01841b5308e7e44306445710acee947ec4a4b2cd" + ) + ) + ) + ); + + Assertions.assertEquals(new MerkleTreePathVerificationResult(true, true), + path.verify(BigInteger.valueOf(0b100010100))); + Assertions.assertEquals(new MerkleTreePathVerificationResult(true, false), + path.verify(BigInteger.valueOf(0b111))); + } + + @Test + public void testEmptyPathVerification() throws JsonProcessingException { + byte[] cbor = CborSerializer.encodeArray( + DataHash.fromImprint( + HexConverter.decode("00001e54402898172f2948615fb17627733abbd120a85381c624ad060d28321be672") + ).toCbor(), + CborSerializer.encodeArray( + CborSerializer.encodeArray( + CborSerializer.encodeByteString(HexConverter.decode("01")), + CborSerializer.encodeNull() + ), + CborSerializer.encodeArray( + CborSerializer.encodeByteString(HexConverter.decode("01")), + CborSerializer.encodeNull() + ) + ) + ); + + SparseMerkleTreePath path = SparseMerkleTreePath.fromCbor(cbor); + + MerkleTreePathVerificationResult result = path.verify(BigInteger.valueOf(101)); + Assertions.assertTrue(result.isPathValid()); + Assertions.assertFalse(result.isPathIncluded()); + } +} diff --git a/src/test/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePathFixture.java b/src/test/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePathFixture.java new file mode 100644 index 0000000..9d3e9f6 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreePathFixture.java @@ -0,0 +1,20 @@ +package org.unicitylabs.sdk.smt.plain; + +import org.unicitylabs.sdk.crypto.hash.DataHasher; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; + +import java.util.List; + +public class SparseMerkleTreePathFixture { + + public static SparseMerkleTreePath create() { + return new SparseMerkleTreePath( + new DataHasher(HashAlgorithm.SHA256) + .update(new byte[]{0}) + .update(new byte[]{0}) + .digest(), + List.of() + ); + } + +} diff --git a/src/test/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreeTest.java b/src/test/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreeTest.java new file mode 100644 index 0000000..3f02344 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/smt/plain/SparseMerkleTreeTest.java @@ -0,0 +1,183 @@ +package org.unicitylabs.sdk.smt.plain; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.smt.BranchExistsException; +import org.unicitylabs.sdk.smt.LeafOutOfBoundsException; +import org.unicitylabs.sdk.smt.MerkleTreePathVerificationResult; +import org.unicitylabs.sdk.util.HexConverter; + +import java.lang.reflect.Field; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +public class SparseMerkleTreeTest { + + private final SparseMerkleTreeRootNode root = SparseMerkleTreeRootNode.create( + new PendingNodeBranch( + BigInteger.valueOf(0b10), + new PendingNodeBranch( + BigInteger.valueOf(0b10), + new PendingNodeBranch( + BigInteger.valueOf(0b100), + new PendingLeafBranch( + BigInteger.valueOf(0b10000), + HexConverter.decode("76616c75653030303030303030") + ), + new PendingNodeBranch( + BigInteger.valueOf(0b1001), + new PendingLeafBranch( + BigInteger.valueOf(0b10), + HexConverter.decode("76616c75653030303130303030") + ), + new PendingLeafBranch( + BigInteger.valueOf(0b11), + HexConverter.decode("76616c75653030303130303030") + ) + ) + ), + new PendingLeafBranch( + BigInteger.valueOf(0b11), + HexConverter.decode("76616c7565313030") + ) + ), + new PendingLeafBranch( + BigInteger.valueOf(0b1000101), + HexConverter.decode("76616c756530303031303130") + ) + ).finalize(HashAlgorithm.SHA256), + new PendingNodeBranch( + BigInteger.valueOf(0b11), + new PendingNodeBranch( + BigInteger.valueOf(0b1010), + new PendingLeafBranch( + BigInteger.valueOf(0b11110), + HexConverter.decode("76616c75653131313030313031") + ), + new PendingLeafBranch( + BigInteger.valueOf(0b1101), + HexConverter.decode("76616c756531303130313031") + ) + ), + new PendingNodeBranch( + BigInteger.valueOf(0b11), + new PendingLeafBranch( + BigInteger.valueOf(0b10), + HexConverter.decode("76616c7565303131") + ), + new PendingLeafBranch( + BigInteger.valueOf(0b1111011), + HexConverter.decode("76616c75653131313031313131") + ) + ) + ).finalize(HashAlgorithm.SHA256), + HashAlgorithm.SHA256 + ); + + @Test + public void treeShouldBeHalfCalculated() throws Exception { + SparseMerkleTree smt = new SparseMerkleTree(HashAlgorithm.SHA256); + + smt.addLeaf(BigInteger.valueOf(0b10), new byte[]{1, 2, 3}); + smt.calculateRoot(); + smt.addLeaf(BigInteger.valueOf(0b11), new byte[]{1, 2, 3, 4}); + + FinalizedLeafBranch left = new PendingLeafBranch(BigInteger.valueOf(2), + new byte[]{1, 2, 3}).finalize(HashAlgorithm.SHA256); + PendingLeafBranch right = new PendingLeafBranch(BigInteger.valueOf(3), new byte[]{1, 2, 3, 4}); + + Field leftField = SparseMerkleTree.class.getDeclaredField("left"); + leftField.setAccessible(true); + Field rightField = SparseMerkleTree.class.getDeclaredField("right"); + rightField.setAccessible(true); + + Assertions.assertEquals(left, leftField.get(smt)); + Assertions.assertEquals(right, rightField.get(smt)); + } + + @Test + public void shouldVerifyTheTree() throws Exception { + SparseMerkleTree smt = new SparseMerkleTree(HashAlgorithm.SHA256); + Map leaves = Map.ofEntries( + Map.entry(0b110010000, "value00010000"), + Map.entry(0b100000000, "value00000000"), + Map.entry(0b100010000, "value00010000"), + Map.entry(0b111100101, "value11100101"), + Map.entry(0b1100, "value100"), + Map.entry(0b1011, "value011"), + Map.entry(0b111101111, "value11101111"), + Map.entry(0b10001010, "value0001010"), + Map.entry(0b11010101, "value1010101") + ); + for (Map.Entry leaf : leaves.entrySet()) { + smt.addLeaf(BigInteger.valueOf(leaf.getKey()), + leaf.getValue().getBytes(StandardCharsets.UTF_8)); + } + + Assertions.assertThrows(BranchExistsException.class, () -> + smt.addLeaf(BigInteger.valueOf(0b10000000), "OnPath".getBytes(StandardCharsets.UTF_8)) + ); + + Assertions.assertThrows(LeafOutOfBoundsException.class, () -> + smt.addLeaf(BigInteger.valueOf(0b1000000000), + "ThroughLeaf".getBytes(StandardCharsets.UTF_8)) + ); + + Assertions.assertEquals(smt.calculateRoot(), this.root); + } + + @Test + public void shouldGetWorkingPath() throws Exception { + SparseMerkleTree smt = new SparseMerkleTree(HashAlgorithm.SHA256); + Map leaves = Map.ofEntries( + Map.entry(0b110010000, "value00010000"), + Map.entry(0b100000000, "value00000000"), + Map.entry(0b100010000, "value00010000"), + Map.entry(0b111100101, "value11100101"), + Map.entry(0b1100, "value100"), + Map.entry(0b1011, "value011"), + Map.entry(0b111101111, "value11101111"), + Map.entry(0b10001010, "value0001010"), + Map.entry(0b11010101, "value1010101") + ); + for (Map.Entry leaf : leaves.entrySet()) { + smt.addLeaf(BigInteger.valueOf(leaf.getKey()), + leaf.getValue().getBytes(StandardCharsets.UTF_8)); + } + SparseMerkleTreeRootNode root = smt.calculateRoot(); + + SparseMerkleTreePath path = root.getPath(BigInteger.valueOf(0b11010)); + MerkleTreePathVerificationResult result = path.verify(BigInteger.valueOf(0b11010)); + Assertions.assertFalse(result.isPathIncluded()); + Assertions.assertTrue(result.isPathValid()); + Assertions.assertFalse(result.isSuccessful()); + + path = root.getPath(BigInteger.valueOf(0b110010000)); + result = path.verify(BigInteger.valueOf(0b110010000)); + Assertions.assertTrue(result.isPathIncluded()); + Assertions.assertTrue(result.isPathValid()); + Assertions.assertTrue(result.isSuccessful()); + + path = root.getPath(BigInteger.valueOf(0b110010000)); + result = path.verify(BigInteger.valueOf(0b11010)); + Assertions.assertFalse(result.isPathIncluded()); + Assertions.assertTrue(result.isPathValid()); + Assertions.assertFalse(result.isSuccessful()); + + path = root.getPath(BigInteger.valueOf(0b111100101)); + result = path.verify(BigInteger.valueOf(0b111100101)); + Assertions.assertTrue(result.isPathIncluded()); + Assertions.assertTrue(result.isPathValid()); + Assertions.assertTrue(result.isSuccessful()); + + SparseMerkleTree emptyTree = new SparseMerkleTree(HashAlgorithm.SHA256); + SparseMerkleTreeRootNode emptyRoot = emptyTree.calculateRoot(); + path = emptyRoot.getPath(BigInteger.valueOf(0b100)); + result = path.verify(BigInteger.valueOf(0b10)); + Assertions.assertFalse(result.isPathIncluded()); + Assertions.assertTrue(result.isPathValid()); + Assertions.assertFalse(result.isSuccessful()); + } +} diff --git a/src/test/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreeTest.java b/src/test/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreeTest.java new file mode 100644 index 0000000..2dfc635 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/smt/sum/SparseMerkleSumTreeTest.java @@ -0,0 +1,90 @@ +package org.unicitylabs.sdk.smt.sum; + + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.smt.MerkleTreePathVerificationResult; +import org.unicitylabs.sdk.smt.sum.SparseMerkleSumTree.LeafValue; +import org.unicitylabs.sdk.util.HexConverter; + +import java.math.BigInteger; +import java.util.Map; +import java.util.Map.Entry; + +class SparseMerkleSumTreeTest { + + @Test + void shouldAgreeWithSpecExamples() throws Exception { + SparseMerkleSumTree treeLeftOnly = new SparseMerkleSumTree(HashAlgorithm.SHA256); + treeLeftOnly.addLeaf(new BigInteger("100", 2), new LeafValue("a".getBytes(), BigInteger.valueOf(1))); + SparseMerkleSumTreeRootNode rootLeftOnly = treeLeftOnly.calculateRoot(); + Assertions.assertEquals(BigInteger.valueOf(1), rootLeftOnly.getValue()); + Assertions.assertArrayEquals(HexConverter.decode("5822" + "0000" + "34e0cf342d70c0d10e3ba481f72db532ecfd723afa3c25812a4bef61b5198d0b"), rootLeftOnly.getRootHash().toCbor()); + + SparseMerkleSumTree treeRightOnly = new SparseMerkleSumTree(HashAlgorithm.SHA256); + treeRightOnly.addLeaf(new BigInteger("111", 2), new LeafValue("b".getBytes(), BigInteger.valueOf(2))); + SparseMerkleSumTreeRootNode rootRightOnly = treeRightOnly.calculateRoot(); + Assertions.assertEquals(BigInteger.valueOf(2), rootRightOnly.getValue()); + Assertions.assertArrayEquals(HexConverter.decode("5822" + "0000" + "da47d1cda8dab5159b2bed1ea27c3d24ed990989fac3c62ace05273fea51f958"), rootRightOnly.getRootHash().toCbor()); + + SparseMerkleSumTree treeFourLeaves = new SparseMerkleSumTree(HashAlgorithm.SHA256); + treeFourLeaves.addLeaf(new BigInteger("1000", 2), new LeafValue("a".getBytes(), BigInteger.valueOf(1))); + treeFourLeaves.addLeaf(new BigInteger("1100", 2), new LeafValue("b".getBytes(), BigInteger.valueOf(2))); + treeFourLeaves.addLeaf(new BigInteger("1011", 2), new LeafValue("c".getBytes(), BigInteger.valueOf(3))); + treeFourLeaves.addLeaf(new BigInteger("1111", 2), new LeafValue("d".getBytes(), BigInteger.valueOf(4))); + SparseMerkleSumTreeRootNode rootFourLeaves = treeFourLeaves.calculateRoot(); + Assertions.assertEquals(BigInteger.valueOf(10), rootFourLeaves.getValue()); + Assertions.assertArrayEquals(HexConverter.decode("5822" + "0000" + "adfefa7c86b18d1216eece9fe0ce82ca58fd8cf482305c3c4e1a0a1361dc9d15"), rootFourLeaves.getRootHash().toCbor()); + } + + @Test + void shouldBuildTreeWithNumericValues() throws Exception { + Map leaves = Map.of( + new BigInteger("1000", 2), new LeafValue("left-1".getBytes(), BigInteger.valueOf(10)), + new BigInteger("1001", 2), new LeafValue("right-1".getBytes(), BigInteger.valueOf(20)), + new BigInteger("1010", 2), new LeafValue("left-2".getBytes(), BigInteger.valueOf(30)), + new BigInteger("1011", 2), new LeafValue("right-2".getBytes(), BigInteger.valueOf(40)) + ); + + SparseMerkleSumTree tree = new SparseMerkleSumTree(HashAlgorithm.SHA256); + for (Entry entry : leaves.entrySet()) { + tree.addLeaf(entry.getKey(), entry.getValue()); + } + + SparseMerkleSumTreeRootNode root = tree.calculateRoot(); + Assertions.assertEquals(BigInteger.valueOf(100), root.getValue()); + + for (Entry entry : leaves.entrySet()) { + SparseMerkleSumTreePath path = root.getPath(entry.getKey()); + MerkleTreePathVerificationResult verificationResult = path.verify(entry.getKey()); + Assertions.assertTrue(verificationResult.isPathIncluded()); + Assertions.assertTrue(verificationResult.isPathValid()); + Assertions.assertTrue(verificationResult.isSuccessful()); + + Assertions.assertEquals(root.getRootHash(), path.getRootHash()); + Assertions.assertArrayEquals( + entry.getValue().getValue(), + path.getSteps().get(0).getData().orElse(null) + ); + Assertions.assertEquals( + entry.getValue().getCounter(), + path.getSteps().get(0).getValue() + ); + } + + tree.addLeaf(new BigInteger("1110", 2), new LeafValue(new byte[32], BigInteger.valueOf(100))); + root = tree.calculateRoot(); + Assertions.assertEquals(BigInteger.valueOf(200), root.getValue()); + } + + @Test + void shouldThrowErrorOnNonPositivePathOrSum() { + SparseMerkleSumTree tree = new SparseMerkleSumTree(HashAlgorithm.SHA256); + Assertions.assertThrows(IllegalArgumentException.class, + () -> tree.addLeaf(BigInteger.valueOf(-1), + new LeafValue(new byte[32], BigInteger.valueOf(100)))); + Assertions.assertThrows(IllegalArgumentException.class, + () -> tree.addLeaf(BigInteger.ONE, new LeafValue(new byte[32], BigInteger.valueOf(-1)))); + } +} diff --git a/src/test/java/org/unicitylabs/sdk/token/TokenIdTest.java b/src/test/java/org/unicitylabs/sdk/token/TokenIdTest.java index 9335be6..9f3172d 100644 --- a/src/test/java/org/unicitylabs/sdk/token/TokenIdTest.java +++ b/src/test/java/org/unicitylabs/sdk/token/TokenIdTest.java @@ -1,14 +1,20 @@ package org.unicitylabs.sdk.token; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.unicitylabs.sdk.transaction.TokenId; + +import java.math.BigInteger; class TokenIdTest { - @Test - void toBigInt() { - TokenId tokenId = new TokenId(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}); -// BigInteger expected = new BigInteger(1, tokenId.toCBOR()); -// assertEquals(expected, tokenId.toBigInt()); - } + @Test + void toBigInt() { + TokenId tokenId = new TokenId(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}); + Assertions.assertEquals( + new BigInteger("116247956593636886635080929986192315456660021052790183176621769190627866451744"), + tokenId.toBitString().toBigInteger() + ); + } } diff --git a/src/test/java/org/unicitylabs/sdk/token/TokenSplitBuilderTest.java b/src/test/java/org/unicitylabs/sdk/token/TokenSplitBuilderTest.java deleted file mode 100644 index 19e81d6..0000000 --- a/src/test/java/org/unicitylabs/sdk/token/TokenSplitBuilderTest.java +++ /dev/null @@ -1,185 +0,0 @@ -package org.unicitylabs.sdk.token; - -import java.io.IOException; -import java.math.BigInteger; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.unicitylabs.sdk.bft.UnicityCertificate; -import org.unicitylabs.sdk.bft.UnicityCertificateUtils; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.mtree.BranchExistsException; -import org.unicitylabs.sdk.mtree.LeafOutOfBoundsException; -import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreePathFixture; -import org.unicitylabs.sdk.predicate.Predicate; -import org.unicitylabs.sdk.predicate.embedded.MaskedPredicate; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.fungible.CoinId; -import org.unicitylabs.sdk.token.fungible.TokenCoinData; -import org.unicitylabs.sdk.transaction.InclusionProofFixture; -import org.unicitylabs.sdk.transaction.MintTransaction; -import org.unicitylabs.sdk.transaction.MintTransactionFixture; -import org.unicitylabs.sdk.transaction.split.TokenSplitBuilder; -import org.unicitylabs.sdk.verification.VerificationException; - -public class TokenSplitBuilderTest { - - private Token createToken(TokenCoinData coinData) throws VerificationException { - SigningService signingService = new SigningService(SigningService.generatePrivateKey()); - UnicityCertificate unicityCertificate = UnicityCertificateUtils.generateCertificate( - signingService, DataHash.fromImprint(new byte[34])); - - TokenId tokenId = new TokenId(new byte[10]); - TokenType tokenType = new TokenType(new byte[10]); - - Predicate predicate = new MaskedPredicate( - tokenId, - tokenType, - new byte[32], - "secp256k1", - HashAlgorithm.SHA256, - new byte[32] - ); - - return new Token<>( - new TokenState(predicate, null), - MintTransactionFixture.create( - new MintTransaction.Data<>( - tokenId, - tokenType, - null, - coinData, - predicate.getReference().toAddress(), - new byte[20], - null, - null - ), - InclusionProofFixture.create( - SparseMerkleTreePathFixture.create(), - null, - null, - unicityCertificate - ) - ), - List.of(), - List.of() - ); - } - - @Test - public void testTokenSplitIntoMultipleTokens() - throws LeafOutOfBoundsException, BranchExistsException, VerificationException, IOException { - Token token = this.createToken( - new TokenCoinData( - Map.of( - new CoinId("coin1".getBytes()), - BigInteger.valueOf(100) - ) - )); - - Predicate predicate = new MaskedPredicate( - token.getId(), - token.getType(), - new byte[32], - "secp256k1", - HashAlgorithm.SHA256, - new byte[32] - ); - - TokenSplitBuilder builder = new TokenSplitBuilder(); - - Assertions.assertThrows(IllegalArgumentException.class, - () -> builder.createToken( - new TokenId(UUID.randomUUID().toString().getBytes()), - token.getType(), - null, - new TokenCoinData(Map.of()), - predicate.getReference().toAddress(), - new byte[20], - null - ) - ); - - builder.createToken( - new TokenId(UUID.randomUUID().toString().getBytes()), - token.getType(), - null, - new TokenCoinData( - Map.of( - new CoinId("coin1".getBytes()), - BigInteger.valueOf(50) - ) - ), - predicate.getReference().toAddress(), - new byte[20], - null - ); - - Exception exception = Assertions.assertThrows( - IllegalArgumentException.class, - () -> builder.build(token) - ); - - Assertions.assertEquals("Token contained 100 CoinId{bytes=636f696e31} coins, but tree has 50", - exception.getMessage()); - - builder.createToken( - new TokenId(UUID.randomUUID().toString().getBytes()), - token.getType(), - null, - new TokenCoinData( - Map.of( - new CoinId("coin1".getBytes()), - BigInteger.valueOf(50) - ) - ), - predicate.getReference().toAddress(), - new byte[20], - null - ); - - builder.build(token); - } - - @Test - public void testTokenSplitUnknownSplitCoin() throws VerificationException, IOException { - Token token = this.createToken(null); - - Predicate predicate = new MaskedPredicate( - token.getId(), - token.getType(), - new byte[32], - "secp256k1", - HashAlgorithm.SHA256, - new byte[32] - ); - - Exception exception = Assertions.assertThrows( - IllegalArgumentException.class, () -> { - TokenSplitBuilder builder = new TokenSplitBuilder(); - builder - .createToken( - new TokenId(UUID.randomUUID().toString().getBytes()), - token.getType(), - null, - new TokenCoinData( - Map.of( - new CoinId("coin1".getBytes()), - BigInteger.valueOf(100) - ) - ), - predicate.getReference().toAddress(), - new byte[20], - null - ) - .build(token); - }); - Assertions.assertEquals( - "Token has different number of coins than expected", - exception.getMessage() - ); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/token/TokenTest.java b/src/test/java/org/unicitylabs/sdk/token/TokenTest.java deleted file mode 100644 index f0fe1d1..0000000 --- a/src/test/java/org/unicitylabs/sdk/token/TokenTest.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.unicitylabs.sdk.token; - -import java.io.IOException; -import java.math.BigInteger; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.unicitylabs.sdk.address.DirectAddress; -import org.unicitylabs.sdk.bft.UnicityCertificate; -import org.unicitylabs.sdk.bft.UnicityCertificateUtils; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreePathFixture; -import org.unicitylabs.sdk.predicate.embedded.MaskedPredicate; -import org.unicitylabs.sdk.serializer.UnicityObjectMapper; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.fungible.CoinId; -import org.unicitylabs.sdk.token.fungible.TokenCoinData; -import org.unicitylabs.sdk.transaction.InclusionProofFixture; -import org.unicitylabs.sdk.transaction.MintTransaction; -import org.unicitylabs.sdk.transaction.MintTransactionFixture; -import org.unicitylabs.sdk.utils.TestUtils; -import org.unicitylabs.sdk.verification.VerificationException; - -public class TokenTest { - - @Test - public void testJsonSerialization() throws IOException, VerificationException { - SigningService signingService = new SigningService(SigningService.generatePrivateKey()); - UnicityCertificate unicityCertificate = UnicityCertificateUtils.generateCertificate( - signingService, DataHash.fromImprint(new byte[34])); - - MintTransaction.Data genesisData = new MintTransaction.Data<>( - new TokenId(TestUtils.randomBytes(32)), - new TokenType(TestUtils.randomBytes(32)), - TestUtils.randomBytes(10), - new TokenCoinData( - Map.of( - new CoinId(TestUtils.randomBytes(10)), BigInteger.valueOf(100), - new CoinId(TestUtils.randomBytes(4)), BigInteger.valueOf(3) - ) - ), - DirectAddress.create(new DataHash(HashAlgorithm.SHA256, TestUtils.randomBytes(32))), - TestUtils.randomBytes(32), - null, - null - ); - - byte[] nametagNonce = TestUtils.randomBytes(32); - MintTransaction.NametagData nametagGenesisData = new MintTransaction.NametagData( - UUID.randomUUID().toString(), - new TokenType(TestUtils.randomBytes(32)), - DirectAddress.create(new DataHash(HashAlgorithm.SHA256, TestUtils.randomBytes(32))), - TestUtils.randomBytes(32), - DirectAddress.create(new DataHash(HashAlgorithm.SHA256, TestUtils.randomBytes(32))) - ); - - Token nametagToken = new Token<>( - new TokenState( - MaskedPredicate.create( - nametagGenesisData.getTokenId(), - nametagGenesisData.getTokenType(), - SigningService.createFromMaskedSecret(TestUtils.randomBytes(32), nametagNonce), - HashAlgorithm.SHA256, - nametagNonce), - null), - MintTransactionFixture.create( - nametagGenesisData, - InclusionProofFixture.create( - SparseMerkleTreePathFixture.create(), - null, - null, - unicityCertificate - ) - ), - List.of(), - List.of() - ); - - Token token = new Token<>( - new TokenState( - MaskedPredicate.create( - genesisData.getTokenId(), - genesisData.getTokenType(), - SigningService.createFromMaskedSecret( - TestUtils.randomBytes(32), - genesisData.getTokenId().getBytes() - ), - HashAlgorithm.SHA256, - TestUtils.randomBytes(24)), - null - ), - MintTransactionFixture.create( - genesisData, - InclusionProofFixture.create( - SparseMerkleTreePathFixture.create(), - null, - null, - unicityCertificate - ) - ), - List.of(), - List.of(nametagToken) - ); - - Assertions.assertEquals(token, - UnicityObjectMapper.JSON.readValue( - UnicityObjectMapper.JSON.writeValueAsString(token), - Token.class)); - } - -} diff --git a/src/test/java/org/unicitylabs/sdk/transaction/CommitmentTest.java b/src/test/java/org/unicitylabs/sdk/transaction/CommitmentTest.java deleted file mode 100644 index 102866b..0000000 --- a/src/test/java/org/unicitylabs/sdk/transaction/CommitmentTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.unicitylabs.sdk.transaction; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -import java.io.IOException; -import java.math.BigInteger; -import java.util.Map; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.predicate.embedded.MaskedPredicateReference; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.token.TokenType; -import org.unicitylabs.sdk.token.fungible.CoinId; -import org.unicitylabs.sdk.token.fungible.TokenCoinData; -import org.unicitylabs.sdk.util.HexConverter; - -public class CommitmentTest { - - @Test - public void testJsonSerialization() throws IOException { - SigningService signingService = new SigningService( - HexConverter.decode("0000000000000000000000000000000000000000000000000000000000000001")); - TokenType tokenType = new TokenType(new byte[32]); - byte[] nonce = new byte[64]; - - MaskedPredicateReference predicateReference = MaskedPredicateReference.create(tokenType, - signingService.getAlgorithm(), signingService.getPublicKey(), HashAlgorithm.SHA256, nonce); - MintTransaction.Data transactionData = new MintTransaction.Data<>( - new TokenId(new byte[32]), - tokenType, - new byte[5], - new TokenCoinData(Map.of( - new CoinId(new byte[10]), BigInteger.ONE, - new CoinId(new byte[5]), BigInteger.TEN - )), - predicateReference.toAddress(), - new byte[10], - new DataHash(HashAlgorithm.SHA256, new byte[32]), - null - ); - MintCommitment commitment = MintCommitment.create(transactionData); - - ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(new Jdk8Module()); - - Assertions.assertEquals(commitment, - mapper.readValue( - mapper.writeValueAsString(commitment), - MintCommitment.class - ) - ); - } - -} diff --git a/src/test/java/org/unicitylabs/sdk/transaction/InclusionProofFixture.java b/src/test/java/org/unicitylabs/sdk/transaction/InclusionProofFixture.java deleted file mode 100644 index 51bddf1..0000000 --- a/src/test/java/org/unicitylabs/sdk/transaction/InclusionProofFixture.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.unicitylabs.sdk.transaction; - -import org.unicitylabs.sdk.api.Authenticator; -import org.unicitylabs.sdk.bft.UnicityCertificate; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreePath; - -public class InclusionProofFixture { - public static InclusionProof create( - SparseMerkleTreePath path, - Authenticator authenticator, - DataHash transactionHash, - UnicityCertificate certificate - ) { - return new InclusionProof(path, authenticator, transactionHash, certificate); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/transaction/InclusionProofTest.java b/src/test/java/org/unicitylabs/sdk/transaction/InclusionProofTest.java deleted file mode 100644 index 031de46..0000000 --- a/src/test/java/org/unicitylabs/sdk/transaction/InclusionProofTest.java +++ /dev/null @@ -1,150 +0,0 @@ -package org.unicitylabs.sdk.transaction; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.unicitylabs.sdk.api.Authenticator; -import org.unicitylabs.sdk.api.LeafValue; -import org.unicitylabs.sdk.api.RequestId; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.bft.RootTrustBaseUtils; -import org.unicitylabs.sdk.bft.UnicityCertificate; -import org.unicitylabs.sdk.bft.UnicityCertificateUtils; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.mtree.plain.SparseMerkleTree; -import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreePath; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.util.HexConverter; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class InclusionProofTest { - - RequestId requestId; - DataHash transactionHash; - SparseMerkleTreePath merkleTreePath; - Authenticator authenticator; - RootTrustBase trustBase; - UnicityCertificate unicityCertificate; - - @BeforeAll - public void createMerkleTreePath() throws Exception { - SigningService signingService = new SigningService( - HexConverter.decode("0000000000000000000000000000000000000000000000000000000000000001")); - - transactionHash = new DataHash(HashAlgorithm.SHA256, new byte[32]); - authenticator = Authenticator.create(signingService, transactionHash, - new DataHash(HashAlgorithm.SHA256, new byte[32])); - - LeafValue leaf = LeafValue.create(authenticator, transactionHash); - requestId = RequestId.create(signingService.getPublicKey(), authenticator.getStateHash()); - - SparseMerkleTree smt = new SparseMerkleTree(HashAlgorithm.SHA256); - smt.addLeaf(requestId.toBitString().toBigInteger(), leaf.getBytes()); - - merkleTreePath = smt.calculateRoot().getPath(requestId.toBitString().toBigInteger()); - SigningService ucSigningService = new SigningService(SigningService.generatePrivateKey()); - trustBase = RootTrustBaseUtils.generateRootTrustBase(ucSigningService.getPublicKey()); - unicityCertificate = UnicityCertificateUtils.generateCertificate(ucSigningService, - merkleTreePath.getRootHash()); - } - - @Test - public void testJsonSerialization() throws Exception { - InclusionProof inclusionProof = new InclusionProof( - merkleTreePath, - authenticator, - transactionHash, - unicityCertificate - ); - Assertions.assertEquals(inclusionProof, InclusionProof.fromJson(inclusionProof.toJson())); - } - - @Test - public void testStructure() { - Assertions.assertThrows(IllegalArgumentException.class, - () -> new InclusionProof( - merkleTreePath, - authenticator, - null, - unicityCertificate - ) - ); - Assertions.assertThrows(IllegalArgumentException.class, - () -> new InclusionProof( - merkleTreePath, - null, - transactionHash, - unicityCertificate - ) - ); - Assertions.assertThrows(NullPointerException.class, - () -> new InclusionProof( - null, - authenticator, - transactionHash, - unicityCertificate - ) - ); - Assertions.assertThrows(NullPointerException.class, - () -> new InclusionProof( - merkleTreePath, - authenticator, - transactionHash, - null - ) - ); - Assertions.assertInstanceOf(InclusionProof.class, - new InclusionProof( - merkleTreePath, - authenticator, - transactionHash, - unicityCertificate - ) - ); - Assertions.assertInstanceOf(InclusionProof.class, - new InclusionProof( - merkleTreePath, - null, - null, - unicityCertificate - ) - ); - } - - @Test - public void testItVerifies() { - InclusionProof inclusionProof = new InclusionProof( - this.merkleTreePath, - this.authenticator, - this.transactionHash, - this.unicityCertificate - ); - Assertions.assertEquals( - InclusionProofVerificationStatus.OK, - inclusionProof.verify(this.requestId, this.trustBase) - ); - Assertions.assertEquals(InclusionProofVerificationStatus.PATH_NOT_INCLUDED, - inclusionProof.verify( - RequestId.create(new byte[32], new DataHash(HashAlgorithm.SHA256, new byte[32])), - this.trustBase - ) - ); - - InclusionProof invalidInclusionProof = new InclusionProof( - this.merkleTreePath, - this.authenticator, - new DataHash( - HashAlgorithm.SHA224, - HexConverter.decode("FF000000000000000000000000000000000000000000000000000000000000FF") - ), - this.unicityCertificate - ); - - Assertions.assertEquals( - InclusionProofVerificationStatus.NOT_AUTHENTICATED, - invalidInclusionProof.verify(this.requestId, this.trustBase) - ); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/transaction/MintTransactionFixture.java b/src/test/java/org/unicitylabs/sdk/transaction/MintTransactionFixture.java deleted file mode 100644 index d0cdfe2..0000000 --- a/src/test/java/org/unicitylabs/sdk/transaction/MintTransactionFixture.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.unicitylabs.sdk.transaction; - -import org.unicitylabs.sdk.api.Authenticator; -import org.unicitylabs.sdk.bft.UnicityCertificate; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreePath; - -public class MintTransactionFixture { - public static MintTransaction create( - MintTransaction.Data data, - InclusionProof inclusionProof - ) { - return new MintTransaction<>(data, inclusionProof); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/transaction/verification/ShardIdMatchesStateIdRuleTest.java b/src/test/java/org/unicitylabs/sdk/transaction/verification/ShardIdMatchesStateIdRuleTest.java new file mode 100644 index 0000000..d14f927 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/transaction/verification/ShardIdMatchesStateIdRuleTest.java @@ -0,0 +1,94 @@ +package org.unicitylabs.sdk.transaction.verification; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.unicitylabs.sdk.api.StateId; +import org.unicitylabs.sdk.api.bft.ShardTreeCertificate; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.util.HexConverter; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +public class ShardIdMatchesStateIdRuleTest { + + /** 32 bytes of 0xAB — a valid SHA-256-shaped state id. */ + private static final byte[] STATE_ID_BYTES = + HexConverter.decode("ABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABAB"); + + @Test + public void verifyFailsWhenShardTreeCertificateIsNull() { + StateId stateId = StateId.fromCbor(CborSerializer.encodeByteString(STATE_ID_BYTES)); + + VerificationResult result = + ShardIdMatchesStateIdRule.verify(stateId, null); + + Assertions.assertEquals(VerificationStatus.FAIL, result.getStatus()); + Assertions.assertEquals("Shard tree certificate is missing.", result.getMessage()); + } + + @Test + public void verifyFailsWhenStateIdIsNull() { + // Empty shard id (length 0). + ShardTreeCertificate certificate = ShardTreeCertificate.fromCbor( + HexConverter.decode("d9985b8301418080")); + + VerificationResult result = + ShardIdMatchesStateIdRule.verify(null, certificate); + + Assertions.assertEquals(VerificationStatus.FAIL, result.getStatus()); + Assertions.assertEquals("State ID is missing.", result.getMessage()); + } + + @Test + public void verifyPassesWhenShardIdIsEmpty() { + StateId stateId = StateId.fromCbor(CborSerializer.encodeByteString(STATE_ID_BYTES)); + ShardTreeCertificate certificate = ShardTreeCertificate.fromCbor( + HexConverter.decode("d9985b8301418080")); + + VerificationResult result = + ShardIdMatchesStateIdRule.verify(stateId, certificate); + + Assertions.assertEquals(VerificationStatus.OK, result.getStatus()); + } + + @Test + public void verifyPassesWhenShardIdIsPrefixOfStateId() { + // Shard id of length 8 with bits=[0xAB]. + StateId stateId = StateId.fromCbor(CborSerializer.encodeByteString(STATE_ID_BYTES)); + ShardTreeCertificate certificate = ShardTreeCertificate.fromCbor( + HexConverter.decode("d9985b830142ab8080")); + + VerificationResult result = + ShardIdMatchesStateIdRule.verify(stateId, certificate); + + Assertions.assertEquals(VerificationStatus.OK, result.getStatus()); + } + + @Test + public void verifyFailsWhenShardIdIsNotPrefixOfStateId() { + // Shard id of length 8 with bits=[0x12] — does not match. + StateId stateId = StateId.fromCbor(CborSerializer.encodeByteString(STATE_ID_BYTES)); + ShardTreeCertificate certificate = ShardTreeCertificate.fromCbor( + HexConverter.decode("d9985b830142128080")); + + VerificationResult result = + ShardIdMatchesStateIdRule.verify(stateId, certificate); + + Assertions.assertEquals(VerificationStatus.FAIL, result.getStatus()); + } + + @Test + public void verifyFailsWhenStateIdIsShorterThanShardId() { + // SHA-256 state id is 32 bytes (256 bits). Use a 264-bit shard id (33 full bytes 0xAB + + // 0x80 end marker), so the state id has fewer bits than the shard id requires. + StateId stateId = StateId.fromCbor(CborSerializer.encodeByteString(STATE_ID_BYTES)); + ShardTreeCertificate certificate = ShardTreeCertificate.fromCbor( + HexConverter.decode( + "d9985b83015822ababababababababababababababababababababababababababababababababab8080")); + + VerificationResult result = + ShardIdMatchesStateIdRule.verify(stateId, certificate); + + Assertions.assertEquals(VerificationStatus.FAIL, result.getStatus()); + } +} diff --git a/src/test/java/org/unicitylabs/sdk/utils/TestTokenData.java b/src/test/java/org/unicitylabs/sdk/utils/TestTokenData.java deleted file mode 100644 index 9c6abd6..0000000 --- a/src/test/java/org/unicitylabs/sdk/utils/TestTokenData.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.unicitylabs.sdk.utils; - -import org.unicitylabs.sdk.util.HexConverter; - -import java.util.Arrays; - -/** - * Test token data implementation - */ -public class TestTokenData { - private final byte[] data; - - public TestTokenData(byte[] data) { - this.data = Arrays.copyOf(data, data.length); - } - - public byte[] getData() { - return Arrays.copyOf(data, data.length); - } - - @Override - public String toString() { - return "TestTokenData: " + HexConverter.encode(data); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TestTokenData that = (TestTokenData) o; - return Arrays.equals(data, that.data); - } - - @Override - public int hashCode() { - return Arrays.hashCode(data); - } -} diff --git a/src/test/java/org/unicitylabs/sdk/utils/TestUtils.java b/src/test/java/org/unicitylabs/sdk/utils/TestUtils.java deleted file mode 100644 index bc35e1f..0000000 --- a/src/test/java/org/unicitylabs/sdk/utils/TestUtils.java +++ /dev/null @@ -1,335 +0,0 @@ -package org.unicitylabs.sdk.utils; - -import java.math.BigInteger; -import java.nio.charset.StandardCharsets; -import java.security.SecureRandom; -import java.util.List; -import java.util.Map; - -import org.unicitylabs.sdk.StateTransitionClient; -import org.unicitylabs.sdk.address.Address; -import org.unicitylabs.sdk.api.Authenticator; -import org.unicitylabs.sdk.api.RequestId; -import org.unicitylabs.sdk.api.SubmitCommitmentResponse; -import org.unicitylabs.sdk.api.SubmitCommitmentStatus; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.DataHasher; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.predicate.PredicateEngineService; -import org.unicitylabs.sdk.predicate.embedded.MaskedPredicate; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.token.TokenState; -import org.unicitylabs.sdk.token.TokenType; -import org.unicitylabs.sdk.token.fungible.CoinId; -import org.unicitylabs.sdk.token.fungible.TokenCoinData; -import org.unicitylabs.sdk.transaction.*; -import org.unicitylabs.sdk.util.InclusionProofUtils; - -/** - * Utility methods for tests. - */ -public class TestUtils { - - private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - private static final SecureRandom RANDOM = new SecureRandom(); - - /** - * Generate random bytes of specified length. - */ - public static byte[] randomBytes(int length) { - byte[] bytes = new byte[length]; - RANDOM.nextBytes(bytes); - return bytes; - } - - /** - * Generate a random coin amount between 10 and 99. - */ - public static BigInteger randomCoinAmount() { - return BigInteger.valueOf(10 + RANDOM.nextInt(90)); - } - - /** - * Create random coin data with specified number of coins. - */ - public static TokenCoinData randomCoinData(int numCoins) { - Map coins = new java.util.HashMap<>(); - for (int i = 0; i < numCoins; i++) { - coins.put(new CoinId(randomBytes(32)), randomCoinAmount()); - } - return new TokenCoinData(coins); - } - - private static final SecureRandom SECURE_RANDOM = new SecureRandom(); - - /** - * Creates a token mint commitment and submits it to the client - */ - public static Token mintTokenForUser( - StateTransitionClient client, - SigningService signingService, - byte[] nonce, - TokenId tokenId, - TokenType tokenType, - TokenCoinData coinData, - RootTrustBase trustBase - ) throws Exception { - - MaskedPredicate predicate = MaskedPredicate.create(tokenId, tokenType, signingService, HashAlgorithm.SHA256, nonce); - Address address = predicate.getReference().toAddress(); - TokenState tokenState = new TokenState(predicate, null); - - MintCommitment mintCommitment = MintCommitment.create( - new MintTransaction.Data<>( - tokenId, - tokenType, - new TestTokenData(randomBytes(32)).getData(), - coinData, - address, - randomBytes(5), - null, - null - ) - ); - - SubmitCommitmentResponse response = client.submitCommitment(mintCommitment).get(); - if (response.getStatus() != SubmitCommitmentStatus.SUCCESS) { - throw new Exception("Failed to submit mint commitment: " + response.getStatus()); - } - - InclusionProof inclusionProof = InclusionProofUtils - .waitInclusionProof( - client, - trustBase, - mintCommitment - ).get(); - return Token.create( - trustBase, - tokenState, - mintCommitment.toTransaction(inclusionProof) - ); - } - - /** - * Transfers a token from one user to another - */ - public static Token transferToken( - StateTransitionClient client, - Token sourceToken, - SigningService fromSigningService, - SigningService toSigningService, - byte[] toNonce, - Address toAddress, - byte[] customData, - List> additionalTokens, - RootTrustBase trustBase - ) throws Exception { - - // Create data hash if custom data provided - DataHash dataHash = null; - if (customData != null) { - dataHash = hashData(customData); - } - - // Submit transfer commitment - TransferCommitment transferCommitment = TransferCommitment.create( - sourceToken, - toAddress, - randomBytes(32), - dataHash, - null, - fromSigningService - ); - - SubmitCommitmentResponse response = client.submitCommitment(transferCommitment).get(); - if (response.getStatus() != SubmitCommitmentStatus.SUCCESS) { - throw new Exception("Failed to submit transfer commitment: " + response.getStatus()); - } - - // Wait for inclusion proof - InclusionProof inclusionProof = InclusionProofUtils.waitInclusionProof(client, trustBase, transferCommitment).get(); - TransferTransaction transferTransaction = transferCommitment.toTransaction(inclusionProof); - - // Create predicate for recipient - MaskedPredicate toPredicate = MaskedPredicate - .create( - sourceToken.getId(), - sourceToken.getType(), - toSigningService, - HashAlgorithm.SHA256, - toNonce - ); - - // Finalize transaction - return client.finalizeTransaction( - trustBase, - sourceToken, - new TokenState(toPredicate, customData), - transferTransaction - ); - } - - /** - * Creates random coin data with specified number of coins - */ - public static TokenCoinData createRandomCoinData(int coinCount) { - Map coins = new java.util.HashMap<>(); - for (int i = 0; i < coinCount; i++) { - CoinId coinId = new CoinId(randomBytes(32)); - BigInteger value = BigInteger.valueOf(SECURE_RANDOM.nextInt(1000) + 100); // Random value between 100-1099 - coins.put(coinId, value); - } - return new TokenCoinData(coins); - } - - /** - * Generates random bytes of specified length - */ - public static byte[] generateRandomBytes(int length) { - byte[] bytes = new byte[length]; - SECURE_RANDOM.nextBytes(bytes); - return bytes; - } - - /** - * Creates a hash of the provided data - */ - public static DataHash hashData(byte[] data) { - return new DataHasher(HashAlgorithm.SHA256).update(data).digest(); - } - - /** - * Creates a hash of string data - */ - public static DataHash hashData(String data) { - return hashData(data.getBytes(StandardCharsets.UTF_8)); - } - - /** - * Creates a signing service from a user name and optional nonce - */ - public static SigningService createSigningServiceForUser(String userName, byte[] nonce) { - byte[] secret = userName.getBytes(StandardCharsets.UTF_8); - return SigningService.createFromMaskedSecret(secret, nonce); - } - - /** - * Sets up a user with signing service and nonce in the provided maps - */ - public static void setupUser(String userName, - Map userSigningServices, - Map userNonces, - Map userSecret) { - byte[] secret = userName.getBytes(StandardCharsets.UTF_8); - byte[] nonce = generateRandomBytes(32); - SigningService signingService = SigningService.createFromMaskedSecret(secret, nonce); - - userSigningServices.put(userName, signingService); - userNonces.put(userName, nonce); - userSecret.put(userName,secret); - } - - /** - * Validates that a token is properly owned by a signing service - */ - public static boolean validateTokenOwnership(Token token, SigningService signingService, RootTrustBase trustBase) { - if (!token.verify(trustBase).isSuccessful()) { - return false; - } - return PredicateEngineService.createPredicate(token.getState().getPredicate()).isOwner(signingService.getPublicKey()); - } - - public static RequestId createRequestId(SigningService signingService, DataHash stateHash) { - return RequestId.create(signingService.getPublicKey(), stateHash); - } - - public static Authenticator createAuthenticator(SigningService signingService, DataHash txDataHash, DataHash stateHash) { - return Authenticator.create(signingService, txDataHash, stateHash); - } - - /** - * Generates a random token ID - */ - public static TokenId generateRandomTokenId() { - return new TokenId(randomBytes(32)); - } - - /** - * Generates a random token type - */ - public static TokenType generateRandomTokenType() { - return new TokenType(randomBytes(32)); - } - - /** - * Creates a token type from a string identifier - */ - public static TokenType createTokenTypeFromString(String identifier) { - return new TokenType(identifier.getBytes(StandardCharsets.UTF_8)); - } - - /** - * Validates performance metrics - */ - public static class PerformanceValidator { - public static void validateDuration(long actualDuration, long maxDuration, String operation) { - if (actualDuration >= maxDuration) { - throw new AssertionError(String.format( - "%s took %d ms, should be less than %d ms", - operation, actualDuration, maxDuration)); - } - } - - public static void validateSuccessRate(long successful, long total, double minSuccessRate, String operation) { - double actualRate = (double) successful / total; - if (actualRate < minSuccessRate) { - throw new AssertionError(String.format( - "%s success rate %.2f%% is below required %.2f%%", - operation, actualRate * 100, minSuccessRate * 100)); - } - } - } - - /** - * Token operation result wrapper - */ - public static class TokenOperationResult { - private final boolean success; - private final String message; - private final Token token; - private final Exception error; - - public TokenOperationResult(boolean success, String message, Token token, Exception error) { - this.success = success; - this.message = message; - this.token = token; - this.error = error; - } - - public static TokenOperationResult success(String message, Token token) { - return new TokenOperationResult(true, message, token, null); - } - - public static TokenOperationResult failure(String message, Exception error) { - return new TokenOperationResult(false, message, null, error); - } - - public boolean isSuccess() { return success; } - public String getMessage() { return message; } - public Token getToken() { return token; } - public Exception getError() { return error; } - } - - public static String generateRandomString(int length) { - StringBuilder sb = new StringBuilder(length); - for (int i = 0; i < length; i++) { - sb.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length()))); - } - return sb.toString(); - } - - -} \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java b/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java index 6ccff5d..1fa7845 100644 --- a/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java +++ b/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java @@ -1,188 +1,202 @@ package org.unicitylabs.sdk.utils; -import static org.unicitylabs.sdk.utils.TestUtils.randomBytes; -import static org.unicitylabs.sdk.utils.TestUtils.randomCoinData; - +import org.junit.jupiter.api.Assertions; import org.unicitylabs.sdk.StateTransitionClient; -import org.unicitylabs.sdk.address.Address; -import org.unicitylabs.sdk.api.SubmitCommitmentResponse; -import org.unicitylabs.sdk.api.SubmitCommitmentStatus; -import org.unicitylabs.sdk.bft.RootTrustBase; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.predicate.embedded.MaskedPredicate; -import org.unicitylabs.sdk.predicate.embedded.MaskedPredicateReference; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.token.TokenId; -import org.unicitylabs.sdk.token.TokenState; -import org.unicitylabs.sdk.token.TokenType; -import org.unicitylabs.sdk.token.fungible.TokenCoinData; -import org.unicitylabs.sdk.transaction.InclusionProof; -import org.unicitylabs.sdk.transaction.MintCommitment; -import org.unicitylabs.sdk.transaction.MintTransaction; +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.Predicate; +import org.unicitylabs.sdk.predicate.UnlockScript; +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.*; +import org.unicitylabs.sdk.transaction.verification.MintJustificationVerifierService; import org.unicitylabs.sdk.util.InclusionProofUtils; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +import java.security.SecureRandom; +/** + * Test helpers for minting and transferring certified tokens. + */ public class TokenUtils { - public static Token mintToken( - StateTransitionClient client, - RootTrustBase trustBase, - byte[] secret + public static Token mintToken( + StateTransitionClient client, + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + MintJustificationVerifierService mintJustificationVerifier, + Predicate recipient ) throws Exception { return TokenUtils.mintToken( - client, - trustBase, - secret, - new TokenId(randomBytes(32)), - new TokenType(randomBytes(32)), - randomBytes(32), - randomCoinData(2), - randomBytes(32), - randomBytes(32), - null + client, + trustBase, + predicateVerifier, + mintJustificationVerifier, + TokenId.generate(), + TokenType.generate(), + recipient, + null, + null ); } - public static Token mintToken( - StateTransitionClient client, - RootTrustBase trustBase, - byte[] secret, - TokenId tokenId, - TokenType tokenType, - byte[] tokenData, - TokenCoinData coinData, - byte[] nonce, - byte[] salt, - DataHash dataHash + public static Token mintToken( + StateTransitionClient client, + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + MintJustificationVerifierService mintJustificationVerifier, + Predicate recipient, + byte[] justification, + byte[] data ) throws Exception { - SigningService signingService = SigningService.createFromMaskedSecret(secret, nonce); - - MaskedPredicate predicate = MaskedPredicate.create( - tokenId, - tokenType, - signingService, - HashAlgorithm.SHA256, - nonce + return TokenUtils.mintToken( + client, + trustBase, + predicateVerifier, + mintJustificationVerifier, + TokenId.generate(), + TokenType.generate(), + recipient, + justification, + data ); + } - Address address = predicate.getReference().toAddress(); - TokenState tokenState = new TokenState(predicate, null); - - MintCommitment commitment = MintCommitment.create( - new MintTransaction.Data<>( + public static Token mintToken( + StateTransitionClient client, + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + MintJustificationVerifierService mintJustificationVerifier, + TokenId tokenId, + TokenType tokenType, + Predicate recipient, + byte[] justification, + byte[] data + ) throws Exception { + MintTransaction transaction = MintTransaction.create( + recipient, tokenId, tokenType, - tokenData, - coinData, - address, - salt, - dataHash, - null - ) + justification, + data ); - // Submit mint transaction using StateTransitionClient - SubmitCommitmentResponse response = client - .submitCommitment(commitment) - .get(); - if (response.getStatus() != SubmitCommitmentStatus.SUCCESS) { - throw new Exception(String.format("Failed to submit mint commitment: %s", - response.getStatus())); - } + CertificationData certificationData = CertificationData.fromMintTransaction(transaction); - // Wait for inclusion proof - InclusionProof inclusionProof = InclusionProofUtils.waitInclusionProof( - client, - trustBase, - commitment - ).get(); + CertificationResponse response = client.submitCertificationRequest(certificationData).get(); + if (response.getStatus() != CertificationStatus.SUCCESS) { + throw new RuntimeException( + String.format("Certification Request failed with status '%s'", response.getStatus())); + } - // Create mint transaction - return Token.create( - trustBase, - tokenState, - commitment.toTransaction(inclusionProof) + return Token.mint( + trustBase, + predicateVerifier, + mintJustificationVerifier, + transaction.toCertifiedTransaction( + trustBase, + predicateVerifier, + InclusionProofUtils.waitInclusionProof(client, trustBase, predicateVerifier, transaction).get() + ) ); } - public static Token mintNametagToken( - StateTransitionClient client, - RootTrustBase trustBase, - byte[] secret, - String nametag, - Address targetAddress - ) throws Exception { - return mintNametagToken( - client, - trustBase, - secret, - new TokenType(randomBytes(32)), - nametag, - targetAddress, - randomBytes(32), - randomBytes(32) - ); - } - public static Token mintNametagToken( - StateTransitionClient client, - RootTrustBase trustBase, - byte[] secret, - TokenType tokenType, - String nametag, - Address targetAddress, - byte[] nonce, - byte[] salt + /** + * Deserialize token, build transfer transaction and submit certified transfer. + * + * @param client state transition client + * @param trustBase trust base + * @param predicateVerifier predicate verifier + * @param tokenBytes serialized token bytes + * @param recipient recipient address + * @param signingService sender signing service + * + * @return transferred token + * + * @throws Exception when request or verification fails + */ + public static Token transferToken( + StateTransitionClient client, + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + MintJustificationVerifierService mintJustificationVerifier, + byte[] tokenBytes, + Predicate recipient, + SigningService signingService ) throws Exception { - SigningService signingService = SigningService.createFromMaskedSecret(secret, nonce); + Token token = Token.fromCbor(tokenBytes); + Assertions.assertEquals(VerificationStatus.OK, token.verify(trustBase, predicateVerifier, mintJustificationVerifier).getStatus()); - Address address = MaskedPredicateReference.create( - tokenType, - signingService, - HashAlgorithm.SHA256, - nonce).toAddress(); + byte[] x = new byte[32]; + new SecureRandom().nextBytes(x); - MintCommitment commitment = MintCommitment.create( - new MintTransaction.NametagData( - nametag, - tokenType, - address, - salt, - targetAddress - ) + TransferTransaction transaction = TransferTransaction.create( + token, + recipient, + x, + CborSerializer.encodeArray() ); - // Submit mint transaction using StateTransitionClient - SubmitCommitmentResponse response = client - .submitCommitment(commitment) - .get(); - if (response.getStatus() != SubmitCommitmentStatus.SUCCESS) { - throw new Exception(String.format("Failed to submit mint commitment: %s", - response.getStatus())); - } + return TokenUtils.transferToken( + client, + trustBase, + predicateVerifier, + token, + transaction, + SignaturePredicateUnlockScript.create(transaction, signingService) + ); + } - // Wait for inclusion proof - InclusionProof inclusionProof = InclusionProofUtils.waitInclusionProof( - client, - trustBase, - commitment + /** + * Submit a prepared transfer transaction and return resulting transferred token. + * + * @param client state transition client + * @param trustBase trust base + * @param predicateVerifier predicate verifier + * @param token source token + * @param transaction transfer transaction + * @param unlockScript unlock script for transaction + * + * @return transferred token + * + * @throws Exception when request or verification fails + */ + public static Token transferToken( + StateTransitionClient client, + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + Token token, + TransferTransaction transaction, + UnlockScript unlockScript + ) throws Exception { + CertificationResponse response = client.submitCertificationRequest( + CertificationData.fromTransaction(transaction, unlockScript) ).get(); - // Create mint transaction - return Token.create( - trustBase, - new TokenState( - MaskedPredicate.create( - commitment.getTransactionData().getTokenId(), - commitment.getTransactionData().getTokenType(), - signingService, - HashAlgorithm.SHA256, - nonce - ), - null - ), - commitment.toTransaction(inclusionProof) + if (response.getStatus() != CertificationStatus.SUCCESS) { + throw new RuntimeException( + String.format("Certification Request failed with status '%s'", response.getStatus())); + } + + return token.transfer( + trustBase, + predicateVerifier, + transaction.toCertifiedTransaction( + trustBase, + predicateVerifier, + InclusionProofUtils.waitInclusionProof( + client, + trustBase, + predicateVerifier, + transaction + ).get() + ) ); } + } diff --git a/src/test/java/org/unicitylabs/sdk/utils/helpers/CommitmentResult.java b/src/test/java/org/unicitylabs/sdk/utils/helpers/CommitmentResult.java deleted file mode 100644 index 345f56d..0000000 --- a/src/test/java/org/unicitylabs/sdk/utils/helpers/CommitmentResult.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.unicitylabs.sdk.utils.helpers; - -import org.unicitylabs.sdk.api.RequestId; -import org.unicitylabs.sdk.transaction.InclusionProofVerificationStatus; - -public class CommitmentResult { - private final String userName; - private final String threadName; - private final RequestId requestId; - private final boolean success; - private final long startTime; - private final long endTime; - - public boolean verified; - private long inclusionStart; - private long inclusionEnd; - private String status; - - public CommitmentResult(String userName, String threadName, RequestId requestId, - boolean success, long startTime, long endTime) { - this.userName = userName; - this.threadName = threadName; - this.requestId = requestId; - this.success = success; - this.startTime = startTime; - this.endTime = endTime; - } - - public boolean isSuccess() { return success; } - - public void markVerified(long start, long end) { - this.verified = true; - this.inclusionStart = start; - this.inclusionEnd = end; - this.status = InclusionProofVerificationStatus.OK.toString(); - } - - public RequestId getRequestId() { - return this.requestId; - } - - public void markFailedVerification(long start, long end, String status) { - this.verified = false; - this.inclusionStart = start; - this.inclusionEnd = end; - this.status = status.toString(); - } - - public boolean isVerified() { - return this.verified; - } - - public String getStatus(){ - return this.status; - } - - // Add these getter methods for the multi-aggregator functionality - public String getUserName() { - return this.userName; - } - - public String getThreadName() { - return this.threadName; - } - - // Helper method to get inclusion proof verification duration - public long getInclusionDurationNanos() { - return this.inclusionEnd - this.inclusionStart; - } - - public double getInclusionDurationMillis() { - return getInclusionDurationNanos() / 1_000_000.0; - } - -} diff --git a/src/test/java/org/unicitylabs/sdk/utils/helpers/PendingTransfer.java b/src/test/java/org/unicitylabs/sdk/utils/helpers/PendingTransfer.java deleted file mode 100644 index bb2ac69..0000000 --- a/src/test/java/org/unicitylabs/sdk/utils/helpers/PendingTransfer.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.unicitylabs.sdk.utils.helpers; - -import org.unicitylabs.sdk.token.Token; -import org.unicitylabs.sdk.transaction.TransferTransaction; - -public class PendingTransfer { - private final Token sourceToken; - private final TransferTransaction transaction; - - public PendingTransfer(Token sourceToken, TransferTransaction transaction) { - this.sourceToken = sourceToken; - this.transaction = transaction; - } - - public Token getSourceToken() { return sourceToken; } - public TransferTransaction getTransaction() { return transaction; } -} diff --git a/src/test/resources/docker/aggregator/docker-compose.yml b/src/test/resources/docker/aggregator/docker-compose.yml index c071b84..8572d79 100644 --- a/src/test/resources/docker/aggregator/docker-compose.yml +++ b/src/test/resources/docker/aggregator/docker-compose.yml @@ -51,9 +51,9 @@ services: entrypoint: [ "mongosh", "--host", "mongo1:27017", "--file", "/mongo-init.js" ] # mongo default port 27017 aggregator-test: -# build: -# context: ${AGGREGATOR_HOME:-../../../../../aggregators_net} # path to aggregator dockerfile directory -# dockerfile: Dockerfile + # build: + # context: ${AGGREGATOR_HOME:-../../../../../aggregators_net} # path to aggregator dockerfile directory + # dockerfile: Dockerfile image: ghcr.io/unicitynetwork/aggregators_net:bbabb5f093e829fa789ed6e83f57af98df3f1752 container_name: aggregator-test # must be specified for Testcontainers docker compose API depends_on: diff --git a/src/test/resources/docker/aggregator/mongo-init.js b/src/test/resources/docker/aggregator/mongo-init.js index 8202b68..79a96b1 100644 --- a/src/test/resources/docker/aggregator/mongo-init.js +++ b/src/test/resources/docker/aggregator/mongo-init.js @@ -1,9 +1,9 @@ /* eslint-disable */ rs.initiate({ - _id: "rs0", - members: [ - { _id: 0, host: "mongo1:27017" }, - { _id: 1, host: "mongo2:27017" }, - { _id: 2, host: "mongo3:27017" } - ] + _id: "rs0", + members: [ + {_id: 0, host: "mongo1:27017"}, + {_id: 1, host: "mongo2:27017"}, + {_id: 2, host: "mongo3:27017"} + ] }); \ No newline at end of file