Skip to content
This repository was archived by the owner on Jun 11, 2026. It is now read-only.
Closed
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
10 changes: 10 additions & 0 deletions crates/uplc/src/flat/decode/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ pub struct Ctx<'a> {
}

impl<'a> Ctx<'a> {
/// Returns true when constr/case terms are allowed.
///
/// Both the protocol version (>= 9, i.e. Conway) and the program version
/// (>= 1.1.0) must permit them.
pub fn is_constr_case_available(&self) -> bool {
let protocol_ok = self.protocol_version.is_none_or(|pv| pv >= 9);
let version_ok = self.version.is_none_or(|v| v.is_constr_case_available());
protocol_ok && version_ok
}

/// Returns true if the given builtin is NOT available under the current
/// plutus_version / protocol_version combination.
pub fn is_builtin_gated(&self, func: &DefaultFunction) -> bool {
Expand Down
4 changes: 2 additions & 2 deletions crates/uplc/src/flat/decode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ where
}
// Constr
tag::CONSTR => {
if ctx.version.is_some_and(|v| v.is_less_than_1_1_0()) {
if !ctx.is_constr_case_available() {
return Err(FlatDecodeError::TermNotAvailable(tag::CONSTR, "constr"));
}

Expand All @@ -175,7 +175,7 @@ where
}
// Case
tag::CASE => {
if ctx.version.is_some_and(|v| v.is_less_than_1_1_0()) {
if !ctx.is_constr_case_available() {
return Err(FlatDecodeError::TermNotAvailable(tag::CASE, "case"));
}

Expand Down
139 changes: 139 additions & 0 deletions crates/uplc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod tests {
use pretty_assertions::assert_eq;

use crate::binder::DeBruijn;
use crate::machine::{default_v3_cost_model, ExBudget, PlutusVersion};
use crate::program::Version;

use super::arena::Arena;
Expand Down Expand Up @@ -137,4 +138,142 @@ mod tests {

assert_eq!(result.term.unwrap(), Term::integer_from(arena, 610));
}

// --- eval_with_params protocol_version gating tests ---

#[test]
fn eval_with_params_base_builtin_same_budget_across_protocol_versions() {
// add_integer is a base V3 builtin (positions 0-3 in the cost key list).
// Its costs should be identical regardless of protocol_version since they
// are always included in the base key section.
let arena = Arena::new();
let costs = default_v3_cost_model();

let term = Term::add_integer(&arena)
.apply(&arena, Term::integer_from(&arena, 1))
.apply(&arena, Term::integer_from(&arena, 3));
let version = Version::plutus_v3(&arena);
let program = Program::<DeBruijn>::new(&arena, version, term);

let r9 = program.eval_with_params(
&arena,
PlutusVersion::V3,
(9, 0),
&costs,
ExBudget::default(),
);
let r10 = program.eval_with_params(
&arena,
PlutusVersion::V3,
(10, 0),
&costs,
ExBudget::default(),
);
let r11 = program.eval_with_params(
&arena,
PlutusVersion::V3,
(11, 0),
&costs,
ExBudget::default(),
);

// All three should produce the correct result
assert_eq!(r9.term.unwrap(), Term::integer_from(&arena, 4));
assert_eq!(r10.term.unwrap(), Term::integer_from(&arena, 4));
assert_eq!(r11.term.unwrap(), Term::integer_from(&arena, 4));

// Base builtin budgets should be identical regardless of protocol version
assert_eq!(r9.info.consumed_budget, r10.info.consumed_budget);
assert_eq!(r10.info.consumed_budget, r11.info.consumed_budget);
}

#[test]
fn eval_with_params_plomin_builtin_succeeds_at_protocol_v10() {
// ripemd_160 is a Plomin builtin. With protocol_version >= 10,
// PLOMIN_KEYS are included in the cost map so the real costs apply.
let arena = Arena::new();
let costs = default_v3_cost_model();

let term =
Term::<DeBruijn>::ripemd_160(&arena).apply(&arena, Term::byte_string(&arena, b"test"));
let version = Version::plutus_v3(&arena);
let program = Program::<DeBruijn>::new(&arena, version, term);

let result = program.eval_with_params(
&arena,
PlutusVersion::V3,
(10, 0),
&costs,
ExBudget::default(),
);

assert!(
result.term.is_ok(),
"post-Plomin ripemd_160 should succeed with real costs"
);
}

#[test]
fn eval_with_params_plomin_builtin_exceeds_budget_at_protocol_v9() {
// With protocol_version < 10, PLOMIN_KEYS are NOT included. The ripemd_160
// cost keys are absent from the map, so the cost model falls back to the
// sentinel value (30_000_000_000) which exceeds the default budget.
let arena = Arena::new();
let costs = default_v3_cost_model();

let term =
Term::<DeBruijn>::ripemd_160(&arena).apply(&arena, Term::byte_string(&arena, b"test"));
let version = Version::plutus_v3(&arena);
let program = Program::<DeBruijn>::new(&arena, version, term);

let result = program.eval_with_params(
&arena,
PlutusVersion::V3,
(9, 0),
&costs,
ExBudget::default(),
);

assert!(
result.term.is_err(),
"pre-Plomin ripemd_160 should fail: sentinel costs exceed budget"
);
}

#[test]
fn eval_with_params_plomin_builtin_different_budget_by_protocol_version() {
// The same ripemd_160 program with protocol_version 10 vs 11 should both
// succeed, but protocol_version 11 also adds PV11_KEYS. Since the cost
// array only has 297 values (base + Plomin), PV11 keys get no values and
// fall back to sentinel. The ripemd_160 cost itself is the same in both
// cases because it's in PLOMIN_KEYS which are included at both PV 10 and 11.
let arena = Arena::new();
let costs = default_v3_cost_model();

let term =
Term::<DeBruijn>::ripemd_160(&arena).apply(&arena, Term::byte_string(&arena, b"test"));
let version = Version::plutus_v3(&arena);
let program = Program::<DeBruijn>::new(&arena, version, term);

let r10 = program.eval_with_params(
&arena,
PlutusVersion::V3,
(10, 0),
&costs,
ExBudget::default(),
);
let r11 = program.eval_with_params(
&arena,
PlutusVersion::V3,
(11, 0),
&costs,
ExBudget::default(),
);

assert!(r10.term.is_ok());
assert!(r11.term.is_ok());

// ripemd_160 cost should be identical at PV 10 and 11 (same PLOMIN keys)
assert_eq!(r10.info.consumed_budget, r11.info.consumed_budget);
}
}
2 changes: 1 addition & 1 deletion crates/uplc/src/machine/cek.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ impl<'a, B: BuiltinCostModel> Machine<'a, B> {
Err(MachineError::MissingCaseBranch(branches, value))
}
}
Value::Con(constant) if self.version.is_at_least_1_1_0() => {
Value::Con(constant) if self.version.is_constr_case_available() => {
let (tag, max_branches, fields) = self.constant_as_tag_fields(constant)?;

if branches.len() > max_branches {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ pub struct BuiltinCostsV1 {
mk_pair_data: TwoArgumentsCosting,
mk_nil_data: OneArgumentCosting,
mk_nil_pair_data: OneArgumentCosting,
// bitwise
ripemd_160: OneArgumentCosting,

exp_mod_integer: ThreeArgumentsCosting,
drop_list: TwoArgumentsCosting,
length_of_array: OneArgumentCosting,
list_to_array: TwoArgumentsCosting,
index_array: TwoArgumentsCosting,
}

impl Default for BuiltinCostsV1 {
Expand Down Expand Up @@ -285,6 +293,30 @@ impl Default for BuiltinCostsV1 {
OneArgumentCosting::constant_cost(32),
OneArgumentCosting::constant_cost(7391),
),
ripemd_160: OneArgumentCosting::new(
OneArgumentCosting::constant_cost(3),
OneArgumentCosting::linear_cost(1964219, 24520),
),
exp_mod_integer: ThreeArgumentsCosting::new(
ThreeArgumentsCosting::linear_in_z(0, 1),
ThreeArgumentsCosting::exp_mod_cost(607153, 231697, 53144),
),
drop_list: TwoArgumentsCosting::new(
TwoArgumentsCosting::constant_cost(4),
TwoArgumentsCosting::linear_in_x(116711, 1957),
),
length_of_array: OneArgumentCosting::new(
OneArgumentCosting::constant_cost(10),
OneArgumentCosting::constant_cost(198994),
),
list_to_array: TwoArgumentsCosting::new(
TwoArgumentsCosting::linear_in_x(7, 1),
TwoArgumentsCosting::linear_in_x(307802, 8496),
),
index_array: TwoArgumentsCosting::new(
TwoArgumentsCosting::constant_cost(32),
TwoArgumentsCosting::constant_cost(194922),
),
}
}
}
Expand Down Expand Up @@ -630,6 +662,44 @@ impl BuiltinCostModel for BuiltinCostsV1 {
cost_map["verify_ed25519_signature-cpu-arguments-slope"],
),
),
ripemd_160: OneArgumentCosting::new(
OneArgumentCosting::constant_cost(cost_map["ripemd_160-memory-arguments"]),
OneArgumentCosting::linear_cost(
cost_map["ripemd_160-cpu-arguments-intercept"],
cost_map["ripemd_160-cpu-arguments-slope"],
),
),

exp_mod_integer: ThreeArgumentsCosting::new(
ThreeArgumentsCosting::linear_in_z(0, 1),
ThreeArgumentsCosting::exp_mod_cost(607153, 231697, 53144),
),

drop_list: TwoArgumentsCosting::new(
TwoArgumentsCosting::constant_cost(cost_map["drop_list-mem-arguments"]),
TwoArgumentsCosting::linear_in_x(
cost_map["drop_list-cpu-arguments-intercept"],
cost_map["drop_list-cpu-arguments-slope"],
),
),
length_of_array: OneArgumentCosting::new(
OneArgumentCosting::constant_cost(cost_map["length_of_array-mem-arguments"]),
OneArgumentCosting::constant_cost(cost_map["length_of_array-cpu-arguments"]),
),
list_to_array: TwoArgumentsCosting::new(
TwoArgumentsCosting::linear_in_x(
cost_map["list_to_array-mem-arguments-intercept"],
cost_map["list_to_array-mem-arguments-slope"],
),
TwoArgumentsCosting::linear_in_x(
cost_map["list_to_array-cpu-arguments-intercept"],
cost_map["list_to_array-cpu-arguments-slope"],
),
),
index_array: TwoArgumentsCosting::new(
TwoArgumentsCosting::constant_cost(cost_map["index_array-mem-arguments"]),
TwoArgumentsCosting::constant_cost(cost_map["index_array-cpu-arguments"]),
),
}
}

Expand Down Expand Up @@ -851,6 +921,30 @@ impl BuiltinCostModel for BuiltinCostsV1 {
self.mk_nil_pair_data.mem.cost([args[0]]),
self.mk_nil_pair_data.cpu.cost([args[0]]),
)),
DefaultFunction::Ripemd_160 => Some(ExBudget::new(
self.ripemd_160.mem.cost([args[0]]),
self.ripemd_160.cpu.cost([args[0]]),
)),
DefaultFunction::ExpModInteger => Some(ExBudget::new(
self.exp_mod_integer.mem.cost([args[0], args[1], args[2]]),
self.exp_mod_integer.cpu.cost([args[0], args[1], args[2]]),
)),
DefaultFunction::DropList => Some(ExBudget::new(
self.drop_list.mem.cost([args[0], args[1]]),
self.drop_list.cpu.cost([args[0], args[1]]),
)),
DefaultFunction::LengthOfArray => Some(ExBudget::new(
self.length_of_array.mem.cost([args[0]]),
self.length_of_array.cpu.cost([args[0]]),
)),
DefaultFunction::ListToArray => Some(ExBudget::new(
self.list_to_array.mem.cost([args[0], args[1]]),
self.list_to_array.cpu.cost([args[0], args[1]]),
)),
DefaultFunction::IndexArray => Some(ExBudget::new(
self.index_array.mem.cost([args[0], args[1]]),
self.index_array.cpu.cost([args[0], args[1]]),
)),
_ => None,
}
}
Expand Down
Loading
Loading