Skip to content

Commit

Permalink
jsonrpc: add finality to tx requests (tx, EXPERIMENTAL_tx_status)…
Browse files Browse the repository at this point in the history
…, add method `broadcast_tx` (#9644)

Fixes #6837

I'll need to update the [docs](https://github.com/near/docs) and
[jsonrpc_client](https://github.com/near/near-jsonrpc-client-rs) (and
maybe something else, I haven't already checked)

Important updates:
1. We have a new concept for all tx-related methods - `finality`.
`finality` in the response is merged to master 2 weeks ago:
#9556. `finality` field in the
request means "I want at least this level of confidence". So, the
stricter the level, the longer the user needs to wait.
2. I decided to use `Final` as a default `finality` value because it
gives the strongest guarantee and does not change backward compatibility
for any of the methods (though, the waiting time may be increased
sometimes to achieve the strictest level of confidence).
3. Methods `tx`, `EXPERIMENTAL_tx_status` have `finality` as an
additional optional field.
4. A new method appeared - `broadcast_tx`. It allows to send the tx, it
also have the optional field `finality`.
6. `broadcast_tx_async` is equal to `broadcast_tx` with hardcoded
`finality None`, I'll mark it as deprecated in the doc
7. `broadcast_tx_commit` is equal to `broadcast_tx` with hardcoded
`finality Final`, I'll mark it as deprecated in the doc.
  • Loading branch information
telezhnaya authored Oct 24, 2023
1 parent c3e9b5f commit fa1cfa1
Show file tree
Hide file tree
Showing 14 changed files with 350 additions and 188 deletions.
62 changes: 31 additions & 31 deletions chain/client/src/view_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,41 +417,41 @@ impl ViewClientActor {
}
}

// Return the lowest status the node can proof
fn get_tx_execution_status(
&self,
execution_outcome: &FinalExecutionOutcomeView,
) -> Result<TxExecutionStatus, TxStatusError> {
Ok(match execution_outcome.transaction_outcome.outcome.status {
// Return the lowest status the node can proof.
ExecutionStatusView::Unknown => TxExecutionStatus::None,
_ => {
if execution_outcome
.receipts_outcome
.iter()
.all(|e| e.outcome.status != ExecutionStatusView::Unknown)
{
let block_hashes: BTreeSet<CryptoHash> =
execution_outcome.receipts_outcome.iter().map(|e| e.block_hash).collect();
let mut headers = vec![];
for block_hash in block_hashes {
headers.push(self.chain.get_block_header(&block_hash)?);
}
// We can't sort and check only the last block;
// previous blocks may be not in the canonical chain
match self.chain.check_blocks_final_and_canonical(&headers) {
Ok(_) => TxExecutionStatus::Final,
Err(_) => TxExecutionStatus::Executed,
}
} else {
match self.chain.check_blocks_final_and_canonical(&[self
.chain
.get_block_header(&execution_outcome.transaction_outcome.block_hash)?])
{
Ok(_) => TxExecutionStatus::InclusionFinal,
Err(_) => TxExecutionStatus::Inclusion,
}
}
}
if execution_outcome.transaction_outcome.outcome.status == ExecutionStatusView::Unknown {
return Ok(TxExecutionStatus::None);
}

if let Err(_) = self.chain.check_blocks_final_and_canonical(&[self
.chain
.get_block_header(&execution_outcome.transaction_outcome.block_hash)?])
{
return Ok(TxExecutionStatus::Included);
}

if execution_outcome
.receipts_outcome
.iter()
.any(|e| e.outcome.status == ExecutionStatusView::Unknown)
{
return Ok(TxExecutionStatus::IncludedFinal);
}

let block_hashes: BTreeSet<CryptoHash> =
execution_outcome.receipts_outcome.iter().map(|e| e.block_hash).collect();
let mut headers = vec![];
for block_hash in block_hashes {
headers.push(self.chain.get_block_header(&block_hash)?);
}
// We can't sort and check only the last block;
// previous blocks may be not in the canonical chain
Ok(match self.chain.check_blocks_final_and_canonical(&headers) {
Err(_) => TxExecutionStatus::Executed,
Ok(_) => TxExecutionStatus::Final,
})
}

Expand Down
66 changes: 49 additions & 17 deletions chain/jsonrpc-primitives/src/types/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,33 @@ use near_primitives::hash::CryptoHash;
use near_primitives::types::AccountId;
use serde_json::Value;

#[derive(Debug, Clone)]
pub struct RpcBroadcastTransactionRequest {
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct RpcSendTransactionRequest {
#[serde(rename = "signed_tx_base64")]
pub signed_transaction: near_primitives::transaction::SignedTransaction,
#[serde(default)]
pub wait_until: near_primitives::views::TxExecutionStatus,
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct RpcTransactionStatusCommonRequest {
pub struct RpcTransactionStatusRequest {
#[serde(flatten)]
pub transaction_info: TransactionInfo,
#[serde(default)]
pub wait_until: near_primitives::views::TxExecutionStatus,
}

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum TransactionInfo {
#[serde(skip_deserializing)]
Transaction(near_primitives::transaction::SignedTransaction),
TransactionId {
tx_hash: CryptoHash,
sender_account_id: AccountId,
},
Transaction(SignedTransaction),
TransactionId { tx_hash: CryptoHash, sender_account_id: AccountId },
}

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum SignedTransaction {
#[serde(rename = "signed_tx_base64")]
SignedTransaction(near_primitives::transaction::SignedTransaction),
}

#[derive(thiserror::Error, Debug, serde::Serialize, serde::Deserialize)]
Expand Down Expand Up @@ -56,21 +63,46 @@ pub struct RpcBroadcastTxSyncResponse {
pub transaction_hash: near_primitives::hash::CryptoHash,
}

impl From<TransactionInfo> for RpcTransactionStatusCommonRequest {
fn from(transaction_info: TransactionInfo) -> Self {
Self { transaction_info }
impl TransactionInfo {
pub fn from_signed_tx(tx: near_primitives::transaction::SignedTransaction) -> Self {
Self::Transaction(SignedTransaction::SignedTransaction(tx))
}
}

impl From<near_primitives::transaction::SignedTransaction> for RpcTransactionStatusCommonRequest {
fn from(transaction_info: near_primitives::transaction::SignedTransaction) -> Self {
Self { transaction_info: transaction_info.into() }
pub fn to_signed_tx(&self) -> Option<&near_primitives::transaction::SignedTransaction> {
match self {
TransactionInfo::Transaction(tx) => match tx {
SignedTransaction::SignedTransaction(tx) => Some(tx),
},
TransactionInfo::TransactionId { .. } => None,
}
}

pub fn to_tx_hash_and_account(&self) -> (CryptoHash, &AccountId) {
match self {
TransactionInfo::Transaction(tx) => match tx {
SignedTransaction::SignedTransaction(tx) => {
(tx.get_hash(), &tx.transaction.signer_id)
}
},
TransactionInfo::TransactionId { tx_hash, sender_account_id } => {
(*tx_hash, sender_account_id)
}
}
}
}

impl From<near_primitives::transaction::SignedTransaction> for TransactionInfo {
fn from(transaction_info: near_primitives::transaction::SignedTransaction) -> Self {
Self::Transaction(transaction_info)
Self::Transaction(SignedTransaction::SignedTransaction(transaction_info))
}
}

impl From<near_primitives::views::TxStatusView> for RpcTransactionResponse {
fn from(view: near_primitives::views::TxStatusView) -> Self {
Self {
final_execution_outcome: view.execution_outcome,
final_execution_status: view.status,
}
}
}

Expand Down
10 changes: 6 additions & 4 deletions chain/jsonrpc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

## 0.2.3

* Added `final_execution_status` field to the response of methods `tx`, `EXPERIMENTAL_tx_status`, `broadcast_tx_commit`
* Added `send_tx` method which gives configurable execution guarantees options and potentially replaces existing `broadcast_tx_async`, `broadcast_tx_commit`
* Field `final_execution_status` is presented in the response of methods `tx`, `EXPERIMENTAL_tx_status`, `broadcast_tx_commit`, `send_tx`
* Allowed use json in request for methods `EXPERIMENTAL_tx_status`, `tx`, `broadcast_tx_commit`, `broadcast_tx_async`, `send_tx`
* Parameter `wait_until` (same entity as `final_execution_status` in the response) is presented as optional request parameter for methods `EXPERIMENTAL_tx_status`, `tx`, `send_tx`. The response will be returned only when the desired level of finality is reached

### Breaking changes

* Response from `broadcast_tx_async` changed the type from String to Dict.
The response has the same structure as in `broadcast_tx_commit`.
It no longer contains transaction hash (which may be retrieved from Signed Transaction passed in request).
* Removed `EXPERIMENTAL_check_tx` method. Use `tx` method instead
* `EXPERIMENTAL_tx_status`, `tx` methods now wait for recently sent tx (~3-6 seconds) and then show it. Previously, `UnknownTransaction` was immediately returned
* `EXPERIMENTAL_tx_status`, `tx` methods wait 10 seconds and then return `TimeoutError` for never existed transactions. Previously, `UnknownTransaction` was immediately returned

## 0.2.2

Expand Down
11 changes: 8 additions & 3 deletions chain/jsonrpc/client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ use near_jsonrpc_primitives::message::{from_slice, Message};
use near_jsonrpc_primitives::types::changes::{
RpcStateChangesInBlockByTypeRequest, RpcStateChangesInBlockByTypeResponse,
};
use near_jsonrpc_primitives::types::transactions::RpcTransactionResponse;
use near_jsonrpc_primitives::types::transactions::{
RpcTransactionResponse, RpcTransactionStatusRequest,
};
use near_jsonrpc_primitives::types::validator::RpcValidatorsOrderedRequest;
use near_primitives::hash::CryptoHash;
use near_primitives::types::{AccountId, BlockId, BlockReference, MaybeBlockId, ShardId};
use near_primitives::types::{BlockId, BlockReference, MaybeBlockId, ShardId};
use near_primitives::views::validator_stake_view::ValidatorStakeView;
use near_primitives::views::{
BlockView, ChunkView, EpochValidatorInfo, GasPriceView, StatusResponse,
Expand Down Expand Up @@ -186,7 +188,6 @@ jsonrpc_client!(pub struct JsonRpcClient {
#[allow(non_snake_case)]
pub fn EXPERIMENTAL_tx_status(&self, tx: String) -> RpcRequest<RpcTransactionResponse>;
pub fn health(&self) -> RpcRequest<()>;
pub fn tx(&self, hash: String, account_id: AccountId) -> RpcRequest<RpcTransactionResponse>;
pub fn chunk(&self, id: ChunkId) -> RpcRequest<ChunkView>;
pub fn validators(&self, block_id: MaybeBlockId) -> RpcRequest<EpochValidatorInfo>;
pub fn gas_price(&self, block_id: MaybeBlockId) -> RpcRequest<GasPriceView>;
Expand Down Expand Up @@ -218,6 +219,10 @@ impl JsonRpcClient {
call_method(&self.client, &self.server_addr, "block", request)
}

pub fn tx(&self, request: RpcTransactionStatusRequest) -> RpcRequest<RpcTransactionResponse> {
call_method(&self.client, &self.server_addr, "tx", request)
}

#[allow(non_snake_case)]
pub fn EXPERIMENTAL_changes(
&self,
Expand Down
2 changes: 1 addition & 1 deletion chain/jsonrpc/jsonrpc-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub fn start_all_with_validity_period_and_no_epoch_sync(
enable_doomslug: bool,
) -> (Addr<ViewClientActor>, tcp::ListenerAddr) {
let actor_handles = setup_no_network_with_validity_period_and_no_epoch_sync(
vec!["test1".parse().unwrap(), "test2".parse().unwrap()],
vec!["test1".parse().unwrap()],
if let NodeType::Validator = node_type {
"test1".parse().unwrap()
} else {
Expand Down
6 changes: 3 additions & 3 deletions chain/jsonrpc/jsonrpc-tests/tests/rpc_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ fn test_block_query() {
fn test_chunk_by_hash() {
test_with_client!(test_utils::NodeType::NonValidator, client, async move {
let chunk = client.chunk(ChunkId::BlockShardId(BlockId::Height(0), 0u64)).await.unwrap();
assert_eq!(chunk.author, "test2".parse().unwrap());
assert_eq!(chunk.author, "test1".parse().unwrap());
assert_eq!(chunk.header.balance_burnt, 0);
assert_eq!(chunk.header.chunk_hash.as_ref().len(), 32);
assert_eq!(chunk.header.encoded_length, 8);
Expand Down Expand Up @@ -454,7 +454,7 @@ fn test_validators_ordered() {
.unwrap();
assert_eq!(
validators.into_iter().map(|v| v.take_account_id()).collect::<Vec<_>>(),
vec!["test1".parse().unwrap(), "test2".parse().unwrap()]
vec!["test1".parse().unwrap()]
)
});
}
Expand Down Expand Up @@ -560,7 +560,7 @@ fn test_get_chunk_with_object_in_params() {
)
.await
.unwrap();
assert_eq!(chunk.author, "test2".parse().unwrap());
assert_eq!(chunk.author, "test1".parse().unwrap());
assert_eq!(chunk.header.balance_burnt, 0);
assert_eq!(chunk.header.chunk_hash.as_ref().len(), 32);
assert_eq!(chunk.header.encoded_length, 8);
Expand Down
45 changes: 33 additions & 12 deletions chain/jsonrpc/jsonrpc-tests/tests/rpc_transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use futures::{future, FutureExt, TryFutureExt};
use near_actix_test_utils::run_actix;
use near_crypto::{InMemorySigner, KeyType};
use near_jsonrpc::client::new_client;
use near_jsonrpc_primitives::types::transactions::{RpcTransactionStatusRequest, TransactionInfo};
use near_network::test_utils::WaitOrTimeoutActor;
use near_o11y::testonly::{init_integration_logger, init_test_logger};
use near_primitives::hash::{hash, CryptoHash};
Expand Down Expand Up @@ -59,7 +60,13 @@ fn test_send_tx_async() {
if let Some(tx_hash) = *tx_hash2_2.lock().unwrap() {
actix::spawn(
client1
.tx(tx_hash.to_string(), signer_account_id)
.tx(RpcTransactionStatusRequest {
transaction_info: TransactionInfo::TransactionId {
tx_hash,
sender_account_id: signer_account_id,
},
wait_until: TxExecutionStatus::Executed,
})
.map_err(|err| println!("Error: {:?}", err))
.map_ok(|result| {
if let FinalExecutionStatus::SuccessValue(_) =
Expand Down Expand Up @@ -200,7 +207,14 @@ fn test_replay_protection() {
#[test]
fn test_tx_status_missing_tx() {
test_with_client!(test_utils::NodeType::Validator, client, async move {
match client.tx(CryptoHash::new().to_string(), "test1".parse().unwrap()).await {
let request = RpcTransactionStatusRequest {
transaction_info: TransactionInfo::TransactionId {
tx_hash: CryptoHash::new(),
sender_account_id: "test1".parse().unwrap(),
},
wait_until: TxExecutionStatus::None,
};
match client.tx(request).await {
Err(e) => {
let s = serde_json::to_string(&e.data.unwrap()).unwrap();
assert_eq!(s, "\"Transaction 11111111111111111111111111111111 doesn't exist\"");
Expand All @@ -215,16 +229,23 @@ fn test_check_invalid_tx() {
test_with_client!(test_utils::NodeType::Validator, client, async move {
let signer = InMemorySigner::from_seed("test1".parse().unwrap(), KeyType::ED25519, "test1");
// invalid base hash
let tx = SignedTransaction::send_money(
1,
"test1".parse().unwrap(),
"test2".parse().unwrap(),
&signer,
100,
hash(&[1]),
);
if let Ok(_) = client.tx(tx.get_hash().to_string(), "test1".parse().unwrap()).await {
panic!("transaction should not succeed");
let request = RpcTransactionStatusRequest {
transaction_info: TransactionInfo::from_signed_tx(SignedTransaction::send_money(
1,
"test1".parse().unwrap(),
"test2".parse().unwrap(),
&signer,
100,
hash(&[1]),
)),
wait_until: TxExecutionStatus::None,
};
match client.tx(request).await {
Err(e) => {
let s = serde_json::to_string(&e.data.unwrap()).unwrap();
assert_eq!(s, "{\"TxExecutionError\":{\"InvalidTxError\":\"Expired\"}}");
}
Ok(_) => panic!("transaction should not succeed"),
}
});
}
8 changes: 0 additions & 8 deletions chain/jsonrpc/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,6 @@ mod params {
self.0.unwrap_or_else(Self::parse)
}

/// Finish chain of parsing without trying of parsing to `T` directly
pub fn unwrap(self) -> Result<T, RpcParseError> {
match self.0 {
Ok(res) => res,
Err(e) => Err(RpcParseError(format!("Failed parsing args: {e}"))),
}
}

/// If value hasn’t been parsed yet and it’s a one-element array
/// (i.e. singleton) deserialises the element and calls `func` on it.
///
Expand Down
Loading

0 comments on commit fa1cfa1

Please sign in to comment.