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..3ddd20d 100644 --- a/src/main/java/org/unicitylabs/sdk/StateTransitionClient.java +++ b/src/main/java/org/unicitylabs/sdk/StateTransitionClient.java @@ -1,27 +1,12 @@ package org.unicitylabs.sdk; -import java.util.List; -import java.util.Objects; import java.util.concurrent.CompletableFuture; import org.unicitylabs.sdk.api.AggregatorClient; +import org.unicitylabs.sdk.api.CertificationData; +import org.unicitylabs.sdk.api.CertificationResponse; 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; +import org.unicitylabs.sdk.api.StateId; /** * Client for handling state transitions of tokens, including submitting commitments and finalizing transactions. @@ -43,159 +28,23 @@ public StateTransitionClient(AggregatorClient client) { } /** - * Submits a mint commitment to the aggregator. + * Retrieves the inclusion proof for a given transaction. * - * @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. + * @param stateId The state ID of inclusion proof to retrieve. + * @return inclusion proof response from the aggregator. */ - public - CompletableFuture submitCommitment(MintCommitment commitment) { - return this.client.submitCommitment( - commitment.getRequestId(), - commitment.getTransactionData().calculateHash(), - commitment.getAuthenticator() - ); + public CompletableFuture getInclusionProof(StateId stateId) { + return this.client.getInclusionProof(stateId); } /** - * Submits a transfer commitment to the aggregator after verifying ownership. + * Submits a certification request to the aggregator. * - * @param commitment The transfer commitment to submit. - * @return A CompletableFuture that resolves to the response from the aggregator. - * @throws IllegalArgumentException if ownership verification fails. + * @param certificationData The certification data to submit. + * @return certification response from the aggregator. */ - 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. - * - * @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. - */ - 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); - } - - /** - * Check if token id is already minted. - * - * @param tokenId token id - * @param trustBase root trust base - * @return A CompletableFuture that resolves to true if token id is spent, false otherwise. - */ - 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/CertificationData.java b/src/main/java/org/unicitylabs/sdk/api/CertificationData.java new file mode 100644 index 0000000..a011017 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/CertificationData.java @@ -0,0 +1,186 @@ +package org.unicitylabs.sdk.api; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +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.UnlockScript; +import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +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; + +/** + * Certification data. + */ +public class CertificationData { + + private final Predicate lockScript; + private final DataHash sourceStateHash; + private final DataHash transactionHash; + private final byte[] unlockScript; + + CertificationData( + Predicate lockScript, + DataHash sourceStateHash, + DataHash transactionHash, + byte[] unlockScript + ) { + this.lockScript = lockScript; + this.sourceStateHash = sourceStateHash; + this.transactionHash = transactionHash; + this.unlockScript = Arrays.copyOf(unlockScript, unlockScript.length); + } + + /** + * Get lock script of certified transaction output. + * + * @return lock script + */ + public Predicate 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) { + List data = CborDeserializer.decodeArray(bytes); + + return new CertificationData( + EncodedPredicate.fromCbor(data.get(0)), + new DataHash(HashAlgorithm.SHA256, CborDeserializer.decodeByteString(data.get(1))), + new DataHash(HashAlgorithm.SHA256, CborDeserializer.decodeByteString(data.get(2))), + CborDeserializer.decodeByteString(data.get(3)) + ); + } + + /** + * Build certification data for a mint transaction using the deterministic mint signing service. + * + * @param transaction mint transaction + * + * @return certification data + */ + public static CertificationData fromMintTransaction(MintTransaction transaction) { + SigningService signingService = MintSigningService.create(transaction.getTokenId()); + + return CertificationData.fromTransaction( + transaction, + PayToPublicKeyPredicateUnlockScript.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) { + return CertificationData.fromTransaction(transaction, unlockScript.encode()); + } + + /** + * Build certification data from a transaction and encoded unlock script bytes. + * + * @param transaction transaction to certify + * @param unlockScript encoded unlock script bytes + * + * @return certification data + */ + public static CertificationData fromTransaction(Transaction transaction, byte[] unlockScript) { + return new CertificationData( + transaction.getLockScript(), + transaction.getSourceStateHash(), + transaction.calculateTransactionHash(), + unlockScript + ); + } + + /** + * Calculate leaf value for Merkle tree. + * + * @return leaf value + */ + public DataHash calculateLeafValue() { + return new DataHasher(HashAlgorithm.SHA256) + .update(this.toCbor()) + .digest(); + } + + /** + * Serialize certification data to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeArray( + EncodedPredicate.fromPredicate(this.getLockScript()).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 this.lockScript.isEqualTo(that.lockScript) + && Objects.equals(this.sourceStateHash, that.sourceStateHash) + && Objects.equals(this.transactionHash, that.transactionHash) + && Arrays.equals(this.unlockScript, that.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..54cd212 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/CertificationRequest.java @@ -0,0 +1,68 @@ +package org.unicitylabs.sdk.api; + +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; + +/** + * Submit certification request. + */ +public class CertificationRequest { + + 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; + } + + /** + * 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.encodeArray( + 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 59% rename from src/main/java/org/unicitylabs/sdk/api/SubmitCommitmentResponse.java rename to src/main/java/org/unicitylabs/sdk/api/CertificationResponse.java index 39d88c8..79b0f5e 100644 --- a/src/main/java/org/unicitylabs/sdk/api/SubmitCommitmentResponse.java +++ b/src/main/java/org/unicitylabs/sdk/api/CertificationResponse.java @@ -3,15 +3,16 @@ 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.serializer.UnicityObjectMapper; import org.unicitylabs.sdk.serializer.json.JsonSerializationException; /** * Submit commitment response. */ -public class SubmitCommitmentResponse { +public class CertificationResponse { - private final SubmitCommitmentStatus status; + private final CertificationStatus status; /** * Create submit commitment response. @@ -19,8 +20,8 @@ public class SubmitCommitmentResponse { * @param status status */ @JsonCreator - public SubmitCommitmentResponse( - @JsonProperty("status") SubmitCommitmentStatus status + CertificationResponse( + @JsonProperty("status") CertificationStatus status ) { this.status = status; } @@ -30,21 +31,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 +68,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/InclusionProof.java b/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java new file mode 100644 index 0000000..1df8ea2 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/InclusionProof.java @@ -0,0 +1,109 @@ +package org.unicitylabs.sdk.api; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import org.unicitylabs.sdk.api.bft.UnicityCertificate; +import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreePath; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; + +/** + * Represents a proof of inclusion or non-inclusion in a sparse merkle tree. + */ +public class InclusionProof { + + private final SparseMerkleTreePath merkleTreePath; + private final CertificationData certificationData; + private final UnicityCertificate unicityCertificate; + + InclusionProof( + SparseMerkleTreePath merkleTreePath, + CertificationData certificationData, + UnicityCertificate unicityCertificate + ) { + this.merkleTreePath = Objects.requireNonNull(merkleTreePath, "Merkle tree path cannot be null.");; + this.certificationData = certificationData; + this.unicityCertificate = Objects.requireNonNull(unicityCertificate, "Unicity certificate cannot be null.");; + } + + /** + * 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 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) { + List data = CborDeserializer.decodeArray(bytes); + + return new InclusionProof( + SparseMerkleTreePath.fromCbor(data.get(1)), + CborDeserializer.decodeNullable(data.get(0), CertificationData::fromCbor), + UnicityCertificate.fromCbor(data.get(2)) + ); + } + + /** + * Serialize inclusion proof to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeArray( + CborSerializer.encodeOptional(this.certificationData, CertificationData::toCbor), + this.merkleTreePath.toCbor(), + this.unicityCertificate.toCbor() + ); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof InclusionProof)) { + return false; + } + InclusionProof that = (InclusionProof) o; + return Objects.equals(this.merkleTreePath, that.merkleTreePath) && Objects.equals( + this.certificationData, + that.certificationData); + } + + @Override + public int hashCode() { + return Objects.hash(this.merkleTreePath, this.certificationData); + } + + @Override + public String toString() { + return String.format( + "InclusionProof{merkleTreePath=%s, certificationData=%s, unicityCertificate=%s}", + this.merkleTreePath, + 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..7abed45 100644 --- a/src/main/java/org/unicitylabs/sdk/api/InclusionProofRequest.java +++ b/src/main/java/org/unicitylabs/sdk/api/InclusionProofRequest.java @@ -2,62 +2,36 @@ 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; - } + Objects.requireNonNull(stateId, "stateId cannot be null"); - /** - * Get request id. - * - * @return request id - */ - public RequestId getRequestId() { - return this.requestId; - } - - /** - * 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..5a44091 100644 --- a/src/main/java/org/unicitylabs/sdk/api/InclusionProofResponse.java +++ b/src/main/java/org/unicitylabs/sdk/api/InclusionProofResponse.java @@ -1,17 +1,15 @@ 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 java.util.List; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; /** * Inclusion proof response. */ public class InclusionProofResponse { + private final long blockNumber; private final InclusionProof inclusionProof; /** @@ -19,11 +17,11 @@ public class InclusionProofResponse { * * @param inclusionProof inclusion proof */ - @JsonCreator - public InclusionProofResponse( - @JsonProperty("inclusionProof") + InclusionProofResponse( + long blockNumber, InclusionProof inclusionProof ) { + this.blockNumber = blockNumber; this.inclusionProof = inclusionProof; } @@ -37,29 +35,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); + 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..4471f2e 100644 --- a/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java +++ b/src/main/java/org/unicitylabs/sdk/api/JsonRpcAggregatorClient.java @@ -4,9 +4,10 @@ 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 org.unicitylabs.sdk.api.jsonrpc.JsonRpcHttpTransport; +import org.unicitylabs.sdk.util.HexConverter; /** * Default aggregator client. @@ -34,52 +35,50 @@ 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))); return this.transport.request( - "submit_commitment", - request, - SubmitCommitmentResponse.class, + "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 +86,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); } -} \ 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..4cb910c --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/StateId.java @@ -0,0 +1,131 @@ +package org.unicitylabs.sdk.api; + +import java.util.Objects; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.DataHasher; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.predicate.EncodedPredicate; +import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.transaction.Transaction; +import org.unicitylabs.sdk.util.BitString; +import org.unicitylabs.sdk.util.HexConverter; + +/** + * Represents a state identifier for requests. + */ +public final 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(); + } + + /** + * Returns the hash imprint bytes. + * + * @return state id imprint bytes + */ + public byte[] getImprint() { + return this.hash.getImprint(); + } + + /** + * Deserializes a state id from CBOR. + * + * @param bytes CBOR byte string containing SHA-256 hash bytes + * @return decoded state id + */ + public static StateId fromCbor(byte[] bytes) { + return new StateId( + new DataHash(HashAlgorithm.SHA256, CborDeserializer.decodeByteString(bytes))); + } + + /** + * Creates a state id from certification data. + * + * @param certificationData certification data carrying lock script and source state hash + * @return created state id + * @throws NullPointerException if {@code certificationData} is {@code null} + */ + public static StateId fromCertificationData(CertificationData certificationData) { + Objects.requireNonNull(certificationData, "Certification data cannot be null"); + + 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(Predicate predicate, DataHash stateHash) { + DataHash hash = new DataHasher(HashAlgorithm.SHA256) + .update( + CborSerializer.encodeArray( + EncodedPredicate.fromPredicate(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()); + } + + /** + * Converts this state id to a {@link BitString}. + * + * @return bit string representation of this state id + */ + public BitString toBitString() { + return new BitString(this.getImprint()); + } + + @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.getImprint())); + } +} 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/bft/InputRecord.java b/src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java similarity index 86% rename from src/main/java/org/unicitylabs/sdk/bft/InputRecord.java rename to src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java index 6c35aa3..89be419 100644 --- a/src/main/java/org/unicitylabs/sdk/bft/InputRecord.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/InputRecord.java @@ -1,6 +1,5 @@ -package org.unicitylabs.sdk.bft; +package org.unicitylabs.sdk.api.bft; -import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -145,31 +144,31 @@ public byte[] getExecutedTransactionsHash() { } /** - * Create InputRecord from CBOR bytes. + * Deserialize 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()); + CborTag tag = CborDeserializer.decodeTag(bytes); + List data = CborDeserializer.decodeArray(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) + CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(), + 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) ); } /** - * Convert InputRecord to CBOR bytes. + * Serialize InputRecord to CBOR bytes. * * @return CBOR bytes */ 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 99% 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..6f51ffa 100644 --- a/src/main/java/org/unicitylabs/sdk/bft/RootTrustBase.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/RootTrustBase.java @@ -1,4 +1,4 @@ -package org.unicitylabs.sdk.bft; +package org.unicitylabs.sdk.api.bft; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/unicitylabs/sdk/bft/ShardTreeCertificate.java b/src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java similarity index 84% rename from src/main/java/org/unicitylabs/sdk/bft/ShardTreeCertificate.java rename to src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java index fbc2cb3..d1aedc8 100644 --- a/src/main/java/org/unicitylabs/sdk/bft/ShardTreeCertificate.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/ShardTreeCertificate.java @@ -1,7 +1,5 @@ -package org.unicitylabs.sdk.bft; +package org.unicitylabs.sdk.api.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; @@ -49,24 +47,24 @@ public List getSiblingHashList() { } /** - * Create shard tree certificate from CBOR bytes. + * Deserialize shard tree certificate from CBOR bytes. * * @param bytes CBOR bytes * @return shard tree certificate */ public static ShardTreeCertificate fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); + List data = CborDeserializer.decodeArray(bytes); return new ShardTreeCertificate( - CborDeserializer.readByteString(data.get(0)), - CborDeserializer.readArray(data.get(1)).stream() - .map(CborDeserializer::readByteString) + CborDeserializer.decodeByteString(data.get(0)), + CborDeserializer.decodeArray(data.get(1)).stream() + .map(CborDeserializer::decodeByteString) .collect(Collectors.toList()) ); } /** - * Convert shard tree certificate to CBOR bytes. + * Serialize shard tree certificate to CBOR bytes. * * @return CBOR bytes */ diff --git a/src/main/java/org/unicitylabs/sdk/bft/UnicityCertificate.java b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java similarity index 89% rename from src/main/java/org/unicitylabs/sdk/bft/UnicityCertificate.java rename to src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java index bc3f19d..a60f9cb 100644 --- a/src/main/java/org/unicitylabs/sdk/bft/UnicityCertificate.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityCertificate.java @@ -1,13 +1,11 @@ -package org.unicitylabs.sdk.bft; +package org.unicitylabs.sdk.api.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.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.CborDeserializer.CborTag; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; @@ -16,8 +14,6 @@ /** * Unicity certificate. */ -@JsonSerialize(using = UnicityCertificateJson.Serializer.class) -@JsonDeserialize(using = UnicityCertificateJson.Deserializer.class) public class UnicityCertificate { private final int version; @@ -167,20 +163,20 @@ public static DataHash calculateShardTreeCertificateRootHash( } /** - * Create unicity certificate from CBOR bytes. + * Deserialize 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()); + CborTag tag = CborDeserializer.decodeTag(bytes); + List data = CborDeserializer.decodeArray(tag.getData()); return new UnicityCertificate( - CborDeserializer.readUnsignedInteger(data.get(0)).asInt(), + CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(), InputRecord.fromCbor(data.get(1)), - CborDeserializer.readOptional(data.get(2), CborDeserializer::readByteString), - CborDeserializer.readByteString(data.get(3)), + 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)) @@ -188,7 +184,7 @@ public static UnicityCertificate fromCbor(byte[] bytes) { } /** - * Convert unicity certificate to CBOR bytes. + * Serialize unicity certificate to CBOR bytes. * * @return CBOR bytes */ diff --git a/src/main/java/org/unicitylabs/sdk/bft/UnicitySeal.java b/src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java similarity index 88% rename from src/main/java/org/unicitylabs/sdk/bft/UnicitySeal.java rename to src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java index 7222206..3ebce9b 100644 --- a/src/main/java/org/unicitylabs/sdk/bft/UnicitySeal.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicitySeal.java @@ -1,4 +1,4 @@ -package org.unicitylabs.sdk.bft; +package org.unicitylabs.sdk.api.bft; import java.util.Arrays; import java.util.LinkedHashMap; @@ -171,28 +171,28 @@ public Map getSignatures() { } /** - * Create unicity seal from CBOR bytes. + * Deserialize 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()); + CborTag tag = CborDeserializer.decodeTag(bytes); + List data = CborDeserializer.decodeArray(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() + CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(), + 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() .collect( Collectors.toMap( - entry -> CborDeserializer.readTextString(entry.getKey()), - entry -> CborDeserializer.readByteString(entry.getValue() + entry -> CborDeserializer.decodeTextString(entry.getKey()), + entry -> CborDeserializer.decodeByteString(entry.getValue() ) ) ) @@ -200,7 +200,7 @@ public static UnicitySeal fromCbor(byte[] bytes) { } /** - * Convert unicity seal to CBOR bytes. + * Serialize unicity seal to CBOR bytes. * * @return CBOR bytes */ diff --git a/src/main/java/org/unicitylabs/sdk/bft/UnicityTreeCertificate.java b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityTreeCertificate.java similarity index 85% 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..5143c2c 100644 --- a/src/main/java/org/unicitylabs/sdk/bft/UnicityTreeCertificate.java +++ b/src/main/java/org/unicitylabs/sdk/api/bft/UnicityTreeCertificate.java @@ -1,4 +1,4 @@ -package org.unicitylabs.sdk.bft; +package org.unicitylabs.sdk.api.bft; import java.util.Arrays; import java.util.List; @@ -58,26 +58,26 @@ 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()); + CborTag tag = CborDeserializer.decodeTag(bytes); + List data = CborDeserializer.decodeArray(tag.getData()); return new UnicityTreeCertificate( - CborDeserializer.readUnsignedInteger(data.get(0)).asInt(), - CborDeserializer.readUnsignedInteger(data.get(1)).asInt(), - CborDeserializer.readArray(data.get(2)).stream() + CborDeserializer.decodeUnsignedInteger(data.get(0)).asInt(), + 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 */ @@ -149,22 +149,22 @@ 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); 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 */ 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..26d3018 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/bft/verification/UnicityCertificateVerification.java @@ -0,0 +1,52 @@ +package org.unicitylabs.sdk.api.bft.verification; + +import java.util.ArrayList; +import org.unicitylabs.sdk.api.InclusionProof; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.api.bft.verification.rule.InputRecordCurrentHashVerificationRule; +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; + +/** + * 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 = InputRecordCurrentHashVerificationRule.verify(inclusionProof); + results.add(result); + if (result.getStatus() != VerificationStatus.OK) { + return UnicityCertificateVerificationResult.fail(results); + } + + 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..918c5ea --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/bft/verification/UnicityCertificateVerificationResult.java @@ -0,0 +1,36 @@ +package org.unicitylabs.sdk.api.bft.verification; + +import java.util.List; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +/** + * Verification result type for unicity certificate verification. + */ +public class UnicityCertificateVerificationResult extends VerificationResult { + + private UnicityCertificateVerificationResult(VerificationStatus status, + 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..1cec9b3 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/InputRecordCurrentHashVerificationRule.java @@ -0,0 +1,34 @@ +package org.unicitylabs.sdk.api.bft.verification.rule; + +import org.unicitylabs.sdk.api.InclusionProof; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +/** + * Input record current hash verification rule. + */ +public class InputRecordCurrentHashVerificationRule { + + private InputRecordCurrentHashVerificationRule() { + } + + /** + * Verify that inclusion proof merkle root matches current hash in input record. + * + * @param inclusionProof inclusion proof to verify + * + * @return verification result + */ + public static VerificationResult verify(InclusionProof inclusionProof) { + if (inclusionProof.getMerkleTreePath().getRootHash().equals( + DataHash.fromImprint(inclusionProof.getUnicityCertificate().getInputRecord().getHash()))) { + return new VerificationResult<>("InputRecordCurrentHashVerificationRule", + VerificationStatus.OK); + } + + return new VerificationResult<>("InputRecordCurrentHashVerificationRule", + VerificationStatus.FAIL); + } + +} 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..d2639c4 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealHashMatchesWithRootHashRule.java @@ -0,0 +1,93 @@ +package org.unicitylabs.sdk.api.bft.verification.rule; + +import com.google.common.primitives.UnsignedBytes; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +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; + +/** + * 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..ba7947f --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/api/bft/verification/rule/UnicitySealQuorumSignaturesVerificationRule.java @@ -0,0 +1,109 @@ +package org.unicitylabs.sdk.api.bft.verification.rule; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +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; + +/** + * 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 (Map.Entry entry : unicitySeal.getSignatures().entrySet()) { + String nodeId = entry.getKey(); + byte[] signature = entry.getValue(); + + 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 93% 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..b1e4c6d 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; 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 97% 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..e61cd98 100644 --- a/src/main/java/org/unicitylabs/sdk/jsonrpc/JsonRpcHttpTransport.java +++ b/src/main/java/org/unicitylabs/sdk/api/jsonrpc/JsonRpcHttpTransport.java @@ -1,5 +1,5 @@ -package org.unicitylabs.sdk.jsonrpc; +package org.unicitylabs.sdk.api.jsonrpc; import java.io.IOException; import java.util.List; @@ -45,7 +45,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()); } /** 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 97% 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..705765d 100644 --- a/src/main/java/org/unicitylabs/sdk/jsonrpc/JsonRpcRequest.java +++ b/src/main/java/org/unicitylabs/sdk/api/jsonrpc/JsonRpcRequest.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.JsonGetter; 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 97% 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..8888252 100644 --- a/src/main/java/org/unicitylabs/sdk/jsonrpc/JsonRpcResponse.java +++ b/src/main/java/org/unicitylabs/sdk/api/jsonrpc/JsonRpcResponse.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; 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/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..f498f2b --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/crypto/MintSigningService.java @@ -0,0 +1,41 @@ +package org.unicitylabs.sdk.crypto; + +import java.util.Objects; +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; + +/** + * 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..fbe1492 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,15 @@ -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; /** * 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 +26,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 +91,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 78% 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..eaf61d5 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,14 @@ -package org.unicitylabs.sdk.signing; +package org.unicitylabs.sdk.crypto.secp256k1; -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.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; 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 +49,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 +74,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)) { 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 89% 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..fce64ab 100644 --- a/src/main/java/org/unicitylabs/sdk/signing/SigningService.java +++ b/src/main/java/org/unicitylabs/sdk/crypto/secp256k1/SigningService.java @@ -1,4 +1,4 @@ -package org.unicitylabs.sdk.signing; +package org.unicitylabs.sdk.crypto.secp256k1; import java.math.BigInteger; import java.security.SecureRandom; @@ -13,9 +13,7 @@ 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; /** * Default signing service. @@ -65,7 +63,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 +88,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()); } /** 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/plain/Branch.java b/src/main/java/org/unicitylabs/sdk/mtree/plain/Branch.java index 2cb6f43..c5f1626 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/Branch.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/plain/Branch.java @@ -1,7 +1,7 @@ package org.unicitylabs.sdk.mtree.plain; import java.math.BigInteger; -import org.unicitylabs.sdk.hash.HashAlgorithm; +import org.unicitylabs.sdk.crypto.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/mtree/plain/FinalizedBranch.java index a6c884f..b5770bf 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/FinalizedBranch.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/plain/FinalizedBranch.java @@ -1,6 +1,6 @@ package org.unicitylabs.sdk.mtree.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/mtree/plain/FinalizedLeafBranch.java index ebe9374..a802fa0 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/FinalizedLeafBranch.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/plain/FinalizedLeafBranch.java @@ -3,9 +3,9 @@ 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.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; diff --git a/src/main/java/org/unicitylabs/sdk/mtree/plain/FinalizedNodeBranch.java b/src/main/java/org/unicitylabs/sdk/mtree/plain/FinalizedNodeBranch.java index c790060..de571f6 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/FinalizedNodeBranch.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/plain/FinalizedNodeBranch.java @@ -2,9 +2,9 @@ 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; diff --git a/src/main/java/org/unicitylabs/sdk/mtree/plain/PendingLeafBranch.java b/src/main/java/org/unicitylabs/sdk/mtree/plain/PendingLeafBranch.java index 88022ab..3e6a2aa 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/PendingLeafBranch.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/plain/PendingLeafBranch.java @@ -3,7 +3,7 @@ import java.math.BigInteger; import java.util.Arrays; import java.util.Objects; -import org.unicitylabs.sdk.hash.HashAlgorithm; +import org.unicitylabs.sdk.crypto.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/mtree/plain/PendingNodeBranch.java index c206dfd..537cf37 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/PendingNodeBranch.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/plain/PendingNodeBranch.java @@ -2,7 +2,7 @@ import java.math.BigInteger; import java.util.Objects; -import org.unicitylabs.sdk.hash.HashAlgorithm; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; /** * Pending node branch in a sparse merkle tree. diff --git a/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTree.java b/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTree.java index 96abb35..a3378b6 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTree.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTree.java @@ -2,7 +2,7 @@ import java.math.BigInteger; import java.util.Arrays; -import org.unicitylabs.sdk.hash.HashAlgorithm; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.unicitylabs.sdk.mtree.BranchExistsException; import org.unicitylabs.sdk.mtree.CommonPath; import org.unicitylabs.sdk.mtree.LeafOutOfBoundsException; @@ -31,9 +31,9 @@ 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 { diff --git a/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePath.java b/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePath.java index 53b9012..a3df4cb 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePath.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePath.java @@ -1,20 +1,14 @@ package org.unicitylabs.sdk.mtree.plain; -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.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.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; /** @@ -25,13 +19,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 +32,6 @@ public class SparseMerkleTreePath { * * @return root hash */ - @JsonGetter("root") public DataHash getRootHash() { return this.rootHash; } @@ -61,10 +48,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); } @@ -105,8 +92,8 @@ public MerkleTreePathVerificationResult verify(BigInteger requestId) { CborSerializer.encodeByteString(BigIntegerConverter.encode(step.getPath())), CborSerializer.encodeOptional(left, CborSerializer::encodeByteString), CborSerializer.encodeOptional(right, CborSerializer::encodeByteString) - ) ) + ) .digest(); currentData = hash.getData(); @@ -122,30 +109,30 @@ public MerkleTreePathVerificationResult verify(BigInteger requestId) { boolean pathValid = currentData != null && this.rootHash.equals(new DataHash(this.rootHash.getAlgorithm(), currentData)); - boolean pathIncluded = currentPath.compareTo(requestId) == 0; + 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); return new SparseMerkleTreePath( DataHash.fromCbor(data.get(0)), - CborDeserializer.readArray(data.get(1)).stream() + 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 */ @@ -160,33 +147,6 @@ public byte[] toCbor() { ); } - /** - * 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/mtree/plain/SparseMerkleTreePathStep.java index 1006adf..fcccc81 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePathStep.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePathStep.java @@ -1,8 +1,5 @@ package org.unicitylabs.sdk.mtree.plain; -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; @@ -10,7 +7,6 @@ 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; @@ -28,10 +24,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 +42,6 @@ public SparseMerkleTreePathStep( * * @return step path */ - @JsonSerialize(using = BigIntegerAsStringSerializer.class) public BigInteger getPath() { return this.path; } @@ -62,22 +56,22 @@ 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); 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 */ diff --git a/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreeRootNode.java b/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreeRootNode.java index 76cba44..eef37ed 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreeRootNode.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreeRootNode.java @@ -4,8 +4,8 @@ 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.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.unicitylabs.sdk.mtree.CommonPath; /** diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/Branch.java b/src/main/java/org/unicitylabs/sdk/mtree/sum/Branch.java index 1818db3..6e0aa7a 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/Branch.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/sum/Branch.java @@ -1,7 +1,7 @@ package org.unicitylabs.sdk.mtree.sum; import java.math.BigInteger; -import org.unicitylabs.sdk.hash.HashAlgorithm; +import org.unicitylabs.sdk.crypto.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/mtree/sum/FinalizedBranch.java index ad8475a..0df2cba 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/FinalizedBranch.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/sum/FinalizedBranch.java @@ -1,7 +1,7 @@ package org.unicitylabs.sdk.mtree.sum; import java.math.BigInteger; -import org.unicitylabs.sdk.hash.DataHash; +import org.unicitylabs.sdk.crypto.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/mtree/sum/FinalizedLeafBranch.java index 50ba112..2c7f772 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/FinalizedLeafBranch.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/sum/FinalizedLeafBranch.java @@ -2,9 +2,9 @@ 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.mtree.sum.SparseMerkleSumTree.LeafValue; import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.util.BigIntegerConverter; diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/FinalizedNodeBranch.java b/src/main/java/org/unicitylabs/sdk/mtree/sum/FinalizedNodeBranch.java index 2093a7f..b8fd936 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/FinalizedNodeBranch.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/sum/FinalizedNodeBranch.java @@ -2,9 +2,9 @@ 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; diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/PendingLeafBranch.java b/src/main/java/org/unicitylabs/sdk/mtree/sum/PendingLeafBranch.java index bbdeec0..6b72cda 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/PendingLeafBranch.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/sum/PendingLeafBranch.java @@ -2,7 +2,7 @@ import java.math.BigInteger; import java.util.Objects; -import org.unicitylabs.sdk.hash.HashAlgorithm; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.unicitylabs.sdk.mtree.sum.SparseMerkleSumTree.LeafValue; /** diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/PendingNodeBranch.java b/src/main/java/org/unicitylabs/sdk/mtree/sum/PendingNodeBranch.java index f17cded..4dab3c1 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/PendingNodeBranch.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/sum/PendingNodeBranch.java @@ -2,7 +2,7 @@ import java.math.BigInteger; import java.util.Objects; -import org.unicitylabs.sdk.hash.HashAlgorithm; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; /** * Pending node branch in a sparse merkle sum tree. diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTree.java b/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTree.java index 565ed09..8b308ff 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTree.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTree.java @@ -3,7 +3,7 @@ import java.math.BigInteger; import java.util.Arrays; import java.util.Objects; -import org.unicitylabs.sdk.hash.HashAlgorithm; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.unicitylabs.sdk.mtree.BranchExistsException; import org.unicitylabs.sdk.mtree.CommonPath; import org.unicitylabs.sdk.mtree.LeafOutOfBoundsException; @@ -34,8 +34,7 @@ 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) diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePath.java b/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePath.java index 9925ee4..79a1347 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePath.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePath.java @@ -1,19 +1,14 @@ 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.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.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; /** @@ -24,10 +19,9 @@ public class SparseMerkleSumTreePath { private final DataHash rootHash; private final List steps; - @JsonCreator SparseMerkleSumTreePath( - @JsonProperty("root") DataHash rootHash, - @JsonProperty("steps") List steps + DataHash rootHash, + List steps ) { Objects.requireNonNull(rootHash, "root cannot be null"); Objects.requireNonNull(steps, "steps cannot be null"); @@ -41,7 +35,6 @@ public class SparseMerkleSumTreePath { * * @return root hash */ - @JsonGetter("root") public DataHash getRootHash() { return this.rootHash; } @@ -138,18 +131,18 @@ public MerkleTreePathVerificationResult verify(BigInteger stateId) { * @return path */ public static SparseMerkleSumTreePath fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); + List data = CborDeserializer.decodeArray(bytes); return new SparseMerkleSumTreePath( DataHash.fromCbor(data.get(0)), - CborDeserializer.readArray(data.get(1)).stream() + CborDeserializer.decodeArray(data.get(1)).stream() .map(SparseMerkleSumTreePathStep::fromCbor) .collect(Collectors.toList()) ); } /** - * Convert path to CBOR bytes. + * Serialize path to CBOR bytes. * * @return CBOR bytes */ @@ -191,10 +184,9 @@ public static class Root { private final DataHash hash; private final BigInteger counter; - @JsonCreator Root( - @JsonProperty("hash") DataHash hash, - @JsonProperty("counter") BigInteger counter + DataHash hash, + BigInteger counter ) { this.hash = Objects.requireNonNull(hash, "hash cannot be null"); this.counter = Objects.requireNonNull(counter, "counter cannot be null"); @@ -214,7 +206,6 @@ public DataHash getHash() { * * @return counter */ - @JsonSerialize(using = BigIntegerAsStringSerializer.class) public BigInteger getCounter() { return this.counter; } @@ -226,16 +217,16 @@ public BigInteger getCounter() { * @return root */ public static Root fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); + List data = CborDeserializer.decodeArray(bytes); return new Root( DataHash.fromCbor(data.get(0)), - BigIntegerConverter.decode(CborDeserializer.readByteString(data.get(1))) + BigIntegerConverter.decode(CborDeserializer.decodeByteString(data.get(1))) ); } /** - * Convert root to CBOR bytes. + * Serialize root to CBOR bytes. * * @return CBOR bytes */ diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePathStep.java b/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePathStep.java index f4cd109..4e94ffb 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePathStep.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreePathStep.java @@ -1,8 +1,5 @@ package org.unicitylabs.sdk.mtree.sum; -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; @@ -10,7 +7,6 @@ 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; @@ -23,11 +19,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 +37,6 @@ public class SparseMerkleSumTreePathStep { * * @return path */ - @JsonSerialize(using = BigIntegerAsStringSerializer.class) public BigInteger getPath() { return this.path; } @@ -61,29 +55,28 @@ 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); 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 */ diff --git a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreeRootNode.java b/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreeRootNode.java index 40522f8..d9753c6 100644 --- a/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreeRootNode.java +++ b/src/main/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreeRootNode.java @@ -4,8 +4,8 @@ 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.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.unicitylabs.sdk.mtree.CommonPath; /** 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..5012d8b --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/PaymentData.java @@ -0,0 +1,23 @@ +package org.unicitylabs.sdk.payment; + +import java.util.Set; +import org.unicitylabs.sdk.payment.asset.Asset; + +/** + * Represents payment payload data. + */ +public interface PaymentData { + /** + * Returns the assets included in this payment payload. + * + * @return set of assets + */ + Set getAssets(); + + /** + * Encodes this payment payload into bytes. + * + * @return encoded payment data + */ + byte[] encode(); +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/PaymentDataDeserializer.java b/src/main/java/org/unicitylabs/sdk/payment/PaymentDataDeserializer.java 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/SplitPaymentData.java b/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentData.java new file mode 100644 index 0000000..a3b2804 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentData.java @@ -0,0 +1,13 @@ +package org.unicitylabs.sdk.payment; + +/** + * Payment data for already split payments. + */ +public interface SplitPaymentData extends PaymentData { + /** + * Returns the reason associated with the split. + * + * @return split reason + */ + SplitReason getReason(); +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentDataDeserializer.java b/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentDataDeserializer.java new file mode 100644 index 0000000..d1bc10d --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitPaymentDataDeserializer.java @@ -0,0 +1,15 @@ +package org.unicitylabs.sdk.payment; + +/** + * Functional contract for decoding encoded split payment data. + */ +@FunctionalInterface +public interface SplitPaymentDataDeserializer { + /** + * Decodes split payment data bytes. + * + * @param data encoded split payment data bytes + * @return decoded split payment data + */ + SplitPaymentData decode(byte[] data); +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitReason.java b/src/main/java/org/unicitylabs/sdk/payment/SplitReason.java new file mode 100644 index 0000000..de5ea84 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitReason.java @@ -0,0 +1,90 @@ +package org.unicitylabs.sdk.payment; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.transaction.Token; + +/** + * The reason for token splitting represented by an input token and inclusion proofs. + */ +public final class SplitReason { + + private final Token token; + private final List proofs; + + private SplitReason( + Token token, + List proofs + ) { + this.token = token; + this.proofs = List.copyOf(proofs); + } + + /** + * Get the token being split. + * + * @return token + */ + public Token getToken() { + return this.token; + } + + /** + * Get proofs supporting the split reason. + * + * @return proof list + */ + public List getProofs() { + return this.proofs; + } + + /** + * Create a split reason. + * + * @param token token being split + * @param proofs proofs supporting split eligibility + * + * @return split reason + */ + public static SplitReason create(Token token, List proofs) { + Objects.requireNonNull(token, "token cannot be null"); + Objects.requireNonNull(proofs, "proofs cannot be null"); + + if (proofs.isEmpty()) { + throw new IllegalArgumentException("proofs cannot be empty"); + } + + return new SplitReason(token, proofs); + } + + /** + * Deserialize split reason from CBOR bytes. + * + * @param bytes CBOR bytes + * + * @return split reason + */ + public static SplitReason fromCbor(byte[] bytes) { + List data = CborDeserializer.decodeArray(bytes); + + return new SplitReason( + Token.fromCbor(data.get(0)), + CborDeserializer.decodeArray(data.get(1)).stream().map(SplitReasonProof::fromCbor).collect(Collectors.toList()) + ); + } + + /** + * Serialize split reason to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeArray( + this.token.toCbor(), + CborSerializer.encodeArray(this.proofs.stream().map(SplitReasonProof::toCbor).toArray(byte[][]::new)) + ); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/SplitReasonProof.java b/src/main/java/org/unicitylabs/sdk/payment/SplitReasonProof.java new file mode 100644 index 0000000..72ce197 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitReasonProof.java @@ -0,0 +1,101 @@ +package org.unicitylabs.sdk.payment; + +import java.util.List; +import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreePath; +import org.unicitylabs.sdk.mtree.sum.SparseMerkleSumTreePath; +import org.unicitylabs.sdk.payment.asset.AssetId; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; + +/** + * Proof material for one split reason entry. + */ +public class SplitReasonProof { + private final AssetId assetId; + private final SparseMerkleTreePath aggregationPath; + private final SparseMerkleSumTreePath assetTreePath; + + private SplitReasonProof( + AssetId assetId, + SparseMerkleTreePath aggregationPath, + SparseMerkleSumTreePath assetTreePath + ) { + this.assetId = assetId; + this.aggregationPath = aggregationPath; + this.assetTreePath = assetTreePath; + } + + /** + * 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 SplitReasonProof create( + AssetId assetId, + SparseMerkleTreePath aggregationPath, + SparseMerkleSumTreePath assetTreePath + ) { + return new SplitReasonProof(assetId, aggregationPath, assetTreePath); + } + + /** + * Deserialize split reason proof from CBOR bytes. + * + * @param bytes CBOR bytes + * + * @return split reason proof + */ + public static SplitReasonProof fromCbor(byte[] bytes) { + List data = CborDeserializer.decodeArray(bytes); + + return new SplitReasonProof( + 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() + ); + } +} 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..77c73e7 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/SplitResult.java @@ -0,0 +1,45 @@ +package org.unicitylabs.sdk.payment; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.transaction.TransferTransaction; + +/** + * 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..fe2fa41 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/TokenSplit.java @@ -0,0 +1,313 @@ +package org.unicitylabs.sdk.payment; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.mtree.BranchExistsException; +import org.unicitylabs.sdk.mtree.LeafOutOfBoundsException; +import org.unicitylabs.sdk.mtree.MerkleTreePathVerificationResult; +import org.unicitylabs.sdk.mtree.plain.SparseMerkleTree; +import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreeRootNode; +import org.unicitylabs.sdk.mtree.sum.SparseMerkleSumTree; +import org.unicitylabs.sdk.mtree.sum.SparseMerkleSumTreeRootNode; +import org.unicitylabs.sdk.payment.asset.Asset; +import org.unicitylabs.sdk.payment.asset.AssetId; +import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.builtin.BurnPredicate; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.transaction.Address; +import org.unicitylabs.sdk.transaction.Token; +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.transaction.Transaction; +import org.unicitylabs.sdk.transaction.TransferTransaction; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +/** + * 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 ownerPredicate owner predicate of the source token + * @param paymentDataDeserializer payment data decoder for source token payload + * @param splitTokens destination token ids and their asset allocations + * + * @return split result containing burn transaction and proof map + * + * @throws LeafOutOfBoundsException if a leaf path is invalid for merkle tree insertion + * @throws BranchExistsException if duplicate branches are inserted into a merkle tree + */ + public static SplitResult split( + Token token, + Predicate ownerPredicate, + PaymentDataDeserializer paymentDataDeserializer, + Map> splitTokens + ) throws LeafOutOfBoundsException, BranchExistsException { + Objects.requireNonNull(token, "Token cannot be null"); + Objects.requireNonNull(ownerPredicate, "Owner predicate cannot be null"); + Objects.requireNonNull(paymentDataDeserializer, "Payment data deserializer cannot be null"); + Objects.requireNonNull(splitTokens, "Split tokens cannot be null"); + + HashMap trees = new HashMap(); + for (Entry> entry : splitTokens.entrySet()) { + Objects.requireNonNull(entry, "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(token.getGenesis().getData()); + 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[] x = new byte[32]; + RANDOM.nextBytes(x); + + TransferTransaction burnTransaction = TransferTransaction.create( + token, + ownerPredicate, + Address.fromPredicate(burnPredicate), + x, + CborSerializer.encodeNull() + ); + + HashMap> proofs = new HashMap>(); + for (Entry> entry : splitTokens.entrySet()) { + proofs.put( + entry.getKey(), + List.copyOf( + entry.getValue().stream().map(asset -> SplitReasonProof.create( + asset.getId(), + aggregationRoot.getPath(asset.getId().toBitString().toBigInteger()), + assetTreeRoots.get(asset.getId()).getPath(entry.getKey().toBitString().toBigInteger()) + ) + ).collect(Collectors.toList()) + ) + ); + } + + return new SplitResult(burnTransaction, proofs); + } + + /** + * Verify split reason and proofs embedded in a token. + * + * @param token token to verify + * @param paymentDataDeserializer split payment data deserializer + * @param trustBase trust base for token certification verification + * @param predicateVerifier predicate verifier service + * + * @return verification result + */ + public static VerificationResult verify( + Token token, + SplitPaymentDataDeserializer paymentDataDeserializer, + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier + ) { + Objects.requireNonNull(token, "Token cannot be null"); + Objects.requireNonNull(paymentDataDeserializer, "Payment data deserializer cannot be null"); + Objects.requireNonNull(trustBase, "Trust base cannot be null"); + Objects.requireNonNull(predicateVerifier, "Predicate verifier cannot be null"); + + SplitPaymentData data = paymentDataDeserializer.decode(token.getGenesis().getData()); + + if (data.getAssets() == null) { + return new VerificationResult<>( + "TokenSplitReasonVerificationRule", + VerificationStatus.FAIL, + "Assets data is missing." + ); + } + + if (data.getReason() == null) { + return new VerificationResult<>( + "TokenSplitReasonVerificationRule", + VerificationStatus.FAIL, + "Reason is missing." + ); + } + + VerificationResult verificationResult = data.getReason().getToken() + .verify(trustBase, predicateVerifier); + if (verificationResult.getStatus() != VerificationStatus.OK) { + return new VerificationResult<>( + "TokenSplitReasonVerificationRule", + VerificationStatus.FAIL, + "Burn token verification failed.", + verificationResult + ); + } + + if (data.getAssets().size() != data.getReason().getProofs().size()) { + return new VerificationResult<>( + "TokenSplitReasonVerificationRule", + VerificationStatus.FAIL, + "Total amount of assets differ in token and proofs." + ); + } + + Map assets = new HashMap<>(); + for (Asset asset : data.getAssets()) { + if (asset == null) { + return new VerificationResult<>( + "TokenSplitReasonVerificationRule", + VerificationStatus.FAIL, + "Asset data is missing." + ); + } + + AssetId assetId = asset.getId(); + if (assets.putIfAbsent(assetId, asset) != null) { + return new VerificationResult<>( + "TokenSplitReasonVerificationRule", + VerificationStatus.FAIL, + String.format("Duplicate asset id %s found in asset data.", assetId) + ); + } + } + + Transaction burnTokenLastTransaction = data.getReason().getToken().getLatestTransaction(); + DataHash root = data.getReason().getProofs().get(0).getAggregationPath().getRootHash(); + for (SplitReasonProof proof : data.getReason().getProofs()) { + MerkleTreePathVerificationResult aggregationPathResult = proof.getAggregationPath() + .verify(proof.getAssetId().toBitString().toBigInteger()); + if (!aggregationPathResult.isSuccessful()) { + return new VerificationResult<>( + "TokenSplitReasonVerificationRule", + VerificationStatus.FAIL, + String.format("Aggregation path verification failed for asset: %s", proof.getAssetId()) + ); + } + + MerkleTreePathVerificationResult assetTreePathResult = proof.getAssetTreePath() + .verify(token.getId().toBitString().toBigInteger()); + if (!assetTreePathResult.isSuccessful()) { + return new VerificationResult<>( + "TokenSplitReasonVerificationRule", + VerificationStatus.FAIL, + String.format("Asset tree path verification failed for token: %s", token.getId()) + ); + } + + if (!proof.getAggregationPath().getRootHash().equals(root)) { + return new VerificationResult<>( + "TokenSplitReasonVerificationRule", + VerificationStatus.FAIL, + "Current proof is not derived from the same asset tree as other proofs." + ); + } + + if (!Arrays.equals( + proof.getAssetTreePath().getRootHash().getImprint(), + proof.getAggregationPath().getSteps().get(0).getData().orElse(null) + )) { + return new VerificationResult<>( + "TokenSplitReasonVerificationRule", + VerificationStatus.FAIL, + "Asset tree root does not match aggregation path leaf." + ); + } + + Asset asset = assets.get(proof.getAssetId()); + + if (asset == null) { + return new VerificationResult<>( + "TokenSplitReasonVerificationRule", + VerificationStatus.FAIL, + String.format("Asset id %s not found in asset data.", proof.getAssetId()) + ); + } + + BigInteger amount = asset.getValue(); + + if (!proof.getAssetTreePath().getSteps().get(0).getValue().equals(amount)) { + return new VerificationResult<>( + "TokenSplitReasonVerificationRule", + VerificationStatus.FAIL, + String.format("Asset amount for asset id %s does not match asset tree leaf.", proof.getAssetId()) + ); + } + + if (!burnTokenLastTransaction.getRecipient() + .equals(Address.fromPredicate(BurnPredicate.create(proof.getAggregationPath().getRootHash().getImprint())))) { + return new VerificationResult<>( + "TokenSplitReasonVerificationRule", + VerificationStatus.FAIL, + "Aggregation path root does not match burn predicate." + ); + } + } + + return new VerificationResult<>( + "TokenSplitReasonVerificationRule", + VerificationStatus.OK + ); + } + +} diff --git a/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java b/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java new file mode 100644 index 0000000..b20bce6 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/asset/Asset.java @@ -0,0 +1,96 @@ +package org.unicitylabs.sdk.payment.asset; + +import java.math.BigInteger; +import java.util.List; +import java.util.Objects; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.util.BigIntegerConverter; + +/** + * 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); + + 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..4625845 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/payment/asset/AssetId.java @@ -0,0 +1,83 @@ +package org.unicitylabs.sdk.payment.asset; + +import java.util.Arrays; +import java.util.Objects; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.util.BitString; +import org.unicitylabs.sdk.util.HexConverter; + +/** + * Unique identifier of an asset. + */ +public class AssetId { + private final byte[] bytes; + + /** + * Create asset id from bytes. + * + * @param bytes asset id bytes + */ + public AssetId(byte[] bytes) { + Objects.requireNonNull(bytes, "Asset id cannot be null"); + + this.bytes = Arrays.copyOf(bytes, bytes.length); + } + + /** + * Get asset id bytes. + * + * @return asset id bytes + */ + public byte[] getBytes() { + return Arrays.copyOf(this.bytes, this.bytes.length); + } + + /** + * Deserialize asset id from CBOR bytes. + * + * @param bytes CBOR bytes + * + * @return asset id + */ + public static AssetId fromCbor(byte[] bytes) { + return new AssetId(CborDeserializer.decodeByteString(bytes)); + } + + /** + * Convert asset id to bit string form used by sparse merkle trees. + * + * @return bit string + */ + public BitString toBitString() { + return new BitString(this.bytes); + } + + /** + * Serialize asset id to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeByteString(this.bytes); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AssetId)) { + return false; + } + AssetId assetId = (AssetId) o; + return Arrays.equals(this.bytes, assetId.bytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(bytes); + } + + @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..1807ca3 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/EncodedPredicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/EncodedPredicate.java @@ -4,69 +4,81 @@ import java.util.List; import java.util.Objects; import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.util.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 { - 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) { + List data = CborDeserializer.decodeArray(bytes); + 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.encodeArray( + CborSerializer.encodeUnsignedInteger(this.engine.getId()), + CborSerializer.encodeByteString(this.code), + CborSerializer.encodeByteString(this.parameters) ); } @@ -75,9 +87,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 +99,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..91facb2 100644 --- a/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java +++ b/src/main/java/org/unicitylabs/sdk/predicate/Predicate.java @@ -1,45 +1,46 @@ 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; +import java.util.Arrays; /** - * Predicate structure. + * Base contract for all predicate implementations. */ -public interface Predicate extends SerializablePredicate { +public interface Predicate { /** - * Calculate predicate hash representation. + * Returns the predicate engine used by this predicate. * - * @return predicate hash + * @return the predicate engine */ - DataHash calculateHash(); + PredicateEngine getEngine(); /** - * Get predicate as reference. + * Encodes the predicate type/code portion. * - * @return predicate reference + * @return encoded predicate code bytes */ - PredicateReference getReference(); + byte[] encodeCode(); /** - * Is given public key owner of current predicate. + * Encodes the predicate parameter payload. * - * @param publicKey public key of potential owner - * @return true if is owner + * @return encoded predicate parameter bytes */ - boolean isOwner(byte[] publicKey); + byte[] encodeParameters(); /** - * Verify if predicate is valid for given token state. + * Compares this predicate with another predicate using encoded representation. * - * @param token current token state - * @param transaction current transaction - * @param trustBase trust base to verify against. - * @return true if successful + * @param other the predicate to compare against + * @return {@code true} when engine, code, and parameters are equal; otherwise {@code false} */ - boolean verify(Token token, TransferTransaction transaction, RootTrustBase trustBase); -} + default boolean isEqualTo(Predicate other) { + if (other == null) { + return false; + } + return this.getEngine() == other.getEngine() + && Arrays.equals(this.encodeCode(), other.encodeCode()) + && Arrays.equals(this.encodeParameters(), other.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..75be5fc --- /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. */ + PAY_TO_PUBLIC_KEY(1), + /** Predicate that references a Unicity identifier. */ + UNICITY_ID(2), + /** Predicate that marks state as unspendable (burned). */ + BURN(3); + + private final int id; + + 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..b2c88d3 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/BurnPredicate.java @@ -0,0 +1,81 @@ +package org.unicitylabs.sdk.predicate.builtin; + +import java.util.Arrays; +import java.util.Objects; +import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.PredicateEngine; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; + +/** + * 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(Predicate predicate) { + PredicateEngine engine = predicate.getEngine(); + if (engine != PredicateEngine.BUILT_IN) { + throw new IllegalArgumentException("Predicate engine must be BUILT_IN."); + } + + BuiltInPredicateType type = BuiltInPredicateType.fromId( + CborDeserializer.decodeUnsignedInteger(predicate.encodeCode()).asInt()); + if (type != BuiltInPredicateType.BURN) { + throw new IllegalArgumentException("Predicate type must be BURN."); + } + + return new BurnPredicate(predicate.encodeParameters()); + } + + /** + * 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..07e0bce --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/DefaultBuiltInPredicateVerifier.java @@ -0,0 +1,80 @@ +package org.unicitylabs.sdk.predicate.builtin; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.PredicateEngine; +import org.unicitylabs.sdk.predicate.builtin.verification.BuiltInPredicateVerifier; +import org.unicitylabs.sdk.predicate.builtin.verification.PayToPublicKeyPredicateVerifier; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifier; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +/** + * 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. + * + * @param service predicate verifier service + * @param trustBase root trust base + * @return default built-in predicate verifier + */ + public static DefaultBuiltInPredicateVerifier create(PredicateVerifierService service, RootTrustBase trustBase) { + return new DefaultBuiltInPredicateVerifier( + List.of( + new PayToPublicKeyPredicateVerifier() + ) + ); + } + + @Override + public VerificationResult verify(Predicate 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/PayToPublicKeyPredicate.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java new file mode 100644 index 0000000..df43f36 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicate.java @@ -0,0 +1,94 @@ +package org.unicitylabs.sdk.predicate.builtin; + +import java.util.Arrays; +import java.util.Objects; +import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.PredicateEngine; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; + +/** + * Built-in predicate that locks an output to a secp256k1 public key. + */ +public class PayToPublicKeyPredicate implements BuiltInPredicate { + + private final byte[] publicKey; + + private PayToPublicKeyPredicate(byte[] publicKey) { + this.publicKey = publicKey; + } + + /** + * Get public key bytes. + * + * @return public key bytes + */ + public byte[] getPublicKey() { + return Arrays.copyOf(this.publicKey, this.publicKey.length); + } + + /** + * Get built-in predicate type. + * + * @return predicate type + */ + public BuiltInPredicateType getType() { + return BuiltInPredicateType.PAY_TO_PUBLIC_KEY; + } + + /** + * Create predicate from public key bytes. + * + * @param publicKey public key bytes + * + * @return pay-to-public-key predicate + */ + public static PayToPublicKeyPredicate create(byte[] publicKey) { + return new PayToPublicKeyPredicate(Arrays.copyOf(publicKey, publicKey.length)); + } + + /** + * Parse pay-to-public-key predicate from generic predicate. + * + * @param predicate generic predicate + * + * @return pay-to-public-key predicate + */ + public static PayToPublicKeyPredicate fromPredicate(Predicate predicate) { + PredicateEngine engine = predicate.getEngine(); + if (engine != PredicateEngine.BUILT_IN) { + throw new IllegalArgumentException("Predicate engine must be BUILT_IN."); + } + + BuiltInPredicateType type = BuiltInPredicateType.fromId( + CborDeserializer.decodeUnsignedInteger(predicate.encodeCode()).asInt()); + if (type != BuiltInPredicateType.PAY_TO_PUBLIC_KEY) { + throw new IllegalArgumentException("Predicate type must be PAY_TO_PUBLIC_KEY."); + } + + return new PayToPublicKeyPredicate(predicate.encodeParameters()); + } + + /** + * Create predicate from signing service public key. + * + * @param signingService signing service + * + * @return pay-to-public-key predicate + */ + public static PayToPublicKeyPredicate fromSigningService(SigningService signingService) { + Objects.requireNonNull(signingService, "Signing service cannot be null"); + return new PayToPublicKeyPredicate(signingService.getPublicKey()); + } + + /** + * Encode predicate parameters. + * + * @return encoded parameter bytes + */ + @Override + public byte[] encodeParameters() { + return this.getPublicKey(); + } + +} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicateUnlockScript.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicateUnlockScript.java new file mode 100644 index 0000000..e50843b --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/PayToPublicKeyPredicateUnlockScript.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 PayToPublicKeyPredicate} containing a transaction signature. + */ +public class PayToPublicKeyPredicateUnlockScript implements UnlockScript { + + private final Signature signature; + + private PayToPublicKeyPredicateUnlockScript(Signature signature) { + this.signature = signature; + } + + /** + * Returns the unlock signature. + * + * @return signature used to unlock the predicate + */ + public Signature getSignature() { + return this.signature; + } + + /** + * Creates an unlock script by signing the source-state and transaction-hash payload. + * + * @param transaction transaction being authorized + * @param signingService signing service used to produce the signature + * @return created unlock script + */ + public static PayToPublicKeyPredicateUnlockScript create( + Transaction transaction, + SigningService signingService + ) { + DataHash hash = new DataHasher(HashAlgorithm.SHA256) + .update( + CborSerializer.encodeArray( + CborSerializer.encodeByteString(transaction.getSourceStateHash().getData()), + CborSerializer.encodeByteString(transaction.calculateTransactionHash().getData()) + ) + ) + .digest(); + + return new PayToPublicKeyPredicateUnlockScript(signingService.sign(hash)); + } + + /** + * Decodes an unlock script from encoded signature bytes. + * + * @param bytes encoded signature bytes + * @return decoded unlock script + */ + public static PayToPublicKeyPredicateUnlockScript decode(byte[] bytes) { + return new PayToPublicKeyPredicateUnlockScript(Signature.decode(bytes)); + } + + @Override + public byte[] encode() { + return this.signature.encode(); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/BuiltInPredicateVerifier.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/BuiltInPredicateVerifier.java new file mode 100644 index 0000000..82e595d --- /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.Predicate; +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(Predicate predicate, DataHash sourceStateHash, + DataHash transactionHash, byte[] unlockScript); +} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/PayToPublicKeyPredicateVerifier.java b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/PayToPublicKeyPredicateVerifier.java new file mode 100644 index 0000000..bca8e6e --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/predicate/builtin/verification/PayToPublicKeyPredicateVerifier.java @@ -0,0 +1,57 @@ +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.Predicate; +import org.unicitylabs.sdk.predicate.builtin.BuiltInPredicateType; +import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +/** + * Verifies {@link PayToPublicKeyPredicate} unlock scripts using secp256k1 signatures. + */ +public class PayToPublicKeyPredicateVerifier implements BuiltInPredicateVerifier { + + /** + * Creates a verifier instance for pay-to-public-key predicates. + */ + public PayToPublicKeyPredicateVerifier() {} + + @Override + public BuiltInPredicateType getType() { + return BuiltInPredicateType.PAY_TO_PUBLIC_KEY; + } + + + @Override + public VerificationResult verify(Predicate encodedPredicate, + DataHash sourceStateHash, + DataHash transactionHash, byte[] unlockScript) { + PayToPublicKeyPredicate predicate = PayToPublicKeyPredicate.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<>("PayToPublicKeyPredicateVerifier", VerificationStatus.FAIL, + "Signature verification failed."); + } + + return new VerificationResult<>("PayToPublicKeyPredicateVerifier", 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..9998bee --- /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.Predicate; +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(Predicate predicate, DataHash sourceStateHash, + DataHash transactionHash, byte[] unlockScript); +} diff --git a/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifierService.java b/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifierService.java new file mode 100644 index 0000000..ac03726 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/predicate/verification/PredicateVerifierService.java @@ -0,0 +1,75 @@ +package org.unicitylabs.sdk.predicate.verification; + +import java.util.HashMap; +import java.util.Map; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.PredicateEngine; +import org.unicitylabs.sdk.predicate.builtin.DefaultBuiltInPredicateVerifier; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +/** + * Service registry that routes predicate verification to engine-specific verifiers. + */ +public class PredicateVerifierService { + + private final Map verifiers = new HashMap<>(); + + private PredicateVerifierService() { + + } + + /** + * Creates a predicate verifier service with default verifier registrations. + * + * @param trustBase root trust base used by verifiers that require trust context + * @return initialized predicate verifier service + */ + public static PredicateVerifierService create(RootTrustBase trustBase) { + PredicateVerifierService verifier = new PredicateVerifierService(); + verifier.addVerifier(DefaultBuiltInPredicateVerifier.create(verifier, trustBase)); + + 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(Predicate 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..2519c57 100644 --- a/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializer.java +++ b/src/main/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializer.java @@ -18,7 +18,8 @@ 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. @@ -28,7 +29,7 @@ private CborDeserializer() {} * @param parsed value type * @return parsed value */ - public static T readOptional(byte[] data, Function reader) { + public static T decodeNullable(byte[] data, Function reader) { if (Byte.compareUnsigned(new CborReader(data).readByte(), (byte) 0xf6) == 0) { return null; } @@ -42,7 +43,7 @@ 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)); } @@ -53,7 +54,7 @@ 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)); } @@ -64,7 +65,7 @@ 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))); @@ -76,7 +77,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); @@ -94,7 +95,7 @@ public static List readArray(byte[] data) { * @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); @@ -114,7 +115,7 @@ 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()); @@ -126,7 +127,7 @@ public static CborTag readTag(byte[] data) { * @param data bytes * @return boolean */ - public static boolean readBoolean(byte[] data) { + public static boolean decodeBoolean(byte[] data) { byte byteValue = new CborReader(data).readByte(); if (byteValue == (byte) 0xf5) { return true; 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..88ed416 100644 --- a/src/main/java/org/unicitylabs/sdk/serializer/json/ByteArrayJson.java +++ b/src/main/java/org/unicitylabs/sdk/serializer/json/ByteArrayJson.java @@ -62,8 +62,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 */ 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/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/Address.java b/src/main/java/org/unicitylabs/sdk/transaction/Address.java new file mode 100644 index 0000000..b344895 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/Address.java @@ -0,0 +1,99 @@ +package org.unicitylabs.sdk.transaction; + +import java.util.Arrays; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.DataHasher; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.predicate.EncodedPredicate; +import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.util.HexConverter; + +/** + * Transaction address. + */ +public class Address { + + private final byte[] bytes; + + private Address(byte[] bytes) { + this.bytes = bytes; + } + + /** + * Returns a copy of the address bytes. + * + * @return address bytes + */ + public byte[] getBytes() { + return Arrays.copyOf(this.bytes, this.bytes.length); + } + + /** + * Create an address from bytes. + * + * @param bytes address bytes + * + * @return address + */ + public static Address fromBytes(byte[] bytes) { + if (bytes == null || bytes.length != 32) { + throw new IllegalArgumentException("Invalid address length"); + } + + return new Address(Arrays.copyOf(bytes, bytes.length)); + } + + /** + * Deserialize an address from CBOR bytes. + * + * @param bytes CBOR bytes + * + * @return address + */ + public static Address fromCbor(byte[] bytes) { + return Address.fromBytes(CborDeserializer.decodeByteString(bytes)); + } + + /** + * Create an address from predicate. + * + * @param predicate predicate + * + * @return address + */ + public static Address fromPredicate(Predicate predicate) { + DataHash hash = new DataHasher(HashAlgorithm.SHA256).update( + EncodedPredicate.fromPredicate(predicate).toCbor()).digest(); + return new Address(hash.getData()); + } + + /** + * Serialize address to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeByteString(this.bytes); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Address)) { + return false; + } + Address address = (Address) o; + return Arrays.equals(this.bytes, address.bytes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(this.bytes); + } + + @Override + public String toString() { + return String.format("Address{bytes=%s}", HexConverter.encode(this.bytes)); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java new file mode 100644 index 0000000..a8546e9 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedMintTransaction.java @@ -0,0 +1,139 @@ +package org.unicitylabs.sdk.transaction; + +import java.util.List; +import org.unicitylabs.sdk.api.InclusionProof; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.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; + +/** + * 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 byte[] getData() { + return this.transaction.getData(); + } + + @Override + public Predicate getLockScript() { + return this.transaction.getLockScript(); + } + + @Override + public Address 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(); + } + + @Override + public byte[] getNonce() { + return this.transaction.getNonce(); + } + + /** + * Returns the inclusion proof certifying this transaction. + * + * @return inclusion proof + */ + public InclusionProof getInclusionProof() { + return this.inclusionProof; + } + + /** + * Deserializes a certified mint transaction from CBOR. + * + * @param bytes CBOR-encoded certified mint transaction + * @return decoded certified mint transaction + */ + public static CertifiedMintTransaction fromCbor(byte[] bytes) { + List data = CborDeserializer.decodeArray(bytes); + return new CertifiedMintTransaction(MintTransaction.fromCbor(data.get(0)), + InclusionProof.fromCbor(data.get(1))); + } + + /** + * Creates a certified mint transaction after verifying the inclusion proof. + * + * @param trustBase trust base used to verify inclusion proof signatures + * @param predicateVerifier service used for predicate verification during proof validation + * @param transaction mint transaction to certify + * @param inclusionProof inclusion proof for the transaction + * @return certified mint transaction + * @throws VerificationException if inclusion proof verification fails + */ + public static CertifiedMintTransaction fromTransaction(RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, MintTransaction transaction, + InclusionProof inclusionProof) { + VerificationResult result = InclusionProofVerificationRule.verify( + trustBase, + predicateVerifier, + inclusionProof, + transaction + ); + 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..c386ac3 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/CertifiedTransferTransaction.java @@ -0,0 +1,171 @@ +package org.unicitylabs.sdk.transaction; + +import java.util.List; +import org.unicitylabs.sdk.api.InclusionProof; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.predicate.Predicate; +import org.unicitylabs.sdk.predicate.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; + +/** + * 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; + } + + /** + * Get transaction payload data. + * + * @return payload data bytes + */ + @Override + public byte[] getData() { + return this.transaction.getData(); + } + + /** + * Get predicate locking script for this transaction output. + * + * @return lock script predicate + */ + @Override + public Predicate getLockScript() { + return this.transaction.getLockScript(); + } + + /** + * Get recipient address of this transaction. + * + * @return recipient address + */ + @Override + public Address getRecipient() { + return this.transaction.getRecipient(); + } + + /** + * Get source state hash of this transaction. + * + * @return source state hash + */ + @Override + public DataHash getSourceStateHash() { + return this.transaction.getSourceStateHash(); + } + + /** + * Get transaction chosen random bytes. + * + * @return random bytes + */ + @Override + public byte[] getNonce() { + return this.transaction.getNonce(); + } + + /** + * Get inclusion proof for this transaction. + * + * @return inclusion proof + */ + public InclusionProof getInclusionProof() { + return this.inclusionProof; + } + + /** + * Deserialize a certified transfer transaction from CBOR bytes. + * + * @param bytes CBOR encoded certified transfer transaction + * + * @return certified transfer transaction + */ + public static CertifiedTransferTransaction fromCbor(byte[] bytes) { + List data = CborDeserializer.decodeArray(bytes); + + return new CertifiedTransferTransaction(TransferTransaction.fromCbor(data.get(0)), + 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) { + 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..2c50b34 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/MintTransaction.java @@ -1,434 +1,226 @@ - 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 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.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.Predicate; +import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +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.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> { - - @JsonCreator - MintTransaction( - @JsonProperty("data") - Data data, - @JsonProperty("inclusionProof") - InclusionProof inclusionProof) { - super(data, inclusionProof); +public class MintTransaction implements Transaction { + + private final MintTransactionState sourceStateHash; + private final Predicate lockScript; + private final Address recipient; + private final TokenId tokenId; + private final TokenType tokenType; + private final byte[] data; + + private MintTransaction( + MintTransactionState sourceStateHash, + Predicate lockScript, + Address recipient, + TokenId tokenId, + TokenType tokenType, + byte[] data + ) { + this.sourceStateHash = sourceStateHash; + this.lockScript = lockScript; + this.recipient = recipient; + this.tokenId = tokenId; + this.tokenType = tokenType; + this.data = data; } + /** - * Create mint transaction from CBOR bytes. + * Retrieves the state hash of the source state. * - * @param bytes CBOR bytes - * @return mint transaction + * @return the source state hash represented as a {@code MintTransactionState}. */ - 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 MintTransactionState getSourceStateHash() { + return this.sourceStateHash; } /** - * Create mint transaction from JSON string. + * Retrieves the lock script. * - * @param input JSON string - * @return mint transaction + * @return a {@code Predicate} representing the lock script. */ - public static MintTransaction fromJson(String input) { - try { - return UnicityObjectMapper.JSON.readValue(input, MintTransaction.class); - } catch (JsonProcessingException e) { - throw new JsonSerializationException(MintTransaction.class, e); - } + public Predicate getLockScript() { + return this.lockScript; } /** - * Verify mint transaction. + * Retrieves the initial owner address. * - * @param trustBase root trust base - * @return verification result + * @return the recipient address as an {@code Address}. */ - 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"); - } + public Address getRecipient() { + return this.recipient; + } - SigningService signingService = MintSigningService.create(this.getData().getTokenId()); - if (!Arrays.equals(signingService.getPublicKey(), - this.getInclusionProof().getAuthenticator().get().getPublicKey())) { - return VerificationResult.fail("Authenticator public key mismatch"); - } + /** + * Retrieves the unique token identifier. + * + * @return the token identifier as a {@code TokenId}. + */ + public TokenId getTokenId() { + return this.tokenId; + } - if (!this.getInclusionProof().getAuthenticator().get() - .verify(this.getInclusionProof().getTransactionHash().get())) { - return VerificationResult.fail("Authenticator verification failed"); - } + /** + * Retrieves the type identifier of the token. + * + * @return the token type as a {@code TokenType}. + */ + public TokenType getTokenType() { + return this.tokenType; + } - VerificationResult reasonResult = this.getData().getReason() - .map(reason -> reason.verify(this)) - .orElse(VerificationResult.success()); + @Override + public byte[] getData() { + return this.data; + } - if (!reasonResult.isSuccessful()) { - return VerificationResult.fail("Mint reason verification failed", List.of(reasonResult)); - } + @Override + public byte[] getNonce() { + 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 address + * @param tokenId token identifier + * @param tokenType token type identifier + * @param data payload bytes + * + * @return mint transaction + */ + public static MintTransaction create( + Address recipient, + TokenId tokenId, + TokenType tokenType, + byte[] data + ) { + Objects.requireNonNull(recipient, "Recipient cannot be null"); + Objects.requireNonNull(tokenId, "Token ID cannot be null"); + Objects.requireNonNull(tokenType, "Token type cannot be null"); + Objects.requireNonNull(data, "Data cannot be null"); + + SigningService signingService = MintSigningService.create(tokenId); + return new MintTransaction( + MintTransactionState.create(tokenId), + PayToPublicKeyPredicate.fromSigningService(signingService), + recipient, + tokenId, + tokenType, + Arrays.copyOf(data, data.length) ); - - 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 bytes CBOR bytes * - * @param mint reason + * @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; - } - - /** - * 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 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; + public static MintTransaction fromCbor(byte[] bytes) { + List data = CborDeserializer.decodeArray(bytes); + List aux = CborDeserializer.decodeArray(data.get(2)); + + return MintTransaction.create( + Address.fromCbor(data.get(0)), + TokenId.fromCbor(data.get(1)), + TokenType.fromCbor(aux.get(0)), + CborDeserializer.decodeByteString(aux.get(1)) + ); + } - 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.getNonce()) + ) + ) + .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.encodeArray( + this.recipient.toCbor(), + this.tokenId.toCbor(), + CborSerializer.encodeArray(this.tokenType.toCbor(), + CborSerializer.encodeByteString(this.data)) + ); } /** - * 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..b2147b5 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/MintTransactionState.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/MintTransactionState.java @@ -1,12 +1,14 @@ package org.unicitylabs.sdk.transaction; -import org.unicitylabs.sdk.api.RequestId; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.token.TokenId; +import java.util.Objects; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.DataHasher; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.util.HexConverter; /** - * Token mint state. + * Represents the state of a mint transaction. */ public class MintTransactionState extends DataHash { @@ -18,12 +20,24 @@ private MintTransactionState(DataHash hash) { } /** - * 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..ee1d8ee --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/Token.java @@ -0,0 +1,202 @@ +package org.unicitylabs.sdk.transaction; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +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.verification.CertifiedMintTransactionVerificationRule; +import org.unicitylabs.sdk.transaction.verification.CertifiedTransferTransactionVerificationRule; +import org.unicitylabs.sdk.util.verification.VerificationException; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +/** + * Immutable token aggregate containing the certified genesis mint transaction and transfer history. + */ +public final class Token { + + 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()); + } + + /** + * 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) { + List data = CborDeserializer.decodeArray(bytes); + List transactions = CborDeserializer.decodeArray(data.get(1)); + + return new Token( + CertifiedMintTransaction.fromCbor(data.get(0)), + transactions.stream().map(CertifiedTransferTransaction::fromCbor) + .collect(Collectors.toList()) + ); + } + + /** + * Creates a token from a certified genesis transaction and verifies it. + * + * @param trustBase trust base used for certification checks + * @param predicateVerifier predicate verifier service + * @param genesis certified mint transaction + * @return verified token instance + * @throws VerificationException if genesis verification fails + */ + public static Token mint(RootTrustBase trustBase, PredicateVerifierService predicateVerifier, + CertifiedMintTransaction genesis) { + Token token = new Token(genesis); + VerificationResult result = token.verify(trustBase, predicateVerifier); + 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) { + VerificationResult result = CertifiedTransferTransactionVerificationRule.verify( + trustBase, + predicateVerifier, + this.getLatestTransaction(), + 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 + * @return verification result with nested per-step verification details + */ + public VerificationResult verify(RootTrustBase trustBase, + PredicateVerifierService predicateVerifier) { + List> results = new ArrayList<>(); + VerificationResult result = CertifiedMintTransactionVerificationRule.verify(trustBase, + predicateVerifier, 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, + i == 0 ? this.genesis : this.transactions.get(i - 1), 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.encodeArray( + 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..e64f434 100644 --- a/src/main/java/org/unicitylabs/sdk/token/TokenId.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/TokenId.java @@ -1,12 +1,8 @@ -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.security.SecureRandom; 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; @@ -15,68 +11,55 @@ /** * Globally unique identifier of a token. */ -@JsonSerialize(using = TokenIdJson.Serializer.class) -@JsonDeserialize(using = TokenIdJson.Deserializer.class) -public class TokenId { +public final 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 +67,18 @@ public byte[] toCbor() { return CborSerializer.encodeByteString(this.bytes); } + /** + * Convert token id to bit string. + * + * @return bit string + */ + public BitString toBitString() { + return new BitString(this.bytes); + } + @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..cafdd9e --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/TokenType.java @@ -0,0 +1,97 @@ +package org.unicitylabs.sdk.transaction; + +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Objects; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.util.BitString; +import org.unicitylabs.sdk.util.HexConverter; + +/** + * Type identifier of a token. + */ +public final class TokenType { + + private static final SecureRandom RANDOM = new SecureRandom(); + private final byte[] bytes; + + /** + * Create a token type from byte array. + * + * @param bytes token type bytes + */ + public TokenType(byte[] bytes) { + Objects.requireNonNull(bytes, "Token type cannot be null"); + + this.bytes = Arrays.copyOf(bytes, bytes.length); + } + + /** + * Get token type bytes. + * + * @return token type bytes + */ + public byte[] getBytes() { + return Arrays.copyOf(this.bytes, this.bytes.length); + } + + /** + * Generate a random token type. + * + * @return token type + */ + public static TokenType generate() { + byte[] bytes = new byte[32]; + RANDOM.nextBytes(bytes); + return new TokenType(bytes); + } + + /** + * Deserialize a token type from CBOR bytes. + * + * @param bytes CBOR encoded token type bytes + * + * @return token type + */ + public static TokenType fromCbor(byte[] bytes) { + return new TokenType(CborDeserializer.decodeByteString(bytes)); + } + + /** + * Serialize token type to CBOR bytes. + * + * @return CBOR bytes + */ + public byte[] toCbor() { + return CborSerializer.encodeByteString(this.bytes); + } + + /** + * Convert token type to bit string. + * + * @return bit string + */ + public BitString toBitString() { + return new BitString(this.bytes); + } + + @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..3cdea0e 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/Transaction.java @@ -1,119 +1,66 @@ - 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.Predicate; /** - * Token transaction. - * - * @param transaction data + * Common interface for token transactions. */ -public abstract class Transaction> { - - private final T data; - private final InclusionProof inclusionProof; - - @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"); +public interface Transaction { - this.data = data; - this.inclusionProof = inclusionProof; - } + /** + * Get transaction payload bytes. + * + * @return payload bytes + */ + byte[] getData(); /** - * Get transaction data. + * Gets the predicate that locks this transaction. * - * @return transaction data + * @return lock script predicate */ - public T getData() { - return this.data; - } + Predicate getLockScript(); /** - * Get transaction inclusion proof. + * Gets the transaction recipient address. * - * @return inclusion proof + * @return recipient address */ - public InclusionProof getInclusionProof() { - return this.inclusionProof; - } + Address 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[] getNonce(); - 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..83754ee 100644 --- a/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java +++ b/src/main/java/org/unicitylabs/sdk/transaction/TransferTransaction.java @@ -1,298 +1,173 @@ - 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.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; /** - * 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 { + + private final DataHash sourceStateHash; + private final Predicate lockScript; + private final Address recipient; + private final byte[] nonce; + private final byte[] data; + + private TransferTransaction( + DataHash sourceStateHash, + Predicate lockScript, + Address recipient, + byte[] x, + byte[] data + ) { + this.sourceStateHash = sourceStateHash; + this.lockScript = lockScript; + this.recipient = recipient; + this.nonce = x; + this.data = data; + } - @JsonCreator - TransferTransaction( - @JsonProperty("data") - Data data, - @JsonProperty("inclusionProof") - InclusionProof inclusionProof) { - super(data, inclusionProof); + + @Override + public byte[] getData() { + return Arrays.copyOf(this.data, this.data.length); } - /** - * Create transfer transaction from CBOR bytes. - * - * @param bytes CBOR bytes - * @return transfer transaction - */ - public static TransferTransaction fromCbor(byte[] bytes) { - List data = CborDeserializer.readArray(bytes); + @Override + public Predicate getLockScript() { + return this.lockScript; + } - return new TransferTransaction( - Data.fromCbor(data.get(0)), - InclusionProof.fromCbor(data.get(1)) - ); + @Override + public Address getRecipient() { + return this.recipient; } - /** - * 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 DataHash getSourceStateHash() { + return this.sourceStateHash; + } + + @Override + public byte[] getNonce() { + return Arrays.copyOf(this.nonce, this.nonce.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 owner current owner predicate + * @param recipient recipient address + * @param x transaction randomness component + * @param data transfer payload + * @return created transfer transaction + * @throws RuntimeException if the owner predicate does not match the latest recipient */ - public VerificationResult verify(RootTrustBase trustBase, Token token) { - Predicate predicate = PredicateEngineService.createPredicate(token.getState().getPredicate()); + public static TransferTransaction create(Token token, Predicate owner, Address recipient, + byte[] x, byte[] data) { + Transaction transaction = token.getLatestTransaction(); + if (!transaction.getRecipient().equals(Address.fromPredicate(owner))) { + throw new RuntimeException("Predicate does not match pay to script hash."); + } - return 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(), + owner, + recipient, + x, + data + ); } /** - * Transaction data for token state transitions. + * Deserializes a transfer transaction from CBOR bytes. + * + * @param bytes CBOR-encoded transfer transaction + * @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; - } - - /** - * Get transaction recipient address. - * - * @return recipient address - */ - public Address getRecipient() { - return this.recipient; - } - - /** - * 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) - ) - ); - } + public static TransferTransaction fromCbor(byte[] bytes) { + List data = CborDeserializer.decodeArray(bytes); - /** - * 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 new TransferTransaction( + new DataHash(HashAlgorithm.SHA256, CborDeserializer.decodeByteString(data.get(0))), + EncodedPredicate.fromCbor(data.get(1)), + Address.fromCbor(data.get(2)), + CborDeserializer.decodeByteString(data.get(3)), + CborDeserializer.decodeByteString(data.get(4)) + ); + } - /** - * 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.nonce) + ) + ) + .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( + CborSerializer.encodeArray( + this.recipient.toCbor(), + CborSerializer.encodeByteString(this.nonce), + CborSerializer.encodeByteString(this.data) + ) + ) + .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.encodeArray( + CborSerializer.encodeByteString(this.sourceStateHash.getData()), + EncodedPredicate.fromPredicate(this.lockScript).toCbor(), + this.recipient.toCbor(), + CborSerializer.encodeByteString(this.nonce), + CborSerializer.encodeByteString(this.data) + ); + } - @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, x=%s, data=%s}", + this.sourceStateHash, this.lockScript, this.recipient, HexConverter.encode(this.nonce), + 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..f85a454 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedMintTransactionVerificationRule.java @@ -0,0 +1,67 @@ +package org.unicitylabs.sdk.transaction.verification; + +import java.util.ArrayList; +import java.util.Arrays; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.MintSigningService; +import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.predicate.EncodedPredicate; +import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.transaction.CertifiedMintTransaction; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +/** + * Verification rule set for certified mint transactions. + * + *

The verification checks that the lock script in certification data matches the expected + * mint lock script derived from the token id, and that the inclusion proof is valid. + */ +public class CertifiedMintTransactionVerificationRule { + + private CertifiedMintTransactionVerificationRule() { + } + + /** + * Verify a certified mint transaction. + * + * @param trustBase root trust base used for inclusion proof verification + * @param predicateVerifier predicate verifier used by inclusion proof verification + * @param transaction certified mint transaction to verify + * + * @return verification result with child results for each validation step + */ + public static VerificationResult verify(RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, CertifiedMintTransaction transaction) { + ArrayList> results = new ArrayList>(); + + SigningService signingService = MintSigningService.create(transaction.getTokenId()); + VerificationResult result = Arrays.equals( + EncodedPredicate.fromPredicate(PayToPublicKeyPredicate.fromSigningService(signingService)) + .toCbor(), + transaction.getInclusionProof() + .getCertificationData() + .map(c -> EncodedPredicate.fromPredicate(c.getLockScript()).toCbor()) + .orElse(null)) + ? 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); + } + + 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..9a107fa --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/CertifiedTransferTransactionVerificationRule.java @@ -0,0 +1,73 @@ +package org.unicitylabs.sdk.transaction.verification; + +import java.util.ArrayList; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.transaction.Address; +import org.unicitylabs.sdk.transaction.CertifiedTransferTransaction; +import org.unicitylabs.sdk.transaction.Transaction; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; + +/** + * Verification rule set for certified transfer transactions. + * + *

The verification checks inclusion proof validity, validates that the current transaction + * is spent by previous recipient and ensures source-state-hash continuity. + */ +public class CertifiedTransferTransactionVerificationRule { + + private CertifiedTransferTransactionVerificationRule() { + } + + /** + * Verify a certified transfer transaction against the previous transaction. + * + * @param trustBase root trust base used for inclusion proof verification + * @param predicateVerifier predicate verifier used by inclusion proof verification + * @param latestTransaction latest transaction in token history + * @param transaction certified transfer transaction to verify + * + * @return verification result with child results for each validation step + */ + public static VerificationResult verify( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + Transaction latestTransaction, + 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); + } + + Address payToScriptHash = Address.fromPredicate(transaction.getLockScript()); + result = new VerificationResult<>("RecipientVerificationRule", + latestTransaction.getRecipient().equals(payToScriptHash) ? VerificationStatus.OK + : VerificationStatus.FAIL); + results.add(result); + + if (result.getStatus() != VerificationStatus.OK) { + return new VerificationResult<>("CertifiedTransferTransactionVerificationRule", + VerificationStatus.FAIL, + "Transaction owner does not match the previous transaction recipient", results); + } + + result = new VerificationResult<>("SourceStateHashVerificationRule", + latestTransaction.calculateStateHash().equals(transaction.getSourceStateHash()) + ? VerificationStatus.OK : VerificationStatus.FAIL); + results.add(result); + if (result.getStatus() != VerificationStatus.OK) { + return new VerificationResult<>("CertifiedTransferTransactionVerificationRule", + VerificationStatus.FAIL, + "Source state hash does not match the previous transaction state hash", results); + } + + return new VerificationResult<>("CertifiedTransferTransactionVerificationRule", + VerificationStatus.OK, "", results); + } +} diff --git a/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationRule.java b/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationRule.java new file mode 100644 index 0000000..a8ef5e2 --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationRule.java @@ -0,0 +1,103 @@ +package org.unicitylabs.sdk.transaction.verification; + +import java.util.Arrays; +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.mtree.MerkleTreePathVerificationResult; +import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreePathStep; +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 { + + private InclusionProofVerificationRule() { + } + + /** + * Verifies the provided inclusion proof against the specified trust base and transaction. + * + * @param trustBase the root trust base used to validate the inclusion proof + * @param predicateVerifier the service responsible for evaluating transaction predicates + * @param inclusionProof the inclusion proof containing certification data and merkle tree path + * @param transaction the transaction that is being verified against the proof + * + * @return a {@code VerificationResult} object containing the {@code InclusionProofVerificationStatus} + * and additional details about the verification outcome + */ + public static VerificationResult verify( + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + InclusionProof inclusionProof, + Transaction transaction + ) { + VerificationResult result = UnicityCertificateVerification.verify(trustBase, inclusionProof); + if (result.getStatus() != VerificationStatus.OK) { + return new VerificationResult<>("InclusionProofVerificationRule", + InclusionProofVerificationStatus.INVALID_TRUSTBASE, "", result); + } + + StateId stateId = StateId.fromTransaction(transaction); + MerkleTreePathVerificationResult pathVerificationResult = inclusionProof.getMerkleTreePath() + .verify(stateId.toBitString().toBigInteger()); + if (!pathVerificationResult.isPathValid()) { + return new VerificationResult<>("InclusionProofVerificationRule", + InclusionProofVerificationStatus.PATH_INVALID); + } + + if (!pathVerificationResult.isPathIncluded()) { + return new VerificationResult<>("InclusionProofVerificationRule", + InclusionProofVerificationStatus.PATH_NOT_INCLUDED); + } + + 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); + } + + result = predicateVerifier.verify(certificationData.getLockScript(), + certificationData.getSourceStateHash(), certificationData.getTransactionHash(), + certificationData.getUnlockScript()); + + if (result.getStatus() != VerificationStatus.OK) { + return new VerificationResult<>("InclusionProofVerificationRule", + InclusionProofVerificationStatus.NOT_AUTHENTICATED, "", result); + } + + DataHash leafValue = certificationData.calculateLeafValue(); + byte[] pathValue = inclusionProof.getMerkleTreePath().getSteps().stream().findFirst() + .flatMap(SparseMerkleTreePathStep::getData).orElse(null); + if (pathValue == null || !Arrays.equals(leafValue.getImprint(), pathValue)) { + return new VerificationResult<>("InclusionProofVerificationRule", + InclusionProofVerificationStatus.LEAF_VALUE_MISMATCH); + } + + 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..20178bd --- /dev/null +++ b/src/main/java/org/unicitylabs/sdk/transaction/verification/InclusionProofVerificationStatus.java @@ -0,0 +1,23 @@ +package org.unicitylabs.sdk.transaction.verification; + +/** + * Status codes returned by inclusion proof verification. + */ +public enum InclusionProofVerificationStatus { + /** The provided trust base is invalid or cannot be used for verification. */ + INVALID_TRUSTBASE, + /** Leaf value in the proof does not match the expected transaction. */ + LEAF_VALUE_MISMATCH, + /** Certification data required for verification is missing. */ + MISSING_CERTIFICATION_DATA, + /** Transaction hash does not match the value referenced by the proof. */ + TRANSACTION_HASH_MISMATCH, + /** Proof authentication failed. */ + NOT_AUTHENTICATED, + /** Proof path is not included in the committed tree state. */ + PATH_NOT_INCLUDED, + /** Proof path structure or hashes are invalid. */ + PATH_INVALID, + /** Inclusion proof verification succeeded. */ + OK +} diff --git a/src/main/java/org/unicitylabs/sdk/util/BitString.java b/src/main/java/org/unicitylabs/sdk/util/BitString.java index ef9506e..eb286fa 100644 --- a/src/main/java/org/unicitylabs/sdk/util/BitString.java +++ b/src/main/java/org/unicitylabs/sdk/util/BitString.java @@ -1,11 +1,11 @@ package org.unicitylabs.sdk.util; import java.math.BigInteger; -import org.unicitylabs.sdk.hash.DataHash; +import org.unicitylabs.sdk.api.StateId; /** - * Represents a bit string as a BigInteger. This class is used to ensure that leading zero bits are - * retained when converting between byte arrays and BigInteger. + * Represents a bit string as a BigInteger. This class is used to ensure that leading zero bits are retained when + * converting between byte arrays and BigInteger. */ public class BitString { @@ -24,18 +24,8 @@ public BitString(byte[] data) { } /** - * Creates a BitString from a DataHash imprint. - * - * @param dataHash DataHash - * @return A BitString instance - */ - public static BitString fromDataHash(DataHash dataHash) { - return new BitString(dataHash.getImprint()); - } - - /** - * Converts BitString to BigInteger by adding a leading byte 1 to input byte array. This is to - * ensure that the BigInteger will retain the leading zero bits. + * Converts BitString to BigInteger by adding a leading byte 1 to input byte array. This is to ensure that the + * BigInteger will retain the leading zero bits. * * @return The BigInteger representation of the bit string */ 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..9246c53 100644 --- a/src/main/java/org/unicitylabs/sdk/util/InclusionProofUtils.java +++ b/src/main/java/org/unicitylabs/sdk/util/InclusionProofUtils.java @@ -7,10 +7,15 @@ 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; +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; /** * Utility class for working with inclusion proofs. @@ -28,33 +33,38 @@ 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 + 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, + PredicateVerifierService predicateVerifier, + Transaction transaction, Duration timeout, Duration interval ) { @@ -64,7 +74,8 @@ public static CompletableFuture waitInclusionProof( long startTime = System.currentTimeMillis(); long timeoutMillis = timeout.toMillis(); - checkInclusionProof(client, trustBase, commitment, future, startTime, timeoutMillis, + checkInclusionProof(client, trustBase, predicateVerifier, transaction, future, startTime, + timeoutMillis, interval.toMillis()); return future; @@ -73,32 +84,34 @@ public static CompletableFuture waitInclusionProof( private static void checkInclusionProof( StateTransitionClient client, RootTrustBase trustBase, - Commitment commitment, + 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: CompletableFuture.delayedExecutor(intervalMillis, TimeUnit.MILLISECONDS) - .execute(() -> checkInclusionProof(client, trustBase, commitment, future, startTime, + .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/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..9801716 --- /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..ef4f247 100644 --- a/src/test/java/org/unicitylabs/sdk/AndroidCompatibilityTest.java +++ b/src/test/java/org/unicitylabs/sdk/AndroidCompatibilityTest.java @@ -1,12 +1,13 @@ 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.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.DataHasher; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.junit.jupiter.api.Test; import java.nio.charset.StandardCharsets; +import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.transaction.TokenType; import static org.junit.jupiter.api.Assertions.*; @@ -20,14 +21,12 @@ public class AndroidCompatibilityTest { 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(); + DataHash 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); + SigningService signingService = SigningService.generate(); assertNotNull(signingService.getPublicKey()); // Test 3: Token IDs and Types diff --git a/src/test/java/org/unicitylabs/sdk/MockAggregatorServer.java b/src/test/java/org/unicitylabs/sdk/MockAggregatorServer.java index fc63d44..83dbefe 100644 --- a/src/test/java/org/unicitylabs/sdk/MockAggregatorServer.java +++ b/src/test/java/org/unicitylabs/sdk/MockAggregatorServer.java @@ -27,7 +27,7 @@ public MockAggregatorServer() { this.server = new MockWebServer(); this.objectMapper = new ObjectMapper(); this.protectedMethods = new HashSet<>(); - this.protectedMethods.add("submit_commitment"); + this.protectedMethods.add("certification_request"); server.setDispatcher(new Dispatcher() { @Override @@ -117,7 +117,7 @@ private MockResponse generateSuccessResponse(String method) { String id = UUID.randomUUID().toString(); switch (method != null ? method : "") { - case "submit_commitment": + case "certification_request": responseBody = String.format( "{\n" + " \"jsonrpc\": \"2.0\",\n" + diff --git a/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java b/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java index 3fca876..e61da37 100644 --- a/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java +++ b/src/test/java/org/unicitylabs/sdk/TestAggregatorClient.java @@ -1,72 +1,102 @@ package org.unicitylabs.sdk; -import java.util.AbstractMap; 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.CertificationData; +import org.unicitylabs.sdk.api.CertificationResponse; +import org.unicitylabs.sdk.api.CertificationStatus; 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.api.InclusionProofFixture; +import org.unicitylabs.sdk.api.StateId; +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.mtree.plain.SparseMerkleTree; import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreeRootNode; -import org.unicitylabs.sdk.signing.SigningService; -import org.unicitylabs.sdk.transaction.InclusionProofFixture; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; 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(this.trustBase); + } + + 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.calculateLeafValue(); + this.sparseMerkleTree.addLeaf(stateId.toBitString().toBigInteger(), leafValue.getImprint()); + 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(); - 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()) - ) + public CompletableFuture getInclusionProof(StateId stateId) { + CertificationData certificationData = requests.get(stateId); + SparseMerkleTreeRootNode root = this.sparseMerkleTree.calculateRoot(); + return CompletableFuture.completedFuture( + InclusionProofFixture.create( + root.getPath(stateId.toBitString().toBigInteger()), + certificationData, + root.getRootHash(), + this.signingService ) ); } diff --git a/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java b/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java index e254820..a9056d2 100644 --- a/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java +++ b/src/test/java/org/unicitylabs/sdk/TestApiKeyIntegration.java @@ -1,146 +1,159 @@ package org.unicitylabs.sdk; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import okhttp3.mockwebserver.RecordedRequest; 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.CertificationData; +import org.unicitylabs.sdk.api.CertificationResponse; +import org.unicitylabs.sdk.api.CertificationStatus; 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.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.api.jsonrpc.JsonRpcNetworkException; +import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.transaction.Address; +import org.unicitylabs.sdk.transaction.MintTransaction; +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.transaction.TokenType; import org.unicitylabs.sdk.util.HexConverter; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; +public class TestApiKeyIntegration { -import static org.junit.jupiter.api.Assertions.*; + private static final String TEST_API_KEY = "test-api-key-12345"; -public class TestApiKeyIntegration { + private MockAggregatorServer mockServer; + private AggregatorClient clientWithApiKey; + private AggregatorClient clientWithoutApiKey; - 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( - mockServer.getUrl(), TEST_API_KEY); - clientWithoutApiKey = new JsonRpcAggregatorClient(mockServer.getUrl()); - - 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")); - - 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()); - } + 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()); + + SigningService signingService = new SigningService( + HexConverter.decode("0000000000000000000000000000000000000000000000000000000000000001")); + + MintTransaction transaction = MintTransaction.create( + Address.fromPredicate(PayToPublicKeyPredicate.fromSigningService(signingService)), + TokenId.generate(), + TokenType.generate(), + new byte[32] + ); + certificationData = CertificationData.fromMintTransaction(transaction); + } + + @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..6183626 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/api/InclusionProofFixture.java @@ -0,0 +1,19 @@ +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; +import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreePath; + +public class InclusionProofFixture { + public static InclusionProofResponse create(SparseMerkleTreePath path, CertificationData certificationData, DataHash root, SigningService signingService) { + return new InclusionProofResponse( + 1L, + new InclusionProof( + path, + certificationData, + 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..f16d5bd --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/api/InclusionProofTest.java @@ -0,0 +1,214 @@ +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.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.mtree.plain.SparseMerkleTree; +import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreePath; +import org.unicitylabs.sdk.predicate.EncodedPredicate; +import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.transaction.Address; +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; + SparseMerkleTreePath merkleTreePath; + CertificationData certificationData; + RootTrustBase trustBase; + UnicityCertificate unicityCertificate; + + @BeforeAll + public void createMerkleTreePath() throws Exception { + SigningService signingService = new SigningService( + HexConverter.decode("0000000000000000000000000000000000000000000000000000000000000001")); + + + transaction = MintTransaction.create( + Address.fromPredicate(PayToPublicKeyPredicate.fromSigningService(signingService)), + TokenId.generate(), + TokenType.generate(), + new byte[32] + ); + + certificationData = CertificationData.fromMintTransaction(transaction); + stateId = StateId.fromCertificationData(certificationData); + + SparseMerkleTree smt = new SparseMerkleTree(HashAlgorithm.SHA256); + smt.addLeaf(stateId.toBitString().toBigInteger(), certificationData.calculateLeafValue().getImprint()); + + merkleTreePath = smt.calculateRoot().getPath(stateId.toBitString().toBigInteger()); + SigningService ucSigningService = new SigningService(SigningService.generatePrivateKey()); + trustBase = RootTrustBaseUtils.generateRootTrustBase(ucSigningService.getPublicKey()); + unicityCertificate = UnicityCertificateUtils.generateCertificate(ucSigningService, + merkleTreePath.getRootHash()); + predicateVerifier = PredicateVerifierService.create(trustBase); + } + + @Test + public void testCborSerialization() { + InclusionProof inclusionProof = new InclusionProof( + merkleTreePath, + certificationData, + unicityCertificate + ); + + Assertions.assertEquals(inclusionProof, InclusionProof.fromCbor(inclusionProof.toCbor())); + } + + @Test + public void testStructure() { + Assertions.assertThrows(NullPointerException.class, + () -> new InclusionProof( + null, + this.certificationData, + this.unicityCertificate + ) + ); + Assertions.assertThrows(NullPointerException.class, + () -> new InclusionProof( + this.merkleTreePath, + this.certificationData, + null + ) + ); + Assertions.assertInstanceOf(InclusionProof.class, + new InclusionProof( + this.merkleTreePath, + this.certificationData, + this.unicityCertificate + ) + ); + Assertions.assertInstanceOf(InclusionProof.class, + new InclusionProof( + this.merkleTreePath, + null, + this.unicityCertificate + ) + ); + } + + @Test + public void testItVerifies() { + InclusionProof inclusionProof = new InclusionProof( + this.merkleTreePath, + this.certificationData, + this.unicityCertificate + ); + Assertions.assertEquals( + InclusionProofVerificationStatus.OK, + InclusionProofVerificationRule.verify( + this.trustBase, + this.predicateVerifier, + inclusionProof, + this.transaction + ).getStatus() + ); + + + Assertions.assertEquals( + InclusionProofVerificationStatus.PATH_NOT_INCLUDED, + InclusionProofVerificationRule.verify( + this.trustBase, + this.predicateVerifier, + inclusionProof, + MintTransaction.create( + Address.fromPredicate(transaction.getLockScript()), + TokenId.generate(), + transaction.getTokenType(), + transaction.getData() + ) + ).getStatus() + ); + + InclusionProof invalidTransactionHashInclusionProof = new InclusionProof( + this.merkleTreePath, + new CertificationData( + this.certificationData.getLockScript(), + this.certificationData.getSourceStateHash(), + DataHash.fromImprint( + HexConverter.decode("00000000000000000000000000000000000000000000000000000000000000000001") + ), + this.certificationData.getUnlockScript() + ), + 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( + this.merkleTreePath, + new CertificationData( + this.certificationData.getLockScript(), + this.certificationData.getSourceStateHash(), + this.certificationData.getTransactionHash(), + PayToPublicKeyPredicateUnlockScript.create( + this.transaction, + new SigningService(SigningService.generatePrivateKey()) + ).encode() + ), + this.unicityCertificate + ); + + Assertions.assertEquals( + InclusionProofVerificationStatus.NOT_AUTHENTICATED, + InclusionProofVerificationRule.verify( + this.trustBase, + this.predicateVerifier, + invalidInclusionProof, + this.transaction + ).getStatus() + ); + } + + @Test + public void testVerificationFailsWithInvalidTrustbase() { + InclusionProof inclusionProof = new InclusionProof( + this.merkleTreePath, + this.certificationData, + 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/bft/RootTrustBaseTest.java b/src/test/java/org/unicitylabs/sdk/api/bft/RootTrustBaseTest.java similarity index 94% rename from src/test/java/org/unicitylabs/sdk/bft/RootTrustBaseTest.java rename to src/test/java/org/unicitylabs/sdk/api/bft/RootTrustBaseTest.java index 6c856f4..8fedf12 100644 --- a/src/test/java/org/unicitylabs/sdk/bft/RootTrustBaseTest.java +++ b/src/test/java/org/unicitylabs/sdk/api/bft/RootTrustBaseTest.java @@ -1,13 +1,13 @@ -package org.unicitylabs.sdk.bft; +package org.unicitylabs.sdk.api.bft; -import java.io.IOException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.unicitylabs.sdk.api.bft.RootTrustBase; public class RootTrustBaseTest { @Test - public void testRootTrustBaseDeserializationFromJson() throws IOException { + 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\"}}" ); diff --git a/src/test/java/org/unicitylabs/sdk/bft/RootTrustBaseUtils.java b/src/test/java/org/unicitylabs/sdk/api/bft/RootTrustBaseUtils.java similarity index 84% rename from src/test/java/org/unicitylabs/sdk/bft/RootTrustBaseUtils.java rename to src/test/java/org/unicitylabs/sdk/api/bft/RootTrustBaseUtils.java index ddc75ad..ea27a4f 100644 --- a/src/test/java/org/unicitylabs/sdk/bft/RootTrustBaseUtils.java +++ b/src/test/java/org/unicitylabs/sdk/api/bft/RootTrustBaseUtils.java @@ -1,7 +1,8 @@ -package org.unicitylabs.sdk.bft; +package org.unicitylabs.sdk.api.bft; import java.util.Map; import java.util.Set; +import org.unicitylabs.sdk.api.bft.RootTrustBase; public class RootTrustBaseUtils { public static RootTrustBase generateRootTrustBase(byte[] publicKey) { diff --git a/src/test/java/org/unicitylabs/sdk/bft/UnicityCertificateTest.java b/src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateTest.java similarity index 95% rename from src/test/java/org/unicitylabs/sdk/bft/UnicityCertificateTest.java rename to src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateTest.java index 3f2e5a1..027e93f 100644 --- a/src/test/java/org/unicitylabs/sdk/bft/UnicityCertificateTest.java +++ b/src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateTest.java @@ -1,14 +1,14 @@ -package org.unicitylabs.sdk.bft; +package org.unicitylabs.sdk.api.bft; -import java.io.IOException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.unicitylabs.sdk.api.bft.UnicityCertificate; import org.unicitylabs.sdk.util.HexConverter; public class UnicityCertificateTest { @Test - public void testUnicityCertificateDeserializationFromCbor() throws IOException { + public void testUnicityCertificateDeserializationFromCbor() { byte[] data = HexConverter.decode( "d903ef8701d903f08a0118f70058220000ea2fc549e86b1f8666bd7a6d5dd0721b446856ad80c67ea958c4045dfb0b627c58220000ea2fc549e86b1f8666bd7a6d5dd0721b446856ad80c67ea958c4045dfb0b627c401a68d27af6f600f6582024bf5304778618efe3303e1ac36a5c23b7d9d64bbd2c2ae92ea1488ad59e9cfa58206a362e77353752942e9c8101511861ee7fac83ba0873ee88f34416535ae17c5c82418080d903f68301078182055820b206f60d312bf4422815c861e9330ddc3a29b0c744864d7bea41f94d1d34c669d903e988010319073b001a68d27af958204b3f7f036452271a1245ca79f6c2e35adc0b192e6eeb001dff35d70d1be3b2d55820380451b471673eb68cf750ceedb09755c08e3b0b7dea32d0101465970ca3a661a4783531365569753248416d33506141397a386a6f6e5a7a6676755431574a675478437062466b5634577134505353426b375663746b6d475841c5103160b79e7691cec53448949bbd93a870a38c89ad4c929aa1c8e77f2a1d9053d3f8f04e96bcb38ba234bf3dee52baad4b1df94be0b8a5e569918fb94e18ec00783531365569753248416d383931384473326e5069564c586735356b7970796f586f6977656f6b705178746e67755a6a67787a33704e455841fa3736ebdc729b31d6e3c964787dfcefcbd39286a1619665eac1313e6d35049b4385156f652ad9ac87a769d3bf7b8cc819bb5a6f838e595d2e2092df2592849400783531365569753248416d454545477976595a6e6f37686d3247733846776650656a57647076574333484c6976514435685846624e55685841e49ea9e0ca1bb21a37a203fef7bc64efc660ff208f4e572ad3d58ae1d5f1e86e7af1c030c66b3325f99886b1a1d0836487c16112c43c07248b490121d473287701783531365569753248416d4e776772753751537356526163477158647466656166316f71747a6e7658413672537a4b55313832326b75575841ecb36e24df58876277643117344d5329fc1bb22b51e9e6835ac587c894db06a11110e966fd97acd91659c778494d9b4dcdc1bd22ffb47a90e3bec38db8a7310f01"); UnicityCertificate unicityCertificate = UnicityCertificate.fromCbor(data); diff --git a/src/test/java/org/unicitylabs/sdk/bft/UnicityCertificateUtils.java b/src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateUtils.java similarity index 91% rename from src/test/java/org/unicitylabs/sdk/bft/UnicityCertificateUtils.java rename to src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateUtils.java index 5d8fe61..f265689 100644 --- a/src/test/java/org/unicitylabs/sdk/bft/UnicityCertificateUtils.java +++ b/src/test/java/org/unicitylabs/sdk/api/bft/UnicityCertificateUtils.java @@ -1,14 +1,14 @@ -package org.unicitylabs.sdk.bft; +package org.unicitylabs.sdk.api.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.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 org.unicitylabs.sdk.signing.SigningService; public class UnicityCertificateUtils { 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..8f4dd74 100644 --- a/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java +++ b/src/test/java/org/unicitylabs/sdk/common/CommonTestFlow.java @@ -1,50 +1,15 @@ 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.util.InclusionProofUtils; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.transaction.Address; +import org.unicitylabs.sdk.transaction.Token; +import org.unicitylabs.sdk.util.verification.VerificationStatus; import org.unicitylabs.sdk.utils.TokenUtils; /** @@ -54,307 +19,43 @@ public abstract class CommonTestFlow { protected StateTransitionClient client; protected RootTrustBase trustBase; + protected PredicateVerifierService predicateVerifier; - 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( + 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() + this.predicateVerifier, + Address.fromPredicate(PayToPublicKeyPredicate.create(ALICE_SIGNING_SERVICE.getPublicKey())) ); - 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( + Token bobToken = TokenUtils.transferToken( this.client, this.trustBase, - aliceToBobTransferCommitment - ).get(); - - // Create transfer transaction - TransferTransaction aliceToBobTransferTransaction = aliceToBobTransferCommitment.toTransaction( - aliceToBobTransferInclusionProof + this.predicateVerifier, + aliceToken.toCbor(), + Address.fromPredicate(PayToPublicKeyPredicate.create(BOB_SIGNING_SERVICE.getPublicKey())), + ALICE_SIGNING_SERVICE ); - // 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( + Token carolToken = TokenUtils.transferToken( 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 + this.predicateVerifier, + bobToken.toCbor(), + Address.fromPredicate(PayToPublicKeyPredicate.create(CAROL_SIGNING_SERVICE.getPublicKey())), + BOB_SIGNING_SERVICE ); - 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) - ); - 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 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) - ); - - 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); - - 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); - - TransferCommitment burnCommitment = split.createBurnCommitment( - randomBytes(32), - SigningService.createFromSecret(BOB_SECRET) - ); - - 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() - ) - ); - - 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"); - } - - 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) - ) - .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.assertDoesNotThrow(() -> - Token.create( - this.trustBase, - new TokenState(splitTokenPredicate, null), - splitTransactions.get(0) - ) - ); + Assertions.assertEquals(VerificationStatus.OK, + carolToken.verify(this.trustBase, this.predicateVerifier).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/signing/SignatureRecoveryTest.java b/src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SignatureRecoveryTest.java similarity index 94% rename from src/test/java/org/unicitylabs/sdk/signing/SignatureRecoveryTest.java rename to src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SignatureRecoveryTest.java index 21dd8e8..d5e8130 100644 --- a/src/test/java/org/unicitylabs/sdk/signing/SignatureRecoveryTest.java +++ b/src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SignatureRecoveryTest.java @@ -1,8 +1,8 @@ -package org.unicitylabs.sdk.signing; +package org.unicitylabs.sdk.crypto.secp256k1; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.HashAlgorithm; -import org.unicitylabs.sdk.hash.DataHasher; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.crypto.hash.DataHasher; import org.unicitylabs.sdk.util.HexConverter; import org.junit.jupiter.api.Test; diff --git a/src/test/java/org/unicitylabs/sdk/signing/SigningServiceTest.java b/src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SigningServiceTest.java similarity index 67% rename from src/test/java/org/unicitylabs/sdk/signing/SigningServiceTest.java rename to src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SigningServiceTest.java index 2994166..97ca24d 100644 --- a/src/test/java/org/unicitylabs/sdk/signing/SigningServiceTest.java +++ b/src/test/java/org/unicitylabs/sdk/crypto/secp256k1/SigningServiceTest.java @@ -1,11 +1,9 @@ -package org.unicitylabs.sdk.signing; +package org.unicitylabs.sdk.crypto.secp256k1; -import org.unicitylabs.sdk.hash.DataHash; -import org.unicitylabs.sdk.hash.HashAlgorithm; +import org.unicitylabs.sdk.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.junit.jupiter.api.Test; -import java.nio.charset.StandardCharsets; - import static org.junit.jupiter.api.Assertions.*; public class SigningServiceTest { @@ -23,26 +21,13 @@ public void testGeneratePrivateKey() { 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); + DataHash hash = new DataHash(HashAlgorithm.SHA256, new byte[32]); // Sign the hash Signature signature = service.sign(hash); @@ -63,8 +48,7 @@ public void testVerifyWithPublicKey() { byte[] publicKey = service.getPublicKey(); // Create a test hash - byte[] testData = "test data".getBytes(StandardCharsets.UTF_8); - DataHash hash = new DataHash(HashAlgorithm.SHA256, testData); + DataHash hash = new DataHash(HashAlgorithm.SHA256, new byte[32]); // Sign the hash Signature signature = service.sign(hash); @@ -81,8 +65,7 @@ public void testInvalidSignature() { SigningService service = new SigningService(privateKey); // Create a test hash - byte[] testData = "test data".getBytes(StandardCharsets.UTF_8); - DataHash hash = new DataHash(HashAlgorithm.SHA256, testData); + DataHash hash = new DataHash(HashAlgorithm.SHA256, new byte[32]); // Create an invalid signature byte[] invalidSig = new byte[64]; 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..5c95d1f 100644 --- a/src/test/java/org/unicitylabs/sdk/e2e/TokenE2ETest.java +++ b/src/test/java/org/unicitylabs/sdk/e2e/TokenE2ETest.java @@ -1,14 +1,17 @@ package org.unicitylabs.sdk.e2e; import java.io.IOException; +import java.io.InputStream; + 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.api.bft.RootTrustBase; import org.unicitylabs.sdk.common.CommonTestFlow; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -31,9 +34,11 @@ 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.trustBase); + } } @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..2a8612c 100644 --- a/src/test/java/org/unicitylabs/sdk/functional/FunctionalCommonFlowTest.java +++ b/src/test/java/org/unicitylabs/sdk/functional/FunctionalCommonFlowTest.java @@ -4,15 +4,15 @@ 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; 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.trustBase); } } \ 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..bf4cfed --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/SplitBuilderTest.java @@ -0,0 +1,598 @@ +package org.unicitylabs.sdk.functional.payment; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.unicitylabs.sdk.StateTransitionClient; +import org.unicitylabs.sdk.TestAggregatorClient; +import org.unicitylabs.sdk.api.bft.RootTrustBase; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.crypto.secp256k1.SigningService; +import org.unicitylabs.sdk.mtree.plain.SparseMerkleTree; +import org.unicitylabs.sdk.mtree.plain.SparseMerkleTreeRootNode; +import org.unicitylabs.sdk.mtree.sum.SparseMerkleSumTree; +import org.unicitylabs.sdk.mtree.sum.SparseMerkleSumTreeRootNode; +import org.unicitylabs.sdk.payment.SplitPaymentData; +import org.unicitylabs.sdk.payment.SplitReason; +import org.unicitylabs.sdk.payment.SplitReasonProof; +import org.unicitylabs.sdk.payment.SplitResult; +import org.unicitylabs.sdk.payment.TokenSplit; +import org.unicitylabs.sdk.payment.asset.Asset; +import org.unicitylabs.sdk.payment.asset.AssetId; +import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.transaction.Address; +import org.unicitylabs.sdk.transaction.Token; +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.util.verification.VerificationResult; +import org.unicitylabs.sdk.util.verification.VerificationStatus; +import org.unicitylabs.sdk.utils.TokenUtils; + +/** + * Functional tests for minting and splitting tokens with proof verification. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class SplitBuilderTest { + + private StateTransitionClient client; + private RootTrustBase trustBase; + private PredicateVerifierService predicateVerifier; + private Asset asset1; + private Asset asset2; + private Token splitToken; + + @BeforeAll + public void setupFixture() throws Exception { + TestAggregatorClient aggregatorClient = TestAggregatorClient.create(); + this.trustBase = aggregatorClient.getTrustBase(); + + this.client = new StateTransitionClient(aggregatorClient); + this.predicateVerifier = PredicateVerifierService.create(this.trustBase); + + SigningService signingService = SigningService.generate(); + PayToPublicKeyPredicate ownerPredicate = PayToPublicKeyPredicate.fromSigningService(signingService); + + this.asset1 = new Asset(new AssetId("ASSET_1".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); + this.asset2 = new Asset(new AssetId("ASSET_2".getBytes(StandardCharsets.UTF_8)), BigInteger.valueOf(500)); + + this.splitToken = createSplitToken( + this.client, + signingService, + ownerPredicate, + Set.of(this.asset1, this.asset2), + Set.of(this.asset1, this.asset2) + ); + } + + /** + * Verifies end-to-end mint, split, burn and validation flow. + * + * @throws Exception when async client interactions fail + */ + @Test + public void verifyTokenSplitIsSuccessful() throws Exception { + SigningService signingService = SigningService.generate(); + PayToPublicKeyPredicate predicate = PayToPublicKeyPredicate.fromSigningService(signingService); + + Set assets = Set.of(this.asset1, this.asset2); + TestPaymentData paymentData = new TestPaymentData(assets); + + Token token = TokenUtils.mintToken( + this.client, + this.trustBase, + this.predicateVerifier, + Address.fromPredicate(predicate), + paymentData.encode() + ); + + IllegalArgumentException exception = Assertions.assertThrows( + IllegalArgumentException.class, + () -> TokenSplit.split( + token, + predicate, + TestPaymentData::decode, + Map.of(TokenId.generate(), Set.of(this.asset1)) + ) + ); + + Assertions.assertEquals("Token and split tokens asset counts differ.", exception.getMessage()); + + exception = Assertions.assertThrows( + IllegalArgumentException.class, + () -> TokenSplit.split( + token, + predicate, + TestPaymentData::decode, + Map.of( + TokenId.generate(), + Set.of(this.asset1, new Asset(this.asset2.getId(), BigInteger.valueOf(400))) + ) + ) + ); + + Assertions.assertEquals("Token contained 500 AssetId{bytes=41535345545f32} assets, but tree has 400", + exception.getMessage()); + + exception = Assertions.assertThrows( + IllegalArgumentException.class, + () -> TokenSplit.split( + token, + predicate, + TestPaymentData::decode, + Map.of( + TokenId.generate(), + Set.of(this.asset1, new Asset(this.asset2.getId(), BigInteger.valueOf(1500))) + ) + ) + ); + + Assertions.assertEquals("Token contained 500 AssetId{bytes=41535345545f32} assets, but tree has 1500", + exception.getMessage()); + + Map> splitTokens = Map.of( + TokenId.generate(), Set.of(this.asset1), + TokenId.generate(), Set.of(this.asset2) + ); + + SplitResult result = TokenSplit.split(token, predicate, TestPaymentData::decode, splitTokens); + + Token burnToken = TokenUtils.transferToken( + this.client, + this.trustBase, + this.predicateVerifier, + token, + result.getBurnTransaction(), + PayToPublicKeyPredicateUnlockScript.create(result.getBurnTransaction(), signingService) + ); + + for (Entry> entry : splitTokens.entrySet()) { + List proofs = result.getProofs().get(entry.getKey()); + Assertions.assertNotNull(proofs); + + Token splitToken = TokenUtils.mintToken( + this.client, + this.trustBase, + this.predicateVerifier, + entry.getKey(), + Address.fromPredicate(predicate), + new TestSplitPaymentData( + entry.getValue(), + SplitReason.create( + burnToken, + proofs + ) + ).encode() + ); + + Assertions.assertEquals( + VerificationStatus.OK, + splitToken.verify(this.trustBase, this.predicateVerifier).getStatus() + ); + Assertions.assertEquals(VerificationStatus.OK, + TokenSplit.verify( + Token.fromCbor(splitToken.toCbor()), + TestSplitPaymentData::decode, + this.trustBase, + this.predicateVerifier + ).getStatus()); + } + } + + @Test + public void verifyFailsWhenTokenIsNull() { + assertNpe("Token cannot be null", + () -> TokenSplit.verify(null, TestSplitPaymentData::decode, this.trustBase, this.predicateVerifier)); + } + + @Test + public void verifyFailsWhenDeserializerIsNull() { + assertNpe("Payment data deserializer cannot be null", + () -> TokenSplit.verify(this.splitToken, null, this.trustBase, this.predicateVerifier)); + } + + @Test + public void verifyFailsWhenTrustBaseIsNull() { + assertNpe("Trust base cannot be null", + () -> TokenSplit.verify(this.splitToken, TestSplitPaymentData::decode, null, this.predicateVerifier)); + } + + @Test + public void verifyFailsWhenPredicateVerifierIsNull() { + assertNpe("Predicate verifier cannot be null", + () -> TokenSplit.verify(this.splitToken, TestSplitPaymentData::decode, this.trustBase, null)); + } + + @Test + public void verifyFailsWhenAssetsAreMissing() { + VerificationResult result = verifyWithData( + this.splitToken, + new TestSplitPaymentData(null, TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()).getReason()) + ); + + assertFailWithMessage(result, "Assets data is missing."); + } + + @Test + public void verifyFailsWhenReasonIsMissing() { + VerificationResult result = verifyWithData( + this.splitToken, + new TestSplitPaymentData(Set.of(this.asset1), null) + ); + + assertFailWithMessage(result, "Reason is missing."); + } + + @Test + public void verifyFailsWhenBurnTokenVerificationFails() { + List payloadData = CborDeserializer.decodeArray(this.splitToken.getGenesis().getData()); + List reasonData = CborDeserializer.decodeArray(payloadData.get(1)); + List reasonTokenData = CborDeserializer.decodeArray(reasonData.get(0)); + List transactions = CborDeserializer.decodeArray(reasonTokenData.get(1)); + List certifiedTransfer = CborDeserializer.decodeArray(transactions.get(0)); + List transfer = CborDeserializer.decodeArray(certifiedTransfer.get(0)); + + // Corrupt burn transaction recipient address so burn token verification fails. + byte[] invalidRecipient = new byte[32]; + invalidRecipient[0] = 1; + transfer.set(2, Address.fromBytes(invalidRecipient).toCbor()); + + certifiedTransfer.set(0, encodeArray(transfer)); + transactions.set(0, encodeArray(certifiedTransfer)); + reasonTokenData.set(1, encodeArray(transactions)); + reasonData.set(0, encodeArray(reasonTokenData)); + payloadData.set(1, encodeArray(reasonData)); + byte[] payload = encodeArray(payloadData); + + VerificationResult result = verifyWithPayload(this.splitToken, payload); + assertFailWithMessage(result, "Burn token verification failed."); + Assertions.assertFalse(result.getResults().isEmpty()); + } + + @Test + public void verifyFailsWhenAssetAndProofCountsDiffer() { + VerificationResult result = verifyWithData( + this.splitToken, + new TestSplitPaymentData(Set.of(this.asset1), + TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()).getReason()) + ); + + assertFailWithMessage(result, "Total amount of assets differ in token and proofs."); + } + + @Test + public void verifyFailsWhenAssetEntryIsNull() { + Set invalidAssets = new NonUniqueAssetSet(Arrays.asList(null, this.asset1)); + VerificationResult result = verifyWithData( + this.splitToken, + new TestSplitPaymentData(invalidAssets, + TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()).getReason()) + ); + + assertFailWithMessage(result, "Asset data is missing."); + } + + @Test + public void verifyFailsWhenAssetIdsAreDuplicated() { + Asset duplicate = new Asset(this.asset1.getId(), this.asset1.getValue().add(BigInteger.ONE)); + Set duplicatedAssets = new NonUniqueAssetSet(List.of(this.asset1, duplicate)); + + VerificationResult result = verifyWithData( + this.splitToken, + new TestSplitPaymentData(duplicatedAssets, + TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()).getReason()) + ); + + assertFailWithMessage(result, + String.format("Duplicate asset id %s found in asset data.", this.asset1.getId())); + } + + @Test + public void verifyFailsWhenAggregationPathVerificationFails() throws Exception { + SplitPaymentData splitPaymentData = TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()); + SplitReason splitReason = splitPaymentData.getReason(); + List proofs = new ArrayList<>(splitReason.getProofs()); + SplitReasonProof proof = proofs.get(0); + SparseMerkleTreeRootNode aggregationRoot = new SparseMerkleTree(HashAlgorithm.SHA256).calculateRoot(); + + proofs.set( + 0, + SplitReasonProof.create( + proof.getAssetId(), + aggregationRoot.getPath(proof.getAssetId().toBitString().toBigInteger()), + proof.getAssetTreePath() + ) + ); + + byte[] payload = new TestSplitPaymentData( + splitPaymentData.getAssets(), + SplitReason.create(splitReason.getToken(), proofs) + ).encode(); + + VerificationResult result = verifyWithPayload(this.splitToken, payload); + + assertFailWithMessage(result, + String.format("Aggregation path verification failed for asset: %s", proof.getAssetId())); + } + + @Test + public void verifyFailsWhenAssetTreePathVerificationFails() throws Exception { + SplitPaymentData splitPaymentData = TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()); + SplitReason splitReason = splitPaymentData.getReason(); + List proofs = new ArrayList<>(splitReason.getProofs()); + SplitReasonProof proof = proofs.get(0); + + SparseMerkleSumTreeRootNode assetTreeRoot = new SparseMerkleSumTree(HashAlgorithm.SHA256).calculateRoot(); + + SplitReasonProof mutated = SplitReasonProof.create( + proof.getAssetId(), + proof.getAggregationPath(), + assetTreeRoot.getPath(this.splitToken.getId().toBitString().toBigInteger()) + ); + proofs.set(0, mutated); + + byte[] payload = new TestSplitPaymentData( + splitPaymentData.getAssets(), + SplitReason.create(splitReason.getToken(), proofs) + ).encode(); + + VerificationResult result = verifyWithPayload(this.splitToken, payload); + + assertFailWithMessage(result, + String.format("Asset tree path verification failed for token: %s", this.splitToken.getId())); + } + + @Test + public void verifyFailsWhenProofsUseDifferentAssetTrees() throws Exception { + SplitPaymentData splitPaymentData = TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()); + SplitReason splitReason = splitPaymentData.getReason(); + List proofs = new ArrayList<>(splitReason.getProofs()); + SplitReasonProof secondProof = proofs.get(1); + + SparseMerkleTree aggregationTree = new SparseMerkleTree(HashAlgorithm.SHA256); + aggregationTree.addLeaf( + secondProof.getAssetId().toBitString().toBigInteger(), + secondProof.getAssetTreePath().getRootHash().getImprint() + ); + SparseMerkleTreeRootNode otherAggregationRoot = aggregationTree.calculateRoot(); + + SplitReasonProof mutated = SplitReasonProof.create( + secondProof.getAssetId(), + otherAggregationRoot.getPath(secondProof.getAssetId().toBitString().toBigInteger()), + secondProof.getAssetTreePath() + ); + proofs.set(1, mutated); + + byte[] payload = new TestSplitPaymentData( + splitPaymentData.getAssets(), + SplitReason.create(splitReason.getToken(), proofs) + ).encode(); + + VerificationResult result = verifyWithPayload(this.splitToken, payload); + assertFailWithMessage(result, "Current proof is not derived from the same asset tree as other proofs."); + } + + @Test + public void verifyFailsWhenAssetTreeRootDoesNotMatchAggregationLeaf() throws Exception { + SplitPaymentData splitPaymentData = TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()); + SplitReason splitReason = splitPaymentData.getReason(); + List proofs = new ArrayList<>(splitReason.getProofs()); + SplitReasonProof proof = proofs.get(0); + + SparseMerkleSumTree assetTree = new SparseMerkleSumTree(HashAlgorithm.SHA256); + assetTree.addLeaf( + this.splitToken.getId().toBitString().toBigInteger(), + new SparseMerkleSumTree.LeafValue( + proof.getAssetId().getBytes(), + proof.getAssetTreePath().getSteps().get(0).getValue().add(BigInteger.ONE) + ) + ); + + SplitReasonProof mutated = SplitReasonProof.create( + proof.getAssetId(), + proof.getAggregationPath(), + assetTree.calculateRoot().getPath(this.splitToken.getId().toBitString().toBigInteger()) + ); + proofs.set(0, mutated); + + byte[] payload = new TestSplitPaymentData( + splitPaymentData.getAssets(), + SplitReason.create(splitReason.getToken(), proofs) + ).encode(); + + VerificationResult result = verifyWithPayload(this.splitToken, payload); + assertFailWithMessage(result, "Asset tree root does not match aggregation path leaf."); + } + + @Test + public void verifyFailsWhenProofAssetIdIsMissingFromAssetData() { + SplitPaymentData splitPaymentData = TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()); + SplitReason splitReason = splitPaymentData.getReason(); + List proofs = List.of(splitReason.getProofs().get(0)); + Set assets = splitPaymentData.getAssets().stream() + .filter(asset -> !asset.getId().equals(proofs.get(0).getAssetId())) + .collect(Collectors.toSet()); + byte[] payload = new TestSplitPaymentData( + assets, + SplitReason.create(splitReason.getToken(), proofs) + ).encode(); + + VerificationResult result = verifyWithPayload(this.splitToken, payload); + assertFailWithMessage(result, + String.format("Asset id %s not found in asset data.", proofs.get(0).getAssetId())); + } + + @Test + public void verifyFailsWhenAssetAmountDoesNotMatchLeafAmount() { + SplitPaymentData splitPaymentData = TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()); + SplitReason splitReason = splitPaymentData.getReason(); + List assets = new ArrayList<>(splitPaymentData.getAssets()); + Asset asset = assets.get(0); + Asset modified = new Asset(asset.getId(), asset.getValue().add(BigInteger.ONE)); + assets.set(0, modified); + + byte[] payload = new TestSplitPaymentData(Set.copyOf(assets), splitReason).encode(); + + VerificationResult result = verifyWithPayload(this.splitToken, payload); + assertFailWithMessage(result, + String.format("Asset amount for asset id %s does not match asset tree leaf.", asset.getId())); + } + + @Test + public void verifyFailsWhenAggregationRootDoesNotMatchBurnPredicate() throws Exception { + SplitPaymentData splitPaymentData = TestSplitPaymentData.decode(this.splitToken.getGenesis().getData()); + SplitReason splitReason = splitPaymentData.getReason(); + List proofs = new ArrayList<>(splitReason.getProofs()); + SplitReasonProof proof = proofs.get(0); + + SparseMerkleTree aggregationTree = new SparseMerkleTree(HashAlgorithm.SHA256); + aggregationTree.addLeaf( + proof.getAssetId().toBitString().toBigInteger(), + proof.getAssetTreePath().getRootHash().getImprint() + ); + SparseMerkleTreeRootNode aggregationRoot = aggregationTree.calculateRoot(); + + SplitReasonProof mutated = SplitReasonProof.create( + proof.getAssetId(), + aggregationRoot.getPath(proof.getAssetId().toBitString().toBigInteger()), + proof.getAssetTreePath() + ); + proofs.set(0, mutated); + + byte[] payload = new TestSplitPaymentData( + splitPaymentData.getAssets(), + SplitReason.create(splitReason.getToken(), proofs) + ).encode(); + + VerificationResult result = verifyWithPayload(this.splitToken, payload); + assertFailWithMessage(result, "Aggregation path root does not match burn predicate."); + } + + private Token createSplitToken( + StateTransitionClient client, + SigningService signingService, + PayToPublicKeyPredicate ownerPredicate, + Set sourceAssets, + Set outputAssets + ) throws Exception { + Token sourceToken = TokenUtils.mintToken( + client, + this.trustBase, + this.predicateVerifier, + Address.fromPredicate(ownerPredicate), + new TestPaymentData(sourceAssets).encode() + ); + + TokenId outputTokenId = TokenId.generate(); + SplitResult split = TokenSplit.split( + sourceToken, + ownerPredicate, + TestPaymentData::decode, + Map.of(outputTokenId, outputAssets) + ); + + Token burnToken = TokenUtils.transferToken( + client, + this.trustBase, + this.predicateVerifier, + sourceToken, + split.getBurnTransaction(), + PayToPublicKeyPredicateUnlockScript.create(split.getBurnTransaction(), signingService) + ); + + return TokenUtils.mintToken( + client, + this.trustBase, + this.predicateVerifier, + outputTokenId, + Address.fromPredicate(ownerPredicate), + new TestSplitPaymentData( + outputAssets, + SplitReason.create(burnToken, split.getProofs().get(outputTokenId)) + ).encode() + ); + } + + private VerificationResult verify(Token token) { + return TokenSplit.verify( + Token.fromCbor(token.toCbor()), + TestSplitPaymentData::decode, + this.trustBase, + this.predicateVerifier + ); + } + + private VerificationResult verifyWithData(Token token, SplitPaymentData paymentData) { + return TokenSplit.verify( + Token.fromCbor(token.toCbor()), + ignored -> paymentData, + this.trustBase, + this.predicateVerifier + ); + } + + private VerificationResult verifyWithPayload(Token token, byte[] payload) { + return this.verify(withPayload(token, payload)); + } + + private Token withPayload(Token token, byte[] payload) { + List tokenData = CborDeserializer.decodeArray(token.toCbor()); + List genesis = CborDeserializer.decodeArray(tokenData.get(0)); + List mint = CborDeserializer.decodeArray(genesis.get(0)); + List aux = CborDeserializer.decodeArray(mint.get(2)); + + aux.set(1, CborSerializer.encodeByteString(payload)); + mint.set(2, encodeArray(aux)); + genesis.set(0, encodeArray(mint)); + tokenData.set(0, encodeArray(genesis)); + + return Token.fromCbor(encodeArray(tokenData)); + } + + private void assertFailWithMessage(VerificationResult result, String message) { + Assertions.assertEquals(VerificationStatus.FAIL, result.getStatus()); + Assertions.assertEquals(message, result.getMessage()); + } + + private void assertNpe(String message, Runnable callback) { + NullPointerException error = Assertions.assertThrows(NullPointerException.class, callback::run); + Assertions.assertEquals(message, error.getMessage()); + } + + private byte[] encodeArray(List data) { + return CborSerializer.encodeArray(data.toArray(new byte[0][])); + } + + private static final class NonUniqueAssetSet extends AbstractSet { + + private final List items; + + private NonUniqueAssetSet(List items) { + this.items = new ArrayList<>(items); + } + + @Override + public Iterator iterator() { + return this.items.iterator(); + } + + @Override + public int size() { + return this.items.size(); + } + } +} diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/TestPaymentData.java b/src/test/java/org/unicitylabs/sdk/functional/payment/TestPaymentData.java new file mode 100644 index 0000000..73e7167 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/TestPaymentData.java @@ -0,0 +1,39 @@ +package org.unicitylabs.sdk.functional.payment; + +import java.util.Set; +import java.util.stream.Collectors; +import org.unicitylabs.sdk.payment.PaymentData; +import org.unicitylabs.sdk.payment.asset.Asset; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; + +public class TestPaymentData implements PaymentData { + + private final Set assets; + + public TestPaymentData(Set assets) { + this.assets = Set.copyOf(assets); + } + + @Override + public Set getAssets() { + return this.assets; + } + + public static TestPaymentData decode(byte[] bytes) { + Set assets = CborDeserializer.decodeArray(bytes).stream() + .map(Asset::fromCbor) + .collect(Collectors.toSet()); + + return new TestPaymentData(assets); + } + + @Override + public byte[] encode() { + return CborSerializer.encodeArray( + this.assets.stream() + .map(Asset::toCbor) + .toArray(byte[][]::new) + ); + } +} \ No newline at end of file diff --git a/src/test/java/org/unicitylabs/sdk/functional/payment/TestSplitPaymentData.java b/src/test/java/org/unicitylabs/sdk/functional/payment/TestSplitPaymentData.java new file mode 100644 index 0000000..bbf5062 --- /dev/null +++ b/src/test/java/org/unicitylabs/sdk/functional/payment/TestSplitPaymentData.java @@ -0,0 +1,94 @@ +package org.unicitylabs.sdk.functional.payment; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.unicitylabs.sdk.payment.SplitPaymentData; +import org.unicitylabs.sdk.payment.SplitReason; +import org.unicitylabs.sdk.payment.asset.Asset; +import org.unicitylabs.sdk.serializer.cbor.CborDeserializer; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; + +/** + * Test implementation of split payment payload used by functional tests. + */ +public class TestSplitPaymentData implements SplitPaymentData { + + private final Set assets; + private final SplitReason reason; + + /** + * Create test split payment data. + * + * @param assets split assets + * @param reason split reason with proofs + */ + public TestSplitPaymentData(Set assets, SplitReason reason) { + this.assets = assets; + this.reason = reason; + } + + /** + * Get split assets. + * + * @return split assets + */ + public Set getAssets() { + return this.assets; + } + + /** + * Get split reason. + * + * @return split reason + */ + @Override + public SplitReason getReason() { + return this.reason; + } + + /** + * Decode split payment data from CBOR bytes. + * + * @param bytes encoded split payment data + * + * @return decoded split payment data + */ + public static TestSplitPaymentData decode(byte[] bytes) { + List data = CborDeserializer.decodeArray(bytes); + + Set assets = CborDeserializer.decodeNullable( + data.get(0), + result -> CborDeserializer.decodeArray(result).stream() + .map(asset -> CborDeserializer.decodeNullable(asset, Asset::fromCbor)) + .collect(Collectors.toSet()) + ); + + SplitReason reason = CborDeserializer.decodeNullable(data.get(1), SplitReason::fromCbor); + + return new TestSplitPaymentData(assets, reason); + } + + /** + * Encode split payment data to CBOR bytes. + * + * @return encoded payload + */ + @Override + public byte[] encode() { + return CborSerializer.encodeArray( + CborSerializer.encodeOptional( + this.assets, + assets -> CborSerializer.encodeArray( + assets.stream().map(asset -> CborSerializer.encodeOptional(asset, Asset::toCbor)).toArray(byte[][]::new) + ) + ), + CborSerializer.encodeOptional(this.reason, SplitReason::toCbor) + ); + } + + @Override + public String toString() { + return String.format("SplitPaymentData{assets=%s, reason=%s}", this.assets, this.reason); + } +} diff --git a/src/test/java/org/unicitylabs/sdk/hash/DataHashTest.java b/src/test/java/org/unicitylabs/sdk/hash/DataHashTest.java index 2737000..9e6cffb 100644 --- a/src/test/java/org/unicitylabs/sdk/hash/DataHashTest.java +++ b/src/test/java/org/unicitylabs/sdk/hash/DataHashTest.java @@ -1,9 +1,12 @@ 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.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.serializer.json.JsonSerializationException; +import org.unicitylabs.sdk.util.HexConverter; public class DataHashTest { @@ -18,22 +21,20 @@ public void testInvalidDataHashArguments() { } @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..abcad60 100644 --- a/src/test/java/org/unicitylabs/sdk/hash/DataHasherTest.java +++ b/src/test/java/org/unicitylabs/sdk/hash/DataHasherTest.java @@ -1,6 +1,9 @@ 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; 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..48ce9ea 100644 --- a/src/test/java/org/unicitylabs/sdk/jsonrpc/JsonRpcRequestTest.java +++ b/src/test/java/org/unicitylabs/sdk/jsonrpc/JsonRpcRequestTest.java @@ -5,6 +5,7 @@ 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; public class JsonRpcRequestTest { diff --git a/src/test/java/org/unicitylabs/sdk/jsonrpc/JsonRpcResponseTest.java b/src/test/java/org/unicitylabs/sdk/jsonrpc/JsonRpcResponseTest.java index a989eed..6999726 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 { diff --git a/src/test/java/org/unicitylabs/sdk/mtree/CommonPathTest.java b/src/test/java/org/unicitylabs/sdk/mtree/CommonPathTest.java index 47134a9..6535860 100644 --- a/src/test/java/org/unicitylabs/sdk/mtree/CommonPathTest.java +++ b/src/test/java/org/unicitylabs/sdk/mtree/CommonPathTest.java @@ -8,7 +8,7 @@ public class CommonPathTest { @Test - public void shouldCalculateCommonPath() throws Exception { + public void shouldCalculateCommonPath() { Assertions.assertEquals(CommonPath.create( BigInteger.valueOf(0b11), BigInteger.valueOf(0b111101111) 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 index de9210c..652f2f2 100644 --- a/src/test/java/org/unicitylabs/sdk/mtree/plain/MerkleTreePathTest.java +++ b/src/test/java/org/unicitylabs/sdk/mtree/plain/MerkleTreePathTest.java @@ -7,10 +7,10 @@ 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.crypto.hash.DataHash; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; import org.unicitylabs.sdk.mtree.MerkleTreePathVerificationResult; -import org.unicitylabs.sdk.serializer.UnicityObjectMapper; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; import org.unicitylabs.sdk.util.HexConverter; import java.math.BigInteger; import java.util.List; @@ -31,36 +31,6 @@ public void testConstructorThrowsOnNullArguments() { 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( @@ -97,9 +67,23 @@ public void testShouldVerifyInclusionProof() { @Test public void testEmptyPathVerification() throws JsonProcessingException { - SparseMerkleTreePath path = UnicityObjectMapper.JSON.readValue( - "{\"root\":\"00001e54402898172f2948615fb17627733abbd120a85381c624ad060d28321be672\",\"steps\":[{\"path\":\"1\",\"data\":null},{\"path\":\"1\",\"data\":null}]}", - SparseMerkleTreePath.class); + 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()); diff --git a/src/test/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePathFixture.java b/src/test/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePathFixture.java index 1b65309..e417528 100644 --- a/src/test/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePathFixture.java +++ b/src/test/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreePathFixture.java @@ -1,10 +1,8 @@ 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; +import org.unicitylabs.sdk.crypto.hash.DataHasher; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; public class SparseMerkleTreePathFixture { diff --git a/src/test/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreeTest.java b/src/test/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreeTest.java index ffa2b73..bb70203 100644 --- a/src/test/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreeTest.java +++ b/src/test/java/org/unicitylabs/sdk/mtree/plain/SparseMerkleTreeTest.java @@ -6,7 +6,7 @@ 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.crypto.hash.HashAlgorithm; import org.unicitylabs.sdk.mtree.BranchExistsException; import org.unicitylabs.sdk.mtree.LeafOutOfBoundsException; import org.unicitylabs.sdk.mtree.MerkleTreePathVerificationResult; diff --git a/src/test/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreeTest.java b/src/test/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreeTest.java index 4e805bf..3898909 100644 --- a/src/test/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreeTest.java +++ b/src/test/java/org/unicitylabs/sdk/mtree/sum/SparseMerkleSumTreeTest.java @@ -1,43 +1,45 @@ package org.unicitylabs.sdk.mtree.sum; -import org.unicitylabs.sdk.hash.HashAlgorithm; +import org.unicitylabs.sdk.crypto.hash.HashAlgorithm; +import org.unicitylabs.sdk.mtree.MerkleTreePathVerificationResult; 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; +import org.unicitylabs.sdk.util.HexConverter; class SparseMerkleSumTreeTest { @Test void shouldAgreeWithSpecExamples() throws Exception { - var treeLeftOnly = new SparseMerkleSumTree(HashAlgorithm.SHA256); + SparseMerkleSumTree treeLeftOnly = new SparseMerkleSumTree(HashAlgorithm.SHA256); treeLeftOnly.addLeaf(new BigInteger("100", 2), new LeafValue("a".getBytes(), BigInteger.valueOf(1))); - var rootLeftOnly = treeLeftOnly.calculateRoot(); + SparseMerkleSumTreeRootNode rootLeftOnly = treeLeftOnly.calculateRoot(); Assertions.assertEquals(BigInteger.valueOf(1), rootLeftOnly.getValue()); - Assertions.assertEquals('"' + "0000" + "34e0cf342d70c0d10e3ba481f72db532ecfd723afa3c25812a4bef61b5198d0b" + '"', rootLeftOnly.getRootHash().toJson()); + Assertions.assertArrayEquals(HexConverter.decode("5822" + "0000" + "34e0cf342d70c0d10e3ba481f72db532ecfd723afa3c25812a4bef61b5198d0b"), rootLeftOnly.getRootHash().toCbor()); - var treeRightOnly = new SparseMerkleSumTree(HashAlgorithm.SHA256); + SparseMerkleSumTree treeRightOnly = new SparseMerkleSumTree(HashAlgorithm.SHA256); treeRightOnly.addLeaf(new BigInteger("111", 2), new LeafValue("b".getBytes(), BigInteger.valueOf(2))); - var rootRightOnly = treeRightOnly.calculateRoot(); + SparseMerkleSumTreeRootNode rootRightOnly = treeRightOnly.calculateRoot(); Assertions.assertEquals(BigInteger.valueOf(2), rootRightOnly.getValue()); - Assertions.assertEquals('"' + "0000" + "da47d1cda8dab5159b2bed1ea27c3d24ed990989fac3c62ace05273fea51f958" + '"', rootRightOnly.getRootHash().toJson()); + Assertions.assertArrayEquals(HexConverter.decode("5822" + "0000" + "da47d1cda8dab5159b2bed1ea27c3d24ed990989fac3c62ace05273fea51f958"), rootRightOnly.getRootHash().toCbor()); - var treeFourLeaves = new SparseMerkleSumTree(HashAlgorithm.SHA256); + 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))); - var rootFourLeaves = treeFourLeaves.calculateRoot(); + SparseMerkleSumTreeRootNode rootFourLeaves = treeFourLeaves.calculateRoot(); Assertions.assertEquals(BigInteger.valueOf(10), rootFourLeaves.getValue()); - Assertions.assertEquals('"' + "0000" + "adfefa7c86b18d1216eece9fe0ce82ca58fd8cf482305c3c4e1a0a1361dc9d15" + '"', rootFourLeaves.getRootHash().toJson()); + Assertions.assertArrayEquals(HexConverter.decode("5822" + "0000" + "adfefa7c86b18d1216eece9fe0ce82ca58fd8cf482305c3c4e1a0a1361dc9d15"), rootFourLeaves.getRootHash().toCbor()); } @Test void shouldBuildTreeWithNumericValues() throws Exception { - var leaves = Map.of( + 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)), @@ -49,12 +51,12 @@ void shouldBuildTreeWithNumericValues() throws Exception { tree.addLeaf(entry.getKey(), entry.getValue()); } - var root = tree.calculateRoot(); + SparseMerkleSumTreeRootNode 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()); + 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()); @@ -77,7 +79,7 @@ void shouldBuildTreeWithNumericValues() throws Exception { @Test void shouldThrowErrorOnNonPositivePathOrSum() { - var tree = new SparseMerkleSumTree(HashAlgorithm.SHA256); + SparseMerkleSumTree tree = new SparseMerkleSumTree(HashAlgorithm.SHA256); Assertions.assertThrows(IllegalArgumentException.class, () -> tree.addLeaf(BigInteger.valueOf(-1), new LeafValue(new byte[32], BigInteger.valueOf(100)))); 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..3de6126 100644 --- a/src/test/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializerTest.java +++ b/src/test/java/org/unicitylabs/sdk/serializer/cbor/CborDeserializerTest.java @@ -16,32 +16,32 @@ public class CborDeserializerTest { void testReadUnsignedInteger() { Assertions.assertEquals( 5, - CborDeserializer.readUnsignedInteger(HexConverter.decode("05")).asLong() + CborDeserializer.decodeUnsignedInteger(HexConverter.decode("05")).asLong() ); Assertions.assertEquals( 100, - CborDeserializer.readUnsignedInteger(HexConverter.decode("1864")).asLong() + CborDeserializer.decodeUnsignedInteger(HexConverter.decode("1864")).asLong() ); Assertions.assertEquals( 10000, - CborDeserializer.readUnsignedInteger(HexConverter.decode("192710")).asLong() + CborDeserializer.decodeUnsignedInteger(HexConverter.decode("192710")).asLong() ); Assertions.assertEquals( 66000, - CborDeserializer.readUnsignedInteger(HexConverter.decode("1a000101d0")).asLong() + CborDeserializer.decodeUnsignedInteger(HexConverter.decode("1a000101d0")).asLong() ); Assertions.assertEquals( 8147483647L, - CborDeserializer.readUnsignedInteger(HexConverter.decode("1b00000001e5a0bbff")).asLong() + CborDeserializer.decodeUnsignedInteger(HexConverter.decode("1b00000001e5a0bbff")).asLong() ); Assertions.assertEquals( -5, - CborDeserializer.readUnsignedInteger(HexConverter.decode("1bfffffffffffffffb")).asLong() + CborDeserializer.decodeUnsignedInteger(HexConverter.decode("1bfffffffffffffffb")).asLong() ); } @@ -49,12 +49,12 @@ void testReadUnsignedInteger() { void testReadByteString() { Assertions.assertArrayEquals( new byte[5], - CborDeserializer.readByteString(HexConverter.decode("450000000000")) + CborDeserializer.decodeByteString(HexConverter.decode("450000000000")) ); Assertions.assertArrayEquals( new byte[25], - CborDeserializer.readByteString( + CborDeserializer.decodeByteString( HexConverter.decode("581900000000000000000000000000000000000000000000000000")) ); } @@ -63,31 +63,31 @@ void testReadByteString() { void testReadTextString() { Assertions.assertEquals( "Hello, world!", - CborDeserializer.readTextString(HexConverter.decode("6d48656c6c6f2c20776f726c6421")) + CborDeserializer.decodeTextString(HexConverter.decode("6d48656c6c6f2c20776f726c6421")) ); Assertions.assertEquals( new String(new byte[25]), - CborDeserializer.readTextString( + CborDeserializer.decodeTextString( HexConverter.decode("781900000000000000000000000000000000000000000000000000")) ); } @Test void testReadArray() { - List data = CborDeserializer.readArray( + 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( + Set data = CborDeserializer.decodeMap( HexConverter.decode( "a4430000006d48656c6c6f2c20776f726c6421430000016d48656c6c6f2c20776f726c64216454657374f66d48656c6c6f2c20776f726c6421581900000000000000000000000000000000000000000000000000") ); @@ -136,24 +136,24 @@ void testReadMap() { @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( + CborDeserializer.decodeNullable( HexConverter.decode("f6"), - CborDeserializer::readUnsignedInteger + CborDeserializer::decodeUnsignedInteger ) ); } @Test void testEncodeTag() { - CborTag tag = CborDeserializer.readTag( + CborTag tag = CborDeserializer.decodeTag( HexConverter.decode("d4781a746167206e756d62657220736d616c6c6572207468616e203234") ); Assertions.assertEquals( diff --git a/src/test/java/org/unicitylabs/sdk/token/TokenIdTest.java b/src/test/java/org/unicitylabs/sdk/token/TokenIdTest.java index 9335be6..8436b3a 100644 --- a/src/test/java/org/unicitylabs/sdk/token/TokenIdTest.java +++ b/src/test/java/org/unicitylabs/sdk/token/TokenIdTest.java @@ -1,14 +1,19 @@ package org.unicitylabs.sdk.token; +import java.math.BigInteger; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.unicitylabs.sdk.transaction.TokenId; 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()); + 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/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..559e7cc 100644 --- a/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java +++ b/src/test/java/org/unicitylabs/sdk/utils/TokenUtils.java @@ -1,188 +1,264 @@ package org.unicitylabs.sdk.utils; -import static org.unicitylabs.sdk.utils.TestUtils.randomBytes; -import static org.unicitylabs.sdk.utils.TestUtils.randomCoinData; - +import java.security.SecureRandom; +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.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.UnlockScript; +import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicate; +import org.unicitylabs.sdk.predicate.builtin.PayToPublicKeyPredicateUnlockScript; +import org.unicitylabs.sdk.predicate.verification.PredicateVerifierService; +import org.unicitylabs.sdk.serializer.cbor.CborSerializer; +import org.unicitylabs.sdk.transaction.Address; import org.unicitylabs.sdk.transaction.MintTransaction; +import org.unicitylabs.sdk.transaction.Token; +import org.unicitylabs.sdk.transaction.TokenId; +import org.unicitylabs.sdk.transaction.TokenType; +import org.unicitylabs.sdk.transaction.TransferTransaction; import org.unicitylabs.sdk.util.InclusionProofUtils; +import org.unicitylabs.sdk.util.verification.VerificationStatus; +/** + * Test helpers for minting and transferring certified tokens. + */ public class TokenUtils { - public static Token mintToken( + /** + * Mint a token with empty payload. + * + * @param client state transition client + * @param trustBase trust base + * @param predicateVerifier predicate verifier + * @param recipient recipient address + * + * @return minted token + * + * @throws Exception when request or verification fails + */ + public static Token mintToken( StateTransitionClient client, RootTrustBase trustBase, - byte[] secret + PredicateVerifierService predicateVerifier, + Address 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 + predicateVerifier, + recipient, + CborSerializer.encodeArray() + ); + } + + /** + * Mint a token with explicit payload. + * + * @param client state transition client + * @param trustBase trust base + * @param predicateVerifier predicate verifier + * @param recipient recipient address + * @param data token payload + * + * @return minted token + * + * @throws Exception when request or verification fails + */ + public static Token mintToken( + StateTransitionClient client, + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + Address recipient, + byte[] data + ) throws Exception { + return TokenUtils.mintToken( + client, + trustBase, + predicateVerifier, + TokenId.generate(), + recipient, + data ); } - public static Token mintToken( + /** + * Mint a token with provided token id and generated type. + * + * @param client state transition client + * @param trustBase trust base + * @param predicateVerifier predicate verifier + * @param tokenId token id + * @param recipient recipient address + * @param data token payload + * + * @return minted token + * + * @throws Exception when request or verification fails + */ + public static Token mintToken( StateTransitionClient client, RootTrustBase trustBase, - byte[] secret, + PredicateVerifierService predicateVerifier, TokenId tokenId, - TokenType tokenType, - byte[] tokenData, - TokenCoinData coinData, - byte[] nonce, - byte[] salt, - DataHash dataHash + Address recipient, + byte[] data ) throws Exception { - SigningService signingService = SigningService.createFromMaskedSecret(secret, nonce); + return TokenUtils.mintToken( + client, + trustBase, + predicateVerifier, + tokenId, + TokenType.generate(), + recipient, + data + ); + } - MaskedPredicate predicate = MaskedPredicate.create( + /** + * Mint a token with fully specified token id and type. + * + * @param client state transition client + * @param trustBase trust base + * @param predicateVerifier predicate verifier + * @param tokenId token id + * @param tokenType token type + * @param recipient recipient address + * @param data token payload + * + * @return minted token + * + * @throws Exception when request or verification fails + */ + public static Token mintToken( + StateTransitionClient client, + RootTrustBase trustBase, + PredicateVerifierService predicateVerifier, + TokenId tokenId, + TokenType tokenType, + Address recipient, + byte[] data + ) throws Exception { + MintTransaction transaction = MintTransaction.create( + recipient, tokenId, tokenType, - signingService, - HashAlgorithm.SHA256, - nonce + data ); - Address address = predicate.getReference().toAddress(); - TokenState tokenState = new TokenState(predicate, null); - - MintCommitment commitment = MintCommitment.create( - new MintTransaction.Data<>( - tokenId, - tokenType, - tokenData, - coinData, - address, - salt, - dataHash, - null - ) - ); + CertificationData certificationData = CertificationData.fromMintTransaction(transaction); - // 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())); + CertificationResponse response = client.submitCertificationRequest(certificationData).get(); + if (response.getStatus() != CertificationStatus.SUCCESS) { + throw new RuntimeException( + String.format("Certification Request failed with status '%s'", response.getStatus())); } - // Wait for inclusion proof - InclusionProof inclusionProof = InclusionProofUtils.waitInclusionProof( - client, - trustBase, - commitment - ).get(); - - // Create mint transaction - return Token.create( + return Token.mint( trustBase, - tokenState, - commitment.toTransaction(inclusionProof) + predicateVerifier, + transaction.toCertifiedTransaction( + trustBase, + predicateVerifier, + InclusionProofUtils.waitInclusionProof(client, trustBase, predicateVerifier, transaction).get() + ) ); } - public static Token mintNametagToken( + + /** + * 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, - byte[] secret, - String nametag, - Address targetAddress + PredicateVerifierService predicateVerifier, + byte[] tokenBytes, + Address recipient, + SigningService signingService ) throws Exception { - return mintNametagToken( + Token token = Token.fromCbor(tokenBytes); + Assertions.assertEquals(VerificationStatus.OK, token.verify(trustBase, predicateVerifier).getStatus()); + + byte[] x = new byte[32]; + new SecureRandom().nextBytes(x); + + TransferTransaction transaction = TransferTransaction.create( + token, + PayToPublicKeyPredicate.create(signingService.getPublicKey()), + recipient, + x, + CborSerializer.encodeArray() + ); + + return TokenUtils.transferToken( client, trustBase, - secret, - new TokenType(randomBytes(32)), - nametag, - targetAddress, - randomBytes(32), - randomBytes(32) + predicateVerifier, + token, + transaction, + PayToPublicKeyPredicateUnlockScript.create(transaction, signingService) ); } - public static Token mintNametagToken( + /** + * 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, - byte[] secret, - TokenType tokenType, - String nametag, - Address targetAddress, - byte[] nonce, - byte[] salt + PredicateVerifierService predicateVerifier, + Token token, + TransferTransaction transaction, + UnlockScript unlockScript ) throws Exception { - SigningService signingService = SigningService.createFromMaskedSecret(secret, nonce); - - Address address = MaskedPredicateReference.create( - tokenType, - signingService, - HashAlgorithm.SHA256, - nonce).toAddress(); - - MintCommitment commitment = MintCommitment.create( - new MintTransaction.NametagData( - nametag, - tokenType, - address, - salt, - targetAddress - ) - ); + CertificationResponse response = client.submitCertificationRequest( + CertificationData.fromTransaction(transaction, unlockScript) + ).get(); - // 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())); + if (response.getStatus() != CertificationStatus.SUCCESS) { + throw new RuntimeException( + String.format("Certification Request failed with status '%s'", response.getStatus())); } - // Wait for inclusion proof - InclusionProof inclusionProof = InclusionProofUtils.waitInclusionProof( - client, + return token.transfer( trustBase, - commitment - ).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) + 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; } -}