Skip to content

Commit

Permalink
count refund receipts toward gas limit calculation (#4405)
Browse files Browse the repository at this point in the history
Fixes #4400
  • Loading branch information
mzhangmzz authored Jun 28, 2021
1 parent f746335 commit cfed276
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 7 deletions.
3 changes: 2 additions & 1 deletion chain/client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@ protocol_feature_add_account_versions = ["near-primitives/protocol_feature_add_a
protocol_feature_fix_storage_usage = ["near-primitives/protocol_feature_fix_storage_usage"]
protocol_feature_restore_receipts_after_fix = []
protocol_feature_cap_max_gas_price = ["near-primitives/protocol_feature_cap_max_gas_price"]
protocol_feature_count_refund_receipts_in_gas_limit = ["near-primitives/protocol_feature_count_refund_receipts_in_gas_limit", "node-runtime/protocol_feature_count_refund_receipts_in_gas_limit"]
nightly_protocol = []
nightly_protocol_features = ["nightly_protocol", "near-chain/nightly_protocol_features", "protocol_feature_block_header_v3", "protocol_feature_add_account_versions", "protocol_feature_fix_storage_usage", "protocol_feature_restore_receipts_after_fix", "protocol_feature_cap_max_gas_price"]
nightly_protocol_features = ["nightly_protocol", "near-chain/nightly_protocol_features", "protocol_feature_block_header_v3", "protocol_feature_add_account_versions", "protocol_feature_fix_storage_usage", "protocol_feature_restore_receipts_after_fix", "protocol_feature_cap_max_gas_price", "protocol_feature_count_refund_receipts_in_gas_limit"]
sandbox = ["near-network/sandbox", "near-chain/sandbox", "node-runtime/sandbox"]

[[test]]
Expand Down
121 changes: 121 additions & 0 deletions chain/client/tests/process_blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ use near_network::{
};
use near_primitives::block::{Approval, ApprovalInner};
use near_primitives::block_header::BlockHeader;
use near_primitives::checked_feature;

use near_primitives::errors::InvalidTxError;
use near_primitives::errors::TxExecutionError;
use near_primitives::hash::{hash, CryptoHash};
use near_primitives::merkle::verify_hash;
use near_primitives::receipt::DelayedReceiptIndices;
Expand Down Expand Up @@ -2293,6 +2295,125 @@ fn test_block_execution_outcomes() {
assert!(execution_outcomes_from_block[0].outcome_with_id.id == delayed_receipt_id[0]);
}

#[test]
fn test_refund_receipts_processing() {
init_test_logger();

let epoch_length = 5;
let min_gas_price = 10000;
let mut genesis = Genesis::test(vec!["test0", "test1"], 1);
genesis.config.epoch_length = epoch_length;
genesis.config.min_gas_price = min_gas_price;
// set gas limit to be small
genesis.config.gas_limit = 1_000_000;
let chain_genesis = ChainGenesis::from(&genesis);
let mut env =
TestEnv::new_with_runtime(chain_genesis, 1, 1, create_nightshade_runtimes(&genesis, 1));
let genesis_block = env.clients[0].chain.get_block_by_height(0).unwrap().clone();
let signer = InMemorySigner::from_seed("test0", KeyType::ED25519, "test0");
let mut tx_hashes = vec![];
// send transactions to a non-existing account to generate refund
for i in 0..3 {
// send transaction to the same account to generate local receipts
let tx = SignedTransaction::send_money(
i + 1,
"test0".to_string(),
"random_account".to_string(),
&signer,
1,
*genesis_block.hash(),
);
tx_hashes.push(tx.get_hash());
env.clients[0].process_tx(tx, false, false);
}

env.produce_block(0, 3);
env.produce_block(0, 4);
let mut block_height = 5;
loop {
env.produce_block(0, block_height);
let block = env.clients[0].chain.get_block_by_height(block_height).unwrap().clone();
let prev_block =
env.clients[0].chain.get_block_by_height(block_height - 1).unwrap().clone();
let chunk_extra =
env.clients[0].chain.get_chunk_extra(prev_block.hash(), 0).unwrap().clone();
let state_update = env.clients[0]
.runtime_adapter
.get_tries()
.new_trie_update(0, *chunk_extra.state_root());
let delayed_indices =
get::<DelayedReceiptIndices>(&state_update, &TrieKey::DelayedReceiptIndices).unwrap();
let finished_all_delayed_receipts = match delayed_indices {
None => false,
Some(delayed_indices) => {
delayed_indices.next_available_index > 0
&& delayed_indices.first_index == delayed_indices.next_available_index
}
};
let chunk =
env.clients[0].chain.get_chunk(&block.chunks()[0].chunk_hash()).unwrap().clone();
if chunk.receipts().len() == 0
&& chunk.transactions().len() == 0
&& finished_all_delayed_receipts
{
break;
}
block_height += 1;
}

let mut refund_receipt_ids = HashSet::new();
for (_, id) in tx_hashes.into_iter().enumerate() {
let execution_outcome = env.clients[0].chain.get_execution_outcome(&id).unwrap();
assert_eq!(execution_outcome.outcome_with_id.outcome.receipt_ids.len(), 1);
match execution_outcome.outcome_with_id.outcome.status {
ExecutionStatus::SuccessReceiptId(id) => {
let receipt_outcome = env.clients[0].chain.get_execution_outcome(&id).unwrap();
assert!(matches!(
receipt_outcome.outcome_with_id.outcome.status,
ExecutionStatus::Failure(TxExecutionError::ActionError(_))
));
receipt_outcome.outcome_with_id.outcome.receipt_ids.iter().for_each(|id| {
refund_receipt_ids.insert(id.clone());
});
}
_ => assert!(false),
};
}

let ending_block_height = block_height - 1;
let count_refund_receipts_in_gas_limit = checked_feature!(
"protocol_feature_count_refund_receipts_in_gas_limit",
CountRefundReceiptsInGasLimit,
genesis.config.protocol_version
);
let begin_block_height = if count_refund_receipts_in_gas_limit {
ending_block_height - refund_receipt_ids.len() as u64 + 1
} else {
ending_block_height
};
let mut processed_refund_receipt_ids = HashSet::new();
for i in begin_block_height..=ending_block_height {
let block = env.clients[0].chain.get_block_by_height(i).unwrap().clone();
let execution_outcomes_from_block = env.clients[0]
.chain
.get_block_execution_outcomes(block.hash())
.unwrap()
.remove(&0)
.unwrap();
execution_outcomes_from_block.iter().for_each(|outcome| {
processed_refund_receipt_ids.insert(outcome.outcome_with_id.id);
});
let chunk_extra = env.clients[0].chain.get_chunk_extra(block.hash(), 0).unwrap().clone();
if count_refund_receipts_in_gas_limit {
assert_eq!(execution_outcomes_from_block.len(), 1);
assert!(chunk_extra.gas_used() >= chunk_extra.gas_limit());
} else {
assert_eq!(chunk_extra.gas_used(), 0);
}
}
assert_eq!(processed_refund_receipt_ids, refund_receipt_ids);
}

#[test]
fn test_epoch_protocol_version_change() {
init_test_logger();
Expand Down
3 changes: 2 additions & 1 deletion core/primitives/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ protocol_feature_allow_create_account_on_delete = []
protocol_feature_fix_storage_usage = []
protocol_feature_restore_receipts_after_fix = []
protocol_feature_cap_max_gas_price = []
nightly_protocol_features = ["nightly_protocol", "protocol_feature_evm", "protocol_feature_block_header_v3", "protocol_feature_alt_bn128", "protocol_feature_add_account_versions", "protocol_feature_tx_size_limit", "protocol_feature_allow_create_account_on_delete", "protocol_feature_fix_storage_usage", "protocol_feature_restore_receipts_after_fix", "protocol_feature_cap_max_gas_price"]
protocol_feature_count_refund_receipts_in_gas_limit = []
nightly_protocol_features = ["nightly_protocol", "protocol_feature_evm", "protocol_feature_block_header_v3", "protocol_feature_alt_bn128", "protocol_feature_add_account_versions", "protocol_feature_tx_size_limit", "protocol_feature_allow_create_account_on_delete", "protocol_feature_fix_storage_usage", "protocol_feature_restore_receipts_after_fix", "protocol_feature_cap_max_gas_price", "protocol_feature_count_refund_receipts_in_gas_limit"]
nightly_protocol = []

[dev-dependencies]
Expand Down
6 changes: 5 additions & 1 deletion core/primitives/src/version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ pub enum ProtocolFeature {
RestoreReceiptsAfterFix,
#[cfg(feature = "protocol_feature_cap_max_gas_price")]
CapMaxGasPrice,
#[cfg(feature = "protocol_feature_count_refund_receipts_in_gas_limit")]
CountRefundReceiptsInGasLimit,
}

/// Current latest stable version of the protocol.
Expand All @@ -117,7 +119,7 @@ pub const PROTOCOL_VERSION: ProtocolVersion = 45;

/// Current latest nightly version of the protocol.
#[cfg(feature = "nightly_protocol")]
pub const PROTOCOL_VERSION: ProtocolVersion = 113;
pub const PROTOCOL_VERSION: ProtocolVersion = 114;

impl ProtocolFeature {
pub const fn protocol_version(self) -> ProtocolVersion {
Expand Down Expand Up @@ -149,6 +151,8 @@ impl ProtocolFeature {
ProtocolFeature::RestoreReceiptsAfterFix => 112,
#[cfg(feature = "protocol_feature_cap_max_gas_price")]
ProtocolFeature::CapMaxGasPrice => 113,
#[cfg(feature = "protocol_feature_count_refund_receipts_in_gas_limit")]
ProtocolFeature::CountRefundReceiptsInGasLimit => 114,
}
}
}
Expand Down
1 change: 1 addition & 0 deletions runtime/runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ protocol_feature_tx_size_limit = []
protocol_feature_allow_create_account_on_delete = ["near-primitives/protocol_feature_allow_create_account_on_delete", "near-vm-logic/protocol_feature_allow_create_account_on_delete"]
protocol_feature_fix_storage_usage = ["near-primitives/protocol_feature_fix_storage_usage"]
protocol_feature_restore_receipts_after_fix = []
protocol_feature_count_refund_receipts_in_gas_limit = ["near-primitives/protocol_feature_count_refund_receipts_in_gas_limit"]
sandbox = []

[dev-dependencies]
Expand Down
85 changes: 81 additions & 4 deletions runtime/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub use near_primitives;
use near_primitives::runtime::get_insufficient_storage_stake;
use near_primitives::{
account::Account,
checked_feature,
errors::{ActionError, ActionErrorKind, RuntimeError, TxExecutionError},
hash::CryptoHash,
receipt::{
Expand Down Expand Up @@ -555,10 +556,18 @@ impl Runtime {
}
}

// If the receipt is a refund, then we consider it free without burnt gas.
let gas_deficit_amount = if receipt.predecessor_id == system_account() {
result.gas_burnt = 0;
result.gas_used = 0;
// We will set gas_burnt for refund receipts to be 0 when we calculate tx_burnt_amount
// Here we don't set result.gas_burnt to be zero if CountRefundReceiptsInGasLimit is
// enabled because we want it to be counted in gas limit calculation later
if !checked_feature!(
"protocol_feature_count_refund_receipts_in_gas_limit",
CountRefundReceiptsInGasLimit,
apply_state.current_protocol_version
) {
result.gas_burnt = 0;
result.gas_used = 0;
}
// If the refund fails tokens are burned.
if result.result.is_err() {
stats.other_burnt_amount = safe_add_balance(
Expand Down Expand Up @@ -595,9 +604,12 @@ impl Runtime {
}
};

// If the receipt is a refund, then we consider it free without burnt gas.
let gas_burnt: Gas =
if receipt.predecessor_id == system_account() { 0 } else { result.gas_burnt };
// `gas_deficit_amount` is strictly less than `gas_price * gas_burnt`.
let mut tx_burnt_amount =
safe_gas_to_balance(apply_state.gas_price, result.gas_burnt)? - gas_deficit_amount;
safe_gas_to_balance(apply_state.gas_price, gas_burnt)? - gas_deficit_amount;
// The amount of tokens burnt for the execution of this receipt. It's used in the execution
// outcome.
let tokens_burnt = tx_burnt_amount;
Expand Down Expand Up @@ -1209,6 +1221,9 @@ impl Runtime {
let mut validator_proposals = vec![];
let mut local_receipts = vec![];
let mut outcomes = vec![];
// This contains the gas "burnt" for refund receipts. Even though we don't actually
// charge any gas for refund receipts, we still count the gas use towards the block gas
// limit
let mut total_gas_burnt = gas_used_for_migrations;

for signed_transaction in transactions {
Expand Down Expand Up @@ -1625,6 +1640,58 @@ mod tests {
.unwrap();
}

#[test]
fn test_apply_refund_receipts() {
let initial_balance = to_yocto(1_000_000);
let initial_locked = to_yocto(500_000);
let small_transfer = to_yocto(10_000);
let gas_limit = 1;
let (runtime, tries, mut root, apply_state, _, epoch_info_provider) =
setup_runtime(initial_balance, initial_locked, gas_limit);

let n = 10;
let receipts = generate_refund_receipts(small_transfer, n);

// Checking n receipts delayed
for i in 1..=n + 3 {
let prev_receipts: &[Receipt] = if i == 1 { &receipts } else { &[] };
let apply_result = runtime
.apply(
tries.get_trie_for_shard(0),
root,
&None,
&apply_state,
prev_receipts,
&[],
&epoch_info_provider,
None,
)
.unwrap();
let (store_update, new_root) = tries.apply_all(&apply_result.trie_changes, 0).unwrap();
root = new_root;
store_update.commit().unwrap();
let state = tries.new_trie_update(0, root);
let account = get_account(&state, &alice_account()).unwrap().unwrap();
// Check that refund receipts are delayed if CountRefundReceiptsInGasLimit is enabled,
// and otherwise processed all at once
let capped_i = if checked_feature!(
"protocol_feature_count_refund_receipts_in_gas_limit",
CountRefundReceiptsInGasLimit,
apply_state.current_protocol_version
) {
std::cmp::min(i, n)
} else {
n
};
assert_eq!(
account.amount(),
initial_balance
+ small_transfer * Balance::from(capped_i)
+ Balance::from(capped_i * (capped_i - 1) / 2)
);
}
}

#[test]
fn test_apply_delayed_receipts_feed_all_at_once() {
let initial_balance = to_yocto(1_000_000);
Expand Down Expand Up @@ -1804,6 +1871,16 @@ mod tests {
.collect()
}

fn generate_refund_receipts(small_transfer: u128, n: u64) -> Vec<Receipt> {
let mut receipt_id = CryptoHash::default();
(0..n)
.map(|i| {
receipt_id = hash(receipt_id.as_ref());
Receipt::new_balance_refund(&alice_account(), small_transfer + Balance::from(i))
})
.collect()
}

#[test]
fn test_apply_delayed_receipts_local_tx() {
let initial_balance = to_yocto(1_000_000);
Expand Down

0 comments on commit cfed276

Please sign in to comment.