Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Unreleased

* Fix `RedeemNode::prune` producing programs that consensus rejects, and add `ExecutionError::PruningFailed` [#391](https://github.com/BlockstreamResearch/rust-simplicity/issues/391).
* Enforce libsimplicity's limits on cells and program length: `BitMachine::for_program` rejects programs needing more than `CELLS_MAX` cells, and decoding fails with the new `decode::Error::TooManyNodes` beyond `DAG_LEN_MAX` nodes. Export both constants from `simplicity-sys`.

# 0.9.0 - 2026-09-16

Expand Down
13 changes: 13 additions & 0 deletions fuzz/fuzz_targets/c_rust_merkle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ fn do_test(data: &[u8]) {
&Midstate::from(c_data.ihr).to_parts().0,
rust_data.ihr().as_ref(),
);

// Our Bit Machine lacks tail call optimization, so it needs at least as
// many cells and frames as libsimplicity's. Both count two frames for
// the input and output.
let arrow = rust_data.arrow();
let bounds = rust_data.bounds();
let cells = arrow
.source
.bit_width()
.saturating_add(arrow.target.bit_width())
.saturating_add(bounds.extra_cells);
assert!(cells >= c_data.cell_bound as usize);
assert!(bounds.extra_frames + 2 >= c_data.frame_bound as usize);
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions simplicity-sys/depend/wrapper.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@
#include "simplicity/type.h"
#include "simplicity/bounded.h"
#include "simplicity/frame.h"
#include "simplicity/limitations.h"

#include <stdalign.h>

const uint64_t c_overhead = overhead;

const ubounded c_budget_max = BUDGET_MAX;
const ubounded c_cells_max = CELLS_MAX;
const size_t c_dag_len_max = DAG_LEN_MAX;

const size_t c_sizeof_uchar = sizeof(unsigned char);
const size_t c_sizeof_int = sizeof(int);
const size_t c_sizeof_uint = sizeof(unsigned int);
Expand Down
20 changes: 20 additions & 0 deletions simplicity-sys/src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ extern "C" {
pub type ubounded = c_uint_least32_t;
/// Used with `evalTCOProgram` to enforce consensus limits.
pub const BUDGET_MAX: ubounded = 4000050;
/// The maximum number of cells (bits) that consensus allows the Bit Machine
/// to need when evaluating a program, as bounded by `analyseBounds`.
pub const CELLS_MAX: ubounded = 0x500000;
/// The maximum number of nodes in a program accepted by consensus.
pub const DAG_LEN_MAX: c_size_t = 8000000;
/// The max value of UBOUNDED_MAX
pub const UBOUNDED_MAX: ubounded = ubounded::MAX;

Expand All @@ -87,6 +92,12 @@ extern "C" {
pub static c_alignof_ubounded: c_size_t;
}

extern "C" {
pub static c_budget_max: ubounded;
pub static c_cells_max: ubounded;
pub static c_dag_len_max: c_size_t;
}

pub mod bounded {
use super::ubounded;
extern "C" {
Expand Down Expand Up @@ -167,6 +178,15 @@ mod tests {
}
}

#[test]
fn test_limits() {
unsafe {
assert_eq!(BUDGET_MAX, c_budget_max);
assert_eq!(CELLS_MAX, c_cells_max);
assert_eq!(DAG_LEN_MAX, c_dag_len_max);
}
}

#[test]
#[rustfmt::skip]
fn test_aligns() {
Expand Down
8 changes: 8 additions & 0 deletions simplicity-sys/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ pub struct TestOutput {
pub ihr: CSha256Midstate,
/// The cost bound of the program
pub cost_bound: ubounded,
/// The bound on the cells needed to evaluate the program
pub cell_bound: ubounded,
/// The bound on the frames needed to evaluate the program
pub frame_bound: ubounded,
/// Whether or not evaluation succeded
pub eval_result: SimplicityErr,
}
Expand Down Expand Up @@ -127,6 +131,8 @@ pub fn run_program(
cmr: CSha256Midstate::default(),
ihr: CSha256Midstate::default(),
cost_bound: 0,
cell_bound: 0,
frame_bound: 0,
eval_result: SimplicityErr::NoError,
};

Expand Down Expand Up @@ -211,6 +217,8 @@ pub fn run_program(
)
.into_result()?;
result.cost_bound = cost_bound;
result.cell_bound = cell_bound;
result.frame_bound = frame_bound;
if test_up_to <= TestUpTo::ComputeCostUnbounded {
return Ok(result);
}
Expand Down
8 changes: 5 additions & 3 deletions src/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,9 @@ impl NodeBounds {
/// Node bounds for a `comp` node
pub fn comp(left: Self, right: Self, mid_ty_bit_width: usize) -> NodeBounds {
NodeBounds {
extra_cells: mid_ty_bit_width + cmp::max(left.extra_cells, right.extra_cells),
// Type widths saturate at usize::MAX, so the sums here must too.
extra_cells: mid_ty_bit_width
.saturating_add(cmp::max(left.extra_cells, right.extra_cells)),
extra_frames: 1 + cmp::max(left.extra_frames, right.extra_frames),
cost: Cost::OVERHEAD + Cost::of_type(mid_ty_bit_width) + left.cost + right.cost,
}
Expand Down Expand Up @@ -351,8 +353,8 @@ impl NodeBounds {
) -> NodeBounds {
NodeBounds {
extra_cells: left_source_bit_width
+ left_target_bit_width
+ cmp::max(left.extra_cells, right.extra_cells),
.saturating_add(left_target_bit_width)
.saturating_add(cmp::max(left.extra_cells, right.extra_cells)),
extra_frames: 2 + cmp::max(left.extra_frames, right.extra_frames),
cost: Cost::OVERHEAD
+ Cost::of_type(left_source_bit_width)
Expand Down
47 changes: 47 additions & 0 deletions src/bit_encoding/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::node::{
use crate::types;
use crate::value::Word;
use crate::{BitIter, FailEntropy};
use simplicity_sys::ffi::DAG_LEN_MAX;
use std::collections::HashSet;
use std::sync::Arc;
use std::{cmp, error, fmt};
Expand Down Expand Up @@ -42,6 +43,13 @@ pub enum Error {
NotInCanonicalOrder,
/// Program does not have maximal sharing
SharingNotMaximal,
/// Program has more nodes than consensus allows
TooManyNodes {
/// The number of nodes in the program.
got: usize,
/// The maximum allowed number of nodes.
max: usize,
},
/// Type-checking error
Type(crate::types::Error),
}
Expand Down Expand Up @@ -81,6 +89,13 @@ impl fmt::Display for Error {
Error::Natural(ref e) => e.fmt(f),
Error::NotInCanonicalOrder => f.write_str("program not in canonical order"),
Error::SharingNotMaximal => f.write_str("Decoded programs must have maximal sharing"),
Error::TooManyNodes { got, max } => {
write!(
f,
"program has {} nodes, more than the maximum {}",
got, max
)
}
Error::Type(ref e) => fmt::Display::fmt(e, f),
}
}
Expand All @@ -97,6 +112,7 @@ impl error::Error for Error {
Error::Natural(ref e) => Some(e),
Error::NotInCanonicalOrder => None,
Error::SharingNotMaximal => None,
Error::TooManyNodes { .. } => None,
Error::Type(ref e) => Some(e),
}
}
Expand Down Expand Up @@ -172,6 +188,12 @@ pub fn decode_expression<'brand, I: Iterator<Item = u8>, J: Jet>(

let len = bits.read_natural::<usize>(None)?;
assert_ne!(len, 0, "impossible to encode 0 in Simplicity");
if len > DAG_LEN_MAX {
return Err(Error::TooManyNodes {
got: len,
max: DAG_LEN_MAX,
});
}

let mut nodes = Vec::with_capacity(cmp::min(len, 10_000));
for _ in 0..len {
Expand Down Expand Up @@ -326,6 +348,31 @@ mod tests {
RedeemNode::decode::<_, _, Core>(iter, BitIter::from(&[][..])).unwrap_err();
}

#[test]
fn too_many_nodes() {
for (len, too_many) in [(DAG_LEN_MAX, false), (DAG_LEN_MAX + 1, true)] {
let mut prog = Vec::<u8>::new();
let mut w = BitWriter::new(&mut prog);
encode::encode_natural(len, &mut w).unwrap();
w.flush_all().unwrap();

types::Context::with_context(|ctx| {
let mut iter = BitIter::from(&prog[..]);
let err = decode_expression::<_, Core>(&ctx, &mut iter)
.expect_err("there are no nodes after the length");
match err {
Error::TooManyNodes { got, max } => {
assert!(too_many, "{} nodes should be allowed", len);
assert_eq!(got, len);
assert_eq!(max, DAG_LEN_MAX);
}
// An allowed length fails later, when decoding the nodes.
_ => assert!(!too_many, "{} nodes should be too many: {}", len, err),
}
});
}
}

#[test]
fn decode_fixed_natural() {
let tries: Vec<(usize, usize, &[u8])> = vec![
Expand Down
41 changes: 30 additions & 11 deletions src/bit_machine/limits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,27 @@ use std::error;
/// The maximum number of cells needed by the bit machine.
///
/// This roughly corresponds to the maximum size of any type used by a
/// program, in bits. In a blockchain context this should be limited by
/// the transaction's weight budget.
/// program, in bits.
///
/// The limit here is an absolute limit enforced by the library to avoid
/// unbounded allocations.
/// This is libsimplicity's consensus limit. libsimplicity bounds the cells
/// needed by a Bit Machine with tail call optimization, which this one lacks,
/// so this bound on a program is never smaller than libsimplicity's and is
/// sometimes larger. Every program which consensus rejects for needing too
/// many cells is rejected here, and so are some which consensus accepts.
// This value must be less than usize::MAX / 2 to avoid panics in this module.
const MAX_CELLS: usize = 2 * 1024 * 1024 * 1024 - 1;
const MAX_CELLS: usize = simplicity_sys::ffi::CELLS_MAX as usize;

/// The maximum number of frames needed by the bit machine.
///
/// This roughly corresponds to the maximum depth of nested `comp` and
/// `disconnect` combinators. In a blockchain context this should be
/// limited by the transaction's weight budget.
/// `disconnect` combinators.
///
/// The limit here is an absolute limit enforced by the library to avoid
/// unbounded allocations.
/// libsimplicity has no limit on frames of its own. Programs need at most
/// two frames per node, here and in libsimplicity, and consensus limits
/// programs to `DAG_LEN_MAX` nodes, so this only rejects programs which are
/// too long for consensus anyway.
// This value must be less than usize::MAX / 2 to avoid panics in this module.
const MAX_FRAMES: usize = 1024 * 1024;
const MAX_FRAMES: usize = 2 * simplicity_sys::ffi::DAG_LEN_MAX;

#[non_exhaustive]
#[derive(Clone, Debug)]
Expand Down Expand Up @@ -84,7 +87,7 @@ impl LimitError {
if got > MAX_FRAMES {
Err(Self::MaxFramesExceeded {
got,
max: MAX_CELLS,
max: MAX_FRAMES,
bound,
})
} else {
Expand Down Expand Up @@ -118,3 +121,19 @@ impl LimitError {
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn frame_error_reports_frame_limit() {
match LimitError::check_max_frames(MAX_FRAMES + 1, "test") {
Err(LimitError::MaxFramesExceeded { got, max, .. }) => {
assert_eq!(got, MAX_FRAMES + 1);
assert_eq!(max, MAX_FRAMES);
}
res => panic!("expected MaxFramesExceeded, got {:?}", res),
}
}
}
14 changes: 11 additions & 3 deletions src/node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,7 @@ impl<N: Marker> Drop for Node<N> {
mod tests {
use ffi::tests::TestData;

use crate::analysis::Cost;
use crate::analysis::{Cost, IO_EXTRA_FRAMES};
use crate::ffi;
use crate::jet::Elements;
use crate::BitIter;
Expand All @@ -846,7 +846,7 @@ mod tests {
fn check_merkle_roots(test: &TestData) {
let prog = BitIter::from(test.prog.as_slice());
let witness = BitIter::from(test.witness.as_slice());
ffi::tests::run_program(
let c_result = ffi::tests::run_program(
&test.prog,
&test.witness,
ffi::tests::TestUpTo::CheckOneOne,
Expand All @@ -858,7 +858,15 @@ mod tests {
assert_eq!(prog.cmr().to_byte_array(), test.cmr);
assert_eq!(prog.amr().to_byte_array(), test.amr);
assert_eq!(prog.ihr().to_byte_array(), test.ihr);
assert_eq!(prog.bounds().cost, Cost::from_milliweight(test.cost))
assert_eq!(prog.bounds().cost, Cost::from_milliweight(test.cost));

// Our Bit Machine lacks tail call optimization, so it needs at least as
// many cells and frames as libsimplicity's.
let io_cells = prog.arrow().source.bit_width() + prog.arrow().target.bit_width();
let cells = io_cells + prog.bounds().extra_cells;
let frames = prog.bounds().extra_frames + IO_EXTRA_FRAMES;
assert!(cells >= c_result.cell_bound as usize);
assert!(frames >= c_result.frame_bound as usize);
}

#[test]
Expand Down
Loading