mirror of
https://github.com/pezkuwichain/revive.git
synced 2026-10-22 03:36:19 +00:00
Existing test coverage didn't catch a signed remainder overflow semantics edge case behavior where at least one the operators is being constant folded by LLVM instead of solc. This PR fixes the SREM translation bug and adds missing test coverage for division ops with constants. Closes #524 --------- Signed-off-by: xermicus <cyrill@parity.io>
394 lines
12 KiB
Rust
394 lines
12 KiB
Rust
//! Experimental test runner for testing [pallet-revive](https://github.com/paritytech/polkadot-sdk/tree/master/substrate/frame/revive) contracts.
|
|
//! The crate implements [`Specs`] that defines tests a declarative way:
|
|
//! - The Genesis configuration.
|
|
//! - A list of [`SpecsAction`] that will be executed in order.
|
|
//!
|
|
//! ## Example
|
|
//! ```rust
|
|
//! use revive_runner::*;
|
|
//! use SpecsAction::*;
|
|
//! Specs {
|
|
//! differential: false,
|
|
//! balances: vec![(ALICE, 1_000_000_000)],
|
|
//! actions: vec![Instantiate {
|
|
//! origin: TestAddress::Alice,
|
|
//! value: 0,
|
|
//! gas_limit: Some(GAS_LIMIT),
|
|
//! storage_deposit_limit: Some(DEPOSIT_LIMIT),
|
|
//! code: Code::Bytes(include_bytes!("../fixtures/Baseline.pvm").to_vec()),
|
|
//! data: vec![],
|
|
//! salt: Default::default(),
|
|
//! }],
|
|
//! }
|
|
//! .run();
|
|
//! ```
|
|
|
|
use std::time::Duration;
|
|
|
|
use hex::{FromHex, ToHex};
|
|
use pallet_revive::{AddressMapper, ExecReturnValue, InstantiateReturnValue};
|
|
use polkadot_sdk::frame_support::traits::Currency;
|
|
use polkadot_sdk::pallet_revive::{Config, DebugSettings, Pallet};
|
|
use polkadot_sdk::*;
|
|
use polkadot_sdk::{
|
|
pallet_revive::ContractResult,
|
|
polkadot_runtime_common::BuildStorage,
|
|
polkadot_sdk_frame::testing_prelude::*,
|
|
sp_core::{H160, H256},
|
|
sp_keystore::{testing::MemoryKeystore, KeystoreExt},
|
|
sp_runtime::AccountId32,
|
|
};
|
|
#[cfg(feature = "resolc")]
|
|
use revive_solc_json_interface::SolcStandardJsonInputSettingsLibraries;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::runtime::*;
|
|
pub use crate::specs::*;
|
|
|
|
mod runtime;
|
|
mod specs;
|
|
|
|
#[cfg(not(feature = "resolc"))]
|
|
pub(crate) const NO_SOLIDITY_FRONTEND: &str =
|
|
"revive-runner was built without the solidity frontend; please enable the 'solidity' feature!";
|
|
|
|
/// The alice test account
|
|
pub const ALICE: H160 = H160([1u8; 20]);
|
|
/// The bob test account
|
|
pub const BOB: H160 = H160([2u8; 20]);
|
|
/// The charlie test account
|
|
pub const CHARLIE: H160 = H160([3u8; 20]);
|
|
/// Default gas limit
|
|
pub const GAS_LIMIT: Weight = Weight::from_parts(100_000_000_000_000, 3 * 1024 * 1024 * 1024);
|
|
/// Default deposit limit
|
|
pub const DEPOSIT_LIMIT: Balance = 100_000_000_000;
|
|
/// The native to ETH balance factor.
|
|
pub const ETH_RATIO: Balance = 1_000_000;
|
|
|
|
/// Externalities builder
|
|
#[derive(Default)]
|
|
pub struct ExtBuilder {
|
|
/// List of endowments at genesis
|
|
balance_genesis_config: Vec<(AccountId32, Balance)>,
|
|
}
|
|
|
|
impl ExtBuilder {
|
|
/// Set the balance of an account at genesis
|
|
fn balance_genesis_config(self, value: Vec<(H160, Balance)>) -> Self {
|
|
Self {
|
|
balance_genesis_config: value
|
|
.iter()
|
|
.map(|(address, balance)| (AccountId::to_fallback_account_id(address), *balance))
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
/// Build the externalities
|
|
pub fn build(self) -> sp_io::TestExternalities {
|
|
sp_tracing::try_init_simple();
|
|
|
|
let mut t = frame_system::GenesisConfig::<Runtime>::default()
|
|
.build_storage()
|
|
.unwrap();
|
|
|
|
pallet_revive::GenesisConfig::<Runtime> {
|
|
debug_settings: Some(
|
|
DebugSettings::default()
|
|
.set_allow_unlimited_contract_size(true)
|
|
.set_bypass_eip_3607(true)
|
|
.set_enable_pvm_logs(true),
|
|
),
|
|
..Default::default()
|
|
}
|
|
.assimilate_storage(&mut t)
|
|
.unwrap();
|
|
|
|
pallet_balances::GenesisConfig::<Runtime> {
|
|
balances: self.balance_genesis_config,
|
|
dev_accounts: None,
|
|
}
|
|
.assimilate_storage(&mut t)
|
|
.unwrap();
|
|
|
|
let mut ext = sp_io::TestExternalities::new(t);
|
|
let checking_account = Pallet::<Runtime>::account_id();
|
|
ext.register_extension(KeystoreExt::new(MemoryKeystore::new()));
|
|
ext.execute_with(|| {
|
|
let _ = <Runtime as Config>::Currency::deposit_creating(
|
|
&checking_account,
|
|
1_000_000_000_000,
|
|
);
|
|
|
|
System::set_block_number(1);
|
|
|
|
assert_ok!(Pallet::<Runtime>::map_account(RuntimeOrigin::signed(
|
|
checking_account
|
|
)));
|
|
});
|
|
|
|
ext
|
|
}
|
|
}
|
|
|
|
/// Expectation for a call. This struct is initialized by the user and compared to the actual call result.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct VerifyCallExpectation {
|
|
/// When provided, the expected gas consumed
|
|
pub gas_consumed: Option<u128>,
|
|
/// When provided, the expected output
|
|
#[serde(default, with = "hex")]
|
|
pub output: OptionalHex<Vec<u8>>,
|
|
/// Expected call result
|
|
pub success: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq)]
|
|
pub struct OptionalHex<T>(Option<T>);
|
|
|
|
impl<I: FromHex + AsRef<[u8]>> FromHex for OptionalHex<I> {
|
|
type Error = <I as FromHex>::Error;
|
|
|
|
fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
|
|
let value = I::from_hex(hex)?;
|
|
Ok(Self(Some(value)))
|
|
}
|
|
}
|
|
|
|
impl<I: AsRef<[u8]>> ToHex for &OptionalHex<I> {
|
|
fn encode_hex<T: std::iter::FromIterator<char>>(&self) -> T {
|
|
match self.0.as_ref() {
|
|
None => T::from_iter("".chars()),
|
|
Some(data) => I::encode_hex::<T>(data),
|
|
}
|
|
}
|
|
|
|
fn encode_hex_upper<T: std::iter::FromIterator<char>>(&self) -> T {
|
|
match self.0.as_ref() {
|
|
None => T::from_iter("".chars()),
|
|
Some(data) => I::encode_hex_upper(data),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: AsRef<[u8]>> From<T> for OptionalHex<T> {
|
|
fn from(value: T) -> Self {
|
|
if value.as_ref().is_empty() {
|
|
OptionalHex(None)
|
|
} else {
|
|
OptionalHex(Some(value))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for VerifyCallExpectation {
|
|
fn default() -> Self {
|
|
Self {
|
|
gas_consumed: None,
|
|
output: OptionalHex(None),
|
|
success: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl VerifyCallExpectation {
|
|
/// Verify that the expectations are met
|
|
fn verify(self, result: &CallResult) {
|
|
// Check if the call was successful. Here `self.success` is an expectation given by the user and `result` is the actual call result.
|
|
assert_eq!(
|
|
self.success,
|
|
!result.did_revert(),
|
|
"contract execution result mismatch: {result:?}"
|
|
);
|
|
|
|
if let Some(gas_consumed) = self.gas_consumed {
|
|
assert_eq!(gas_consumed, result.gas_consumed());
|
|
}
|
|
|
|
if let OptionalHex(Some(data)) = self.output {
|
|
assert_eq!(data, result.output());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Result of a call
|
|
#[derive(Clone, Debug)]
|
|
pub enum CallResult {
|
|
Exec {
|
|
result: ContractResult<ExecReturnValue, Balance>,
|
|
wall_time: Duration,
|
|
},
|
|
Instantiate {
|
|
result: ContractResult<InstantiateReturnValue, Balance>,
|
|
wall_time: Duration,
|
|
code_hash: H256,
|
|
},
|
|
}
|
|
|
|
impl CallResult {
|
|
/// Check if the call was successful
|
|
fn did_revert(&self) -> bool {
|
|
match self {
|
|
Self::Exec { result, .. } => result
|
|
.result
|
|
.as_ref()
|
|
.map(|r| r.did_revert())
|
|
.unwrap_or(true),
|
|
Self::Instantiate { result, .. } => result
|
|
.result
|
|
.as_ref()
|
|
.map(|r| r.result.did_revert())
|
|
.unwrap_or(true),
|
|
}
|
|
}
|
|
|
|
/// Get the output of the call
|
|
fn output(&self) -> Vec<u8> {
|
|
match self {
|
|
Self::Exec { result, .. } => result
|
|
.result
|
|
.as_ref()
|
|
.map(|r| r.data.clone())
|
|
.unwrap_or_default(),
|
|
Self::Instantiate { result, .. } => result
|
|
.result
|
|
.as_ref()
|
|
.map(|r| r.result.data.clone())
|
|
.unwrap_or_default(),
|
|
}
|
|
}
|
|
|
|
/// Get the gas consumed by the call
|
|
fn gas_consumed(&self) -> u128 {
|
|
match self {
|
|
Self::Exec { result, .. } => result.gas_consumed,
|
|
Self::Instantiate { result, .. } => result.gas_consumed,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub enum Code {
|
|
#[cfg(feature = "resolc")]
|
|
/// Compile a single solidity source and use the blob of `contract`
|
|
Solidity {
|
|
path: Option<std::path::PathBuf>,
|
|
solc_optimizer: Option<bool>,
|
|
contract: String,
|
|
#[serde(default)]
|
|
libraries: SolcStandardJsonInputSettingsLibraries,
|
|
},
|
|
/// Compile a raw Yul source file and use the blob of `contract`.
|
|
///
|
|
/// The PVM blob is produced via revive's Yul-to-LLVM path (bypassing solc's
|
|
/// Yul optimizer), and the EVM bytecode is produced via `solc --strict-assembly`
|
|
/// without `--optimize` so literal operands reach both runtimes intact.
|
|
#[cfg(feature = "resolc")]
|
|
Yul {
|
|
path: std::path::PathBuf,
|
|
contract: String,
|
|
},
|
|
/// Read the contract blob from disk
|
|
Path(std::path::PathBuf),
|
|
/// A contract blob
|
|
Bytes(Vec<u8>),
|
|
/// Pre-existing contract hash
|
|
Hash(crate::runtime::Hash),
|
|
}
|
|
|
|
impl Default for Code {
|
|
fn default() -> Self {
|
|
Self::Bytes(vec![])
|
|
}
|
|
}
|
|
|
|
impl From<Code> for pallet_revive::Code {
|
|
fn from(val: Code) -> Self {
|
|
match val {
|
|
#[cfg(feature = "solidity")]
|
|
Code::Solidity {
|
|
path,
|
|
contract,
|
|
solc_optimizer,
|
|
libraries,
|
|
} => {
|
|
let Some(path) = path else {
|
|
panic!("Solidity source of contract '{contract}' missing path");
|
|
};
|
|
let Ok(source_code) = std::fs::read_to_string(&path) else {
|
|
panic!("Failed to reead source code from {}", path.display());
|
|
};
|
|
pallet_revive::Code::Upload(resolc::test_utils::compile_blob_with_options(
|
|
&contract,
|
|
&source_code,
|
|
solc_optimizer.unwrap_or(true),
|
|
revive_llvm_context::OptimizerSettings::cycles(),
|
|
libraries,
|
|
))
|
|
}
|
|
#[cfg(feature = "solidity")]
|
|
Code::Yul { path, contract } => {
|
|
let Ok(source_code) = std::fs::read_to_string(&path) else {
|
|
panic!("Failed to read Yul source from {}", path.display());
|
|
};
|
|
pallet_revive::Code::Upload(resolc::test_utils::compile_yul_blob(
|
|
&contract,
|
|
&source_code,
|
|
))
|
|
}
|
|
Code::Path(path) => pallet_revive::Code::Upload(std::fs::read(path).unwrap()),
|
|
Code::Bytes(bytes) => pallet_revive::Code::Upload(bytes),
|
|
Code::Hash(hash) => pallet_revive::Code::Existing(hash),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::*;
|
|
|
|
#[test]
|
|
fn instantiate_works() {
|
|
use specs::SpecsAction::*;
|
|
let specs = Specs {
|
|
differential: false,
|
|
balances: vec![(ALICE, 1_000_000_000)],
|
|
actions: vec![Instantiate {
|
|
origin: TestAddress::Alice,
|
|
value: 0,
|
|
gas_limit: Some(GAS_LIMIT),
|
|
storage_deposit_limit: Some(DEPOSIT_LIMIT),
|
|
code: Code::Bytes(include_bytes!("../fixtures/Baseline.pvm").to_vec()),
|
|
data: vec![],
|
|
salt: OptionalHex::default(),
|
|
}],
|
|
};
|
|
specs.run();
|
|
}
|
|
|
|
#[test]
|
|
fn instantiate_with_json() {
|
|
serde_json::from_str::<Specs>(
|
|
r#"
|
|
{
|
|
"balances": [
|
|
[ "0101010101010101010101010101010101010101", 1000000000 ]
|
|
],
|
|
"actions": [
|
|
{
|
|
"Instantiate": {
|
|
"origin": "Alice",
|
|
"value": 0,
|
|
"code": {
|
|
"Path": "fixtures/Baseline.pvm"
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
"#,
|
|
)
|
|
.unwrap()
|
|
.run();
|
|
}
|
|
}
|