Skip to content

Commit

Permalink
RPC: improve supporting tx execution levels (#10792)
Browse files Browse the repository at this point in the history
More about tx execution levels:
https://docs.near.org/api/rpc/transactions#tx-status-result

The levels that allow having not all the execution outcomes (Included,
IncludedFinal), actually had to wait for all of the execution outcomes
anyway.
[1.37.3 had a separate partial fix for supporting Included status, but
let's not overcomplicate]
With the current change, we can show everything we have at the moment
without extra waiting.
Also, current change helps us to be 1 step closer to resolving
#6834
That would be my next step after we merge this PR.

TLDR: I partially copy pasted the code.
I could have rewritten `chain:get_final_transaction_result` instead, but
it has LOTS of usages everywhere. I decided it's just not safe.
I anyway changed it a little with the new function
`chain:get_execution_status` (hopefully in a better way) - see the
details below in the comment.
But the new logic with partial execution outcomes list is used only in
view_client (aka RPC, as far as I see from the usages).
  • Loading branch information
telezhnaya authored Apr 4, 2024
1 parent 22b603c commit 1d082db
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 53 deletions.
121 changes: 83 additions & 38 deletions chain/chain/src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3077,53 +3077,68 @@ impl Chain {
Ok(self.chain_store.get_outcomes_by_id(id)?.into_iter().map(Into::into).collect())
}

/// Returns execution status based on the list of currently existing outcomes
fn get_execution_status(
&self,
outcomes: &[ExecutionOutcomeWithIdView],
) -> FinalExecutionStatus {
let mut awaiting_receipt_ids = HashSet::new();
let mut seen_receipt_ids = HashSet::new();
let mut result: FinalExecutionStatus = FinalExecutionStatus::NotStarted;
for outcome in outcomes {
seen_receipt_ids.insert(&outcome.id);
match &outcome.outcome.status {
ExecutionStatusView::Unknown => return FinalExecutionStatus::Started,
ExecutionStatusView::Failure(e) => return FinalExecutionStatus::Failure(e.clone()),
ExecutionStatusView::SuccessValue(v) => {
if result == FinalExecutionStatus::NotStarted {
// historically, we used the first SuccessValue we have seen
// let's continue sticking to it
result = FinalExecutionStatus::SuccessValue(v.clone());
}
}
ExecutionStatusView::SuccessReceiptId(_) => {
awaiting_receipt_ids.extend(&outcome.outcome.receipt_ids);
}
}
}
return if awaiting_receipt_ids.is_subset(&seen_receipt_ids) {
result
} else {
FinalExecutionStatus::Started
};
}

/// Collect all the execution outcomes existing at the current moment
/// Fails if there are non executed receipts, and require_all_outcomes == true
fn get_recursive_transaction_results(
&self,
outcomes: &mut Vec<ExecutionOutcomeWithIdView>,
id: &CryptoHash,
require_all_outcomes: bool,
) -> Result<(), Error> {
outcomes.push(ExecutionOutcomeWithIdView::from(self.get_execution_outcome(id)?));
let outcome = match self.get_execution_outcome(id) {
Ok(outcome) => outcome,
Err(err) => return if require_all_outcomes { Err(err) } else { Ok(()) },
};
outcomes.push(ExecutionOutcomeWithIdView::from(outcome));
let outcome_idx = outcomes.len() - 1;
for idx in 0..outcomes[outcome_idx].outcome.receipt_ids.len() {
let id = outcomes[outcome_idx].outcome.receipt_ids[idx];
self.get_recursive_transaction_results(outcomes, &id)?;
self.get_recursive_transaction_results(outcomes, &id, require_all_outcomes)?;
}
Ok(())
}

/// Returns FinalExecutionOutcomeView for the given transaction.
/// Waits for the end of the execution of all corresponding receipts
pub fn get_final_transaction_result(
&self,
transaction_hash: &CryptoHash,
) -> Result<FinalExecutionOutcomeView, Error> {
let mut outcomes = Vec::new();
self.get_recursive_transaction_results(&mut outcomes, transaction_hash)?;
let mut looking_for_id = *transaction_hash;
let num_outcomes = outcomes.len();
let status = outcomes
.iter()
.find_map(|outcome_with_id| {
if outcome_with_id.id == looking_for_id {
match &outcome_with_id.outcome.status {
ExecutionStatusView::Unknown if num_outcomes == 1 => {
Some(FinalExecutionStatus::NotStarted)
}
ExecutionStatusView::Unknown => Some(FinalExecutionStatus::Started),
ExecutionStatusView::Failure(e) => {
Some(FinalExecutionStatus::Failure(e.clone()))
}
ExecutionStatusView::SuccessValue(v) => {
Some(FinalExecutionStatus::SuccessValue(v.clone()))
}
ExecutionStatusView::SuccessReceiptId(id) => {
looking_for_id = *id;
None
}
}
} else {
None
}
})
.expect("results should resolve to a final outcome");
self.get_recursive_transaction_results(&mut outcomes, transaction_hash, true)?;
let status = self.get_execution_status(&outcomes);
let receipts_outcome = outcomes.split_off(1);
let transaction = self.chain_store.get_transaction(transaction_hash)?.ok_or_else(|| {
Error::DBNotFoundErr(format!("Transaction {} is not found", transaction_hash))
Expand All @@ -3133,16 +3148,45 @@ impl Chain {
Ok(FinalExecutionOutcomeView { status, transaction, transaction_outcome, receipts_outcome })
}

pub fn get_final_transaction_result_with_receipt(
/// Returns FinalExecutionOutcomeView for the given transaction.
/// Does not wait for the end of the execution of all corresponding receipts
pub fn get_partial_transaction_result(
&self,
transaction_hash: &CryptoHash,
) -> Result<FinalExecutionOutcomeView, Error> {
let transaction = self.chain_store.get_transaction(transaction_hash)?.ok_or_else(|| {
Error::DBNotFoundErr(format!("Transaction {} is not found", transaction_hash))
})?;
let transaction: SignedTransactionView = SignedTransaction::clone(&transaction).into();

let mut outcomes = Vec::new();
self.get_recursive_transaction_results(&mut outcomes, transaction_hash, false)?;
if outcomes.is_empty() {
// It can't be, we would fail with tx not found error earlier in this case
// But if so, let's return meaningful error instead of panic on split_off
return Err(Error::DBNotFoundErr(format!(
"Transaction {} is not found",
transaction_hash
)));
}

let status = self.get_execution_status(&outcomes);
let receipts_outcome = outcomes.split_off(1);
let transaction_outcome = outcomes.pop().unwrap();
Ok(FinalExecutionOutcomeView { status, transaction, transaction_outcome, receipts_outcome })
}

/// Returns corresponding receipts for provided outcome
/// The incoming list in receipts_outcome may be partial
pub fn get_transaction_result_with_receipt(
&self,
final_outcome: FinalExecutionOutcomeView,
outcome: FinalExecutionOutcomeView,
) -> Result<FinalExecutionOutcomeWithReceiptView, Error> {
let receipt_id_from_transaction =
final_outcome.transaction_outcome.outcome.receipt_ids.get(0).cloned();
let is_local_receipt =
final_outcome.transaction.signer_id == final_outcome.transaction.receiver_id;
outcome.transaction_outcome.outcome.receipt_ids.get(0).cloned();
let is_local_receipt = outcome.transaction.signer_id == outcome.transaction.receiver_id;

let receipts = final_outcome
let receipts = outcome
.receipts_outcome
.iter()
.filter_map(|outcome| {
Expand All @@ -3158,7 +3202,7 @@ impl Chain {
})
.collect::<Result<Vec<_>, _>>()?;

Ok(FinalExecutionOutcomeWithReceiptView { final_outcome, receipts })
Ok(FinalExecutionOutcomeWithReceiptView { final_outcome: outcome, receipts })
}

pub fn check_blocks_final_and_canonical(
Expand Down Expand Up @@ -4177,7 +4221,8 @@ impl Chain {
Ok(is_first_block_of_epoch?)
}

/// Get transaction result for given hash of transaction or receipt id on the canonical chain
/// Get transaction result for given hash of transaction or receipt id
/// Chain may not be canonical yet
pub fn get_execution_outcome(
&self,
id: &CryptoHash,
Expand Down
21 changes: 9 additions & 12 deletions chain/client/src/view_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,26 +436,23 @@ impl ViewClientActor {
return Ok(TxExecutionStatus::None);
}

let is_execution_finished = match execution_outcome.status {
FinalExecutionStatus::Failure(_) | FinalExecutionStatus::SuccessValue(_) => true,
FinalExecutionStatus::NotStarted | FinalExecutionStatus::Started => false,
};

if let Err(_) = self.chain.check_blocks_final_and_canonical(&[self
.chain
.get_block_header(&execution_outcome.transaction_outcome.block_hash)?])
{
return if execution_outcome
.receipts_outcome
.iter()
.all(|e| e.outcome.status != ExecutionStatusView::Unknown)
{
return if is_execution_finished {
Ok(TxExecutionStatus::ExecutedOptimistic)
} else {
Ok(TxExecutionStatus::Included)
};
}

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

Expand Down Expand Up @@ -507,12 +504,12 @@ impl ViewClientActor {
target_shard_id,
true,
) {
match self.chain.get_final_transaction_result(&tx_hash) {
match self.chain.get_partial_transaction_result(&tx_hash) {
Ok(tx_result) => {
let status = self.get_tx_execution_status(&tx_result)?;
let res = if fetch_receipt {
let final_result =
self.chain.get_final_transaction_result_with_receipt(tx_result)?;
self.chain.get_transaction_result_with_receipt(tx_result)?;
FinalExecutionOutcomeViewEnum::FinalExecutionOutcomeWithReceipt(
final_result,
)
Expand Down
3 changes: 1 addition & 2 deletions chain/jsonrpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -711,8 +711,7 @@ impl JsonRpcHandler {
> {
self.send_tx(RpcSendTransactionRequest {
signed_transaction: request_data.signed_transaction,
// Will be ignored, broadcast_tx_commit is not aligned with existing enum
wait_until: Default::default(),
wait_until: TxExecutionStatus::ExecutedOptimistic,
})
.await
}
Expand Down
2 changes: 1 addition & 1 deletion core/primitives/src/views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1756,7 +1756,7 @@ impl TxStatusView {
}
}

/// Execution outcome of the transaction and all of subsequent the receipts.
/// Execution outcome of the transaction and all the subsequent receipts.
/// Could be not finalised yet
#[derive(
BorshSerialize, BorshDeserialize, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone,
Expand Down
1 change: 1 addition & 0 deletions integration-tests/src/user/runtime_user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ impl RuntimeUser {
transactions
}

// TODO(#10942) get rid of copy pasted code, it's outdated comparing to the original
fn get_final_transaction_result(&self, hash: &CryptoHash) -> FinalExecutionOutcomeView {
let mut outcomes = self.get_recursive_transaction_results(hash);
let mut looking_for_id = *hash;
Expand Down

0 comments on commit 1d082db

Please sign in to comment.