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

impl original_storage in SubstrateStackState #871

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions frame/evm/src/runner/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,13 @@ use fp_evm::{CallInfo, CreateInfo, ExecutionInfo, Log, Vicinity};
use frame_support::traits::{Currency, ExistenceRequirement, Get};
use sp_core::{H160, H256, U256};
use sp_runtime::traits::UniqueSaturatedInto;
use sp_std::{boxed::Box, collections::btree_set::BTreeSet, marker::PhantomData, mem, vec::Vec};
use sp_std::{
boxed::Box,
collections::{btree_map::BTreeMap, btree_set::BTreeSet},
marker::PhantomData,
mem,
vec::Vec,
};

#[derive(Default)]
pub struct Runner<T: Config> {
Expand Down Expand Up @@ -494,6 +500,7 @@ impl<'config> SubstrateStackSubstate<'config> {
pub struct SubstrateStackState<'vicinity, 'config, T> {
vicinity: &'vicinity Vicinity,
substate: SubstrateStackSubstate<'config>,
original_storage: BTreeMap<(H160, H256), H256>,
_marker: PhantomData<T>,
}

Expand All @@ -509,6 +516,7 @@ impl<'vicinity, 'config, T: Config> SubstrateStackState<'vicinity, 'config, T> {
parent: None,
},
_marker: PhantomData,
original_storage: BTreeMap::new(),
}
}
}
Expand Down Expand Up @@ -576,8 +584,15 @@ impl<'vicinity, 'config, T: Config> BackendT for SubstrateStackState<'vicinity,
<AccountStorages<T>>::get(address, index)
}

fn original_storage(&self, _address: H160, _index: H256) -> Option<H256> {
None
fn original_storage(&self, address: H160, index: H256) -> Option<H256> {
// Not being cached means that it was never changed, which means we
// can fetch it from storage.
Some(
self.original_storage
.get(&(address, index))
.cloned()
.unwrap_or_else(|| self.storage(address, index)),
)
}

fn block_base_fee_per_gas(&self) -> sp_core::U256 {
Expand Down Expand Up @@ -629,6 +644,18 @@ where
}

fn set_storage(&mut self, address: H160, index: H256, value: H256) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a check beforehand that:

  • If self.original_storage does not contain the key,
  • and the to-set-value is the same as the current value.

Skip inserting into the original_storage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If self.original_storage does not contain the key,
That's what if let Vacant(e) = self.original_storage.entry(...) does, which was recommanded by clippy instead of contains then insert.
and the to-set-value is the same as the current value.
Yeah, can add this check to not insert for same value as original.

// We cache the current value if this is the first time we modify it
// in the transaction.
use sp_std::collections::btree_map::Entry::Vacant;
if let Vacant(e) = self.original_storage.entry((address, index)) {
let original = <AccountStorages<T>>::get(address, index);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use self.storage here to keep it consistent.

Suggested change
let original = <AccountStorages<T>>::get(address, index);
let original = self.storage(address, index);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cannot use self.storage as we're already borrowing self with self.original_storage.entry. Seems better to keep <AccountStorages<T>>::get than fetching the value every time even when the entry is not vacant.

// No need to cache if same value.
if original != value {
e.insert(original);
}
}

// Then we insert or remove the entry based on the value.
if value == H256::default() {
log::debug!(
target: "evm",
Expand Down Expand Up @@ -693,7 +720,7 @@ where
//
// This function exists in EVM because a design issue
// (arguably a bug) in SELFDESTRUCT that can cause total
// issurance to be reduced. We do not need to replicate this.
// issuance to be reduced. We do not need to replicate this.
}

fn touch(&mut self, _address: H160) {
Expand Down
46 changes: 45 additions & 1 deletion ts-tests/tests/test-contract-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect } from "chai";
import { AbiItem } from "web3-utils";

import Test from "../build/contracts/Storage.json";
import { GENESIS_ACCOUNT, GENESIS_ACCOUNT_PRIVATE_KEY } from "./config";
import { GENESIS_ACCOUNT, GENESIS_ACCOUNT_PRIVATE_KEY, FIRST_CONTRACT_ADDRESS } from "./config";
import { createAndFinalizeBlock, customRequest, describeWithFrontier } from "./util";

describeWithFrontier("Frontier RPC (Contract)", (context) => {
Expand Down Expand Up @@ -80,4 +80,48 @@ describeWithFrontier("Frontier RPC (Contract)", (context) => {

expect(getStorage1.result).to.be.eq(expectedStorage);
});

it("SSTORE cost should properly take into account transaction initial value", async function () {
this.timeout(5000);

let nonce = await context.web3.eth.getTransactionCount(GENESIS_ACCOUNT);

await context.web3.eth.accounts.wallet.add(GENESIS_ACCOUNT_PRIVATE_KEY);
const contract = new context.web3.eth.Contract(TEST_CONTRACT_ABI, FIRST_CONTRACT_ADDRESS, {
from: GENESIS_ACCOUNT,
gasPrice: "0x3B9ACA00",
});

const promisify = (inner) => new Promise((resolve, reject) => inner(resolve, reject));

let tx1 = contract.methods
.setStorage("0x2A", "0x1")
.send({ from: GENESIS_ACCOUNT, gas: "0x100000", nonce: nonce++ });

let tx2 = contract.methods
.setStorage("0x2A", "0x1")
.send({ from: GENESIS_ACCOUNT, gas: "0x100000", nonce: nonce++ });

let tx3 = contract.methods
.setStorage("0x2A", "0x2")
.send(
{ from: GENESIS_ACCOUNT, gas: "0x100000", nonce: nonce++ },
async (hash) => await createAndFinalizeBlock(context.web3)
);

tx1 = await tx1;
tx2 = await tx2;
tx3 = await tx3;

// cost minus SSTORE
const baseCost = 24029;

// going from unset storage to some value (original = 0)
expect(tx1.gasUsed - baseCost).to.be.eq(20000);
// in London config, setting back the same value have cost of warm read
expect(tx2.gasUsed - baseCost).to.be.eq(100);
// - the original storage didn't change in the current transaction
// - the original storage is not zero (otherwise tx1)
expect(tx3.gasUsed - baseCost).to.be.eq(2900);
});
});