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

runtime-sdk: Implement message emission gas #1662

Merged
merged 1 commit into from
Mar 15, 2024
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
6 changes: 2 additions & 4 deletions client-sdk/ts-web/rt/playground/src/consensus.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,7 @@ export const playground = (async function () {
})
.setSignerInfo([siAlice1])
.setFeeAmount(FEE_FREE)
.setFeeGas(0n)
.setFeeConsensusMessages(1);
.setFeeGas(78n); // Enough to emit 1 consensus message (max_batch_gas / max_messages = 10_000 / 128).
await twDeposit.sign([csAlice], consensusChainContext);

const addrAliceBech32 = oasis.staking.addressToBech32(aliceAddr);
Expand Down Expand Up @@ -282,8 +281,7 @@ export const playground = (async function () {
})
.setSignerInfo([siDave2])
.setFeeAmount(FEE_FREE)
.setFeeGas(0n)
.setFeeConsensusMessages(1);
.setFeeGas(78n); // Enough to emit 1 consensus message (max_batch_gas / max_messages = 10_000 / 128).
await twWithdraw.sign([csDave], consensusChainContext);

/** @type {oasisRT.types.ConsensusAccountsWithdrawEvent} */
Expand Down
3 changes: 1 addition & 2 deletions runtime-sdk/modules/evm/src/raw_tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,7 @@ pub fn decode(
fee: transaction::Fee {
amount: token::BaseUnits(resolved_fee_amount, token::Denomination::NATIVE),
gas: gas_limit,
// TODO: Allow customization, maybe through call data?
consensus_messages: 1,
consensus_messages: 0, // Dynamic number of consensus messages, limited by gas.
},
..Default::default()
},
Expand Down
11 changes: 10 additions & 1 deletion runtime-sdk/src/modules/consensus/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
address::{Address, SignatureAddressSpec},
message::MessageEventHookInvocation,
token,
transaction::AddressSpec,
transaction::{AddressSpec, CallerAddress},
},
Runtime,
};
Expand Down Expand Up @@ -384,6 +384,15 @@
fn ensure_compatible_tx_signer() -> Result<(), Error> {
CurrentState::with_env(|env| match env.tx_auth_info().signer_info[0].address_spec {
AddressSpec::Signature(SignatureAddressSpec::Ed25519(_)) => Ok(()),
AddressSpec::Internal(CallerAddress::Address(_)) if env.is_simulation() => {

Check warning on line 387 in runtime-sdk/src/modules/consensus/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/consensus/mod.rs#L387

Added line #L387 was not covered by tests
// During simulations, the caller may be overriden in case of confidential runtimes
// which would cause this check to always fail, making gas estimation incorrect.
//
// Note that this is optimistic as a `CallerAddres::Address(_)` can still be
// incompatible, but as long as this is only allowed during simulations it shouldn't
// result in any problems.
Ok(())

Check warning on line 394 in runtime-sdk/src/modules/consensus/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/consensus/mod.rs#L394

Added line #L394 was not covered by tests
}
_ => Err(Error::ConsensusIncompatibleSigner),
})
}
Expand Down
45 changes: 34 additions & 11 deletions runtime-sdk/src/modules/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1106,38 +1106,61 @@
}

fn after_handle_call<C: Context>(
_ctx: &C,
ctx: &C,
result: module::CallResult,
) -> Result<module::CallResult, Error> {
// Skip handling for internally generated calls.
if CurrentState::with_env(|env| env.is_internal()) {
return Ok(result);
}

// Charge storage update gas cost if this would be greater than the gas use.
let params = Self::params();
if params.gas_costs.storage_byte > 0 {

// Compute storage update gas cost.
let storage_gas = if params.gas_costs.storage_byte > 0 {
let storage_update_bytes =
CurrentState::with(|state| state.pending_store_update_byte_size());
let storage_gas = params
params
.gas_costs
.storage_byte
.saturating_mul(storage_update_bytes as u64);
let used_gas = Self::used_tx_gas();
.saturating_mul(storage_update_bytes as u64)

Check warning on line 1126 in runtime-sdk/src/modules/core/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/core/mod.rs#L1126

Added line #L1126 was not covered by tests
} else {
0
};

if storage_gas > used_gas {
Self::use_tx_gas(storage_gas - used_gas)?;
}
}
// Compute message gas cost.
let message_gas = {

Check warning on line 1132 in runtime-sdk/src/modules/core/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/core/mod.rs#L1132

Added line #L1132 was not covered by tests
let emitted_message_count =
CurrentState::with(|state| state.emitted_messages_local_count());

Check warning on line 1134 in runtime-sdk/src/modules/core/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/core/mod.rs#L1134

Added line #L1134 was not covered by tests
// Determine how much each message emission costs based on max_batch_gas and the number
// of messages that can be emitted per batch.
let message_gas_cost = params
.max_batch_gas

Check warning on line 1138 in runtime-sdk/src/modules/core/mod.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/src/modules/core/mod.rs#L1138

Added line #L1138 was not covered by tests
.checked_div(ctx.max_messages().into())
.unwrap_or(u64::MAX); // If no messages are allowed, cost is infinite.
message_gas_cost.saturating_mul(emitted_message_count as u64)
};

// Emit gas used event (if this is not an internally generated call).
// Compute the gas amount that the transaction should pay in the end.
let used_gas = Self::used_tx_gas();
let max_gas = std::cmp::max(used_gas, std::cmp::max(storage_gas, message_gas));

// Make sure the transaction actually pays for the maximum gas. Note that failure here is
// fine since the extra resources (storage updates or emitted consensus messages) have not
// actually been spent yet (this happens at the end of the round).
let maybe_out_of_gas = Self::use_tx_gas(max_gas - used_gas); // Cannot overflow as max_gas >= used_gas.

// Emit gas used event.
if Cfg::EMIT_GAS_USED_EVENTS {
let used_gas = Self::used_tx_gas();
CurrentState::with(|state| {
state.emit_unconditional_event(Event::GasUsed { amount: used_gas });
});
}

// Evaluate the result of the above `use_tx_gas` here to make sure we emit the event.
maybe_out_of_gas?;

Ok(result)
}
}
Expand Down
157 changes: 155 additions & 2 deletions runtime-sdk/src/modules/core/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use once_cell::unsync::Lazy;

use crate::{
context::Context,
core::common::version::Version,
core::{
common::{version::Version, versioned::Versioned},
consensus::{roothash, staking},
},
crypto::multisig,
error::Error,
event::IntoTags,
Expand All @@ -16,7 +19,10 @@ use crate::{
sender::SenderMeta,
state::{self, CurrentState, Options},
testing::{configmap, keys, mock},
types::{address::Address, token, transaction, transaction::CallerAddress},
types::{
address::Address, message::MessageEventHookInvocation, token, transaction,
transaction::CallerAddress,
},
};

use super::{types, Event, Parameters, API as _};
Expand Down Expand Up @@ -206,6 +212,7 @@ impl GasWasterModule {
const METHOD_SPECIFIC_GAS_REQUIRED_HUGE: &'static str = "test.SpecificGasRequiredHuge";
const METHOD_STORAGE_UPDATE: &'static str = "test.StorageUpdate";
const METHOD_STORAGE_REMOVE: &'static str = "test.StorageRemove";
const METHOD_EMIT_CONSENSUS_MESSAGE: &'static str = "test.EmitConsensusMessage";
}

#[sdk_derive(Module)]
Expand Down Expand Up @@ -318,6 +325,27 @@ impl GasWasterModule {
CurrentState::with_store(|store| store.remove(&args));
Ok(())
}

#[handler(call = Self::METHOD_EMIT_CONSENSUS_MESSAGE)]
fn emit_consensus_message<C: Context>(
ctx: &C,
count: u64,
) -> Result<(), <GasWasterModule as module::Module>::Error> {
<C::Runtime as Runtime>::Core::use_tx_gas(2)?;
CurrentState::with(|state| {
for _ in 0..count {
state.emit_message(
ctx,
roothash::Message::Staking(Versioned::new(
0,
roothash::StakingMessage::Transfer(staking::Transfer::default()),
)),
MessageEventHookInvocation::new("test".to_string(), ""),
)?;
}
Ok(())
})
}
}

impl module::BlockHandler for GasWasterModule {}
Expand Down Expand Up @@ -1125,6 +1153,7 @@ fn test_module_info() {
MethodHandlerInfo { kind: types::MethodHandlerKind::Call, name: "test.SpecificGasRequiredHuge".to_string() },
MethodHandlerInfo { kind: types::MethodHandlerKind::Call, name: "test.StorageUpdate".to_string() },
MethodHandlerInfo { kind: types::MethodHandlerKind::Call, name: "test.StorageRemove".to_string() },
MethodHandlerInfo { kind: types::MethodHandlerKind::Call, name: "test.EmitConsensusMessage".to_string() },
],
},
}
Expand Down Expand Up @@ -1376,3 +1405,127 @@ fn test_storage_gas() {
assert_eq!(events.len(), 1); // Just one gas used event.
assert_eq!(events[0].amount, expected_gas_use);
}

#[test]
fn test_message_gas() {
let mut mock = mock::Mock::default();
let max_messages = 32;
mock.max_messages = max_messages;

let ctx = mock.create_ctx_for_runtime::<GasWasterRuntime>(false);

GasWasterRuntime::migrate(&ctx);

let max_batch_gas = 10_000;
Core::set_params(Parameters {
max_batch_gas,
gas_costs: super::GasCosts {
tx_byte: 0,
..Default::default()
},
..Core::params()
});

let mut signer = mock::Signer::new(0, keys::alice::sigspec());

// Emit 10 messages which is greater than the transaction compute gas cost.
let num_messages = 10u64;
let mut total_messages = num_messages;
let expected_gas_use = num_messages * (max_batch_gas / (max_messages as u64));
let dispatch_result = signer.call_opts(
&ctx,
GasWasterModule::METHOD_EMIT_CONSENSUS_MESSAGE,
num_messages,
mock::CallOptions {
fee: transaction::Fee {
gas: 10_000,
..Default::default()
},
},
);
assert!(dispatch_result.result.is_success(), "call should succeed");

// Simulate multiple transactions in a batch by not taking any messages.

let tags = &dispatch_result.tags;
assert_eq!(tags.len(), 1, "one event should have been emitted");
assert_eq!(tags[0].key, b"core\x00\x00\x00\x01"); // core.GasUsed (code = 1) event

#[derive(Debug, Default, cbor::Decode)]
struct GasUsedEvent {
amount: u64,
}

let events: Vec<GasUsedEvent> = cbor::from_slice(&tags[0].value).unwrap();
assert_eq!(events.len(), 1); // Just one gas used event.
assert_eq!(events[0].amount, expected_gas_use);

// Emit no messages so just the compute gas cost should be charged.
let num_messages = 0u64;
total_messages += num_messages;
let expected_gas_use = 2; // Just compute gas cost.
let dispatch_result = signer.call_opts(
&ctx,
GasWasterModule::METHOD_EMIT_CONSENSUS_MESSAGE,
num_messages,
mock::CallOptions {
fee: transaction::Fee {
gas: 10_000,
..Default::default()
},
},
);
assert!(dispatch_result.result.is_success(), "call should succeed");

let tags = &dispatch_result.tags;
assert_eq!(tags.len(), 1, "one event should have been emitted");
assert_eq!(tags[0].key, b"core\x00\x00\x00\x01"); // core.GasUsed (code = 1) event

let events: Vec<GasUsedEvent> = cbor::from_slice(&tags[0].value).unwrap();
assert_eq!(events.len(), 1); // Just one gas used event.
assert_eq!(events[0].amount, expected_gas_use);

// Take all messages emitted by the above two transactions.
let messages = CurrentState::with(|state| state.take_messages());
assert_eq!(total_messages as usize, messages.len());

// Ensure gas estimation works.
let num_messages = 10;
let expected_gas_use = num_messages * (max_batch_gas / (max_messages as u64));
let tx = transaction::Transaction {
version: 1,
call: transaction::Call {
format: transaction::CallFormat::Plain,
method: GasWasterModule::METHOD_EMIT_CONSENSUS_MESSAGE.to_owned(),
body: cbor::to_value(num_messages),
..Default::default()
},
auth_info: transaction::AuthInfo {
signer_info: vec![transaction::SignerInfo::new_sigspec(
keys::alice::sigspec(),
0,
)],
fee: transaction::Fee {
amount: token::BaseUnits::new(0, token::Denomination::NATIVE),
gas: u64::MAX,
consensus_messages: 0,
},
..Default::default()
},
};
let estimated_gas: u64 = signer
.query(
&ctx,
"core.EstimateGas",
types::EstimateGasQuery {
caller: None,
tx,
propagate_failures: false,
},
)
.expect("gas estimation should succeed");
assert_eq!(
estimated_gas, expected_gas_use,
"gas should be estimated correctly"
);
}
Loading
Loading