Skip to content

Commit

Permalink
refactor: Type-check Simplicity values
Browse files Browse the repository at this point in the history
Values have two bit encodings:

1) Compact: witness encoding, IMR computation
2) Padded: values on the Bit Machine

The code so far used the compact encoding exclusively, which is
incorrect for sum values on the Bit Machine. As a fix, this commit
introduces the padded encoding. However, a value can only be encoded
with padding if its type is known. To this end, this commit refactors
the Value struct so it knows its Simplicity type.

Adding types to all values instead of "upgrading" a typeless value to a
typed value has several benefits:

1) Bit words, products and options get their type for free when they are
   constructed. The caller doesn't have to supply additional type info.
   This covers almost all values that we are using in the code.
2) Values cannot be decoded without type info. Type checking happens
   implicitly during decoding. The decoded value gets its type for free.
3) Values are used as word constants and as witness data. In both cases,
   we want to check if the supplied value is of the correct type.
   There is (almost?) no use case for untyped values.

The new API exposes two Boolean iterators (iter_compact / iter_padded)
instead of do_each_bit. The iterators are more flexible and can be used
in conjunction with BitCollector.

Most changes in this commit happen inside value.rs. The rest is
renamings of Arc<Value> to Value, etc.
  • Loading branch information
uncomputable committed Aug 29, 2024
1 parent bfca4af commit c100ca1
Show file tree
Hide file tree
Showing 25 changed files with 396 additions and 349 deletions.
17 changes: 9 additions & 8 deletions jets-bench/benches/elements/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use simplicity::elements;
use simplicity::jet::elements::ElementsEnv;
use simplicity::jet::{Elements, Jet};
use simplicity::types;
use simplicity::types::Final;
use simplicity::Value;
use simplicity_bench::input::{
self, EqProduct, GenericProduct, InputSample, PrefixBit, Sha256Ctx, UniformBits,
Expand Down Expand Up @@ -751,36 +752,36 @@ fn bench(c: &mut Criterion) {
}

// Input to outpoint hash jet
fn outpoint_hash() -> Arc<Value> {
fn outpoint_hash() -> Value {
let ctx8 = SimplicityCtx8::with_len(511).value();
let genesis_pegin = genesis_pegin();
let outpoint = elements::OutPoint::sample().value();
Value::product(ctx8, Value::product(genesis_pegin, outpoint))
}

fn asset_amount_hash() -> Arc<Value> {
fn asset_amount_hash() -> Value {
let ctx8 = SimplicityCtx8::with_len(511).value();
let asset = confidential::Asset::sample().value();
let amount = confidential::Value::sample().value();
Value::product(ctx8, Value::product(asset, amount))
}

fn nonce_hash() -> Arc<Value> {
fn nonce_hash() -> Value {
let ctx8 = SimplicityCtx8::with_len(511).value();
let nonce = confidential::Nonce::sample().value();
Value::product(ctx8, nonce)
}

fn annex_hash() -> Arc<Value> {
fn annex_hash() -> Value {
let ctx8 = SimplicityCtx8::with_len(511).value();
let annex = if rand::random() {
Value::right(Value::u256(rand::random::<[u8; 32]>()))
Value::some(Value::u256(rand::random::<[u8; 32]>()))
} else {
Value::left(Value::unit())
Value::none(Final::u256())
};
Value::product(ctx8, annex)
}
let arr: [(Elements, Arc<dyn Fn() -> Arc<Value>>); 4] = [
let arr: [(Elements, Arc<dyn Fn() -> Value>); 4] = [
(Elements::OutpointHash, Arc::new(&outpoint_hash)),
(Elements::AssetAmountHash, Arc::new(&asset_amount_hash)),
(Elements::NonceHash, Arc::new(nonce_hash)),
Expand Down Expand Up @@ -814,7 +815,7 @@ fn bench(c: &mut Criterion) {
}

// Operations that use tx input or output index.
fn index_value(bound: u32) -> Arc<Value> {
fn index_value(bound: u32) -> Value {
let v = rand::random::<u32>() % bound;
Value::u32(v)
}
Expand Down
63 changes: 33 additions & 30 deletions jets-bench/src/data_structures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ use simplicity::{
bitcoin, elements,
hashes::Hash,
hex::FromHex,
types::{self, Type},
types::Final,
BitIter, Error, Value,
};
use std::sync::Arc;

/// Engine to compute SHA256 hash function.
/// We can't use hashes::sha256::HashEngine because it does not accept
Expand Down Expand Up @@ -55,21 +54,19 @@ impl SimplicityCtx8 {
/// # Panics:
///
/// Panics if the length of the slice is >= 2^(n + 1) bytes
pub fn var_len_buf_from_slice(v: &[u8], mut n: usize) -> Result<Arc<Value>, Error> {
pub fn var_len_buf_from_slice(v: &[u8], mut n: usize) -> Result<Value, Error> {
// Simplicity consensus rule for n < 16 while reading buffers.
assert!(n < 16);
assert!(v.len() < (1 << (n + 1)));
let mut iter = BitIter::new(v.iter().copied());
let ctx = types::Context::new();
let types = Type::powers_of_two(&ctx, n); // size n + 1
let mut res = None;
while n > 0 {
let ty = Final::two_two_n(n);
let v = if v.len() >= (1 << (n + 1)) {
let ty = &types[n];
let val = iter.read_value(&ty.final_data().unwrap())?;
Value::right(val)
let val = iter.read_value(&ty)?;
Value::some(val)
} else {
Value::left(Value::unit())
Value::none(ty)
};
res = match res {
Some(prod) => Some(Value::product(prod, v)),
Expand Down Expand Up @@ -155,11 +152,11 @@ pub struct SimplicityPoint(pub bitcoin::secp256k1::PublicKey);
/// Trait defining how to encode a data structure into a Simplicity value
/// This is then used to write these vales into the bit machine.
pub trait SimplicityEncode {
fn value(&self) -> Arc<Value>;
fn value(&self) -> Value;
}

impl SimplicityEncode for SimplicityCtx8 {
fn value(&self) -> Arc<Value> {
fn value(&self) -> Value {
let buf_len = self.length % 512;
let buf = var_len_buf_from_slice(&self.buffer[..buf_len], 8).unwrap();
let len = Value::u64(self.length as u64);
Expand All @@ -174,74 +171,80 @@ impl SimplicityEncode for SimplicityCtx8 {
}

impl SimplicityEncode for elements::OutPoint {
fn value(&self) -> Arc<Value> {
fn value(&self) -> Value {
let txid = Value::u256(self.txid.to_byte_array());
let vout = Value::u32(self.vout);
Value::product(txid, vout)
}
}

impl SimplicityEncode for elements::confidential::Asset {
fn value(&self) -> Arc<Value> {
fn value(&self) -> Value {
match self {
elements::confidential::Asset::Explicit(a) => {
Value::right(Value::u256(a.into_inner().to_byte_array()))
let left = Final::product(Final::u1(), Final::u256());
Value::right(left, Value::u256(a.into_inner().to_byte_array()))
}
elements::confidential::Asset::Confidential(gen) => {
let ser = gen.serialize();
let odd_gen = ser[0] & 1 == 1;
let x_bytes = (&ser[1..33]).try_into().unwrap();
let x_pt = Value::u256(x_bytes);
let y_pt = Value::u1(odd_gen as u8);
Value::left(Value::product(y_pt, x_pt))
Value::left(Value::product(y_pt, x_pt), Final::u256())
}
elements::confidential::Asset::Null => panic!("Tried to encode Null asset"),
}
}
}

impl SimplicityEncode for elements::confidential::Value {
fn value(&self) -> Arc<Value> {
fn value(&self) -> Value {
match self {
elements::confidential::Value::Explicit(v) => Value::right(Value::u64(*v)),
elements::confidential::Value::Explicit(v) => {
let left = Final::product(Final::u1(), Final::u256());
Value::right(left, Value::u64(*v))
},
elements::confidential::Value::Confidential(v) => {
let ser = v.serialize();
let x_bytes = (&ser[1..33]).try_into().unwrap();
let x_pt = Value::u256(x_bytes);
let y_pt = Value::u1((ser[0] & 1 == 1) as u8);
Value::left(Value::product(y_pt, x_pt))
Value::left(Value::product(y_pt, x_pt), Final::u64())
}
elements::confidential::Value::Null => panic!("Tried to encode Null value"),
}
}
}

impl SimplicityEncode for elements::confidential::Nonce {
fn value(&self) -> Arc<Value> {
fn value(&self) -> Value {
let ty_l = Final::product(Final::u1(), Final::u256());
let ty_r = Final::u256();
match self {
elements::confidential::Nonce::Explicit(n) => {
Value::right(Value::right(Value::u256(*n)))
Value::some(Value::right(ty_l, Value::u256(*n)))
}
elements::confidential::Nonce::Confidential(n) => {
let ser = n.serialize();
let x_bytes = (&ser[1..33]).try_into().unwrap();
let x_pt = Value::u256(x_bytes);
let y_pt = Value::u1((ser[0] & 1 == 1) as u8);
Value::right(Value::left(Value::product(y_pt, x_pt)))
Value::some(Value::left(Value::product(y_pt, x_pt), ty_r))
}
elements::confidential::Nonce::Null => Value::left(Value::unit()),
elements::confidential::Nonce::Null => Value::none(Final::sum(ty_l, ty_r)),
}
}
}

impl SimplicityEncode for SimplicityFe {
fn value(&self) -> Arc<Value> {
fn value(&self) -> Value {
Value::u256(*self.as_inner())
}
}

impl SimplicityEncode for SimplicityGe {
fn value(&self) -> Arc<Value> {
fn value(&self) -> Value {
let ser = match &self {
SimplicityGe::ValidPoint(p) => p.serialize_uncompressed(),
SimplicityGe::InvalidPoint(x, y) => {
Expand All @@ -261,21 +264,21 @@ impl SimplicityEncode for SimplicityGe {
}

impl SimplicityEncode for SimplicityGej {
fn value(&self) -> Arc<Value> {
fn value(&self) -> Value {
let ge = self.ge.value();
let z = self.z.value();
Value::product(ge, z)
}
}

impl SimplicityEncode for SimplicityScalar {
fn value(&self) -> Arc<Value> {
fn value(&self) -> Value {
Value::u256(self.0)
}
}

impl SimplicityEncode for SimplicityPoint {
fn value(&self) -> Arc<Value> {
fn value(&self) -> Value {
let ser = self.0.serialize(); // compressed
let x_bytes = (&ser[1..33]).try_into().unwrap();
let y_pt = Value::u1((ser[0] & 1 == 1) as u8);
Expand Down Expand Up @@ -407,11 +410,11 @@ impl BenchSample for SimplicityPoint {
}

// Sample genesis pegin with 50% probability
pub fn genesis_pegin() -> Arc<Value> {
pub fn genesis_pegin() -> Value {
if rand::random() {
Value::left(Value::unit())
Value::none(Final::two_two_n(8))
} else {
let genesis_hash = rand::random::<[u8; 32]>();
Value::right(Value::u256(genesis_hash))
Value::some(Value::u256(genesis_hash))
}
}
38 changes: 22 additions & 16 deletions jets-bench/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ use simplicity::jet::Elements;
use simplicity::types::{self, CompleteBound};
use simplicity::Value;

pub fn random_value(ty: &types::Final, rng: &mut ThreadRng) -> Arc<Value> {
pub fn random_value(ty: &types::Final, rng: &mut ThreadRng) -> Value {
enum StackItem<'a> {
Type(&'a types::Final),
LeftSum,
RightSum,
LeftSum(Arc<types::Final>),
RightSum(Arc<types::Final>),
Product,
}

Expand All @@ -27,10 +27,10 @@ pub fn random_value(ty: &types::Final, rng: &mut ThreadRng) -> Arc<Value> {
CompleteBound::Unit => value_stack.push(Value::unit()),
CompleteBound::Sum(left, right) => {
if rng.gen() {
call_stack.push(StackItem::LeftSum);
call_stack.push(StackItem::LeftSum(Arc::clone(right)));
call_stack.push(StackItem::Type(left));
} else {
call_stack.push(StackItem::RightSum);
call_stack.push(StackItem::RightSum(Arc::clone(left)));
call_stack.push(StackItem::Type(right));
}
}
Expand All @@ -40,13 +40,13 @@ pub fn random_value(ty: &types::Final, rng: &mut ThreadRng) -> Arc<Value> {
call_stack.push(StackItem::Type(left));
}
},
StackItem::LeftSum => {
StackItem::LeftSum(right) => {
let left = value_stack.pop().unwrap();
value_stack.push(Value::left(left));
value_stack.push(Value::left(left, right));
}
StackItem::RightSum => {
StackItem::RightSum(left) => {
let right = value_stack.pop().unwrap();
value_stack.push(Value::right(right));
value_stack.push(Value::right(left, right));
}
StackItem::Product => {
let right = value_stack.pop().unwrap();
Expand Down Expand Up @@ -411,10 +411,10 @@ pub enum InputSampling {
Random,
/// A given, fixed bit string (whose length is multiple of 8)
/// Worst-case inputs
Fixed(Arc<Value>),
Fixed(Value),
/// Custom sampling method, read first src type bits from input
/// Useful for cases where we want to sample inputs according to some distributions
Custom(Arc<dyn Fn() -> Arc<Value>>),
Custom(Arc<dyn Fn() -> Value>),
}

impl InputSampling {
Expand All @@ -424,19 +424,25 @@ impl InputSampling {
src_ty: &types::Final,
rng: &mut ThreadRng,
) {
let write_bit = |bit: bool| unsafe { c_writeBit(src_frame, bit) };
let mut write_bit = |bit: bool| unsafe { c_writeBit(src_frame, bit) };

match self {
InputSampling::Random => {
let value = random_value(src_ty, rng);
value.do_each_bit(write_bit);
for bit in value.iter_padded() {
write_bit(bit);
}
}
InputSampling::Fixed(v) => {
v.do_each_bit(write_bit);
InputSampling::Fixed(value) => {
for bit in value.iter_padded() {
write_bit(bit);
}
}
InputSampling::Custom(gen_bytes) => {
let value = gen_bytes();
value.do_each_bit(write_bit);
for bit in value.iter_padded() {
write_bit(bit);
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ impl NodeBounds {
NodeBounds {
extra_cells: 0,
extra_frames: 0,
cost: Cost::OVERHEAD + Cost::of_type(value.len()),
cost: Cost::OVERHEAD + Cost::of_type(value.padded_len()),
}
}

Expand Down
Loading

0 comments on commit c100ca1

Please sign in to comment.