Skip to content
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
266 changes: 49 additions & 217 deletions executor/crates/common/tests/fees_abi.rs
Original file line number Diff line number Diff line change
@@ -1,230 +1,62 @@
use genlayer_sdk::calldata::{self, codec::Decode as _};
use genvm_modules_interfaces::fees::{
ExternalMessageParams, InternalMessageParams, MessageAllocationNode,
MessageAllocationNodeParams,
InternalMessageParams, MessageAllocationNode, MessageAllocationNodeParams,
};
use primitive_types::U256;

/// Concatenates 32-byte big-endian ABI words into one buffer.
fn words(values: &[U256]) -> Vec<u8> {
let mut buf = Vec::with_capacity(values.len() * 32);
for v in values {
buf.extend_from_slice(&v.to_big_endian());
}
buf
}

/// Reads the `i`-th 32-byte ABI word from an encoded buffer.
fn word(buf: &[u8], i: usize) -> U256 {
U256::from_big_endian(&buf[i * 32..i * 32 + 32])
}

fn external_node(
recipient: Option<[u8; 20]>,
call_key: Option<[u8; 32]>,
budget: u64,
gas_limit: u64,
max_gas_price: u64,
children: Vec<MessageAllocationNode>,
) -> MessageAllocationNode {
MessageAllocationNode {
recipient: recipient.map(genlayer_sdk::calldata::Address::from),
call_key: call_key.map(genvm_modules_interfaces::CallKey),
budget: U256::from(budget),
// External messages have no acceptance/finalize lifecycle; value is unused.
on: genvm_modules_interfaces::On::Finalized,
fee_params: MessageAllocationNodeParams::External(ExternalMessageParams {
gas_limit: U256::from(gas_limit),
max_gas_price: U256::from(max_gas_price),
}),
children,
}
}

fn internal_node(
on: genvm_modules_interfaces::On,
budget: u64,
rotations: &[u64],
children: Vec<MessageAllocationNode>,
) -> MessageAllocationNode {
MessageAllocationNode {
#[test]
fn flat_allocation_preserves_opaque_payload_and_requires_descendant_budget() {
let allocation = MessageAllocationNode {
recipient: None,
call_key: None,
budget: U256::from(budget),
on,
budget: Some(U256::from(100)),
on: genvm_modules_interfaces::On::Decided,
fee_params: MessageAllocationNodeParams::Internal(std::sync::Arc::new(
InternalMessageParams {
leader_timeunits_allocation: U256::from(1),
validator_timeunits_allocation: U256::from(2),
execution_budget_per_round: U256::from(3),
rotations: rotations.iter().map(|r| U256::from(*r)).collect(),
max_price_gen_per_time_unit: U256::from(11),
storage_fee_max_gas_price: U256::from(12),
receipt_fee_max_gas_price: U256::from(13),
leader_timeunits_allocation: U256::one(),
validator_timeunits_allocation: U256::one(),
execution_budget_per_round: U256::one(),
rotations: vec![U256::zero()],
max_price_gen_per_time_unit: U256::one(),
storage_fee_max_gas_price: U256::one(),
receipt_fee_max_gas_price: U256::one(),
},
)),
children,
children_budget: U256::from(60),
subtree: vec![0, 255, 7, 0, 128].into(),
};
let value = calldata::to_value(&allocation);
let decoded =
MessageAllocationNode::decode(calldata::codec::ValueDeserializer(value.clone())).unwrap();
assert_eq!(decoded.children_budget, allocation.children_budget);
assert_eq!(decoded.subtree, allocation.subtree);
assert_eq!(decoded.budget, allocation.budget);

for budget in [Some(U256::zero()), None] {
let mut allocation = allocation.clone();
allocation.budget = budget;
let decoded = MessageAllocationNode::decode(calldata::codec::ValueDeserializer(
calldata::to_value(&allocation),
))
.unwrap();
assert_eq!(decoded.budget, budget);
let decoded: MessageAllocationNode =
serde_json::from_value(serde_json::to_value(&allocation).unwrap()).unwrap();
assert_eq!(decoded.budget, budget);
}
}

// -- Exact wire layout -----------------------------------------------

#[test]
fn external_root_node_matches_exact_encoding() {
let recipient = [0x11u8; 20];
let root = external_node(Some(recipient), None, 5, 7, 9, vec![]);
let encoded = root.abi_encode();

// `abi.encode(MessageAllocationNode[])`: array offset, length, element
// offset, then the 10-word element tuple
// (messageType=External, onAcceptance=false, parent=sentinel, recipient,
// callKey wildcard, budget, feeParams offset, feeParams len, gasLimit, maxGasPrice).
let expected = words(&[
U256::from(0x20), // offset to array
U256::from(1), // array length
U256::from(0x20), // element[0] head offset
U256::from(0), // messageType = External
U256::from(0), // onAcceptance = false
U256::MAX, // parentIndex = NODE_ROOT_SENTINEL
U256::from_big_endian(&recipient), // recipient (left-padded)
U256::from_big_endian(&genvm_modules_interfaces::fees::CALL_KEY_WILDCARD.0),
U256::from(5), // budget
U256::from(0xE0), // feeParams offset (7 head words)
U256::from(64), // feeParams bytes length
U256::from(7), // gasLimit
U256::from(9), // maxGasPrice
]);

assert_eq!(encoded, expected);
}

// -- Tree flattening to parent-pointer form --------------------------

#[test]
fn nested_internal_flattens_with_parent_pointers() {
let grandchild = external_node(Some([0x44u8; 20]), None, 1, 100, 200, vec![]);
let first_child = external_node(Some([0x22u8; 20]), None, 2, 100, 200, vec![grandchild]);
let second_child = external_node(Some([0x33u8; 20]), None, 3, 100, 200, vec![]);
let root = internal_node(
genvm_modules_interfaces::On::Decided,
10,
&[2, 3],
vec![first_child, second_child],
);

let encoded = root.abi_encode();

assert_eq!(word(&encoded, 0), U256::from(0x20));
assert_eq!(word(&encoded, 1), U256::from(4), "four flattened nodes");

// Heads region begins right after the array length word, and the
// per-element offsets there are relative to it.
let heads_base = 2 * 32;
let element_idx = |index: usize| (heads_base + word(&encoded, 2 + index).as_usize()) / 32;
let root_idx = element_idx(0);
let first_child_idx = element_idx(1);
let second_child_idx = element_idx(2);
let grandchild_idx = element_idx(3);

// Root: messageType Internal (1), onAcceptance true, parent = sentinel.
assert_eq!(
word(&encoded, root_idx),
U256::from(1),
"root messageType Internal"
);
assert_eq!(
word(&encoded, root_idx + 1),
U256::one(),
"root onAcceptance"
);
assert_eq!(
word(&encoded, root_idx + 2),
U256::MAX,
"root parent = sentinel"
);

// Both children precede the grandchild in BFS order.
assert_eq!(
word(&encoded, first_child_idx + 2),
U256::zero(),
"first child parent index 0"
);
assert_eq!(
word(&encoded, second_child_idx + 2),
U256::zero(),
"second child parent index 0"
);
assert_eq!(
word(&encoded, grandchild_idx + 2),
U256::one(),
"grandchild parent index 1"
);
}

// -- Chain-derived feeParams fields ----------------------------------

#[test]
fn internal_params_encode_derived_appeal_rounds() {
// appealRounds is not stored on the Rust side; it is reconstructed as
// len(rotations) - 1 when encoding.
let root = internal_node(
genvm_modules_interfaces::On::Finalized,
10,
&[2, 3, 4],
vec![],
);
let encoded = root.abi_encode();

// Walk to the feeParams bytes inside the single element.
let heads_base = 2 * 32;
let elem_idx = (heads_base + word(&encoded, 2).as_usize()) / 32;
// element head is 7 words; the feeParams offset word is the 7th (elem_idx + 6).
let fee_params_len_idx = elem_idx + word(&encoded, elem_idx + 6).as_usize() / 32;
// feeParams = [len][ abi.encode(InternalMessageParams) ], tuple starts after len.
let inner_base = fee_params_len_idx + 1;

// InternalMessageParams tuple (v0.6-dev, 8 head words):
// [offset][leader][validator][appealRounds][exec][rot off][maxPriceGen]
// [storageCap][receiptCap][rot len][rot...]
assert_eq!(
word(&encoded, inner_base),
U256::from(0x20),
"struct offset"
);
assert_eq!(word(&encoded, inner_base + 1), U256::from(1), "leader");
assert_eq!(word(&encoded, inner_base + 2), U256::from(2), "validator");
assert_eq!(
word(&encoded, inner_base + 3),
U256::from(2),
"appealRounds = 3 - 1"
);
assert_eq!(
word(&encoded, inner_base + 4),
U256::from(3),
"execBudgetPerRound"
);
assert_eq!(
word(&encoded, inner_base + 5),
U256::from(0x100),
"rotations offset (8 head words)"
);
assert_eq!(
word(&encoded, inner_base + 6),
U256::from(11),
"maxPriceGenPerTimeUnit"
);
assert_eq!(
word(&encoded, inner_base + 7),
U256::from(12),
"storageFeeMaxGasPrice"
);
assert_eq!(
word(&encoded, inner_base + 8),
U256::from(13),
"receiptFeeMaxGasPrice"
);
assert_eq!(
word(&encoded, inner_base + 9),
U256::from(3),
"rotations length"
);
for field in ["children_budget", "subtree"] {
let calldata::Value::Map(mut fields) = value.clone() else {
panic!("allocation must encode as a map")
};
fields.remove(field);
let error = MessageAllocationNode::decode(calldata::codec::ValueDeserializer(
calldata::Value::Map(fields),
))
.unwrap_err();
assert!(
error.to_string().contains(field),
"unexpected error: {error}"
);
}
}
16 changes: 12 additions & 4 deletions executor/install/config/genvm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,14 @@ fees:
else
let overlaySplit =
idiv (timeUnitPool * node.overlaySplitBps) (10000 - node.overlaySplitBps) in
# 7. minimum primary fees
let minPrimaryFees = timeUnitPool + overlaySplit + executionTerm in
# 7. Successful appeals return the bond plus floor(1.5 * bond) profit
let appealProfitReserve = foldRange 1 (appealRounds + 1) 0 (\acc i =
let bond = (arrayGetElem rotations i + 1)
* (leaderTU + arrayGetElem validatorsPerRound (2 * i) * validatorTU) in
let pricedBond = if multiplier > 0 then multiplier * bond else bond in
acc + pricedBond + idiv pricedBond 2) in
# The appeal-profit reserve is not subject to the developer/DAO overlay
let minPrimaryFees = timeUnitPool + overlaySplit + executionTerm + appealProfitReserve in
minPrimaryFees
in
storage:
Expand Down Expand Up @@ -175,12 +181,13 @@ fees:
# is conservative and avoids depending on RLP prefix lengths
let codeBytes = if a.isDeploy then 32 + 32 + ceilDiv a.codeLength 32 * 32 else 0 in
let abiBytes = fixedBytes + calldataBytes + feeParamsBytes + subtreeBytes + codeBytes in
[ # execution_data_gas
let validReceiptPrice = if a.isInternal then true else node.lockedReceiptGasPrice > 0 in
if validReceiptPrice then [ # execution_data_gas
revealGas + abiBytes * node.receiptGasPerByte + node.gasPerChangedSlot
# submitted_messages
, revealBytes + abiBytes
# submitted_messages_count
, 1]
, 1] else vmError "fee below_minimum"
nondet_output:
# Gas cost and conservative compact LeaderPublicData size
buckets: [execution_data_gas, nondet_outputs]
Expand Down Expand Up @@ -240,6 +247,7 @@ fees:
else if budgetTooLow then vmError "fee below_minimum"
else messageFeeFloor a.matchedFeeParams
# external messages reserve gasLimit at the effective chain price
else if node.lockedReceiptGasPrice <= 0 then vmError "fee below_minimum"
else a.matchedFeeParams.gasLimit
* (if node.lockedReceiptGasPrice < a.matchedFeeParams.maxGasPrice
then node.lockedReceiptGasPrice
Expand Down
9 changes: 9 additions & 0 deletions executor/src/exe/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,15 @@ pub fn handle(args: Args, mut config: config::Config) -> Result<()> {
})
};
if let Some(setup_run_ok) = setup_run_ok {
let setup_run_ok = match genvm::validate_leader_output_count(
shared_data.run_mode,
0,
leader_nondet_results.as_ref().map_or(0, Vec::len),
&setup_run_ok,
) {
Some(error) => genvm::rt::vm::RunOk::FatalVMError(error, None),
None => setup_run_ok,
};
let host_for = |method: genvm::host::host_fns::Methods| -> usize {
let m = method as usize;
if m < method_hosts.len() {
Expand Down
25 changes: 17 additions & 8 deletions executor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,19 +271,28 @@ fn extra_leader_nondet_output_error(
supervisor: &rt::supervisor::Supervisor,
run_ok: &rt::vm::RunOk,
) -> Option<public_abi::VmError> {
if matches!(run_ok, rt::vm::RunOk::FatalVMError(..)) {
return None;
}

let executed = supervisor
.nondet_call_no
.load(std::sync::atomic::Ordering::SeqCst);
let published = supervisor
.leader_nondet_results
.as_ref()
.map_or(0, |v| v.len().into_int_downcast_panicking());
.map_or(0, Vec::len);

validate_leader_output_count(supervisor.shared_data.run_mode, executed, published, run_ok)
}

pub fn validate_leader_output_count(
run_mode: rt::RunMode,
executed: u32,
published: usize,
run_ok: &rt::vm::RunOk,
) -> Option<public_abi::VmError> {
if matches!(run_ok, rt::vm::RunOk::FatalVMError(..)) {
return None;
}

if !has_extra_leader_output(supervisor.shared_data.run_mode, executed, published) {
if !has_extra_leader_output(run_mode, executed, published) {
return None;
}

Expand All @@ -293,8 +302,8 @@ fn extra_leader_nondet_output_error(
Some(rt::errors::vm_error_for_leader_extra(encoded.as_slice()))
}

fn has_extra_leader_output(run_mode: rt::RunMode, executed: u32, published: u32) -> bool {
run_mode != rt::RunMode::Leader && published > executed
fn has_extra_leader_output(run_mode: rt::RunMode, executed: u32, published: usize) -> bool {
run_mode != rt::RunMode::Leader && published > executed as usize
}

pub async fn run_with_impl(
Expand Down
3 changes: 1 addition & 2 deletions executor/src/rt/vm/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -618,8 +618,7 @@ impl<HS: HostStorageLocking + Send + Sync> Storage<HS> {
code_slot: SlotID,
code_size: u32,
) -> rt::errors::Result<Box<[u8]>> {
let res = Box::new_uninit_slice(code_size.into_int_comptime());
let mut res = unsafe { res.assume_init() };
let mut res = vec![0u8; code_size.into_int_comptime()].into_boxed_slice();

self.read(code_slot, 4, &mut res).await?;

Expand Down
Loading