Skip to content

Commit 6ee057b

Browse files
scarmuegaclaude
andcommitted
fix: correct inverted native script timelock comparisons
`eval_native_script` evaluated both timelock opcodes with inverted comparisons. `InvalidBefore(n)` was satisfied when the validity interval started before slot `n` (should be at or after), and `InvalidHereafter(n)` when it ended after `n` (should be at or before), per mary-ledger.pdf p.20. Adds unit tests covering both opcodes and nested combinators. Fixes #718 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4c2eaac commit 6ee057b

1 file changed

Lines changed: 59 additions & 2 deletions

File tree

pallas-validate/src/phase1/shelley_ma.rs

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -727,15 +727,72 @@ fn eval_native_script(
727727
}
728728
NativeScript::InvalidBefore(val) => {
729729
match low_bnd {
730-
Some(time) => val >= time,
730+
// The script is satisfied when the transaction cannot be applied
731+
// before `val`, i.e. its validity interval starts at or after it.
732+
Some(time) => val <= time,
731733
None => false, // as per mary-ledger.pdf, p.20
732734
}
733735
}
734736
NativeScript::InvalidHereafter(val) => {
735737
match upp_bnd {
736-
Some(time) => val <= time,
738+
// The script is satisfied when the transaction cannot be applied
739+
// at or after `val`, i.e. its validity interval ends at or before it.
740+
Some(time) => val >= time,
737741
None => false, // as per mary-ledger.pdf, p.20
738742
}
739743
}
740744
}
741745
}
746+
747+
#[cfg(test)]
748+
mod tests {
749+
use super::eval_native_script;
750+
use pallas_primitives::alonzo::NativeScript;
751+
752+
// Evaluates `native_script` with no vkey witnesses and the given validity
753+
// interval bounds (`low_bnd` is `validity_interval_start`, `upp_bnd` is `ttl`).
754+
fn eval(native_script: &NativeScript, low_bnd: Option<u64>, upp_bnd: Option<u64>) -> bool {
755+
eval_native_script(&Vec::new(), native_script, &low_bnd, &upp_bnd)
756+
}
757+
758+
#[test]
759+
// `InvalidBefore(n)` holds only when the transaction's validity interval
760+
// starts at or after slot `n` (mary-ledger.pdf, p.20).
761+
fn invalid_before() {
762+
let script = NativeScript::InvalidBefore(100);
763+
assert!(eval(&script, Some(150), None), "starts after the lock");
764+
assert!(eval(&script, Some(100), None), "starts exactly at the lock");
765+
assert!(!eval(&script, Some(50), None), "starts before the lock");
766+
assert!(!eval(&script, None, None), "no lower bound");
767+
}
768+
769+
#[test]
770+
// `InvalidHereafter(n)` holds only when the transaction's validity interval
771+
// ends at or before slot `n` (mary-ledger.pdf, p.20).
772+
fn invalid_hereafter() {
773+
let script = NativeScript::InvalidHereafter(200);
774+
assert!(eval(&script, None, Some(150)), "ends before the lock");
775+
assert!(eval(&script, None, Some(200)), "ends exactly at the lock");
776+
assert!(!eval(&script, None, Some(250)), "ends after the lock");
777+
assert!(!eval(&script, None, None), "no upper bound");
778+
}
779+
780+
#[test]
781+
// Recursion combinators must thread the bounds down to nested timelocks.
782+
fn nested_timelocks() {
783+
let all = NativeScript::ScriptAll(vec![
784+
NativeScript::InvalidBefore(100),
785+
NativeScript::InvalidHereafter(200),
786+
]);
787+
assert!(eval(&all, Some(120), Some(180)), "interval within the window");
788+
assert!(!eval(&all, Some(80), Some(180)), "starts too early");
789+
assert!(!eval(&all, Some(120), Some(220)), "ends too late");
790+
791+
let any = NativeScript::ScriptAny(vec![
792+
NativeScript::InvalidBefore(100),
793+
NativeScript::InvalidHereafter(200),
794+
]);
795+
assert!(eval(&any, Some(80), Some(180)), "satisfies the hereafter arm");
796+
assert!(!eval(&any, Some(80), Some(220)), "satisfies neither arm");
797+
}
798+
}

0 commit comments

Comments
 (0)