diff --git a/cli-tools/src/main/java/com/radixdlt/shell/RadixShell.java b/cli-tools/src/main/java/com/radixdlt/shell/RadixShell.java index 6f1acdddba..435987fb1e 100644 --- a/cli-tools/src/main/java/com/radixdlt/shell/RadixShell.java +++ b/cli-tools/src/main/java/com/radixdlt/shell/RadixShell.java @@ -203,7 +203,7 @@ public Node build() throws Exception { if (properties.get("network.genesis_data", "").isEmpty()) { final var encodedGenesisData = NodeSborCodecs.encode( - GenesisData.testingWithSingleValidator(), + GenesisData.withSingleValidatorForRadixShell(), NodeSborCodecs.resolveCodec(new TypeToken<>() {})); final var compressedGenesisData = Compress.compress(encodedGenesisData); final var genesisDataBase64 = Base64.getEncoder().encodeToString(compressedGenesisData); diff --git a/core-rust-bridge/src/main/java/com/radixdlt/genesis/GenesisData.java b/core-rust-bridge/src/main/java/com/radixdlt/genesis/GenesisData.java index f23d3a250f..c31c9506eb 100644 --- a/core-rust-bridge/src/main/java/com/radixdlt/genesis/GenesisData.java +++ b/core-rust-bridge/src/main/java/com/radixdlt/genesis/GenesisData.java @@ -103,7 +103,9 @@ public static void registerCodec(CodecMap codecMap) { GenesisData.class, codecs -> StructCodec.fromRecordComponents(GenesisData.class, codecs)); } - public static GenesisData testingWithSingleValidator() { + // NOTE: From tests, see also GenesisBuilder + + public static GenesisData withSingleValidatorForRadixShell() { final var validatorKey = ECKeyPair.fromSeed(new byte[] {0x02}).getPublicKey(); return new GenesisData( UInt64.fromNonNegativeLong(1L), diff --git a/core-rust-bridge/src/main/java/com/radixdlt/transaction/PrepareTransactionIntentV2Request.java b/core-rust-bridge/src/main/java/com/radixdlt/transaction/PrepareTransactionIntentV2Request.java index 8770343f88..c77065d054 100644 --- a/core-rust-bridge/src/main/java/com/radixdlt/transaction/PrepareTransactionIntentV2Request.java +++ b/core-rust-bridge/src/main/java/com/radixdlt/transaction/PrepareTransactionIntentV2Request.java @@ -69,9 +69,10 @@ import com.radixdlt.sbor.codec.CodecMap; import com.radixdlt.sbor.codec.StructCodec; import com.radixdlt.utils.UInt64; +import java.util.List; public record PrepareTransactionIntentV2Request( - NetworkDefinition network, TransactionHeader header, UInt64 subintentCount) { + NetworkDefinition network, TransactionHeader header, List subintentCount) { public static void registerCodec(CodecMap codecMap) { codecMap.register( PrepareTransactionIntentV2Request.class, diff --git a/core-rust-bridge/src/main/java/com/radixdlt/transaction/TransactionPreparer.java b/core-rust-bridge/src/main/java/com/radixdlt/transaction/TransactionPreparer.java index b9a8b62488..918f0ff239 100644 --- a/core-rust-bridge/src/main/java/com/radixdlt/transaction/TransactionPreparer.java +++ b/core-rust-bridge/src/main/java/com/radixdlt/transaction/TransactionPreparer.java @@ -103,11 +103,11 @@ public static PreparedIntent prepareIntent( Natives.builder(TransactionPreparer::prepareIntent).build(new TypeToken<>() {}); public static PreparedTransactionIntentV2 prepareTransactionIntentV2( - NetworkDefinition network, TransactionHeader header, long subintentCount) { + NetworkDefinition network, TransactionHeader header, List subintentDiscriminators) { + var rustSubintentDiscriminators = + subintentDiscriminators.stream().map(UInt64::fromNonNegativeLong).toList(); return prepareTransactionIntentV2Func - .call( - new PrepareTransactionIntentV2Request( - network, header, UInt64.fromNonNegativeLong(subintentCount))) + .call(new PrepareTransactionIntentV2Request(network, header, rustSubintentDiscriminators)) .unwrap(TransactionPreparationException::new); } @@ -131,15 +131,18 @@ public static PreparedSignedIntent prepareSignedIntent( Natives.builder(TransactionPreparer::prepareSignedIntent).build(new TypeToken<>() {}); public static PreparedSignedIntent prepareSignedIntentV2( - byte[] transactionIntent, List signatures) { + byte[] transactionIntent, + List signatures, + List> subintentSignatures) { return prepareSignedTransactionIntentV2Func - .call(tuple(transactionIntent, signatures)) + .call(tuple(transactionIntent, signatures, subintentSignatures)) .unwrap(TransactionPreparationException::new); } private static final Natives.Call1< - Tuple.Tuple2>, Result> + Tuple.Tuple3, List>>, + Result> prepareSignedTransactionIntentV2Func = Natives.builder(TransactionPreparer::prepareSignedTransactionIntentV2) .build(new TypeToken<>() {}); diff --git a/core-rust/mesh-api-server/src/mesh_api/conversions/common.rs b/core-rust/mesh-api-server/src/mesh_api/conversions/common.rs index 17279efa5b..1f1b8647b6 100644 --- a/core-rust/mesh-api-server/src/mesh_api/conversions/common.rs +++ b/core-rust/mesh-api-server/src/mesh_api/conversions/common.rs @@ -1,5 +1,9 @@ use crate::prelude::*; +pub fn from_hex>(v: T) -> Result, ExtractionError> { + hex::decode(v).map_err(|_| ExtractionError::InvalidHex) +} + pub fn to_mesh_api_operation( mapping_context: &MappingContext, database: &StateManagerDatabase, diff --git a/core-rust/mesh-api-server/src/mesh_api/conversions/hashes.rs b/core-rust/mesh-api-server/src/mesh_api/conversions/hashes.rs index 6d58bb07ba..6c9a848c39 100644 --- a/core-rust/mesh-api-server/src/mesh_api/conversions/hashes.rs +++ b/core-rust/mesh-api-server/src/mesh_api/conversions/hashes.rs @@ -9,3 +9,20 @@ pub fn to_api_transaction_hash_bech32m( .encode(hash) .map_err(|err| MappingError::InvalidTransactionHash { encode_error: err }) } + +pub fn extract_transaction_intent_hash( + context: &ExtractionContext, + hash_str: String, +) -> Result { + from_hex(&hash_str) + .ok() + .and_then(|bytes| Hash::try_from(bytes.as_slice()).ok()) + .map(TransactionIntentHash::from_hash) + .or_else(|| { + context + .transaction_hash_decoder + .validate_and_decode(&hash_str) + .ok() + }) + .ok_or(ExtractionError::InvalidHash) +} diff --git a/core-rust/mesh-api-server/src/mesh_api/handlers/mempool.rs b/core-rust/mesh-api-server/src/mesh_api/handlers/mempool.rs new file mode 100644 index 0000000000..6d5e04ee1b --- /dev/null +++ b/core-rust/mesh-api-server/src/mesh_api/handlers/mempool.rs @@ -0,0 +1,62 @@ +use crate::prelude::*; + +pub(crate) async fn handle_mempool( + state: State, + Json(request): Json, +) -> Result, ResponseError> { + assert_matching_network(&request.network_identifier, &state.network)?; + + let mapping_context = MappingContext::new(&state.network); + let mempool = state.state_manager.mempool.read(); + + Ok(Json(models::MempoolResponse::new( + mempool + .all_hashes_iter() + .map(|(intent_hash, _)| { + Ok(models::TransactionIdentifier { + hash: to_api_transaction_hash_bech32m(&mapping_context, intent_hash)?, + }) + }) + .collect::, MappingError>>()?, + ))) +} + +pub(crate) async fn handle_mempool_transaction( + state: State, + Json(request): Json, +) -> Result, ResponseError> { + assert_matching_network(&request.network_identifier, &state.network)?; + + let extraction_context = ExtractionContext::new(&state.network); + let mapping_context = MappingContext::new(&state.network); + let mempool = state.state_manager.mempool.read(); + + let intent_hash = extract_transaction_intent_hash( + &extraction_context, + request.transaction_identifier.hash.clone(), + ) + .map_err(|err| err.into_response_error("intent_hash"))?; + + if mempool + .get_notarized_transaction_hashes_for_intent(&intent_hash) + .is_empty() + { + return Err( + ResponseError::from(ApiError::TransactionNotFound).with_details(format!( + "transaction {} not found in mempool transactions", + &request.transaction_identifier.hash + )), + ); + } + + // TODO:MESH prepare transaction estimates + let transaction = models::Transaction::new( + models::TransactionIdentifier::new(to_api_transaction_hash_bech32m( + &mapping_context, + &intent_hash, + )?), + vec![], + ); + + Ok(Json(models::MempoolTransactionResponse::new(transaction))) +} diff --git a/core-rust/mesh-api-server/src/mesh_api/handlers/mod.rs b/core-rust/mesh-api-server/src/mesh_api/handlers/mod.rs index 76ddf3ca10..233819b6e8 100644 --- a/core-rust/mesh-api-server/src/mesh_api/handlers/mod.rs +++ b/core-rust/mesh-api-server/src/mesh_api/handlers/mod.rs @@ -1,11 +1,13 @@ mod account_balance; mod block; +mod mempool; mod network_list; mod network_options; mod network_status; pub(crate) use account_balance::*; pub(crate) use block::*; +pub(crate) use mempool::*; pub(crate) use network_list::*; pub(crate) use network_options::*; pub(crate) use network_status::*; diff --git a/core-rust/mesh-api-server/src/mesh_api/server.rs b/core-rust/mesh-api-server/src/mesh_api/server.rs index ecbbdf7859..21d0e0c291 100644 --- a/core-rust/mesh-api-server/src/mesh_api/server.rs +++ b/core-rust/mesh-api-server/src/mesh_api/server.rs @@ -108,8 +108,8 @@ pub async fn create_server( .route("/block", post(handle_block)) .route("/block/transaction", post(handle_block_transaction)) // TODO:MESH mempool - .route("/mempool", post(handle_endpoint_todo)) - .route("/mempool/transaction", post(handle_endpoint_todo)) + .route("/mempool", post(handle_mempool)) + .route("/mempool/transaction", post(handle_mempool_transaction)) .route("/construction/derive", post(handle_endpoint_todo)) .route("/construction/preprocess", post(handle_endpoint_todo)) .route("/construction/metadata", post(handle_endpoint_todo)) diff --git a/core-rust/state-manager/src/jni/transaction_preparer.rs b/core-rust/state-manager/src/jni/transaction_preparer.rs index c5c5cf1c69..b28be5de4e 100644 --- a/core-rust/state-manager/src/jni/transaction_preparer.rs +++ b/core-rust/state-manager/src/jni/transaction_preparer.rs @@ -121,7 +121,7 @@ extern "system" fn Java_com_radixdlt_transaction_TransactionPreparer_prepareInte struct PrepareTransactionIntentV2Request { network_definition: NetworkDefinition, header: TransactionHeaderJava, - subintent_count: usize, + subintent_discriminators: Vec, } #[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)] @@ -144,7 +144,7 @@ extern "system" fn Java_com_radixdlt_transaction_TransactionPreparer_prepareTran let PrepareTransactionIntentV2Request { network_definition, header, - subintent_count, + subintent_discriminators, } = request; let mut subintent_hashes = vec![]; @@ -165,7 +165,7 @@ extern "system" fn Java_com_radixdlt_transaction_TransactionPreparer_prepareTran intent_discriminator: header.nonce as u64, }); - for i in 0..subintent_count { + for subintent_discriminator in subintent_discriminators { let mut subintent_builder: PartialTransactionV2Builder = PartialTransactionV2Builder::new() .intent_header(IntentHeaderV2 { network_id: network_definition.id, @@ -173,16 +173,17 @@ extern "system" fn Java_com_radixdlt_transaction_TransactionPreparer_prepareTran end_epoch_exclusive: Epoch::of(header.end_epoch_exclusive), min_proposer_timestamp_inclusive: None, max_proposer_timestamp_exclusive: None, - intent_discriminator: (header.nonce as u64) * 1000 + (i as u64), + intent_discriminator: subintent_discriminator, }) .manifest_builder(|builder| { builder .yield_to_parent(()) }); + let child_name = format!("child-{subintent_discriminator}"); subintent_hashes.push(subintent_builder.subintent_hash()); - subintent_names.push(format!("child-{i}")); + subintent_names.push(child_name.clone()); transaction_builder = transaction_builder.add_signed_child( - format!("child-{i}"), + child_name, subintent_builder.build(), ); } @@ -330,7 +331,7 @@ impl From for DecryptorsByCurve { #[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)] struct PrepareSignedIntentRequest { intent_bytes: RawTransactionIntent, - signatures: Vec, + transaction_signatures: Vec, } #[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)] @@ -354,7 +355,7 @@ extern "system" fn Java_com_radixdlt_transaction_TransactionPreparer_prepareSign intent: IntentV1::from_raw(&request.intent_bytes)?, intent_signatures: IntentSignaturesV1 { signatures: request - .signatures + .transaction_signatures .into_iter() .map(IntentSignatureV1) .collect(), @@ -372,6 +373,13 @@ extern "system" fn Java_com_radixdlt_transaction_TransactionPreparer_prepareSign ) } +#[derive(Debug, Clone, PartialEq, Eq, ScryptoSbor)] +struct PrepareSignedTransactionIntentV2Request { + intent_bytes: RawTransactionIntent, + transaction_signatures: Vec, + subintent_signatures: Vec>, +} + #[no_mangle] extern "system" fn Java_com_radixdlt_transaction_TransactionPreparer_prepareSignedTransactionIntentV2( env: JNIEnv, @@ -381,18 +389,22 @@ extern "system" fn Java_com_radixdlt_transaction_TransactionPreparer_prepareSign jni_sbor_coded_call( &env, request_payload, - |request: PrepareSignedIntentRequest| -> Result { + |request: PrepareSignedTransactionIntentV2Request| -> Result { let signed_intent = SignedTransactionIntentV2 { transaction_intent: TransactionIntentV2::from_raw(&request.intent_bytes)?, transaction_intent_signatures: IntentSignaturesV2 { signatures: request - .signatures + .transaction_signatures .into_iter() .map(IntentSignatureV1) .collect(), }, non_root_subintent_signatures: NonRootSubintentSignaturesV2 { - by_subintent: vec![], + by_subintent: request.subintent_signatures.into_iter().map(|signatures| { + IntentSignaturesV2 { + signatures: signatures.into_iter().map(IntentSignatureV1).collect(), + } + }).collect(), }, }; diff --git a/core/src/test-core/java/com/radixdlt/api/CoreApiHelper.java b/core/src/test-core/java/com/radixdlt/api/CoreApiHelper.java new file mode 100644 index 0000000000..f04c029551 --- /dev/null +++ b/core/src/test-core/java/com/radixdlt/api/CoreApiHelper.java @@ -0,0 +1,505 @@ +/* Copyright 2021 Radix Publishing Ltd incorporated in Jersey (Channel Islands). + * + * Licensed under the Radix License, Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * + * radixfoundation.org/licenses/LICENSE-v1 + * + * The Licensor hereby grants permission for the Canonical version of the Work to be + * published, distributed and used under or by reference to the Licensor’s trademark + * Radix ® and use of any unregistered trade names, logos or get-up. + * + * The Licensor provides the Work (and each Contributor provides its Contributions) on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, + * including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, + * MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + * + * Whilst the Work is capable of being deployed, used and adopted (instantiated) to create + * a distributed ledger it is your responsibility to test and validate the code, together + * with all logic and performance of that code under all foreseeable scenarios. + * + * The Licensor does not make or purport to make and hereby excludes liability for all + * and any representation, warranty or undertaking in any form whatsoever, whether express + * or implied, to any entity or person, including any representation, warranty or + * undertaking, as to the functionality security use, value or other characteristics of + * any distributed ledger nor in respect the functioning or value of any tokens which may + * be created stored or transferred using the Work. The Licensor does not warrant that the + * Work or any use of the Work complies with any law or regulation in any territory where + * it may be implemented or used or that it will be appropriate for any specific purpose. + * + * Neither the licensor nor any current or former employees, officers, directors, partners, + * trustees, representatives, agents, advisors, contractors, or volunteers of the Licensor + * shall be liable for any direct or indirect, special, incidental, consequential or other + * losses of any kind, in tort, contract or otherwise (including but not limited to loss + * of revenue, income or profits, or loss of use or data, or loss of reputation, or loss + * of any economic or other opportunity of whatsoever nature or howsoever arising), arising + * out of or in connection with (without limitation of any use, misuse, of any ledger system + * or use made or its functionality or any performance or operation of any code or protocol + * caused by bugs or programming or logic errors or otherwise); + * + * A. any offer, purchase, holding, use, sale, exchange or transmission of any + * cryptographic keys, tokens or assets created, exchanged, stored or arising from any + * interaction with the Work; + * + * B. any failure in a transmission or loss of any token or assets keys or other digital + * artefacts due to errors in transmission; + * + * C. bugs, hacks, logic errors or faults in the Work or any communication; + * + * D. system software or apparatus including but not limited to losses caused by errors + * in holding or transmitting tokens by any third-party; + * + * E. breaches or failure of security including hacker attacks, loss or disclosure of + * password, loss of private key, unauthorised use or misuse of such passwords or keys; + * + * F. any losses including loss of anticipated savings or other benefits resulting from + * use of the Work or any changes to the Work (however implemented). + * + * You are solely responsible for; testing, validating and evaluation of all operation + * logic, functionality, security and appropriateness of using the Work for any commercial + * or non-commercial purpose and for any reproduction or redistribution by You of the + * Work. You assume all risks associated with Your use of the Work and the exercise of + * permissions under this License. + */ + +package com.radixdlt.api; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowableOfType; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.reflect.ClassPath; +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.multibindings.ProvidesIntoSet; +import com.radixdlt.addressing.Addressing; +import com.radixdlt.api.core.generated.api.*; +import com.radixdlt.api.core.generated.client.ApiClient; +import com.radixdlt.api.core.generated.client.ApiException; +import com.radixdlt.api.core.generated.models.*; +import com.radixdlt.crypto.ECKeyPair; +import com.radixdlt.environment.CoreApiServerFlags; +import com.radixdlt.environment.StartProcessorOnRunner; +import com.radixdlt.harness.deterministic.DeterministicTest; +import com.radixdlt.lang.Either; +import com.radixdlt.lang.Functions; +import com.radixdlt.networks.Network; +import com.radixdlt.rev2.Manifest; +import com.radixdlt.rev2.NetworkDefinition; +import com.radixdlt.rev2.TransactionBuilder; +import com.radixdlt.transactions.PreparedNotarizedTransaction; +import com.radixdlt.transactions.TransactionIntentHash; +import com.radixdlt.utils.FreePortFinder; +import java.net.http.HttpClient; +import java.util.*; +import org.assertj.core.api.ThrowableAssert; + +public class CoreApiHelper { + + private final int coreApiPort; + private final Network network; + private final NetworkDefinition networkDefinition; + private final Addressing addressing; + private final ApiClient apiClient; + + static { + ensureOpenApiModelsAreReady(); + } + + public static CoreApiHelper forTests() { + return new CoreApiHelper(Network.INTEGRATIONTESTNET); + } + + public CoreApiHelper(Network network) { + this.coreApiPort = FreePortFinder.findFreeLocalPort(); + this.addressing = Addressing.ofNetwork(network); + this.network = network; + this.networkDefinition = NetworkDefinition.from(network); + final var apiClient = new ApiClient(); + apiClient.updateBaseUri("http://127.0.0.1:" + coreApiPort + "/core"); + apiClient.setHttpClientBuilder( + HttpClient.newBuilder().sslContext(DummySslContextFactory.create())); + this.apiClient = apiClient; + } + + private static void ensureOpenApiModelsAreReady() { + /* The generated Open API models are rubbish and requires that static initializers run on models before + * deserialization to work correctly... But that doesn't happen in e.g. models under the response model in + * assertErrorResponseOfType. + * As a workaround for now, let's go through all the types and explicitly ensure their static initializers run + * by using the Class.forName method. + */ + try { + ClassPath.from(ClassLoader.getSystemClassLoader()).getAllClasses().stream() + .filter(clazz -> clazz.getPackageName().equals("com.radixdlt.api.core.generated.models")) + .forEach( + clazz -> { + try { + Class.forName(clazz.getName()); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + }); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + public Module module() { + return new AbstractModule() { + @Override + protected void configure() { + install(new CoreApiServerModule("127.0.0.1", coreApiPort, new CoreApiServerFlags(true))); + } + + @ProvidesIntoSet + private StartProcessorOnRunner startCoreApi(CoreApiServer coreApiServer) { + // This is a slightly hacky way to run something on node start-up in a Deterministic test. + // Stop is called by the AutoClosable binding in CoreApiServerModule + return new StartProcessorOnRunner("coreApi", coreApiServer::start); + } + }; + } + + public ApiClient client() { + return this.apiClient; + } + + public TransactionApi transactionApi() { + return new TransactionApi(client()); + } + + public StreamApi streamApi() { + return new StreamApi(client()); + } + + public StatusApi statusApi() { + return new StatusApi(client()); + } + + public StateApi stateApi() { + return new StateApi(client()); + } + + public LtsApi ltsApi() { + return new LtsApi(client()); + } + + public MempoolApi mempoolApi() { + return new MempoolApi(client()); + } + + public Response assertErrorResponseOfType( + ThrowableAssert.ThrowingCallable apiCall, Class responseClass) { + var apiException = catchThrowableOfType(apiCall, ApiException.class); + try { + return apiClient.getObjectMapper().readValue(apiException.getResponseBody(), responseClass); + } catch (JsonProcessingException ex) { + throw new RuntimeException(ex); + } + } + + public NetworkConfigurationResponseWellKnownAddresses getWellKnownAddresses() + throws ApiException { + return new StatusApi(apiClient).statusNetworkConfigurationPost().getWellKnownAddresses(); + } + + public TransactionSubmitResponse submit(PreparedNotarizedTransaction transaction) + throws ApiException { + return transactionApi() + .transactionSubmitPost( + new TransactionSubmitRequest() + .network(network.getLogicalName()) + .notarizedTransactionHex(transaction.hexPayloadBytes())); + } + + public void ltsSubmit(PreparedNotarizedTransaction transaction) throws ApiException { + var submitResponse = + new LtsApi(apiClient) + .ltsTransactionSubmitPost( + new LtsTransactionSubmitRequest() + .network(networkDefinition.logical_name()) + .notarizedTransactionHex(transaction.hexPayloadBytes())); + + assertThat(submitResponse.getDuplicate()).isFalse(); + } + + public TransactionSubmitRejectedErrorDetails forceRecalculateSubmitExpectingRejection( + PreparedNotarizedTransaction transaction) { + var response = + assertErrorResponseOfType( + () -> + transactionApi() + .transactionSubmitPost( + new TransactionSubmitRequest() + .network(network.getLogicalName()) + .forceRecalculate(true) + .notarizedTransactionHex(transaction.hexPayloadBytes())), + TransactionSubmitErrorResponse.class); + return (TransactionSubmitRejectedErrorDetails) response.getDetails(); + } + + public TransactionSubmitRejectedErrorDetails submitExpectingRejection( + PreparedNotarizedTransaction transaction) { + var response = + assertErrorResponseOfType( + () -> + transactionApi() + .transactionSubmitPost( + new TransactionSubmitRequest() + .network(network.getLogicalName()) + .notarizedTransactionHex(transaction.hexPayloadBytes())), + TransactionSubmitErrorResponse.class); + return (TransactionSubmitRejectedErrorDetails) response.getDetails(); + } + + public TransactionSubmitResponse forceRecalculateSubmit(PreparedNotarizedTransaction transaction) + throws ApiException { + return transactionApi() + .transactionSubmitPost( + new TransactionSubmitRequest() + .network(network.getLogicalName()) + .forceRecalculate(true) + .notarizedTransactionHex(transaction.hexPayloadBytes())); + } + + public PreparedNotarizedTransaction buildTransaction( + Functions.Func1 manifest, List signatories) + throws ApiException { + var metadata = + new LtsApi(apiClient) + .ltsTransactionConstructionPost( + new LtsTransactionConstructionRequest().network(network.getLogicalName())); + return TransactionBuilder.forNetwork(networkDefinition) + .manifest(manifest) + .fromEpoch(metadata.getCurrentEpoch()) + .signatories(signatories) + .prepare(); + } + + public TransactionResult submitAndWaitForResult( + DeterministicTest test, PreparedNotarizedTransaction transaction) throws ApiException { + ltsSubmit(transaction); + return waitForResult(test, transaction.transactionIntentHash()); + } + + public TransactionResult waitForResult( + DeterministicTest test, PreparedNotarizedTransaction transaction) throws ApiException { + return waitForResult(test, transaction.transactionIntentHash()); + } + + public Either permanentResult( + TransactionIntentHash intentHash) throws ApiException { + var statusResponse = ltsTransactionStatus(intentHash); + return switch (statusResponse.getIntentStatus()) { + case COMMITTEDSUCCESS -> Either.left( + new TransactionResult(intentHash, TransactionOutcome.CommittedSuccess, statusResponse)); + case COMMITTEDFAILURE -> Either.left( + new TransactionResult(intentHash, TransactionOutcome.CommittedFailure, statusResponse)); + case PERMANENTREJECTION -> Either.left( + new TransactionResult(intentHash, TransactionOutcome.PermanentRejection, statusResponse)); + default -> Either.right(statusResponse.getIntentStatus()); + }; + } + + public TransactionResult waitForResult(DeterministicTest test, TransactionIntentHash intentHash) + throws ApiException { + + int messagesProcessedPerAttempt = 20; + long attempts = 50; + + LtsTransactionIntentStatus latestIntentStatus = null; + for (long i = 0; i < attempts; i++) { + var result = permanentResult(intentHash); + if (result.isLeft()) { + return result.unwrapLeft(); + } else { + latestIntentStatus = result.unwrapRight(); + } + test.runForCount(messagesProcessedPerAttempt); + } + throw new RuntimeException( + String.format( + "Transaction submit didn't complete in after running for count of %s messages. Status" + + " still: %s", + attempts * messagesProcessedPerAttempt, latestIntentStatus)); + } + + public TransactionResult waitForFirstResult( + DeterministicTest test, TransactionIntentHash... intentHashes) throws ApiException { + int messagesProcessedPerAttempt = 20; + long attempts = 50; + + var latestIntentStatuses = + new LinkedHashMap(); + + for (long i = 0; i < attempts; i++) { + for (TransactionIntentHash intentHash : intentHashes) { + var result = permanentResult(intentHash); + if (result.isLeft()) { + return result.unwrapLeft(); + } else { + latestIntentStatuses.put(intentHash, result.unwrapRight()); + } + } + test.runForCount(messagesProcessedPerAttempt); + } + throw new RuntimeException( + String.format( + "Transaction submit didn't complete in after running for count of %s messages. Statuses" + + " still: %s", + attempts * messagesProcessedPerAttempt, latestIntentStatuses)); + } + + public CommittedResult submitAndWaitForSuccess( + DeterministicTest test, + Functions.Func1 manifest, + List signatories) + throws ApiException { + final var transaction = buildTransaction(manifest, signatories); + return submitAndWaitForSuccess(test, transaction); + } + + public CommittedResult submitAndWaitForSuccess( + DeterministicTest test, PreparedNotarizedTransaction transaction) throws ApiException { + return this.submitAndWaitForResult(test, transaction).assertCommittedSuccess(); + } + + public CommittedResult submitAndWaitForCommittedFailure( + DeterministicTest test, + Functions.Func1 manifest, + List signatories) + throws ApiException { + final var transaction = buildTransaction(manifest, signatories); + return submitAndWaitForResult(test, transaction).assertCommittedFailure(); + } + + public CommittedResult submitAndWaitForCommittedFailure( + DeterministicTest test, PreparedNotarizedTransaction transaction) throws ApiException { + return submitAndWaitForResult(test, transaction).assertCommittedFailure(); + } + + public String submitAndWaitForPermanentRejection( + DeterministicTest test, PreparedNotarizedTransaction transaction) throws ApiException { + return submitAndWaitForResult(test, transaction).assertPermanentRejection(); + } + + public TransactionStatusResponse getStatus(PreparedNotarizedTransaction transaction) + throws ApiException { + return transactionApi() + .transactionStatusPost( + new TransactionStatusRequest() + .network(network.getLogicalName()) + .intentHash(addressing.encode(transaction.transactionIntentHash()))); + } + + public LtsTransactionStatusResponse ltsTransactionStatus(PreparedNotarizedTransaction transaction) + throws ApiException { + return ltsTransactionStatus(transaction.transactionIntentHash()); + } + + public LtsTransactionStatusResponse ltsTransactionStatus(TransactionIntentHash intentHash) + throws ApiException { + return ltsApi() + .ltsTransactionStatusPost( + new LtsTransactionStatusRequest() + .network(networkDefinition.logical_name()) + .intentHash(intentHash.hex())); + } + + public NetworkStatusResponse getNetworkStatus() throws ApiException { + return statusApi() + .statusNetworkStatusPost(new NetworkStatusRequest().network(network.getLogicalName())); + } + + public CommittedTransaction getTransactionFromStream(long stateVersion) throws ApiException { + return streamApi() + .streamTransactionsPost( + new StreamTransactionsRequest() + .network(network.getLogicalName()) + .fromStateVersion(stateVersion) + .limit(1)) + .getTransactions() + .get(0); + } + + public record TransactionResult( + TransactionIntentHash transactionIntentHash, + TransactionOutcome outcome, + LtsTransactionStatusResponse response) { + public CommittedResult assertCommittedSuccess() { + switch (outcome) { + case CommittedSuccess -> { + var stateVersion = response.getCommittedStateVersion(); + if (stateVersion == null) { + throw new RuntimeException( + "Transaction got committed as success without state version on response"); + } + return new CommittedResult(transactionIntentHash, stateVersion, Optional.empty()); + } + case CommittedFailure -> throw new RuntimeException( + String.format( + "Transaction got committed as failure: %s", + response.getKnownPayloads().get(0).getErrorMessage())); + case PermanentRejection -> throw new RuntimeException( + String.format( + "Transaction got permanently rejected: %s", + response.getKnownPayloads().get(0).getErrorMessage())); + } + throw new IllegalStateException("Shouldn't be able to get here"); + } + + public CommittedResult assertCommittedFailure() { + switch (outcome) { + case CommittedSuccess -> throw new RuntimeException( + "Transaction got committed as success, but was expecting committed failure"); + case CommittedFailure -> { + var stateVersion = response.getCommittedStateVersion(); + if (stateVersion == null) { + throw new RuntimeException( + "Transaction got committed as failure without state version on response"); + } + var errorMessage = + Objects.requireNonNull(response.getKnownPayloads().get(0).getErrorMessage()); + return new CommittedResult( + transactionIntentHash, stateVersion, Optional.of(errorMessage)); + } + case PermanentRejection -> throw new RuntimeException( + String.format( + "Transaction got permanently rejected: %s", + response.getKnownPayloads().get(0).getErrorMessage())); + } + throw new IllegalStateException("Shouldn't be able to get here"); + } + + public String assertPermanentRejection() { + switch (outcome) { + case CommittedSuccess -> throw new RuntimeException( + "Transaction got committed as success, but was expecting a permanent rejection"); + case CommittedFailure -> throw new RuntimeException( + "Transaction got committed as failure, but was expecting a permanent rejection"); + case PermanentRejection -> { + return response.getKnownPayloads().get(0).getErrorMessage(); + } + } + throw new IllegalStateException("Shouldn't be able to get here"); + } + + public T apply( + Functions.Func3 + resultMapper) { + return resultMapper.apply(transactionIntentHash, outcome, response); + } + } + + public enum TransactionOutcome { + CommittedSuccess, + CommittedFailure, + PermanentRejection, + } + + public record CommittedResult( + TransactionIntentHash transactionIntentHash, + long stateVersion, + Optional errorMessage) {} +} diff --git a/core/src/test/java/com/radixdlt/api/DummySslContextFactory.java b/core/src/test-core/java/com/radixdlt/api/DummySslContextFactory.java similarity index 100% rename from core/src/test/java/com/radixdlt/api/DummySslContextFactory.java rename to core/src/test-core/java/com/radixdlt/api/DummySslContextFactory.java diff --git a/core/src/test-core/java/com/radixdlt/api/MeshApiHelper.java b/core/src/test-core/java/com/radixdlt/api/MeshApiHelper.java new file mode 100644 index 0000000000..7783312a6f --- /dev/null +++ b/core/src/test-core/java/com/radixdlt/api/MeshApiHelper.java @@ -0,0 +1,173 @@ +/* Copyright 2021 Radix Publishing Ltd incorporated in Jersey (Channel Islands). + * + * Licensed under the Radix License, Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * + * radixfoundation.org/licenses/LICENSE-v1 + * + * The Licensor hereby grants permission for the Canonical version of the Work to be + * published, distributed and used under or by reference to the Licensor’s trademark + * Radix ® and use of any unregistered trade names, logos or get-up. + * + * The Licensor provides the Work (and each Contributor provides its Contributions) on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, + * including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, + * MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + * + * Whilst the Work is capable of being deployed, used and adopted (instantiated) to create + * a distributed ledger it is your responsibility to test and validate the code, together + * with all logic and performance of that code under all foreseeable scenarios. + * + * The Licensor does not make or purport to make and hereby excludes liability for all + * and any representation, warranty or undertaking in any form whatsoever, whether express + * or implied, to any entity or person, including any representation, warranty or + * undertaking, as to the functionality security use, value or other characteristics of + * any distributed ledger nor in respect the functioning or value of any tokens which may + * be created stored or transferred using the Work. The Licensor does not warrant that the + * Work or any use of the Work complies with any law or regulation in any territory where + * it may be implemented or used or that it will be appropriate for any specific purpose. + * + * Neither the licensor nor any current or former employees, officers, directors, partners, + * trustees, representatives, agents, advisors, contractors, or volunteers of the Licensor + * shall be liable for any direct or indirect, special, incidental, consequential or other + * losses of any kind, in tort, contract or otherwise (including but not limited to loss + * of revenue, income or profits, or loss of use or data, or loss of reputation, or loss + * of any economic or other opportunity of whatsoever nature or howsoever arising), arising + * out of or in connection with (without limitation of any use, misuse, of any ledger system + * or use made or its functionality or any performance or operation of any code or protocol + * caused by bugs or programming or logic errors or otherwise); + * + * A. any offer, purchase, holding, use, sale, exchange or transmission of any + * cryptographic keys, tokens or assets created, exchanged, stored or arising from any + * interaction with the Work; + * + * B. any failure in a transmission or loss of any token or assets keys or other digital + * artefacts due to errors in transmission; + * + * C. bugs, hacks, logic errors or faults in the Work or any communication; + * + * D. system software or apparatus including but not limited to losses caused by errors + * in holding or transmitting tokens by any third-party; + * + * E. breaches or failure of security including hacker attacks, loss or disclosure of + * password, loss of private key, unauthorised use or misuse of such passwords or keys; + * + * F. any losses including loss of anticipated savings or other benefits resulting from + * use of the Work or any changes to the Work (however implemented). + * + * You are solely responsible for; testing, validating and evaluation of all operation + * logic, functionality, security and appropriateness of using the Work for any commercial + * or non-commercial purpose and for any reproduction or redistribution by You of the + * Work. You assume all risks associated with Your use of the Work and the exercise of + * permissions under this License. + */ + +package com.radixdlt.api; + +import static org.assertj.core.api.Assertions.catchThrowableOfType; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.reflect.ClassPath; +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.multibindings.ProvidesIntoSet; +import com.radixdlt.addressing.Addressing; +import com.radixdlt.api.mesh.generated.api.*; +import com.radixdlt.api.mesh.generated.client.ApiClient; +import com.radixdlt.api.mesh.generated.client.ApiException; +import com.radixdlt.api.mesh.generated.models.*; +import com.radixdlt.environment.StartProcessorOnRunner; +import com.radixdlt.monitoring.ApplicationVersion; +import com.radixdlt.networks.Network; +import com.radixdlt.rev2.NetworkDefinition; +import com.radixdlt.utils.FreePortFinder; +import java.net.http.HttpClient; +import org.assertj.core.api.ThrowableAssert; + +public class MeshApiHelper { + + private final int meshApiPort; + private final Network network; + private final NetworkDefinition networkDefinition; + private final Addressing addressing; + private final ApiClient apiClient; + + static { + ensureOpenApiModelsAreReady(); + } + + public static MeshApiHelper forTests() { + return new MeshApiHelper(Network.INTEGRATIONTESTNET); + } + + public MeshApiHelper(Network network) { + this.meshApiPort = FreePortFinder.findFreeLocalPort(); + this.addressing = Addressing.ofNetwork(network); + this.network = network; + this.networkDefinition = NetworkDefinition.from(network); + final var apiClient = new ApiClient(); + apiClient.updateBaseUri("http://127.0.0.1:" + meshApiPort + "/mesh"); + apiClient.setHttpClientBuilder( + HttpClient.newBuilder().sslContext(DummySslContextFactory.create())); + this.apiClient = apiClient; + } + + private static void ensureOpenApiModelsAreReady() { + /* The generated Open API models are rubbish and requires that static initializers run on models before + * deserialization to work correctly... But that doesn't happen in e.g. models under the response model in + * assertErrorResponseOfType. + * As a workaround for now, let's go through all the types and explicitly ensure their static initializers run + * by using the Class.forName method. + */ + try { + ClassPath.from(ClassLoader.getSystemClassLoader()).getAllClasses().stream() + .filter(clazz -> clazz.getPackageName().equals("com.radixdlt.api.mesh.generated.models")) + .forEach( + clazz -> { + try { + Class.forName(clazz.getName()); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + }); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + public Module module() { + return new AbstractModule() { + @Override + protected void configure() { + install( + new MeshApiServerModule( + "127.0.0.1", meshApiPort, ApplicationVersion.INSTANCE.display())); + } + + @ProvidesIntoSet + private StartProcessorOnRunner startCoreApi(MeshApiServer meshApiServer) { + // This is a slightly hacky way to run something on node start-up in a Deterministic test. + // Stop is called by the AutoClosable binding in CoreApiServerModule + return new StartProcessorOnRunner("meshApi", meshApiServer::start); + } + }; + } + + public ApiClient client() { + return this.apiClient; + } + + public MempoolApi mempoolApi() { + return new MempoolApi(client()); + } + + public Response assertErrorResponseOfType( + ThrowableAssert.ThrowingCallable apiCall, Class responseClass) { + var apiException = catchThrowableOfType(apiCall, ApiException.class); + try { + return apiClient.getObjectMapper().readValue(apiException.getResponseBody(), responseClass); + } catch (JsonProcessingException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/core/src/test-core/java/com/radixdlt/harness/deterministic/DeterministicTest.java b/core/src/test-core/java/com/radixdlt/harness/deterministic/DeterministicTest.java index 438a96e092..2d79cb7882 100644 --- a/core/src/test-core/java/com/radixdlt/harness/deterministic/DeterministicTest.java +++ b/core/src/test-core/java/com/radixdlt/harness/deterministic/DeterministicTest.java @@ -69,6 +69,7 @@ import com.google.inject.Module; import com.google.inject.util.Modules; import com.radixdlt.addressing.Addressing; +import com.radixdlt.api.CoreApiHelper; import com.radixdlt.consensus.Proposal; import com.radixdlt.consensus.bft.*; import com.radixdlt.consensus.bft.Round; @@ -92,6 +93,7 @@ import java.util.*; import java.util.function.Predicate; import java.util.stream.Stream; +import org.junit.rules.TemporaryFolder; /** * A deterministic test where each event that occurs in the network is emitted and processed @@ -226,6 +228,26 @@ public static Builder builder() { return new Builder(); } + public static DeterministicTest rev2Default( + int numValidators, int roundsPerEpoch, TemporaryFolder temporaryFolder) { + return new Builder() + .addPhysicalNodes(PhysicalNodeConfig.createBatch(numValidators, true)) + .functionalNodeModule( + FunctionalRadixNodeModule.rev2Default(numValidators, roundsPerEpoch, temporaryFolder)); + } + + public static DeterministicTest rev2DefaultWithCoreApi( + int numValidators, + int roundsPerEpoch, + TemporaryFolder temporaryFolder, + CoreApiHelper coreApiHelper) { + return new Builder() + .addPhysicalNodes(PhysicalNodeConfig.createBatch(numValidators, true)) + .addModule(coreApiHelper.module()) + .functionalNodeModule( + FunctionalRadixNodeModule.rev2Default(numValidators, roundsPerEpoch, temporaryFolder)); + } + public DeterministicNetwork getNetwork() { return this.network; } diff --git a/core/src/test-core/java/com/radixdlt/harness/simulation/monitors/consensus/ConsensusMonitors.java b/core/src/test-core/java/com/radixdlt/harness/simulation/monitors/consensus/ConsensusMonitors.java index 106f2ae031..375384c832 100644 --- a/core/src/test-core/java/com/radixdlt/harness/simulation/monitors/consensus/ConsensusMonitors.java +++ b/core/src/test-core/java/com/radixdlt/harness/simulation/monitors/consensus/ConsensusMonitors.java @@ -127,6 +127,18 @@ TestInvariant livenessInvariant(NodeEvents nodeEvents) { }; } + public static Module liveness( + long allowedStartUpDuration, TimeUnit startUpTimeUnit, long duration, TimeUnit timeUnit) { + return new AbstractModule() { + @ProvidesIntoMap + @MonitorKey(Monitor.CONSENSUS_LIVENESS) + TestInvariant livenessInvariant(NodeEvents nodeEvents) { + return new LivenessInvariant( + nodeEvents, allowedStartUpDuration, startUpTimeUnit, duration, timeUnit); + } + }; + } + public static Module safety() { return new AbstractModule() { @ProvidesIntoMap diff --git a/core/src/test-core/java/com/radixdlt/harness/simulation/monitors/consensus/LivenessInvariant.java b/core/src/test-core/java/com/radixdlt/harness/simulation/monitors/consensus/LivenessInvariant.java index 0324e8ae20..7c423e9dc4 100644 --- a/core/src/test-core/java/com/radixdlt/harness/simulation/monitors/consensus/LivenessInvariant.java +++ b/core/src/test-core/java/com/radixdlt/harness/simulation/monitors/consensus/LivenessInvariant.java @@ -75,6 +75,7 @@ import com.radixdlt.harness.simulation.network.SimulationNodes.RunningNetwork; import io.reactivex.rxjava3.core.Observable; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; /** * Check that the network is making progress by ensuring that new QCs and epochs are progressively @@ -84,41 +85,79 @@ */ public class LivenessInvariant implements TestInvariant { private final NodeEvents nodeEvents; + private final long allowedStartUpDuration; + private final TimeUnit startUpTimeUnit; private final long duration; private final TimeUnit timeUnit; public LivenessInvariant(NodeEvents nodeEvents, long duration, TimeUnit timeUnit) { + this(nodeEvents, 3, TimeUnit.SECONDS, duration, timeUnit); + } + + public LivenessInvariant( + NodeEvents nodeEvents, + long allowedStartUpDuration, + TimeUnit startUpTimeUnit, + long duration, + TimeUnit timeUnit) { this.nodeEvents = nodeEvents; + this.allowedStartUpDuration = allowedStartUpDuration; + this.startUpTimeUnit = startUpTimeUnit; this.duration = duration; this.timeUnit = timeUnit; } @Override public Observable check(RunningNetwork network) { - return Observable.create( - emitter -> { - nodeEvents.addListener( - (node, highQCUpdate) -> { - emitter.onNext(highQCUpdate.getHighQC().highestQC()); - }, - BFTHighQCUpdate.class); - nodeEvents.addListener( - (node, committed) -> { - emitter.onNext(committed.vertexStoreState().getHighQC().highestQC()); - }, - BFTCommittedUpdate.class); - }) - .serialize() - .map(QuorumCertificate::getProposedHeader) - .map(header -> EpochRound.of(header.getLedgerHeader().getEpoch(), header.getRound())) - .scan(EpochRound.of(0, Round.epochInitial()), Ordering.natural()::max) - .distinctUntilChanged() - .debounce(duration, timeUnit) - .map( - epochRound -> + var isStartedUp = new AtomicBoolean(false); + + var detectLivenessBreakObservable = + Observable.create( + emitter -> { + nodeEvents.addListener( + (node, highQCUpdate) -> { + emitter.onNext(highQCUpdate.getHighQC().highestQC()); + }, + BFTHighQCUpdate.class); + nodeEvents.addListener( + (node, committed) -> { + emitter.onNext(committed.vertexStoreState().getHighQC().highestQC()); + }, + BFTCommittedUpdate.class); + }) + .serialize() + .map(QuorumCertificate::getProposedHeader) + .map(header -> EpochRound.of(header.getLedgerHeader().getEpoch(), header.getRound())) + .scan(EpochRound.of(0, Round.epochInitial()), Ordering.natural()::max) + // With a large number of nodes, they all boot up their pacemakers at currentRound = 1 + // This takes quite a while. Only when they're all booted up (and we see some event with + // round >= 2) + // do we mark ourselves as "started up" and start to watch for gaps in the stream + .skipWhile(epochRound -> epochRound.getRound().lte(Round.of(1))) + .doOnEach( + epochRound -> { + isStartedUp.set(true); + }) + .distinctUntilChanged() + .debounce(duration, timeUnit) + .map( + epochRound -> + new TestInvariantError( + String.format( + "Highest QC hasn't increased from %s after %s %s", + epochRound, duration, timeUnit))); + + var startUpBrokenObservable = + Observable.just( new TestInvariantError( String.format( - "Highest QC hasn't increased from %s after %s %s", - epochRound, duration, timeUnit))); + "Highest QC hasn't increased beyond Round 1 after %s %s, indicating a stall" + + " at start-up", + allowedStartUpDuration, startUpTimeUnit))) + .delay(allowedStartUpDuration, startUpTimeUnit) + // We skip the start-up error if we're already started up after the delay + .skipWhile(ignored -> isStartedUp.get()); + + return Observable.merge(detectLivenessBreakObservable, startUpBrokenObservable); } } diff --git a/core/src/test-core/java/com/radixdlt/modules/FunctionalRadixNodeModule.java b/core/src/test-core/java/com/radixdlt/modules/FunctionalRadixNodeModule.java index 7b3a6ff87b..4027b16ae3 100644 --- a/core/src/test-core/java/com/radixdlt/modules/FunctionalRadixNodeModule.java +++ b/core/src/test-core/java/com/radixdlt/modules/FunctionalRadixNodeModule.java @@ -74,12 +74,15 @@ import com.radixdlt.consensus.liveness.ProposalGenerator; import com.radixdlt.consensus.sync.BFTSyncPatienceMillis; import com.radixdlt.environment.*; +import com.radixdlt.genesis.GenesisBuilder; +import com.radixdlt.genesis.GenesisConsensusManagerConfig; import com.radixdlt.genesis.RawGenesisDataWithHash; import com.radixdlt.lang.Option; import com.radixdlt.ledger.MockedLedgerModule; import com.radixdlt.ledger.MockedLedgerRecoveryModule; import com.radixdlt.mempool.*; import com.radixdlt.modules.StateComputerConfig.*; +import com.radixdlt.rev2.Decimal; import com.radixdlt.rev2.modules.*; import com.radixdlt.statecomputer.MockedMempoolStateComputerModule; import com.radixdlt.statecomputer.MockedStateComputerModule; @@ -143,6 +146,10 @@ private ConsensusConfig( this.timeoutQuorumResolutionDelayMs = timeoutQuorumResolutionDelayMs; } + public static ConsensusConfig testDefault() { + return ConsensusConfig.of(1000); + } + public static ConsensusConfig of( int bftSyncPatienceMillis, long pacemakerBaseTimeoutMs, @@ -291,6 +298,21 @@ public static FunctionalRadixNodeModule justLedgerWithNumValidators(int numValid LedgerConfig.stateComputerNoSync(StateComputerConfig.mockedNoEpochs(numValidators))); } + public static FunctionalRadixNodeModule rev2Default( + int numValidators, int roundsPerEpoch, TemporaryFolder temporaryFolder) { + var genesis = + GenesisBuilder.createTestGenesisWithNumValidators( + numValidators, + Decimal.ofNonNegative(10000), + GenesisConsensusManagerConfig.Builder.testWithRoundsPerEpoch(roundsPerEpoch)); + return new FunctionalRadixNodeModule( + NodeStorageConfig.tempFolder(temporaryFolder), + true, + FunctionalRadixNodeModule.SafetyRecoveryConfig.REAL, + FunctionalRadixNodeModule.ConsensusConfig.testDefault(), + LedgerConfig.stateComputerNoSync(StateComputerConfig.rev2().withGenesis(genesis))); + } + public boolean supportsEpochs() { return epochs; } diff --git a/core/src/test-core/java/com/radixdlt/rev2/TransactionV2Builder.java b/core/src/test-core/java/com/radixdlt/rev2/TransactionV2Builder.java index b6071b5538..bad849c67f 100644 --- a/core/src/test-core/java/com/radixdlt/rev2/TransactionV2Builder.java +++ b/core/src/test-core/java/com/radixdlt/rev2/TransactionV2Builder.java @@ -90,8 +90,9 @@ public class TransactionV2Builder { private int nonce; private ECKeyPair notary; private boolean notaryIsSignatory; - private long subintentCount; + private List subintentDiscriminators; private List signatories; + private List> subintentSignatories; public TransactionV2Builder(NetworkDefinition networkDefinition) { this.networkDefinition = networkDefinition; @@ -100,8 +101,9 @@ public TransactionV2Builder(NetworkDefinition networkDefinition) { this.nonce = NONCE.getAndIncrement(); this.notary = DEFAULT_NOTARY; this.notaryIsSignatory = false; - this.subintentCount = 0; + this.subintentDiscriminators = List.of(); this.signatories = List.of(); + this.subintentSignatories = List.of(); } public static TransactionV2Builder forTests() { @@ -141,8 +143,10 @@ public TransactionV2Builder notaryIsSignatory(boolean notaryIsSignatory) { return this; } - public TransactionV2Builder subintentCount(long subintentCount) { - this.subintentCount = subintentCount; + public TransactionV2Builder subintentDiscriminators(List subintentDiscriminators) { + this.subintentDiscriminators = subintentDiscriminators; + this.subintentSignatories = + subintentDiscriminators.stream().map(ignored -> List.of()).toList(); return this; } @@ -156,7 +160,18 @@ public TransactionV2Builder signatories(int number) { return this; } + /** Must be called after subintentDiscriminators */ + public TransactionV2Builder subintentSignatories(List> subintentSignatories) { + this.subintentSignatories = subintentSignatories; + return this; + } + public PreparedNotarizedTransaction prepare() { + if (subintentSignatories.size() != subintentDiscriminators.size()) { + throw new RuntimeException( + "subintentSignatories must have the same length as subintentDiscriminators"); + } + var subintentCount = subintentSignatories.size(); var header = TransactionHeader.defaults( this.networkDefinition, @@ -166,7 +181,8 @@ public PreparedNotarizedTransaction prepare() { this.notary.getPublicKey().toPublicKey(), this.notaryIsSignatory); var intent = - TransactionPreparer.prepareTransactionIntentV2(networkDefinition, header, subintentCount); + TransactionPreparer.prepareTransactionIntentV2( + networkDefinition, header, subintentDiscriminators); var intentSignatures = this.signatories.stream() .map( @@ -175,9 +191,24 @@ public PreparedNotarizedTransaction prepare() { .sign(intent.transactionIntentHash().inner()) .toSignatureWithPublicKey()) .toList(); + var subintentSignatures = + IntStream.range(0, subintentDiscriminators.size()) + .mapToObj( + i -> { + var subintentHash = intent.subintentHashes().get(i); + var signatories = subintentSignatories.get(i); + var signatures = + signatories.stream() + .map( + ecKeyPair -> + ecKeyPair.sign(subintentHash.inner()).toSignatureWithPublicKey()) + .toList(); + return signatures; + }) + .toList(); var signedIntent = TransactionPreparer.prepareSignedIntentV2( - intent.transactionIntentBytes(), intentSignatures); + intent.transactionIntentBytes(), intentSignatures, subintentSignatures); var notarySignature = this.notary.sign(signedIntent.signedIntentHash().inner()).toSignature(); return TransactionPreparer.prepareNotarizedTransactionV2( signedIntent.signedIntentBytes(), notarySignature); diff --git a/core/src/test/java/com/radixdlt/api/CoreApiHelper.java b/core/src/test/java/com/radixdlt/api/CoreApiHelper.java deleted file mode 100644 index cfc9d385de..0000000000 --- a/core/src/test/java/com/radixdlt/api/CoreApiHelper.java +++ /dev/null @@ -1,248 +0,0 @@ -/* Copyright 2021 Radix Publishing Ltd incorporated in Jersey (Channel Islands). - * - * Licensed under the Radix License, Version 1.0 (the "License"); you may not use this - * file except in compliance with the License. You may obtain a copy of the License at: - * - * radixfoundation.org/licenses/LICENSE-v1 - * - * The Licensor hereby grants permission for the Canonical version of the Work to be - * published, distributed and used under or by reference to the Licensor’s trademark - * Radix ® and use of any unregistered trade names, logos or get-up. - * - * The Licensor provides the Work (and each Contributor provides its Contributions) on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, - * including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, - * MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. - * - * Whilst the Work is capable of being deployed, used and adopted (instantiated) to create - * a distributed ledger it is your responsibility to test and validate the code, together - * with all logic and performance of that code under all foreseeable scenarios. - * - * The Licensor does not make or purport to make and hereby excludes liability for all - * and any representation, warranty or undertaking in any form whatsoever, whether express - * or implied, to any entity or person, including any representation, warranty or - * undertaking, as to the functionality security use, value or other characteristics of - * any distributed ledger nor in respect the functioning or value of any tokens which may - * be created stored or transferred using the Work. The Licensor does not warrant that the - * Work or any use of the Work complies with any law or regulation in any territory where - * it may be implemented or used or that it will be appropriate for any specific purpose. - * - * Neither the licensor nor any current or former employees, officers, directors, partners, - * trustees, representatives, agents, advisors, contractors, or volunteers of the Licensor - * shall be liable for any direct or indirect, special, incidental, consequential or other - * losses of any kind, in tort, contract or otherwise (including but not limited to loss - * of revenue, income or profits, or loss of use or data, or loss of reputation, or loss - * of any economic or other opportunity of whatsoever nature or howsoever arising), arising - * out of or in connection with (without limitation of any use, misuse, of any ledger system - * or use made or its functionality or any performance or operation of any code or protocol - * caused by bugs or programming or logic errors or otherwise); - * - * A. any offer, purchase, holding, use, sale, exchange or transmission of any - * cryptographic keys, tokens or assets created, exchanged, stored or arising from any - * interaction with the Work; - * - * B. any failure in a transmission or loss of any token or assets keys or other digital - * artefacts due to errors in transmission; - * - * C. bugs, hacks, logic errors or faults in the Work or any communication; - * - * D. system software or apparatus including but not limited to losses caused by errors - * in holding or transmitting tokens by any third-party; - * - * E. breaches or failure of security including hacker attacks, loss or disclosure of - * password, loss of private key, unauthorised use or misuse of such passwords or keys; - * - * F. any losses including loss of anticipated savings or other benefits resulting from - * use of the Work or any changes to the Work (however implemented). - * - * You are solely responsible for; testing, validating and evaluation of all operation - * logic, functionality, security and appropriateness of using the Work for any commercial - * or non-commercial purpose and for any reproduction or redistribution by You of the - * Work. You assume all risks associated with Your use of the Work and the exercise of - * permissions under this License. - */ - -package com.radixdlt.api; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.radixdlt.api.core.generated.api.*; -import com.radixdlt.api.core.generated.client.ApiClient; -import com.radixdlt.api.core.generated.client.ApiException; -import com.radixdlt.api.core.generated.models.*; -import com.radixdlt.crypto.ECKeyPair; -import com.radixdlt.harness.deterministic.DeterministicTest; -import com.radixdlt.lang.Functions; -import com.radixdlt.rev2.*; -import com.radixdlt.rev2.NetworkDefinition; // for disambiguation with models.* only -import com.radixdlt.transactions.TransactionIntentHash; -import java.net.http.HttpClient; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -public class CoreApiHelper { - - private final NetworkDefinition networkDefinition; - - private final ApiClient apiClient; - - public CoreApiHelper(NetworkDefinition networkDefinition, int coreApiPort) { - this.networkDefinition = networkDefinition; - this.apiClient = new ApiClient(); - apiClient.updateBaseUri("http://127.0.0.1:" + coreApiPort + "/core"); - apiClient.setHttpClientBuilder( - HttpClient.newBuilder().sslContext(DummySslContextFactory.create())); - } - - public NetworkConfigurationResponseWellKnownAddresses getWellKnownAddresses() - throws ApiException { - return new StatusApi(apiClient).statusNetworkConfigurationPost().getWellKnownAddresses(); - } - - public StreamApi streamApi() { - return new StreamApi(apiClient); - } - - public enum TransactionOutcome { - CommittedSuccess, - CommittedFailure, - PermanentRejection, - } - - public T submitAndWait( - DeterministicTest test, - Functions.Func1 manifest, - List signatories, - Functions.Func3 - outcomeMapper) - throws Exception { - var metadata = - new LtsApi(apiClient) - .ltsTransactionConstructionPost( - new LtsTransactionConstructionRequest().network(networkDefinition.logical_name())); - - var transaction = - TransactionBuilder.forNetwork(networkDefinition) - .manifest(manifest) - .fromEpoch(metadata.getCurrentEpoch()) - .signatories(signatories) - .prepare(); - - var submitResponse = - new LtsApi(apiClient) - .ltsTransactionSubmitPost( - new LtsTransactionSubmitRequest() - .network(networkDefinition.logical_name()) - .notarizedTransactionHex(transaction.hexPayloadBytes())); - - assertThat(submitResponse.getDuplicate()).isFalse(); - - int messagesProcessedPerAttempt = 20; - long attempts = 50; - - LtsTransactionStatusResponse statusResponse = null; - for (long i = 0; i < attempts; i++) { - statusResponse = - new LtsApi(apiClient) - .ltsTransactionStatusPost( - new LtsTransactionStatusRequest() - .network(networkDefinition.logical_name()) - .intentHash(transaction.hexIntentHash())); - switch (statusResponse.getIntentStatus()) { - case COMMITTEDSUCCESS -> { - return outcomeMapper.apply( - transaction.transactionIntentHash(), - TransactionOutcome.CommittedSuccess, - statusResponse); - } - case COMMITTEDFAILURE -> { - return outcomeMapper.apply( - transaction.transactionIntentHash(), - TransactionOutcome.CommittedFailure, - statusResponse); - } - case PERMANENTREJECTION -> { - return outcomeMapper.apply( - transaction.transactionIntentHash(), - TransactionOutcome.PermanentRejection, - statusResponse); - } - default -> test.runForCount(messagesProcessedPerAttempt); - } - } - throw new RuntimeException( - String.format( - "Transaction submit didn't complete in after running for count of %s. Status still: %s", - attempts * messagesProcessedPerAttempt, statusResponse.getIntentStatus())); - } - - public CommittedResult submitAndWaitForSuccess( - DeterministicTest test, - Functions.Func1 manifest, - List signatories) - throws Exception { - return this.submitAndWait( - test, - manifest, - signatories, - (intentHash, outcome, response) -> { - switch (outcome) { - case CommittedSuccess -> { - var stateVersion = response.getCommittedStateVersion(); - if (stateVersion == null) { - throw new RuntimeException( - "Transaction got committed as success without state version on response"); - } - return new CommittedResult(intentHash, stateVersion, Optional.empty()); - } - case CommittedFailure -> throw new RuntimeException( - String.format( - "Transaction got committed as failure: %s", - response.getKnownPayloads().get(0).getErrorMessage())); - case PermanentRejection -> throw new RuntimeException( - String.format( - "Transaction got permanently rejected: %s", - response.getKnownPayloads().get(0).getErrorMessage())); - } - throw new IllegalStateException("Shouldn't be able to get here"); - }); - } - - public CommittedResult submitAndWaitForCommittedFailure( - DeterministicTest test, - Functions.Func1 manifest, - List signatories) - throws Exception { - return this.submitAndWait( - test, - manifest, - signatories, - (intentHash, outcome, response) -> { - switch (outcome) { - case CommittedSuccess -> throw new RuntimeException( - "Transaction got committed as success, but was expecting committed failure"); - case CommittedFailure -> { - var stateVersion = response.getCommittedStateVersion(); - if (stateVersion == null) { - throw new RuntimeException( - "Transaction got committed as failure without state version on response"); - } - var errorMessage = - Objects.requireNonNull(response.getKnownPayloads().get(0).getErrorMessage()); - return new CommittedResult(intentHash, stateVersion, Optional.of(errorMessage)); - } - case PermanentRejection -> throw new RuntimeException( - String.format( - "Transaction got permanently rejected: %s", - response.getKnownPayloads().get(0).getErrorMessage())); - } - throw new IllegalStateException("Shouldn't be able to get here"); - }); - } - - public record CommittedResult( - TransactionIntentHash transactionIntentHash, - long stateVersion, - Optional errorMessage) {} -} diff --git a/core/src/test/java/com/radixdlt/api/DeterministicCoreApiTestBase.java b/core/src/test/java/com/radixdlt/api/DeterministicCoreApiTestBase.java index 5a98f1ed1d..59b7f6bb04 100644 --- a/core/src/test/java/com/radixdlt/api/DeterministicCoreApiTestBase.java +++ b/core/src/test/java/com/radixdlt/api/DeterministicCoreApiTestBase.java @@ -68,16 +68,9 @@ import static org.assertj.core.api.Assertions.*; import com.fasterxml.jackson.core.JsonProcessingException; -import com.google.common.reflect.ClassPath; -import com.google.inject.AbstractModule; -import com.google.inject.multibindings.ProvidesIntoSet; import com.radixdlt.addressing.Addressing; import com.radixdlt.api.core.generated.api.*; -import com.radixdlt.api.core.generated.client.ApiClient; -import com.radixdlt.api.core.generated.client.ApiException; import com.radixdlt.api.core.generated.models.*; -import com.radixdlt.environment.CoreApiServerFlags; -import com.radixdlt.environment.StartProcessorOnRunner; import com.radixdlt.genesis.GenesisBuilder; import com.radixdlt.genesis.GenesisConsensusManagerConfig; import com.radixdlt.harness.deterministic.DeterministicTest; @@ -85,11 +78,10 @@ import com.radixdlt.modules.FunctionalRadixNodeModule; import com.radixdlt.modules.FunctionalRadixNodeModule.NodeStorageConfig; import com.radixdlt.modules.StateComputerConfig; +import com.radixdlt.networks.Network; import com.radixdlt.rev2.*; import com.radixdlt.rev2.NetworkDefinition; import com.radixdlt.sync.SyncRelayConfig; -import com.radixdlt.utils.FreePortFinder; -import java.net.http.HttpClient; import java.util.List; import org.assertj.core.api.ThrowableAssert; import org.junit.Rule; @@ -102,16 +94,10 @@ public abstract class DeterministicCoreApiTestBase { public static Addressing addressing = Addressing.ofNetwork(NetworkDefinition.INT_TEST_NET); public static String networkLogicalName = networkDefinition.logical_name(); - private final int coreApiPort; - private final ApiClient apiClient; - - static { - ensureOpenApiModelsAreReady(); - } + private final CoreApiHelper coreApiHelper; protected DeterministicCoreApiTestBase() { - this.coreApiPort = FreePortFinder.findFreeLocalPort(); - this.apiClient = buildApiClient(); + this.coreApiHelper = new CoreApiHelper(Network.INTEGRATIONTESTNET); } protected StateComputerConfig.REv2StateComputerConfig defaultConfig() { @@ -130,18 +116,7 @@ protected DeterministicTest buildRunningServerTest(StateComputerConfig stateComp .addPhysicalNodes(PhysicalNodeConfig.createBatch(1, true)) .messageSelector(firstSelector()) .addMonitors() - .addModule( - new CoreApiServerModule("127.0.0.1", coreApiPort, new CoreApiServerFlags(true))) - .addModule( - new AbstractModule() { - @ProvidesIntoSet - private StartProcessorOnRunner startCoreApi(CoreApiServer coreApiServer) { - // This is a slightly hacky way to run something on node start-up in a - // Deterministic test. - // Stop is called by the AutoClosable binding in CoreApiServerModule - return new StartProcessorOnRunner("N/A", coreApiServer::start); - } - }) + .addModule(coreApiHelper.module()) .functionalNodeModule( new FunctionalRadixNodeModule( NodeStorageConfig.tempFolder(folder), @@ -159,76 +134,44 @@ private StartProcessorOnRunner startCoreApi(CoreApiServer coreApiServer) { return test; } - private static void ensureOpenApiModelsAreReady() { - /* The generated Open API models are rubbish and requires that static initializers run on models before - * deserialization to work correctly... But that doesn't happen in eg models under the response model in - * assertErrorResponseOfType. - * As a workaround for now, let's go through all the types and explicitly ensure their static initializers run - * by using the Class.forName method. - */ - try { - ClassPath.from(ClassLoader.getSystemClassLoader()).getAllClasses().stream() - .filter(clazz -> clazz.getPackageName().equals("com.radixdlt.api.core.generated.models")) - .forEach( - clazz -> { - try { - Class.forName(clazz.getName()); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - }); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - protected ApiClient buildApiClient() { - final var apiClient = new ApiClient(); - apiClient.updateBaseUri("http://127.0.0.1:" + coreApiPort + "/core"); - apiClient.setHttpClientBuilder( - HttpClient.newBuilder().sslContext(DummySslContextFactory.create())); - return apiClient; - } - public Response assertErrorResponseOfType( ThrowableAssert.ThrowingCallable apiCall, Class responseClass) throws JsonProcessingException { - var apiException = catchThrowableOfType(apiCall, ApiException.class); - return apiClient.getObjectMapper().readValue(apiException.getResponseBody(), responseClass); + return coreApiHelper.assertErrorResponseOfType(apiCall, responseClass); } public MempoolApi getMempoolApi() { - return new MempoolApi(apiClient); + return coreApiHelper.mempoolApi(); } protected StatusApi getStatusApi() { - return new StatusApi(apiClient); + return coreApiHelper.statusApi(); } protected TransactionApi getTransactionApi() { - return new TransactionApi(apiClient); + return coreApiHelper.transactionApi(); } protected StreamApi getStreamApi() { - return new StreamApi(apiClient); + return coreApiHelper.streamApi(); } protected StateApi getStateApi() { - return new StateApi(apiClient); + return coreApiHelper.stateApi(); } protected LtsApi getLtsApi() { - return new LtsApi(apiClient); + return coreApiHelper.ltsApi(); } - protected CoreApiHelper getApiHelper() { - return new CoreApiHelper(networkDefinition, coreApiPort); + protected CoreApiHelper getCoreApiHelper() { + return coreApiHelper; } public ResourceAddress createFreeMintBurnNonFungibleResource(DeterministicTest test) throws Exception { var committedNewResourceTxn = - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess(test, Manifest.createAllowAllNonFungibleResource(), List.of()); final var receipt = diff --git a/core/src/test/java/com/radixdlt/api/DeterministicEngineStateApiTestBase.java b/core/src/test/java/com/radixdlt/api/DeterministicEngineStateApiTestBase.java index 8ad55dfb96..e1cc8d0d87 100644 --- a/core/src/test/java/com/radixdlt/api/DeterministicEngineStateApiTestBase.java +++ b/core/src/test/java/com/radixdlt/api/DeterministicEngineStateApiTestBase.java @@ -83,6 +83,7 @@ import com.radixdlt.modules.FunctionalRadixNodeModule; import com.radixdlt.modules.FunctionalRadixNodeModule.NodeStorageConfig; import com.radixdlt.modules.StateComputerConfig; +import com.radixdlt.networks.Network; import com.radixdlt.rev2.*; import com.radixdlt.sync.SyncRelayConfig; import com.radixdlt.utils.FreePortFinder; @@ -104,11 +105,11 @@ public abstract class DeterministicEngineStateApiTestBase { private final ApiClient apiClient; - private final int coreApiPort; + private final CoreApiHelper coreApiHelper; protected DeterministicEngineStateApiTestBase() { this.engineStateApiPort = FreePortFinder.findFreeLocalPort(); - this.coreApiPort = FreePortFinder.findFreeLocalPort(); + this.coreApiHelper = new CoreApiHelper(Network.INTEGRATIONTESTNET); this.apiClient = this.buildApiClient(); } @@ -138,15 +139,7 @@ private StartProcessorOnRunner startEngineStateApi( return new StartProcessorOnRunner("N/A", engineStateApiServer::start); } }) - .addModule( - new CoreApiServerModule("127.0.0.1", coreApiPort, new CoreApiServerFlags(true))) - .addModule( - new AbstractModule() { - @ProvidesIntoSet - private StartProcessorOnRunner startCodeApiServer(CoreApiServer coreApiServer) { - return new StartProcessorOnRunner("N/A", coreApiServer::start); - } - }) + .addModule(coreApiHelper.module()) .functionalNodeModule( new FunctionalRadixNodeModule( NodeStorageConfig.tempFolder(folder), @@ -205,6 +198,6 @@ protected TypesApi getTypesApi() { } protected CoreApiHelper getCoreApiHelper() { - return new CoreApiHelper(networkDefinition, coreApiPort); + return coreApiHelper; } } diff --git a/core/src/test/java/com/radixdlt/api/DeterministicMeshApiTestBase.java b/core/src/test/java/com/radixdlt/api/DeterministicMeshApiTestBase.java new file mode 100644 index 0000000000..fcc3f9135c --- /dev/null +++ b/core/src/test/java/com/radixdlt/api/DeterministicMeshApiTestBase.java @@ -0,0 +1,153 @@ +/* Copyright 2021 Radix Publishing Ltd incorporated in Jersey (Channel Islands). + * + * Licensed under the Radix License, Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * + * radixfoundation.org/licenses/LICENSE-v1 + * + * The Licensor hereby grants permission for the Canonical version of the Work to be + * published, distributed and used under or by reference to the Licensor’s trademark + * Radix ® and use of any unregistered trade names, logos or get-up. + * + * The Licensor provides the Work (and each Contributor provides its Contributions) on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, + * including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, + * MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + * + * Whilst the Work is capable of being deployed, used and adopted (instantiated) to create + * a distributed ledger it is your responsibility to test and validate the code, together + * with all logic and performance of that code under all foreseeable scenarios. + * + * The Licensor does not make or purport to make and hereby excludes liability for all + * and any representation, warranty or undertaking in any form whatsoever, whether express + * or implied, to any entity or person, including any representation, warranty or + * undertaking, as to the functionality security use, value or other characteristics of + * any distributed ledger nor in respect the functioning or value of any tokens which may + * be created stored or transferred using the Work. The Licensor does not warrant that the + * Work or any use of the Work complies with any law or regulation in any territory where + * it may be implemented or used or that it will be appropriate for any specific purpose. + * + * Neither the licensor nor any current or former employees, officers, directors, partners, + * trustees, representatives, agents, advisors, contractors, or volunteers of the Licensor + * shall be liable for any direct or indirect, special, incidental, consequential or other + * losses of any kind, in tort, contract or otherwise (including but not limited to loss + * of revenue, income or profits, or loss of use or data, or loss of reputation, or loss + * of any economic or other opportunity of whatsoever nature or howsoever arising), arising + * out of or in connection with (without limitation of any use, misuse, of any ledger system + * or use made or its functionality or any performance or operation of any code or protocol + * caused by bugs or programming or logic errors or otherwise); + * + * A. any offer, purchase, holding, use, sale, exchange or transmission of any + * cryptographic keys, tokens or assets created, exchanged, stored or arising from any + * interaction with the Work; + * + * B. any failure in a transmission or loss of any token or assets keys or other digital + * artefacts due to errors in transmission; + * + * C. bugs, hacks, logic errors or faults in the Work or any communication; + * + * D. system software or apparatus including but not limited to losses caused by errors + * in holding or transmitting tokens by any third-party; + * + * E. breaches or failure of security including hacker attacks, loss or disclosure of + * password, loss of private key, unauthorised use or misuse of such passwords or keys; + * + * F. any losses including loss of anticipated savings or other benefits resulting from + * use of the Work or any changes to the Work (however implemented). + * + * You are solely responsible for; testing, validating and evaluation of all operation + * logic, functionality, security and appropriateness of using the Work for any commercial + * or non-commercial purpose and for any reproduction or redistribution by You of the + * Work. You assume all risks associated with Your use of the Work and the exercise of + * permissions under this License. + */ + +package com.radixdlt.api; + +import static com.radixdlt.environment.deterministic.network.MessageSelector.firstSelector; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.radixdlt.addressing.Addressing; +import com.radixdlt.api.core.generated.api.TransactionApi; +import com.radixdlt.api.mesh.generated.api.MempoolApi; +import com.radixdlt.genesis.GenesisBuilder; +import com.radixdlt.genesis.GenesisConsensusManagerConfig; +import com.radixdlt.harness.deterministic.DeterministicTest; +import com.radixdlt.harness.deterministic.PhysicalNodeConfig; +import com.radixdlt.modules.FunctionalRadixNodeModule; +import com.radixdlt.modules.FunctionalRadixNodeModule.NodeStorageConfig; +import com.radixdlt.modules.StateComputerConfig; +import com.radixdlt.networks.Network; +import com.radixdlt.rev2.Decimal; +import com.radixdlt.rev2.NetworkDefinition; +import com.radixdlt.sync.SyncRelayConfig; +import org.assertj.core.api.ThrowableAssert; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; + +public abstract class DeterministicMeshApiTestBase { + + @Rule public TemporaryFolder folder = new TemporaryFolder(); + public static NetworkDefinition networkDefinition = NetworkDefinition.INT_TEST_NET; + public static Addressing addressing = Addressing.ofNetwork(NetworkDefinition.INT_TEST_NET); + public static String networkLogicalName = networkDefinition.logical_name(); + + /** For now, we use CoreAPI for e.g. submitting transactions */ + private final CoreApiHelper coreApiHelper; + + private final MeshApiHelper meshApiHelper; + + protected DeterministicMeshApiTestBase() { + this.coreApiHelper = new CoreApiHelper(Network.INTEGRATIONTESTNET); + this.meshApiHelper = new MeshApiHelper(Network.INTEGRATIONTESTNET); + } + + protected StateComputerConfig.REv2StateComputerConfig defaultConfig() { + return StateComputerConfig.rev2() + .withGenesis( + GenesisBuilder.createTestGenesisWithNumValidators( + 1, + Decimal.ONE, + GenesisConsensusManagerConfig.Builder.testDefaults() + .epochExactRoundCount(1000000))); + } + + protected DeterministicTest buildRunningServerTest(StateComputerConfig stateComputerConfig) { + var test = + DeterministicTest.builder() + .addPhysicalNodes(PhysicalNodeConfig.createBatch(1, true)) + .messageSelector(firstSelector()) + .addMonitors() + .addModule(coreApiHelper.module()) + .addModule(meshApiHelper.module()) + .functionalNodeModule( + new FunctionalRadixNodeModule( + NodeStorageConfig.tempFolder(folder), + true, + FunctionalRadixNodeModule.SafetyRecoveryConfig.MOCKED, + FunctionalRadixNodeModule.ConsensusConfig.of(1000), + FunctionalRadixNodeModule.LedgerConfig.stateComputerWithSyncRelay( + stateComputerConfig, SyncRelayConfig.of(200, 10, 2000)))); + try { + test.startAllNodes(); + } catch (Exception ex) { + test.close(); + throw ex; + } + return test; + } + + public Response assertErrorResponseOfType( + ThrowableAssert.ThrowingCallable apiCall, Class responseClass) + throws JsonProcessingException { + return coreApiHelper.assertErrorResponseOfType(apiCall, responseClass); + } + + protected TransactionApi getCoreTransactionApi() { + return coreApiHelper.transactionApi(); + } + + public MempoolApi getMempoolApi() { + return meshApiHelper.mempoolApi(); + } +} diff --git a/core/src/test/java/com/radixdlt/api/core/CallPreviewTest.java b/core/src/test/java/com/radixdlt/api/core/CallPreviewTest.java index 87167d14c0..799e30e8c3 100644 --- a/core/src/test/java/com/radixdlt/api/core/CallPreviewTest.java +++ b/core/src/test/java/com/radixdlt/api/core/CallPreviewTest.java @@ -68,7 +68,6 @@ import com.radixdlt.addressing.Addressing; import com.radixdlt.api.DeterministicCoreApiTestBase; -import com.radixdlt.api.core.generated.api.TransactionApi; import com.radixdlt.api.core.generated.client.ApiException; import com.radixdlt.api.core.generated.models.BlueprintFunctionTargetIdentifier; import com.radixdlt.api.core.generated.models.TargetIdentifierType; @@ -133,7 +132,7 @@ public void call_preview_works_without_faucet() throws ApiException { // Act: Preview a transaction final var callPreviewResponse = - new TransactionApi(buildApiClient()).transactionCallPreviewPost(callPreviewRequest); + getTransactionApi().transactionCallPreviewPost(callPreviewRequest); // Assert: It should succeed despite empty faucet assertEquals(TransactionStatus.SUCCEEDED, callPreviewResponse.getStatus()); diff --git a/core/src/test/java/com/radixdlt/api/core/LtsAccountDepositBehaviourTest.java b/core/src/test/java/com/radixdlt/api/core/LtsAccountDepositBehaviourTest.java index eb1ade1060..1b2ba35b99 100644 --- a/core/src/test/java/com/radixdlt/api/core/LtsAccountDepositBehaviourTest.java +++ b/core/src/test/java/com/radixdlt/api/core/LtsAccountDepositBehaviourTest.java @@ -125,7 +125,7 @@ public void account_with_default_config_allows_all_deposits() throws Exception { .allowsTryDeposit(true))); // Follow-up: deposit some actual XRD into that account - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess(test, Manifest.depositFromFaucet(accountAddress), List.of()); // Act: this time also pass some dummy badge to be checked diff --git a/core/src/test/java/com/radixdlt/api/core/LtsAccountResourceBalanceTest.java b/core/src/test/java/com/radixdlt/api/core/LtsAccountResourceBalanceTest.java index a9c13c9aae..774a0e6aa1 100644 --- a/core/src/test/java/com/radixdlt/api/core/LtsAccountResourceBalanceTest.java +++ b/core/src/test/java/com/radixdlt/api/core/LtsAccountResourceBalanceTest.java @@ -88,7 +88,7 @@ public void test_lts_account_xrd_balance() throws Exception { var accountAddress = Address.virtualAccountAddress(accountKeyPair.getPublicKey()); var accountAddressStr = addressing.encode(accountAddress); - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess(test, Manifest.depositFromFaucet(accountAddress), List.of()); final var result = diff --git a/core/src/test/java/com/radixdlt/api/core/LtsTransactionOutcomesTest.java b/core/src/test/java/com/radixdlt/api/core/LtsTransactionOutcomesTest.java index 0bae07f473..d29e9bce02 100644 --- a/core/src/test/java/com/radixdlt/api/core/LtsTransactionOutcomesTest.java +++ b/core/src/test/java/com/radixdlt/api/core/LtsTransactionOutcomesTest.java @@ -98,7 +98,7 @@ public void test_non_fungible_entity_changes() throws Exception { // Single NF mint var tx1Result = getSingleCommittedTransactionOutcome( - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess( test, Manifest.mintNonFungiblesThenWithdrawAndBurnSome( @@ -112,7 +112,7 @@ public void test_non_fungible_entity_changes() throws Exception { // Transient token is not reported var tx2Result = getSingleCommittedTransactionOutcome( - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess( test, Manifest.mintNonFungiblesThenWithdrawAndBurnSome( @@ -123,7 +123,7 @@ public void test_non_fungible_entity_changes() throws Exception { // Multiple NF mint var tx3Result = getSingleCommittedTransactionOutcome( - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess( test, Manifest.mintNonFungiblesThenWithdrawAndBurnSome( @@ -138,7 +138,7 @@ public void test_non_fungible_entity_changes() throws Exception { // Multiple NF burn var tx4Result = getSingleCommittedTransactionOutcome( - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess( test, Manifest.mintNonFungiblesThenWithdrawAndBurnSome( @@ -178,7 +178,7 @@ public void test_resultant_account_balances() throws Exception { var tx1Result = getSingleCommittedTransactionOutcome( - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess( test, Manifest.depositFromFaucet(account1Address), List.of())); assertThat(tx1Result.getResultantAccountFungibleBalances()) @@ -187,7 +187,7 @@ public void test_resultant_account_balances() throws Exception { var tx2Result = getSingleCommittedTransactionOutcome( - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess( test, Manifest.depositFromFaucet(account2Address), List.of())); assertThat(tx2Result.getResultantAccountFungibleBalances()) @@ -199,7 +199,7 @@ public void test_resultant_account_balances() throws Exception { account2ExpectedAmount += tx3Amount; var tx3Result = getSingleCommittedTransactionOutcome( - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess( test, Manifest.transferBetweenAccountsFeeFromFaucet( @@ -216,7 +216,7 @@ public void test_resultant_account_balances() throws Exception { var tx4Result = getSingleCommittedTransactionOutcome( - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess( test, Manifest.transferBetweenAccountsFeeFromFaucet( @@ -284,17 +284,17 @@ public void test_multiple_transactions_have_correct_outcomes() throws Exception var account2AddressStr = account2Address.encode(networkDefinition); var account1FaucetClaim = - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess( test, Manifest.depositFromFaucet(account1Address), List.of()); var account2FaucetClaim = - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess( test, Manifest.depositFromFaucet(account2Address), List.of()); var account1SelfXrdTransferAmount = 1L; var account1SelfXrdTransfer = - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess( test, Manifest.transferBetweenAccountsFeeFromSender( @@ -306,7 +306,7 @@ public void test_multiple_transactions_have_correct_outcomes() throws Exception var account1ToAccount2XrdTransferWithFeeFromAccount1Amount = 5; var account1ToAccount2XrdTransferWithFeeFromAccount1 = - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess( test, Manifest.transferBetweenAccountsFeeFromSender( @@ -318,7 +318,7 @@ public void test_multiple_transactions_have_correct_outcomes() throws Exception var account1ToAccount2XrdTransferWithFeeFromAccount2Amount = 31; var account1ToAccount2XrdTransferWithFeeFromAccount2 = - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess( test, Manifest.transferBetweenAccountsFeeFromReceiver( @@ -330,7 +330,7 @@ public void test_multiple_transactions_have_correct_outcomes() throws Exception var account1ToAccount2XrdTransferWithFeeFromFaucetAmount = 6; var account1ToAccount2XrdTransferWithFeeFromFaucet = - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess( test, Manifest.transferBetweenAccountsFeeFromFaucet( diff --git a/core/src/test/java/com/radixdlt/api/core/NodeMetadataIndexTest.java b/core/src/test/java/com/radixdlt/api/core/NodeMetadataIndexTest.java index 1fde70a72e..84d4af707d 100644 --- a/core/src/test/java/com/radixdlt/api/core/NodeMetadataIndexTest.java +++ b/core/src/test/java/com/radixdlt/api/core/NodeMetadataIndexTest.java @@ -85,7 +85,7 @@ public void createdVaultHasItsAccountAsRootInDatabaseIndex() throws Exception { test.suppressUnusedWarning(); // Set up an account and a vault var accountAddress = Address.virtualAccountAddress(ECKeyPair.generateNew().getPublicKey()); - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess(test, Manifest.depositFromFaucet(accountAddress), List.of()); // Discover the vault's substate node ID diff --git a/core/src/test/java/com/radixdlt/api/core/TransactionPreviewTest.java b/core/src/test/java/com/radixdlt/api/core/TransactionPreviewTest.java index e20de24e63..40eb0d6b52 100644 --- a/core/src/test/java/com/radixdlt/api/core/TransactionPreviewTest.java +++ b/core/src/test/java/com/radixdlt/api/core/TransactionPreviewTest.java @@ -109,7 +109,7 @@ public void transaction_preview_executes_at_historical_version() throws Exceptio // Execute it once, to initialize the account and learn its XRD vault address: var firstCommit = - getApiHelper().submitAndWaitForSuccess(test, manifest, List.of(accountKeyPair)); + getCoreApiHelper().submitAndWaitForSuccess(test, manifest, List.of(accountKeyPair)); var initialVaultBalance = this.getStateApi() .stateAccountPost( @@ -133,7 +133,7 @@ public void transaction_preview_executes_at_historical_version() throws Exceptio // Execute precisely the deposit that was just previewed: var secondCommit = - getApiHelper().submitAndWaitForSuccess(test, manifest, List.of(accountKeyPair)); + getCoreApiHelper().submitAndWaitForSuccess(test, manifest, List.of(accountKeyPair)); assertThat(secondCommit.stateVersion()).isGreaterThan(firstCommit.stateVersion()); // (sanity) // Sanity check - a preview now should give "3x from Faucet" amount: @@ -160,7 +160,7 @@ public void transaction_preview_returns_actual_or_synthetic_ledger_header() thro // Arrange a ledger state where at least one state version does not have ledger proof: test.runUntilState(NodesPredicate.allAtExactlyStateVersion(stateVersionRange.first())); - getApiHelper().submitAndWaitForSuccess(test, Manifest.valid(), List.of()); + getCoreApiHelper().submitAndWaitForSuccess(test, Manifest.valid(), List.of()); test.runUntilState(NodesPredicate.allAtExactlyStateVersion(stateVersionRange.last())); // Locate one example of a state version which has and one which has no ledger proof: diff --git a/core/src/test/java/com/radixdlt/api/core/regression/NonFungibleActionsTest.java b/core/src/test/java/com/radixdlt/api/core/regression/NonFungibleActionsTest.java index 8cef75273e..a63f27e527 100644 --- a/core/src/test/java/com/radixdlt/api/core/regression/NonFungibleActionsTest.java +++ b/core/src/test/java/com/radixdlt/api/core/regression/NonFungibleActionsTest.java @@ -87,14 +87,14 @@ public void test_can_mint_and_burn_in_same_transaction_against_previously_used_r // These particular manifests caused a panic in the engine at Ash / Birch // First - we create some data in the resource to ensure the data index is created - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess( test, Manifest.mintNonFungiblesThenWithdrawAndBurnSome( resourceAddress, accountAddress, List.of(1), List.of()), List.of(accountKeyPair)); // A mint+burn of a non-pristine resource currently panics - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess( test, Manifest.mintNonFungiblesThenWithdrawAndBurnSome( @@ -114,7 +114,7 @@ public void minting_non_fungible_id_which_previously_existed_transiently_in_a_tr var resourceAddress = createFreeMintBurnNonFungibleResource(test); // Mint and burn id 1 inside a single transaction - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess( test, Manifest.mintNonFungiblesThenWithdrawAndBurnSome( @@ -123,7 +123,7 @@ public void minting_non_fungible_id_which_previously_existed_transiently_in_a_tr // We can NOT mint the id "1" again var result = - getApiHelper() + getCoreApiHelper() .submitAndWaitForCommittedFailure( test, Manifest.mintNonFungiblesThenWithdrawAndBurnSome( @@ -144,7 +144,7 @@ public void minting_non_fungible_id_which_existed_persistently_and_then_got_burn var resourceAddress = createFreeMintBurnNonFungibleResource(test); // Mint id 1 - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess( test, Manifest.mintNonFungiblesThenWithdrawAndBurnSome( @@ -152,7 +152,7 @@ public void minting_non_fungible_id_which_existed_persistently_and_then_got_burn List.of(accountKeyPair)); // Burn id 1 - getApiHelper() + getCoreApiHelper() .submitAndWaitForSuccess( test, Manifest.mintNonFungiblesThenWithdrawAndBurnSome( @@ -161,7 +161,7 @@ public void minting_non_fungible_id_which_existed_persistently_and_then_got_burn // We can NOT mint the id "1" again var result = - getApiHelper() + getCoreApiHelper() .submitAndWaitForCommittedFailure( test, Manifest.mintNonFungiblesThenWithdrawAndBurnSome( diff --git a/core/src/test/java/com/radixdlt/api/core/regression/StateHistoryTest.java b/core/src/test/java/com/radixdlt/api/core/regression/StateHistoryTest.java index 1a4c518591..c579f8d1ba 100644 --- a/core/src/test/java/com/radixdlt/api/core/regression/StateHistoryTest.java +++ b/core/src/test/java/com/radixdlt/api/core/regression/StateHistoryTest.java @@ -118,7 +118,7 @@ public void state_history_supports_substate_deletes() throws Exception { .hexPayloadBytes())); // ... and then a burning transaction, so that they end up in one low-level "commit batch": final var result = - getApiHelper().submitAndWaitForSuccess(test, burnManifest, List.of(accountKeyPair)); + getCoreApiHelper().submitAndWaitForSuccess(test, burnManifest, List.of(accountKeyPair)); // Assert: we only need this to succeed (the original bug caused panics) assertThat(result.errorMessage()).isEmpty(); diff --git a/core/src/test/java/com/radixdlt/api/mesh_api/MeshApiMempoolEndpointsTest.java b/core/src/test/java/com/radixdlt/api/mesh_api/MeshApiMempoolEndpointsTest.java new file mode 100644 index 0000000000..580a1c9792 --- /dev/null +++ b/core/src/test/java/com/radixdlt/api/mesh_api/MeshApiMempoolEndpointsTest.java @@ -0,0 +1,156 @@ +/* Copyright 2021 Radix Publishing Ltd incorporated in Jersey (Channel Islands). + * + * Licensed under the Radix License, Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * + * radixfoundation.org/licenses/LICENSE-v1 + * + * The Licensor hereby grants permission for the Canonical version of the Work to be + * published, distributed and used under or by reference to the Licensor’s trademark + * Radix ® and use of any unregistered trade names, logos or get-up. + * + * The Licensor provides the Work (and each Contributor provides its Contributions) on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, + * including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, + * MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + * + * Whilst the Work is capable of being deployed, used and adopted (instantiated) to create + * a distributed ledger it is your responsibility to test and validate the code, together + * with all logic and performance of that code under all foreseeable scenarios. + * + * The Licensor does not make or purport to make and hereby excludes liability for all + * and any representation, warranty or undertaking in any form whatsoever, whether express + * or implied, to any entity or person, including any representation, warranty or + * undertaking, as to the functionality security use, value or other characteristics of + * any distributed ledger nor in respect the functioning or value of any tokens which may + * be created stored or transferred using the Work. The Licensor does not warrant that the + * Work or any use of the Work complies with any law or regulation in any territory where + * it may be implemented or used or that it will be appropriate for any specific purpose. + * + * Neither the licensor nor any current or former employees, officers, directors, partners, + * trustees, representatives, agents, advisors, contractors, or volunteers of the Licensor + * shall be liable for any direct or indirect, special, incidental, consequential or other + * losses of any kind, in tort, contract or otherwise (including but not limited to loss + * of revenue, income or profits, or loss of use or data, or loss of reputation, or loss + * of any economic or other opportunity of whatsoever nature or howsoever arising), arising + * out of or in connection with (without limitation of any use, misuse, of any ledger system + * or use made or its functionality or any performance or operation of any code or protocol + * caused by bugs or programming or logic errors or otherwise); + * + * A. any offer, purchase, holding, use, sale, exchange or transmission of any + * cryptographic keys, tokens or assets created, exchanged, stored or arising from any + * interaction with the Work; + * + * B. any failure in a transmission or loss of any token or assets keys or other digital + * artefacts due to errors in transmission; + * + * C. bugs, hacks, logic errors or faults in the Work or any communication; + * + * D. system software or apparatus including but not limited to losses caused by errors + * in holding or transmitting tokens by any third-party; + * + * E. breaches or failure of security including hacker attacks, loss or disclosure of + * password, loss of private key, unauthorised use or misuse of such passwords or keys; + * + * F. any losses including loss of anticipated savings or other benefits resulting from + * use of the Work or any changes to the Work (however implemented). + * + * You are solely responsible for; testing, validating and evaluation of all operation + * logic, functionality, security and appropriateness of using the Work for any commercial + * or non-commercial purpose and for any reproduction or redistribution by You of the + * Work. You assume all risks associated with Your use of the Work and the exercise of + * permissions under this License. + */ + +package com.radixdlt.api.mesh_api; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.radixdlt.api.DeterministicMeshApiTestBase; +import com.radixdlt.api.core.generated.models.TransactionSubmitRequest; +import com.radixdlt.api.mesh.generated.models.*; +import com.radixdlt.rev2.TransactionBuilder; +import java.util.ArrayList; +import org.junit.Test; + +public class MeshApiMempoolEndpointsTest extends DeterministicMeshApiTestBase { + + @Test + public void test_mempool_endpoint() throws Exception { + try (var test = buildRunningServerTest(defaultConfig())) { + test.suppressUnusedWarning(); + + // Arrange + var network_identifier = + new NetworkIdentifier().blockchain("radix").network(networkLogicalName); + var expected_transaction_identifiers = new ArrayList(); + + for (int i = 0; i < 2; i++) { + var transaction = TransactionBuilder.forTests().prepare(); + + expected_transaction_identifiers.add( + new TransactionIdentifier() + .hash(addressing.encode(transaction.transactionIntentHash()))); + + // Submit transaction to the CoreAPI + var response = + getCoreTransactionApi() + .transactionSubmitPost( + new TransactionSubmitRequest() + .network(networkLogicalName) + .notarizedTransactionHex(transaction.hexPayloadBytes())); + + assertThat(response.getDuplicate()).isFalse(); + } + + // Act + // Get mempool from the MeshAPI + var mempool_response = + getMempoolApi() + .mempool(new NetworkRequest().networkIdentifier(network_identifier)) + .getTransactionIdentifiers(); + + // Assert that both transactions are in the mempool list + assertThat(mempool_response).isEqualTo(expected_transaction_identifiers); + } + } + + @Test + public void test_mempool_transaction_endpoint() throws Exception { + try (var test = buildRunningServerTest(defaultConfig())) { + test.suppressUnusedWarning(); + + // Arrange + var network_identifier = + new NetworkIdentifier().blockchain("radix").network(networkLogicalName); + + var transaction = TransactionBuilder.forTests().prepare(); + var transaction_identifier = + new TransactionIdentifier().hash(addressing.encode(transaction.transactionIntentHash())); + + // Submit transaction to the CoreAPI + var response = + getCoreTransactionApi() + .transactionSubmitPost( + new TransactionSubmitRequest() + .network(networkLogicalName) + .notarizedTransactionHex(transaction.hexPayloadBytes())); + + assertThat(response.getDuplicate()).isFalse(); + + // Act + // Get mempool transaction from the MeshAPI + var mempool_transaction_response = + getMempoolApi() + .mempoolTransaction( + new MempoolTransactionRequest() + .networkIdentifier(network_identifier) + .transactionIdentifier(transaction_identifier)) + .getTransaction(); + + // Assert that transaction1 is in the mempool transaction list + assertThat(mempool_transaction_response) + .isEqualTo(new Transaction().transactionIdentifier(transaction_identifier)); + } + } +} diff --git a/core/src/test/java/com/radixdlt/rev2/protocol/AnemoneProtocolUpdateTest.java b/core/src/test/java/com/radixdlt/rev2/protocol/AnemoneProtocolUpdateTest.java index c3b6f7e6c5..dff635c0d9 100644 --- a/core/src/test/java/com/radixdlt/rev2/protocol/AnemoneProtocolUpdateTest.java +++ b/core/src/test/java/com/radixdlt/rev2/protocol/AnemoneProtocolUpdateTest.java @@ -74,6 +74,7 @@ import com.google.inject.Key; import com.google.inject.Module; import com.google.inject.TypeLiteral; +import com.radixdlt.api.CoreApiHelper; import com.radixdlt.api.core.generated.api.StreamApi; import com.radixdlt.api.core.generated.models.*; import com.radixdlt.environment.EventDispatcher; @@ -196,7 +197,7 @@ public void test_get_current_time_second_precision() { @Test public void core_api_streams_anemone_flash_transactions() throws Exception { - final var coreApiHelper = new ProtocolUpdateTestUtils.CoreApiHelper(Network.INTEGRATIONTESTNET); + final var coreApiHelper = new CoreApiHelper(Network.INTEGRATIONTESTNET); try (var test = createTest(coreApiHelper.module())) { // Start a single node network and run until protocol update: test.startAllNodes(); diff --git a/core/src/test/java/com/radixdlt/rev2/protocol/BottlenoseProtocolUpdateTest.java b/core/src/test/java/com/radixdlt/rev2/protocol/BottlenoseProtocolUpdateTest.java index 190a42f6b4..3dec5a5c82 100644 --- a/core/src/test/java/com/radixdlt/rev2/protocol/BottlenoseProtocolUpdateTest.java +++ b/core/src/test/java/com/radixdlt/rev2/protocol/BottlenoseProtocolUpdateTest.java @@ -72,6 +72,7 @@ import com.google.common.collect.ImmutableList; import com.google.inject.Module; import com.radixdlt.addressing.Addressing; +import com.radixdlt.api.CoreApiHelper; import com.radixdlt.api.core.generated.api.StreamApi; import com.radixdlt.api.core.generated.api.TransactionApi; import com.radixdlt.api.core.generated.client.ApiException; @@ -148,7 +149,7 @@ public void example_bottlenose_feature_is_available_only_after_update() throws A .type(TargetIdentifierType.FUNCTION)) .addArgumentsItem("4d0101"); // hex-encoded SBOR `true` (for `allow_recover` parameter) - final var coreApiHelper = new ProtocolUpdateTestUtils.CoreApiHelper(Network.INTEGRATIONTESTNET); + final var coreApiHelper = new CoreApiHelper(Network.INTEGRATIONTESTNET); try (var test = createTest(coreApiHelper.module())) { // Arrange: Start a single node network, reach state just before Bottlenose: test.startAllNodes(); @@ -184,7 +185,7 @@ public void example_bottlenose_feature_is_available_only_after_update() throws A @Test public void core_api_streams_bottlenose_flash_transactions() throws Exception { - final var coreApiHelper = new ProtocolUpdateTestUtils.CoreApiHelper(Network.INTEGRATIONTESTNET); + final var coreApiHelper = new CoreApiHelper(Network.INTEGRATIONTESTNET); try (var test = createTest(coreApiHelper.module())) { // Arrange: Start a single Node network and capture the state version right before Bottlenose: test.startAllNodes(); diff --git a/core/src/test/java/com/radixdlt/rev2/protocol/CuttlefishProtocolUpdateTest.java b/core/src/test/java/com/radixdlt/rev2/protocol/CuttlefishProtocolUpdateTest.java index 5522d0cf3d..75f45a11f9 100644 --- a/core/src/test/java/com/radixdlt/rev2/protocol/CuttlefishProtocolUpdateTest.java +++ b/core/src/test/java/com/radixdlt/rev2/protocol/CuttlefishProtocolUpdateTest.java @@ -69,9 +69,9 @@ import static org.junit.Assert.*; import com.google.inject.Module; +import com.radixdlt.api.CoreApiHelper; import com.radixdlt.api.core.generated.client.ApiException; import com.radixdlt.api.core.generated.models.*; -import com.radixdlt.environment.deterministic.network.MessageMutator; import com.radixdlt.genesis.GenesisBuilder; import com.radixdlt.genesis.GenesisConsensusManagerConfig; import com.radixdlt.harness.deterministic.DeterministicTest; @@ -103,14 +103,13 @@ private DeterministicTest createTest(ProtocolConfig protocolConfig, Module... ex DeterministicTest.builder() .addPhysicalNodes(PhysicalNodeConfig.createBatch(1, true)) .messageSelector(firstSelector()) - .messageMutator(MessageMutator.dropTimeouts()) .addModules(extraModules) .functionalNodeModule( new FunctionalRadixNodeModule( FunctionalRadixNodeModule.NodeStorageConfig.tempFolder(folder), true, FunctionalRadixNodeModule.SafetyRecoveryConfig.REAL, - FunctionalRadixNodeModule.ConsensusConfig.of(1000), + FunctionalRadixNodeModule.ConsensusConfig.testDefault(), FunctionalRadixNodeModule.LedgerConfig.stateComputerNoSync( StateComputerConfig.rev2() .withGenesis(genesis) @@ -121,7 +120,7 @@ private DeterministicTest createTest(ProtocolConfig protocolConfig, Module... ex @Test public void transaction_v2_behaviour_across_cuttlefish() throws ApiException { - final var coreApiHelper = new ProtocolUpdateTestUtils.CoreApiHelper(Network.INTEGRATIONTESTNET); + final var coreApiHelper = new CoreApiHelper(Network.INTEGRATIONTESTNET); try (var test = createTest(CUTTLEFISH_AT_EPOCH, coreApiHelper.module())) { final var stateComputer = test.getInstance(0, RustStateComputer.class); test.runUntilState(allAtOrOverEpoch(ENACTMENT_EPOCH - 1)); @@ -187,7 +186,7 @@ public void transaction_v2_behaviour_across_cuttlefish() throws ApiException { @Test public void protocol_update_process_updates_status_summary() throws ApiException { - final var coreApiHelper = new ProtocolUpdateTestUtils.CoreApiHelper(Network.INTEGRATIONTESTNET); + final var coreApiHelper = new CoreApiHelper(Network.INTEGRATIONTESTNET); try (var ignored = createTest(IMMEDIATELY_CUTTLEFISH, coreApiHelper.module())) { var latestStateVersion = coreApiHelper.getNetworkStatus().getCurrentStateIdentifier().getStateVersion(); diff --git a/core/src/test/java/com/radixdlt/rev2/protocol/CuttlefishSubintentTests.java b/core/src/test/java/com/radixdlt/rev2/protocol/CuttlefishSubintentTests.java new file mode 100644 index 0000000000..6994ee4ddd --- /dev/null +++ b/core/src/test/java/com/radixdlt/rev2/protocol/CuttlefishSubintentTests.java @@ -0,0 +1,132 @@ +/* Copyright 2021 Radix Publishing Ltd incorporated in Jersey (Channel Islands). + * + * Licensed under the Radix License, Version 1.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * + * radixfoundation.org/licenses/LICENSE-v1 + * + * The Licensor hereby grants permission for the Canonical version of the Work to be + * published, distributed and used under or by reference to the Licensor’s trademark + * Radix ® and use of any unregistered trade names, logos or get-up. + * + * The Licensor provides the Work (and each Contributor provides its Contributions) on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, + * including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, + * MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + * + * Whilst the Work is capable of being deployed, used and adopted (instantiated) to create + * a distributed ledger it is your responsibility to test and validate the code, together + * with all logic and performance of that code under all foreseeable scenarios. + * + * The Licensor does not make or purport to make and hereby excludes liability for all + * and any representation, warranty or undertaking in any form whatsoever, whether express + * or implied, to any entity or person, including any representation, warranty or + * undertaking, as to the functionality security use, value or other characteristics of + * any distributed ledger nor in respect the functioning or value of any tokens which may + * be created stored or transferred using the Work. The Licensor does not warrant that the + * Work or any use of the Work complies with any law or regulation in any territory where + * it may be implemented or used or that it will be appropriate for any specific purpose. + * + * Neither the licensor nor any current or former employees, officers, directors, partners, + * trustees, representatives, agents, advisors, contractors, or volunteers of the Licensor + * shall be liable for any direct or indirect, special, incidental, consequential or other + * losses of any kind, in tort, contract or otherwise (including but not limited to loss + * of revenue, income or profits, or loss of use or data, or loss of reputation, or loss + * of any economic or other opportunity of whatsoever nature or howsoever arising), arising + * out of or in connection with (without limitation of any use, misuse, of any ledger system + * or use made or its functionality or any performance or operation of any code or protocol + * caused by bugs or programming or logic errors or otherwise); + * + * A. any offer, purchase, holding, use, sale, exchange or transmission of any + * cryptographic keys, tokens or assets created, exchanged, stored or arising from any + * interaction with the Work; + * + * B. any failure in a transmission or loss of any token or assets keys or other digital + * artefacts due to errors in transmission; + * + * C. bugs, hacks, logic errors or faults in the Work or any communication; + * + * D. system software or apparatus including but not limited to losses caused by errors + * in holding or transmitting tokens by any third-party; + * + * E. breaches or failure of security including hacker attacks, loss or disclosure of + * password, loss of private key, unauthorised use or misuse of such passwords or keys; + * + * F. any losses including loss of anticipated savings or other benefits resulting from + * use of the Work or any changes to the Work (however implemented). + * + * You are solely responsible for; testing, validating and evaluation of all operation + * logic, functionality, security and appropriateness of using the Work for any commercial + * or non-commercial purpose and for any reproduction or redistribution by You of the + * Work. You assume all risks associated with Your use of the Work and the exercise of + * permissions under this License. + */ + +package com.radixdlt.rev2.protocol; + +import static org.junit.Assert.assertEquals; + +import com.radixdlt.api.CoreApiHelper; +import com.radixdlt.harness.deterministic.DeterministicTest; +import com.radixdlt.networks.Network; +import com.radixdlt.rev2.TransactionV2Builder; +import com.radixdlt.transactions.PreparedNotarizedTransaction; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class CuttlefishSubintentTests { + @Rule public TemporaryFolder folder = new TemporaryFolder(); + + private DeterministicTest createTest(CoreApiHelper coreApiHelper) { + var test = DeterministicTest.rev2DefaultWithCoreApi(1, 100, folder, coreApiHelper); + test.startAllNodes(); + return test; + } + + @Test + public void v2_submit_subintents_in_multiple_transactions() throws Exception { + final var coreApiHelper = new CoreApiHelper(Network.INTEGRATIONTESTNET); + try (var test = createTest(coreApiHelper)) { + // Try one by one - both transactions contain subintent 5 + var transactionAOne = + TransactionV2Builder.forTests().subintentDiscriminators(List.of(1000, 5)).prepare(); + coreApiHelper.submitAndWaitForSuccess(test, transactionAOne); + var transactionATwo = + TransactionV2Builder.forTests().subintentDiscriminators(List.of(5)).prepare(); + var rejection = coreApiHelper.submitExpectingRejection(transactionATwo); + assertEquals(rejection.getIsIntentRejectionPermanent(), true); + + // Try two at the same time - both transactions contain subintent 8 + var transactionBOne = + TransactionV2Builder.forTests().subintentDiscriminators(List.of(8)).prepare(); + coreApiHelper.submit(transactionBOne); + var transactionBTwo = + TransactionV2Builder.forTests().subintentDiscriminators(List.of(8)).prepare(); + coreApiHelper.submit(transactionBTwo); + + var firstResult = + coreApiHelper.waitForFirstResult( + test, + transactionBOne.transactionIntentHash(), + transactionBTwo.transactionIntentHash()); + firstResult.assertCommittedSuccess(); + PreparedNotarizedTransaction otherTransaction; + if (firstResult.transactionIntentHash().equals(transactionBOne.transactionIntentHash())) { + otherTransaction = transactionBTwo; + } else { + otherTransaction = transactionBOne; + } + // TODO:CUTTLEFISH + // >> re-enable these lines when we fix immediate rejection of intents with matching subintent + // hash + // var statusB = coreApiHelper.ltsTransactionStatus(otherTransaction); + // assertEquals(statusB.getIntentStatus(), LtsTransactionIntentStatus.PERMANENTREJECTION); + // var rejectionB = coreApiHelper.submitExpectingRejection(otherTransaction); + // assertEquals(rejectionB.getIsIntentRejectionPermanent(), true); + var rejectionB = coreApiHelper.forceRecalculateSubmitExpectingRejection(otherTransaction); + assertEquals(rejectionB.getIsIntentRejectionPermanent(), true); + } + } +} diff --git a/core/src/test/java/com/radixdlt/rev2/protocol/ProtocolUpdateTestUtils.java b/core/src/test/java/com/radixdlt/rev2/protocol/ProtocolUpdateTestUtils.java index 1f8d191ef6..08114b16cf 100644 --- a/core/src/test/java/com/radixdlt/rev2/protocol/ProtocolUpdateTestUtils.java +++ b/core/src/test/java/com/radixdlt/rev2/protocol/ProtocolUpdateTestUtils.java @@ -70,29 +70,15 @@ import static org.junit.Assert.fail; import com.google.common.collect.Streams; -import com.google.inject.AbstractModule; import com.google.inject.Key; -import com.google.inject.Module; import com.google.inject.TypeLiteral; -import com.google.inject.multibindings.ProvidesIntoSet; -import com.radixdlt.addressing.Addressing; -import com.radixdlt.api.CoreApiServer; -import com.radixdlt.api.CoreApiServerModule; -import com.radixdlt.api.core.generated.api.StatusApi; -import com.radixdlt.api.core.generated.api.StreamApi; -import com.radixdlt.api.core.generated.api.TransactionApi; -import com.radixdlt.api.core.generated.client.ApiClient; -import com.radixdlt.api.core.generated.client.ApiException; import com.radixdlt.api.core.generated.models.*; import com.radixdlt.consensus.BFTConfiguration; -import com.radixdlt.environment.CoreApiServerFlags; import com.radixdlt.environment.EventDispatcher; -import com.radixdlt.environment.StartProcessorOnRunner; import com.radixdlt.harness.deterministic.DeterministicTest; import com.radixdlt.identifiers.Address; import com.radixdlt.lang.Option; import com.radixdlt.mempool.MempoolAdd; -import com.radixdlt.networks.Network; import com.radixdlt.rev2.Decimal; import com.radixdlt.rev2.Manifest; import com.radixdlt.rev2.TransactionBuilder; @@ -100,8 +86,6 @@ import com.radixdlt.statecomputer.commit.NextEpoch; import com.radixdlt.sync.TransactionsAndProofReader; import com.radixdlt.transaction.REv2TransactionAndProofStore; -import com.radixdlt.transactions.PreparedNotarizedTransaction; -import com.radixdlt.utils.FreePortFinder; import com.radixdlt.utils.PrivateKeys; import java.util.List; import java.util.Map; @@ -253,95 +237,4 @@ public static void verifyFlashTransactionReceipts( assertEquals(fromFlash.getDeletedSubstates(), deletedFromReceipt); }); } - - public static class CoreApiHelper { - - private final int coreApiPort; - private final Network network; - private final Addressing addressing; - - public CoreApiHelper(Network network) { - this.coreApiPort = FreePortFinder.findFreeLocalPort(); - this.addressing = Addressing.ofNetwork(network); - this.network = network; - } - - public Module module() { - return new AbstractModule() { - @Override - protected void configure() { - install(new CoreApiServerModule("127.0.0.1", coreApiPort, new CoreApiServerFlags(true))); - } - - @ProvidesIntoSet - private StartProcessorOnRunner startCoreApi(CoreApiServer coreApiServer) { - // This is a slightly hacky way to run something on node start-up in a Deterministic test. - // Stop is called by the AutoClosable binding in CoreApiServerModule - return new StartProcessorOnRunner("coreApi", coreApiServer::start); - } - }; - } - - public ApiClient client() { - final var apiClient = new ApiClient(); - apiClient.updateBaseUri("http://127.0.0.1:" + coreApiPort + "/core"); - return apiClient; - } - - public TransactionApi transactionApi() { - return new TransactionApi(client()); - } - - public StreamApi streamApi() { - return new StreamApi(client()); - } - - public StatusApi statusApi() { - return new StatusApi(client()); - } - - public TransactionSubmitResponse submit(PreparedNotarizedTransaction transaction) - throws ApiException { - return transactionApi() - .transactionSubmitPost( - new TransactionSubmitRequest() - .network(network.getLogicalName()) - .notarizedTransactionHex(transaction.hexPayloadBytes())); - } - - public TransactionSubmitResponse forceRecalculateSubmit( - PreparedNotarizedTransaction transaction) throws ApiException { - return transactionApi() - .transactionSubmitPost( - new TransactionSubmitRequest() - .network(network.getLogicalName()) - .forceRecalculate(true) - .notarizedTransactionHex(transaction.hexPayloadBytes())); - } - - public TransactionStatusResponse getStatus(PreparedNotarizedTransaction transaction) - throws ApiException { - return transactionApi() - .transactionStatusPost( - new TransactionStatusRequest() - .network(network.getLogicalName()) - .intentHash(addressing.encode(transaction.transactionIntentHash()))); - } - - public NetworkStatusResponse getNetworkStatus() throws ApiException { - return statusApi() - .statusNetworkStatusPost(new NetworkStatusRequest().network(network.getLogicalName())); - } - - public CommittedTransaction getTransactionFromStream(long stateVersion) throws ApiException { - return streamApi() - .streamTransactionsPost( - new StreamTransactionsRequest() - .network(network.getLogicalName()) - .fromStateVersion(stateVersion) - .limit(1)) - .getTransactions() - .get(0); - } - } }