Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

count refund receipts toward gas limit calculation #4405

Merged
merged 14 commits into from
Jun 28, 2021
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