diff --git a/rpc/README.md b/rpc/README.md index 6625be5773..ecd547a817 100644 --- a/rpc/README.md +++ b/rpc/README.md @@ -104,6 +104,7 @@ The crate `ckb-rpc`'s minimum supported rustc version is 1.71.1. * [Module Pool](#module-pool) [👉 OpenRPC spec](http://playground.open-rpc.org/?uiSchema[appBar][ui:title]=CKB-Pool&uiSchema[appBar][ui:splitView]=false&uiSchema[appBar][ui:examplesDropdown]=false&uiSchema[appBar][ui:logoUrl]=https://raw.githubusercontent.com/nervosnetwork/ckb-rpc-resources/develop/ckb-logo.jpg&schemaUrl=https://raw.githubusercontent.com/nervosnetwork/ckb-rpc-resources/develop/json/pool_rpc_doc.json) * [Method `send_transaction`](#pool-send_transaction) + * [Method `test_tx_pool_accept`](#pool-test_tx_pool_accept) * [Method `remove_transaction`](#pool-remove_transaction) * [Method `tx_pool_info`](#pool-tx_pool_info) * [Method `clear_tx_pool`](#pool-clear_tx_pool) @@ -162,6 +163,7 @@ The crate `ckb-rpc`'s minimum supported rustc version is 1.71.1. * [Type `DeploymentInfo`](#type-deploymentinfo) * [Type `DeploymentState`](#type-deploymentstate) * [Type `DeploymentsInfo`](#type-deploymentsinfo) + * [Type `EntryCompleted`](#type-entrycompleted) * [Type `EpochNumber`](#type-epochnumber) * [Type `EpochNumber`](#type-epochnumber) * [Type `EpochNumberWithFraction`](#type-epochnumberwithfraction) @@ -4420,6 +4422,114 @@ Response } ``` + +#### Method `test_tx_pool_accept` +* `test_tx_pool_accept(tx, outputs_validator)` + * `tx`: [`Transaction`](#type-transaction) + * `outputs_validator`: [`OutputsValidator`](#type-outputsvalidator) `|` `null` +* result: [`EntryCompleted`](#type-entrycompleted) + +Test if a transaction can be accepted by the transaction pool without inserting it into the pool or rebroadcasting it to peers. +The parameters and errors of this method are the same as `send_transaction`. + +###### Params + +* `transaction` - The transaction. +* `outputs_validator` - Validates the transaction outputs before entering the tx-pool. (**Optional**, default is "passthrough"). + +###### Errors + +* [`PoolRejectedTransactionByOutputsValidator (-1102)`](../enum.RPCError.html#variant.PoolRejectedTransactionByOutputsValidator) - The transaction is rejected by the validator specified by `outputs_validator`. If you really want to send transactions with advanced scripts, please set `outputs_validator` to "passthrough". +* [`PoolRejectedTransactionByMinFeeRate (-1104)`](../enum.RPCError.html#variant.PoolRejectedTransactionByMinFeeRate) - The transaction fee rate must be greater than or equal to the config option `tx_pool.min_fee_rate`. +* [`PoolRejectedTransactionByMaxAncestorsCountLimit (-1105)`](../enum.RPCError.html#variant.PoolRejectedTransactionByMaxAncestorsCountLimit) - The ancestors count must be greater than or equal to the config option `tx_pool.max_ancestors_count`. +* [`PoolIsFull (-1106)`](../enum.RPCError.html#variant.PoolIsFull) - Pool is full. +* [`PoolRejectedDuplicatedTransaction (-1107)`](../enum.RPCError.html#variant.PoolRejectedDuplicatedTransaction) - The transaction is already in the pool. +* [`TransactionFailedToResolve (-301)`](../enum.RPCError.html#variant.TransactionFailedToResolve) - Failed to resolve the referenced cells and headers used in the transaction, as inputs or dependencies. +* [`TransactionFailedToVerify (-302)`](../enum.RPCError.html#variant.TransactionFailedToVerify) - Failed to verify the transaction. + +###### Examples + +Request + +```json +{ + "id": 42, + "jsonrpc": "2.0", + "method": "test_tx_pool_accept", + "params": [ + { + "cell_deps": [ + { + "dep_type": "code", + "out_point": { + "index": "0x0", + "tx_hash": "0xa4037a893eb48e18ed4ef61034ce26eba9c585f15c9cee102ae58505565eccc3" + } + } + ], + "header_deps": [ + "0x7978ec7ce5b507cfb52e149e36b1a23f6062ed150503c85bbf825da3599095ed" + ], + "inputs": [ + { + "previous_output": { + "index": "0x0", + "tx_hash": "0x075fe030c1f4725713c5aacf41c2f59b29b284008fdb786e5efd8a058be51d0c" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x2431ac129", + "lock": { + "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + "hash_type": "data", + "args": "0x" + }, + "type": null + } + ], + "outputs_data": [ + "0x" + ], + "version": "0x0", + "witnesses": [] + }, + "passthrough" + ] +} +``` + +Response + +```json +{ + "id": 42, + "jsonrpc": "2.0", + "result": { + "cycles": "0x219", + "fee": "0x2a66f36e90" + } +} +``` + + +The response looks like below if the transaction pool check fails + +```text +{ + "id": 42, + "jsonrpc": "2.0", + "result": null, + "error": { + "code": -1107, + "data": "Duplicated(Byte32(0xa0ef4eb5f4ceeb08a4c8524d84c5da95dce2f608e0ca2ec8091191b0f330c6e3))", + "message": "PoolRejectedDuplicatedTransaction: Transaction(Byte32(0xa0ef4eb5f4ceeb08a4c8524d84c5da95dce2f608e0ca2ec8091191b0f330c6e3)) already exists in transaction_pool" + } +} +``` + #### Method `remove_transaction` * `remove_transaction(tx_hash)` @@ -5739,6 +5849,17 @@ Chain information. * `hash`: [`H256`](#type-h256) - requested block hash +### Type `EntryCompleted` +Transaction's verify result by test_tx_pool_accept + +#### Fields + +`EntryCompleted` is a JSON object with the following fields. + +* `cycles`: [`Uint64`](#type-uint64) - Cached tx cycles + +* `fee`: [`Uint64`](#type-uint64) - Cached tx fee + ### Type `EpochNumber` Consecutive epoch number starting from 0. diff --git a/rpc/src/module/pool.rs b/rpc/src/module/pool.rs index dc081ae5fc..aab1071e0d 100644 --- a/rpc/src/module/pool.rs +++ b/rpc/src/module/pool.rs @@ -3,10 +3,11 @@ use async_trait::async_trait; use ckb_chain_spec::consensus::Consensus; use ckb_constant::hardfork::{mainnet, testnet}; use ckb_jsonrpc_types::{ - OutputsValidator, PoolTxDetailInfo, RawTxPool, Script, Transaction, TxPoolInfo, + EntryCompleted, OutputsValidator, PoolTxDetailInfo, RawTxPool, Script, Transaction, TxPoolInfo, }; use ckb_logger::error; use ckb_shared::shared::Shared; +use ckb_types::core::TransactionView; use ckb_types::{core, packed, prelude::*, H256}; use ckb_verification::{Since, SinceMetric}; use jsonrpc_core::Result; @@ -109,6 +110,113 @@ pub trait PoolRpc { outputs_validator: Option, ) -> Result; + /// Test if a transaction can be accepted by the transaction pool without inserting it into the pool or rebroadcasting it to peers. + /// The parameters and errors of this method are the same as `send_transaction`. + /// + /// ## Params + /// + /// * `transaction` - The transaction. + /// * `outputs_validator` - Validates the transaction outputs before entering the tx-pool. (**Optional**, default is "passthrough"). + /// + /// ## Errors + /// + /// * [`PoolRejectedTransactionByOutputsValidator (-1102)`](../enum.RPCError.html#variant.PoolRejectedTransactionByOutputsValidator) - The transaction is rejected by the validator specified by `outputs_validator`. If you really want to send transactions with advanced scripts, please set `outputs_validator` to "passthrough". + /// * [`PoolRejectedTransactionByMinFeeRate (-1104)`](../enum.RPCError.html#variant.PoolRejectedTransactionByMinFeeRate) - The transaction fee rate must be greater than or equal to the config option `tx_pool.min_fee_rate`. + /// * [`PoolRejectedTransactionByMaxAncestorsCountLimit (-1105)`](../enum.RPCError.html#variant.PoolRejectedTransactionByMaxAncestorsCountLimit) - The ancestors count must be greater than or equal to the config option `tx_pool.max_ancestors_count`. + /// * [`PoolIsFull (-1106)`](../enum.RPCError.html#variant.PoolIsFull) - Pool is full. + /// * [`PoolRejectedDuplicatedTransaction (-1107)`](../enum.RPCError.html#variant.PoolRejectedDuplicatedTransaction) - The transaction is already in the pool. + /// * [`TransactionFailedToResolve (-301)`](../enum.RPCError.html#variant.TransactionFailedToResolve) - Failed to resolve the referenced cells and headers used in the transaction, as inputs or dependencies. + /// * [`TransactionFailedToVerify (-302)`](../enum.RPCError.html#variant.TransactionFailedToVerify) - Failed to verify the transaction. + /// + /// ## Examples + /// + /// Request + /// + /// ```json + /// { + /// "id": 42, + /// "jsonrpc": "2.0", + /// "method": "test_tx_pool_accept", + /// "params": [ + /// { + /// "cell_deps": [ + /// { + /// "dep_type": "code", + /// "out_point": { + /// "index": "0x0", + /// "tx_hash": "0xa4037a893eb48e18ed4ef61034ce26eba9c585f15c9cee102ae58505565eccc3" + /// } + /// } + /// ], + /// "header_deps": [ + /// "0x7978ec7ce5b507cfb52e149e36b1a23f6062ed150503c85bbf825da3599095ed" + /// ], + /// "inputs": [ + /// { + /// "previous_output": { + /// "index": "0x0", + /// "tx_hash": "0x075fe030c1f4725713c5aacf41c2f59b29b284008fdb786e5efd8a058be51d0c" + /// }, + /// "since": "0x0" + /// } + /// ], + /// "outputs": [ + /// { + /// "capacity": "0x2431ac129", + /// "lock": { + /// "code_hash": "0x28e83a1277d48add8e72fadaa9248559e1b632bab2bd60b27955ebc4c03800a5", + /// "hash_type": "data", + /// "args": "0x" + /// }, + /// "type": null + /// } + /// ], + /// "outputs_data": [ + /// "0x" + /// ], + /// "version": "0x0", + /// "witnesses": [] + /// }, + /// "passthrough" + /// ] + /// } + /// ``` + /// + /// Response + /// + /// ```json + /// { + /// "id": 42, + /// "jsonrpc": "2.0", + /// "result": { + /// "cycles": "0x219", + /// "fee": "0x2a66f36e90" + /// } + /// } + /// ``` + /// + /// + /// The response looks like below if the transaction pool check fails + /// + /// ```text + /// { + /// "id": 42, + /// "jsonrpc": "2.0", + /// "result": null, + /// "error": { + /// "code": -1107, + /// "data": "Duplicated(Byte32(0xa0ef4eb5f4ceeb08a4c8524d84c5da95dce2f608e0ca2ec8091191b0f330c6e3))", + /// "message": "PoolRejectedDuplicatedTransaction: Transaction(Byte32(0xa0ef4eb5f4ceeb08a4c8524d84c5da95dce2f608e0ca2ec8091191b0f330c6e3)) already exists in transaction_pool" + /// } + /// } + /// ``` + #[rpc(name = "test_tx_pool_accept")] + fn test_tx_pool_accept( + &self, + tx: Transaction, + outputs_validator: Option, + ) -> Result; + /// Removes a transaction and all transactions which depends on it from tx pool if it exists. /// /// ## Params @@ -359,6 +467,33 @@ impl PoolRpcImpl { well_known_type_scripts, } } + + fn check_output_validator( + &self, + outputs_validator: Option, + tx: &TransactionView, + ) -> Result<()> { + if let Err(e) = match outputs_validator { + None | Some(OutputsValidator::Passthrough) => Ok(()), + Some(OutputsValidator::WellKnownScriptsOnly) => WellKnownScriptsOnlyValidator::new( + self.shared.consensus(), + &self.well_known_lock_scripts, + &self.well_known_type_scripts, + ) + .validate(tx), + } { + return Err(RPCError::custom_with_data( + RPCError::PoolRejectedTransactionByOutputsValidator, + format!( + "The transaction is rejected by OutputsValidator set in params[1]: {}. \ + Please check the related information in https://github.com/nervosnetwork/ckb/wiki/Transaction-%C2%BB-Default-Outputs-Validator", + outputs_validator.unwrap_or(OutputsValidator::WellKnownScriptsOnly).json_display() + ), + e, + )); + } + Ok(()) + } } /// Build well known lock scripts @@ -452,25 +587,7 @@ impl PoolRpc for PoolRpcImpl { let tx: packed::Transaction = tx.into(); let tx: core::TransactionView = tx.into_view(); - if let Err(e) = match outputs_validator { - None | Some(OutputsValidator::Passthrough) => Ok(()), - Some(OutputsValidator::WellKnownScriptsOnly) => WellKnownScriptsOnlyValidator::new( - self.shared.consensus(), - &self.well_known_lock_scripts, - &self.well_known_type_scripts, - ) - .validate(&tx), - } { - return Err(RPCError::custom_with_data( - RPCError::PoolRejectedTransactionByOutputsValidator, - format!( - "The transaction is rejected by OutputsValidator set in params[1]: {}. \ - Please check the related information in https://github.com/nervosnetwork/ckb/wiki/Transaction-%C2%BB-Default-Outputs-Validator", - outputs_validator.unwrap_or(OutputsValidator::WellKnownScriptsOnly).json_display() - ), - e, - )); - } + self.check_output_validator(outputs_validator, &tx)?; let tx_pool = self.shared.tx_pool_controller(); let submit_tx = tx_pool.submit_local_tx(tx.clone()); @@ -487,6 +604,31 @@ impl PoolRpc for PoolRpcImpl { } } + fn test_tx_pool_accept( + &self, + tx: Transaction, + outputs_validator: Option, + ) -> Result { + let tx: packed::Transaction = tx.into(); + let tx: core::TransactionView = tx.into_view(); + + self.check_output_validator(outputs_validator, &tx)?; + + let tx_pool = self.shared.tx_pool_controller(); + + let test_accept_tx_reslt = tx_pool.test_accept_tx(tx).map_err(|e| { + error!("Send test_tx_pool_accept_tx request error {}", e); + RPCError::ckb_internal_error(e) + })?; + + test_accept_tx_reslt + .map(|test_accept_result| test_accept_result.into()) + .map_err(|reject| { + error!("Send test_tx_pool_accept_tx request error {}", reject); + RPCError::from_submit_transaction_reject(&reject) + }) + } + fn remove_transaction(&self, tx_hash: H256) -> Result { let tx_pool = self.shared.tx_pool_controller(); diff --git a/tx-pool/src/process.rs b/tx-pool/src/process.rs index ee432eda5e..39627ccf9a 100644 --- a/tx-pool/src/process.rs +++ b/tx-pool/src/process.rs @@ -339,6 +339,21 @@ impl TxPoolService { } } + pub(crate) async fn test_accept_tx(&self, tx: TransactionView) -> Result { + // non contextual verify first + self.non_contextual_verify(&tx, None)?; + + if self.chunk_contains(&tx).await { + return Err(Reject::Duplicated(tx.hash())); + } + + if self.orphan_contains(&tx).await { + debug!("reject tx {} already in orphan pool", tx.hash()); + return Err(Reject::Duplicated(tx.hash())); + } + self._test_accept_tx(tx.clone()).await + } + pub(crate) async fn process_tx( &self, tx: TransactionView, @@ -857,6 +872,29 @@ impl TxPoolService { Some((Ok(verified), submit_snapshot)) } + pub(crate) async fn _test_accept_tx(&self, tx: TransactionView) -> Result { + let tx_hash = tx.hash(); + + let (pre_check_ret, snapshot) = self.pre_check(&tx).await; + + let (_tip_hash, rtx, status, _fee, _tx_size) = pre_check_ret?; + + // skip check the delay window + + let verify_cache = self.fetch_tx_verify_cache(&tx_hash).await; + let max_cycles = self.consensus.max_block_cycles(); + let tip_header = snapshot.tip_header(); + let tx_env = Arc::new(status.with_env(tip_header)); + + verify_rtx( + Arc::clone(&snapshot), + Arc::clone(&rtx), + tx_env, + &verify_cache, + max_cycles, + ) + } + pub(crate) async fn update_tx_pool_for_reorg( &self, detached_blocks: VecDeque, diff --git a/tx-pool/src/service.rs b/tx-pool/src/service.rs index 8ad6300a5b..64f17f4aa9 100644 --- a/tx-pool/src/service.rs +++ b/tx-pool/src/service.rs @@ -18,7 +18,7 @@ use ckb_logger::{error, info}; use ckb_network::{NetworkController, PeerIndex}; use ckb_snapshot::Snapshot; use ckb_stop_handler::new_tokio_exit_rx; -use ckb_types::core::tx_pool::{PoolTxDetailInfo, TransactionWithStatus, TxStatus}; +use ckb_types::core::tx_pool::{EntryCompleted, PoolTxDetailInfo, TransactionWithStatus, TxStatus}; use ckb_types::{ core::{ tx_pool::{Reject, TxPoolEntryInfo, TxPoolIds, TxPoolInfo, TRANSACTION_SIZE_LIMIT}, @@ -74,6 +74,8 @@ type BlockTemplateArgs = (Option, Option, Option); pub(crate) type SubmitTxResult = Result<(), Reject>; +pub(crate) type TestAcceptTxResult = Result; + type GetTxStatusResult = Result<(TxStatus, Option), AnyError>; type GetTransactionWithStatusResult = Result; type FetchTxsWithCyclesResult = Vec<(ProposalShortId, (TransactionView, Cycle))>; @@ -89,6 +91,7 @@ pub(crate) enum Message { BlockTemplate(Request), SubmitLocalTx(Request), RemoveLocalTx(Request), + TestAcceptTx(Request), SubmitRemoteTx(Request<(TransactionView, Cycle, PeerIndex), ()>), NotifyTxs(Notify>), FreshProposalsFilter(Request, Vec>), @@ -225,6 +228,13 @@ impl TxPoolController { send_message!(self, SubmitLocalTx, tx) } + /// test if a tx can be accepted by tx-pool + /// Won't be broadcasted to network + /// won't be insert to tx-pool + pub fn test_accept_tx(&self, tx: TransactionView) -> Result { + send_message!(self, TestAcceptTx, tx) + } + /// Remove tx from tx-pool pub fn remove_local_tx(&self, tx_hash: Byte32) -> Result { send_message!(self, RemoveLocalTx, tx_hash) @@ -696,6 +706,15 @@ async fn process(mut service: TxPoolService, message: Message) { error!("Responder sending remove_tx result failed {:?}", e); }; } + Message::TestAcceptTx(Request { + responder, + arguments: tx, + }) => { + let result = service.test_accept_tx(tx).await; + if let Err(e) = responder.send(result.map(|r| r.into())) { + error!("Responder sending test_accept_tx result failed {:?}", e); + }; + } Message::SubmitRemoteTx(Request { responder, arguments: (tx, declared_cycles, peer), diff --git a/util/jsonrpc-types/src/lib.rs b/util/jsonrpc-types/src/lib.rs index a183318d33..ac70de3ec3 100644 --- a/util/jsonrpc-types/src/lib.rs +++ b/util/jsonrpc-types/src/lib.rs @@ -45,8 +45,9 @@ pub use self::net::{ RemoteNodeProtocol, SyncState, }; pub use self::pool::{ - AncestorsScoreSortKey, OutputsValidator, PoolTransactionEntry, PoolTransactionReject, - PoolTxDetailInfo, RawTxPool, TxPoolEntries, TxPoolEntry, TxPoolIds, TxPoolInfo, + AncestorsScoreSortKey, EntryCompleted, OutputsValidator, PoolTransactionEntry, + PoolTransactionReject, PoolTxDetailInfo, RawTxPool, TxPoolEntries, TxPoolEntry, TxPoolIds, + TxPoolInfo, }; pub use self::proposal_short_id::ProposalShortId; pub use self::subscription::Topic; diff --git a/util/jsonrpc-types/src/pool.rs b/util/jsonrpc-types/src/pool.rs index 5fb26901f4..18f2052dd1 100644 --- a/util/jsonrpc-types/src/pool.rs +++ b/util/jsonrpc-types/src/pool.rs @@ -350,3 +350,21 @@ impl From for PoolTransactionReject { } } } + +/// Transaction's verify result by test_tx_pool_accept +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Debug, JsonSchema)] +pub struct EntryCompleted { + /// Cached tx cycles + pub cycles: Cycle, + /// Cached tx fee + pub fee: Capacity, +} + +impl From for EntryCompleted { + fn from(value: ckb_types::core::EntryCompleted) -> Self { + Self { + cycles: value.cycles.into(), + fee: value.fee.into(), + } + } +} diff --git a/util/types/src/core/mod.rs b/util/types/src/core/mod.rs index ae8602a577..5b0cbdb5b3 100644 --- a/util/types/src/core/mod.rs +++ b/util/types/src/core/mod.rs @@ -34,7 +34,7 @@ pub use extras::{BlockExt, EpochExt, EpochNumberWithFraction, TransactionInfo}; pub use fee_rate::FeeRate; pub use reward::{BlockEconomicState, BlockIssuance, BlockReward, MinerReward}; pub use transaction_meta::{TransactionMeta, TransactionMetaBuilder}; -pub use tx_pool::TransactionWithStatus; +pub use tx_pool::{EntryCompleted, TransactionWithStatus}; pub use views::{ BlockView, ExtraHashView, HeaderView, TransactionView, UncleBlockVecView, UncleBlockView, }; diff --git a/util/types/src/core/tx_pool.rs b/util/types/src/core/tx_pool.rs index 0a6f3187e8..1ca23a061f 100644 --- a/util/types/src/core/tx_pool.rs +++ b/util/types/src/core/tx_pool.rs @@ -395,3 +395,12 @@ impl PoolTxDetailInfo { } } } + +/// A Tx CacheEntry +#[derive(Clone, PartialEq, Eq, Debug, Default)] +pub struct EntryCompleted { + /// Cached tx cycles + pub cycles: Cycle, + /// Cached tx fee + pub fee: Capacity, +} diff --git a/verification/src/cache.rs b/verification/src/cache.rs index b7ceab66ab..4165f4d1db 100644 --- a/verification/src/cache.rs +++ b/verification/src/cache.rs @@ -2,7 +2,7 @@ use ckb_script::TransactionSnapshot; use ckb_types::{ - core::{Capacity, Cycle}, + core::{Capacity, Cycle, EntryCompleted}, packed::Byte32, }; use std::sync::Arc; @@ -44,6 +44,15 @@ pub struct Completed { pub fee: Capacity, } +impl From for EntryCompleted { + fn from(value: Completed) -> Self { + EntryCompleted { + cycles: value.cycles, + fee: value.fee, + } + } +} + impl CacheEntry { /// Constructs a completed CacheEntry pub fn completed(cycles: Cycle, fee: Capacity) -> Self {