Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Commit

Permalink
contracts: Allow to distinguish out of gas from other traps
Browse files Browse the repository at this point in the history
When a contract encounters a runtime error a wasm trap is
triggered and the execution is halted. Currently, no matter
what was the cause for the trap it is always reported as:
DispatchError::Other("contract trapped during execution").

However, the trap that is triggered if a contract exhausts
its gas budget is particulary interesting. Therefore we add
a seperate error message for this cause:
DispatchError::Other("ran out of gas during contract execution").

A test is added hat executes a contract that never terminates.
Therefore it always exhausts is gas budget.
  • Loading branch information
athei committed Feb 13, 2020
1 parent d78534e commit 8b5df5c
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 13 deletions.
4 changes: 2 additions & 2 deletions bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
// and set impl_version to 0. If only runtime
// implementation changes and behavior does not, then leave spec_version as
// is and increment impl_version.
spec_version: 216,
impl_version: 2,
spec_version: 217,
impl_version: 0,
apis: RUNTIME_API_VERSIONS,
};

Expand Down
45 changes: 45 additions & 0 deletions frame/contracts/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,51 @@ fn dispatch_call_not_dispatched_after_top_level_transaction_failure() {
});
}

const CODE_RUN_OUT_OF_GAS: &str = r#"
(module
(func (export "call")
(loop $inf (br $inf)) ;; just run out of gas
(unreachable)
)
(func (export "deploy"))
)
"#;

#[test]
fn run_out_of_gas() {
let (wasm, code_hash) = compile_module::<Test>(CODE_RUN_OUT_OF_GAS).unwrap();

ExtBuilder::default()
.existential_deposit(50)
.build()
.execute_with(|| {
Balances::deposit_creating(&ALICE, 1_000_000);

assert_ok!(Contract::put_code(Origin::signed(ALICE), 100_000, wasm));

assert_ok!(Contract::instantiate(
Origin::signed(ALICE),
100,
100_000,
code_hash.into(),
vec![],
));

// Call the contract with a fixed gas limit. It must run out of gas because it just
// loops forever.
assert_err!(
Contract::call(
Origin::signed(ALICE),
BOB, // newly created account
0,
1000,
vec![],
),
"ran out of gas during contract execution"
);
});
}

const CODE_SET_RENT: &str = r#"
(module
(import "env" "ext_dispatch_call" (func $ext_dispatch_call (param i32 i32)))
Expand Down
63 changes: 52 additions & 11 deletions frame/contracts/src/wasm/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const TRAP_RETURN_CODE: u32 = 0x0100;
enum SpecialTrap {
/// Signals that trap was generated in response to call `ext_return` host function.
Return(Vec<u8>),
/// Signals that trap was generated because the contract exhausted its gas limit.
OutOfGas,
}

/// Can only be used for one call.
Expand Down Expand Up @@ -74,9 +76,21 @@ pub(crate) fn to_execution_result<E: Ext>(
runtime: Runtime<E>,
sandbox_result: Result<sp_sandbox::ReturnValue, sp_sandbox::Error>,
) -> ExecResult {
// Special case. The trap was the result of the execution `return` host function.
if let Some(SpecialTrap::Return(data)) = runtime.special_trap {
return Ok(ExecReturnValue { status: STATUS_SUCCESS, data });
match runtime.special_trap {
// The trap was the result of the execution `return` host function.
Some(SpecialTrap::Return(data)) => {
return Ok(ExecReturnValue {
status: STATUS_SUCCESS,
data,
})
}
Some(SpecialTrap::OutOfGas) => {
return Err(ExecError {
reason: "ran out of gas during contract execution".into(),
buffer: runtime.scratch_buf,
})
}
_ => (),
}

// Check the exact type of the error.
Expand Down Expand Up @@ -179,11 +193,15 @@ impl<T: Trait> Token<T> for RuntimeToken {
fn charge_gas<T: Trait, Tok: Token<T>>(
gas_meter: &mut GasMeter<T>,
metadata: &Tok::Metadata,
special_trap: &mut Option<SpecialTrap>,
token: Tok,
) -> Result<(), sp_sandbox::HostError> {
match gas_meter.charge(metadata, token) {
GasMeterResult::Proceed => Ok(()),
GasMeterResult::OutOfGas => Err(sp_sandbox::HostError),
GasMeterResult::OutOfGas => {
*special_trap = Some(SpecialTrap::OutOfGas);
Err(sp_sandbox::HostError)
},
}
}

Expand All @@ -200,7 +218,12 @@ fn read_sandbox_memory<E: Ext>(
ptr: u32,
len: u32,
) -> Result<Vec<u8>, sp_sandbox::HostError> {
charge_gas(ctx.gas_meter, ctx.schedule, RuntimeToken::ReadMemory(len))?;
charge_gas(
ctx.gas_meter,
ctx.schedule,
&mut ctx.special_trap,
RuntimeToken::ReadMemory(len),
)?;

let mut buf = vec![0u8; len as usize];
ctx.memory.get(ptr, buf.as_mut_slice()).map_err(|_| sp_sandbox::HostError)?;
Expand All @@ -220,7 +243,12 @@ fn read_sandbox_memory_into_scratch<E: Ext>(
ptr: u32,
len: u32,
) -> Result<(), sp_sandbox::HostError> {
charge_gas(ctx.gas_meter, ctx.schedule, RuntimeToken::ReadMemory(len))?;
charge_gas(
ctx.gas_meter,
ctx.schedule,
&mut ctx.special_trap,
RuntimeToken::ReadMemory(len),
)?;

ctx.scratch_buf.resize(len as usize, 0);
ctx.memory.get(ptr, ctx.scratch_buf.as_mut_slice()).map_err(|_| sp_sandbox::HostError)?;
Expand All @@ -240,7 +268,12 @@ fn read_sandbox_memory_into_buf<E: Ext>(
ptr: u32,
buf: &mut [u8],
) -> Result<(), sp_sandbox::HostError> {
charge_gas(ctx.gas_meter, ctx.schedule, RuntimeToken::ReadMemory(buf.len() as u32))?;
charge_gas(
ctx.gas_meter,
ctx.schedule,
&mut ctx.special_trap,
RuntimeToken::ReadMemory(buf.len() as u32),
)?;

ctx.memory.get(ptr, buf).map_err(Into::into)
}
Expand Down Expand Up @@ -273,12 +306,18 @@ fn read_sandbox_memory_as<E: Ext, D: Decode>(
/// - designated area is not within the bounds of the sandbox memory.
fn write_sandbox_memory<T: Trait>(
schedule: &Schedule,
special_trap: &mut Option<SpecialTrap>,
gas_meter: &mut GasMeter<T>,
memory: &sp_sandbox::Memory,
ptr: u32,
buf: &[u8],
) -> Result<(), sp_sandbox::HostError> {
charge_gas(gas_meter, schedule, RuntimeToken::WriteMemory(buf.len() as u32))?;
charge_gas(
gas_meter,
schedule,
special_trap,
RuntimeToken::WriteMemory(buf.len() as u32),
)?;

memory.set(ptr, buf)?;

Expand All @@ -300,7 +339,7 @@ define_env!(Env, <E: Ext>,
//
// - amount: How much gas is used.
gas(ctx, amount: u32) => {
charge_gas(&mut ctx.gas_meter, ctx.schedule, RuntimeToken::Explicit(amount))?;
charge_gas(&mut ctx.gas_meter, ctx.schedule, &mut ctx.special_trap, RuntimeToken::Explicit(amount))?;
Ok(())
},

Expand Down Expand Up @@ -520,7 +559,7 @@ define_env!(Env, <E: Ext>,
//
// This is the only way to return a data buffer to the caller.
ext_return(ctx, data_ptr: u32, data_len: u32) => {
charge_gas(ctx.gas_meter, ctx.schedule, RuntimeToken::ReturnData(data_len))?;
charge_gas(ctx.gas_meter, ctx.schedule, &mut ctx.special_trap, RuntimeToken::ReturnData(data_len))?;

read_sandbox_memory_into_scratch(ctx, data_ptr, data_len)?;
let output_buf = mem::replace(&mut ctx.scratch_buf, Vec::new());
Expand Down Expand Up @@ -652,7 +691,7 @@ define_env!(Env, <E: Ext>,
let balance_fee = <<E as Ext>::T as Trait>::ComputeDispatchFee::compute_dispatch_fee(&call);
approx_gas_for_balance(ctx.gas_meter.gas_price(), balance_fee)
};
charge_gas(&mut ctx.gas_meter, ctx.schedule, RuntimeToken::ComputedDispatchFee(fee))?;
charge_gas(&mut ctx.gas_meter, ctx.schedule, &mut ctx.special_trap, RuntimeToken::ComputedDispatchFee(fee))?;

ctx.ext.note_dispatch_call(call);

Expand Down Expand Up @@ -756,6 +795,7 @@ define_env!(Env, <E: Ext>,
// Finally, perform the write.
write_sandbox_memory(
ctx.schedule,
&mut ctx.special_trap,
ctx.gas_meter,
&ctx.memory,
dest_ptr,
Expand Down Expand Up @@ -803,6 +843,7 @@ define_env!(Env, <E: Ext>,
charge_gas(
ctx.gas_meter,
ctx.schedule,
&mut ctx.special_trap,
RuntimeToken::DepositEvent(topics.len() as u32, data_len)
)?;
ctx.ext.deposit_event(topics, event_data);
Expand Down

0 comments on commit 8b5df5c

Please sign in to comment.