Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Softfork infrastructure and coinid operator #273

Merged
merged 3 commits into from
Apr 28, 2023
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
7 changes: 7 additions & 0 deletions src/allocator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ impl Allocator {
}

pub fn restore_checkpoint(&mut self, cp: &Checkpoint) {
// if any of these asserts fire, it means we're trying to restore to
// a state that has already been "long-jumped" passed (via another
// restore to an earler state). You can only restore backwards in time,
// not forwards.
assert!(self.u8_vec.len() >= cp.u8s);
assert!(self.pair_vec.len() >= cp.pairs);
assert!(self.atom_vec.len() >= cp.atoms);
self.u8_vec.truncate(cp.u8s);
self.pair_vec.truncate(cp.pairs);
self.atom_vec.truncate(cp.atoms);
Expand Down
63 changes: 50 additions & 13 deletions src/chia_dialect.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use crate::allocator::{Allocator, NodePtr};
use crate::core_ops::{op_cons, op_eq, op_first, op_if, op_listp, op_raise, op_rest};
use crate::cost::Cost;
use crate::dialect::Dialect;
use crate::dialect::{Dialect, Operators};
use crate::err_utils::err;
use crate::more_ops::{
op_add, op_all, op_any, op_ash, op_concat, op_div, op_divmod, op_gr, op_gr_bytes, op_logand,
op_logior, op_lognot, op_logxor, op_lsh, op_multiply, op_not, op_point_add, op_pubkey_for_exp,
op_sha256, op_softfork, op_strlen, op_substr, op_subtract, op_unknown,
op_add, op_all, op_any, op_ash, op_coinid, op_concat, op_div, op_divmod, op_gr, op_gr_bytes,
op_logand, op_logior, op_lognot, op_logxor, op_lsh, op_multiply, op_not, op_point_add,
op_pubkey_for_exp, op_sha256, op_strlen, op_substr, op_subtract, op_unknown,
};
use crate::reduction::Response;

Expand All @@ -21,6 +21,10 @@ pub const LIMIT_HEAP: u32 = 0x0004;
// When set, enforce a stack size limit for CLVM programs
pub const LIMIT_STACK: u32 = 0x0008;

// When set, we allow softfork with extension 0 (which includes coinid and the
// BLS operators). This remains disabled until the soft-fork activates
pub const ENABLE_BLS_OPS: u32 = 0x0010;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see, as we move through the blockchain consensus, we have to adjust how the softfork operator works.

I feel like this can be completely encapsulated in Dialect. For simplicity, suppose a soft fork kicks in that introduces a new dialect that supports coinid.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You would still need some way for the full node to communicate to the rust implementation extensions are available. Before a soft fork activates, the interpreter must behave as if the extension doesn't exist.

Now, in my patch, whether an extension is available or not is controlled by the dialect, see line 137 below. This flag is used to communicate whether the softfork has activated or not.

would you construct a new ChiaDialect object for every softfork operator?
Presumably you wouldn't start a new interpreter, so you would perhaps have a stack of dialect objects, and execute operators using the top dialect of the stack. The two main issues I see is that it makes the dialect object (and operator dispatch) dynamic rather than static (which is likely to be costly). It's not obvious that having separate classes for the separate dialects really buys us anything, since (presumably) they would be mostly the same.


// The default mode when running grnerators in mempool-mode (i.e. the stricter
// mode)
pub const MEMPOOL_MODE: u32 = NO_UNKNOWN_OPS | LIMIT_HEAP | LIMIT_STACK;
Expand Down Expand Up @@ -56,12 +60,15 @@ impl Dialect for ChiaDialect {
o: NodePtr,
argument_list: NodePtr,
max_cost: Cost,
extension: Operators,
) -> Response {
let b = &allocator.atom(o);
if b.len() != 1 {
return unknown_operator(allocator, o, argument_list, self.flags, max_cost);
}
let f = match b[0] {
// 1 = quote
// 2 = apply
3 => op_if,
4 => op_cons,
5 => op_first,
Expand Down Expand Up @@ -95,16 +102,19 @@ impl Dialect for ChiaDialect {
33 => op_any,
34 => op_all,
// 35 ---
36 => {
if (self.flags & NO_UNKNOWN_OPS) != 0 {
return err(o, "no softfork implemented");
} else {
op_softfork
// 36 = softfork
_ => match extension {
Operators::BLS => match b[0] {
48 => op_coinid,
// TODO: add BLS operators here
_ => {
return unknown_operator(allocator, o, argument_list, self.flags, max_cost);
}
},
_ => {
return unknown_operator(allocator, o, argument_list, self.flags, max_cost);
}
}
_ => {
return unknown_operator(allocator, o, argument_list, self.flags, max_cost);
}
},
};
f(allocator, argument_list, max_cost)
}
Expand All @@ -117,11 +127,38 @@ impl Dialect for ChiaDialect {
&[2]
}

fn softfork_kw(&self) -> &[u8] {
&[36]
}

// interpret the extension argument passed to the softfork operator, and
// return the Operators it enables (or None) if we don't know what it means
// We have to pretend that we don't know about the BLS extensions until
// after the soft-fork activation, which is controlled by the ENABLE_BLS_OPS
// flag
fn softfork_extension(&self, ext: u32) -> Operators {
match ext {
0 => {
if (self.flags & ENABLE_BLS_OPS) == 0 {
Operators::None
} else {
Operators::BLS
}
}
// new extensions go here
_ => Operators::None,
}
}

fn stack_limit(&self) -> usize {
if (self.flags & LIMIT_STACK) != 0 {
20000000
} else {
usize::MAX
}
}

fn allow_unknown_ops(&self) -> bool {
(self.flags & NO_UNKNOWN_OPS) == 0
}
}
20 changes: 18 additions & 2 deletions src/dialect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,26 @@ use crate::allocator::{Allocator, NodePtr};
use crate::cost::Cost;
use crate::reduction::Response;

#[repr(u32)]
#[derive(Clone, Copy, Eq, PartialEq)]
pub enum Operators {
None,
BLS,
}

pub trait Dialect {
fn quote_kw(&self) -> &[u8];
fn apply_kw(&self) -> &[u8];
fn op(&self, allocator: &mut Allocator, op: NodePtr, args: NodePtr, max_cost: Cost)
-> Response;
fn softfork_kw(&self) -> &[u8];
fn softfork_extension(&self, ext: u32) -> Operators;
fn op(
&self,
allocator: &mut Allocator,
op: NodePtr,
args: NodePtr,
max_cost: Cost,
extensions: Operators,
) -> Response;
fn stack_limit(&self) -> usize;
fn allow_unknown_ops(&self) -> bool;
}
5 changes: 2 additions & 3 deletions src/f_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::cost::Cost;
use crate::more_ops::{
op_add, op_all, op_any, op_ash, op_concat, op_div, op_divmod, op_gr, op_gr_bytes, op_logand,
op_logior, op_lognot, op_logxor, op_lsh, op_multiply, op_not, op_point_add, op_pubkey_for_exp,
op_sha256, op_softfork, op_strlen, op_substr, op_subtract,
op_sha256, op_strlen, op_substr, op_subtract,
};
use crate::reduction::Response;

Expand All @@ -15,7 +15,7 @@ type OpFn = fn(&mut Allocator, NodePtr, Cost) -> Response;
pub type FLookup = [Option<OpFn>; 256];

pub fn opcode_by_name(name: &str) -> Option<OpFn> {
let opcode_lookup: [(OpFn, &str); 30] = [
let opcode_lookup: [(OpFn, &str); 29] = [
(op_if, "op_if"),
(op_cons, "op_cons"),
(op_first, "op_first"),
Expand Down Expand Up @@ -44,7 +44,6 @@ pub fn opcode_by_name(name: &str) -> Option<OpFn> {
(op_not, "op_not"),
(op_any, "op_any"),
(op_all, "op_all"),
(op_softfork, "op_softfork"),
(op_div, "op_div"),
];
let name: &[u8] = name.as_ref();
Expand Down
65 changes: 47 additions & 18 deletions src/more_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::err_utils::err;
use crate::node::Node;
use crate::number::{number_from_u8, ptr_from_number, Number};
use crate::op_utils::{
arg_count, atom, check_arg_count, i32_atom, int_atom, two_ints, u32_from_u8, uint_atom,
arg_count, atom, check_arg_count, i32_atom, int_atom, two_ints, u32_from_u8,
};
use crate::reduction::{Reduction, Response};
use crate::sha2::{Digest, Sha256};
Expand Down Expand Up @@ -85,6 +85,10 @@ const PUBKEY_BASE_COST: Cost = 1325730;
// increased from 12 to closer model Raspberry PI
const PUBKEY_COST_PER_BYTE: Cost = 38;

// the new coinid operator
const COINID_COST: Cost =
SHA256_BASE_COST + SHA256_COST_PER_ARG * 3 + SHA256_COST_PER_BYTE * (32 + 32 + 8);

fn limbs_for_int(v: &Number) -> usize {
((v.bits() + 7) / 8) as usize
}
Expand Down Expand Up @@ -786,23 +790,6 @@ pub fn op_all(a: &mut Allocator, input: NodePtr, max_cost: Cost) -> Response {
Ok(Reduction(cost, total.node))
}

pub fn op_softfork(a: &mut Allocator, input: NodePtr, max_cost: Cost) -> Response {
let args = Node::new(a, input);
match args.pair() {
Some((p1, _)) => {
let cost = uint_atom::<8>(&p1, "softfork")?;
if cost > max_cost {
return args.err("cost exceeded");
}
if cost == 0 {
return args.err("cost must be > 0");
}
Ok(Reduction(cost, args.null().node))
}
_ => args.err("softfork takes at least 1 argument"),
}
}

lazy_static! {
static ref GROUP_ORDER: Number = {
let order_as_bytes = &[
Expand Down Expand Up @@ -877,3 +864,45 @@ pub fn op_point_add(a: &mut Allocator, input: NodePtr, max_cost: Cost) -> Respon
let total: G1Affine = total.into();
new_atom_and_cost(a, cost, &total.to_compressed())
}

pub fn op_coinid(a: &mut Allocator, input: NodePtr, _max_cost: Cost) -> Response {
let args = Node::new(a, input);
check_arg_count(&args, 3, "coinid")?;

let parent_coin = atom(args.first()?, "coinid")?;
if parent_coin.len() != 32 {
return args.err("coinid: invalid parent coin id (must be 32 bytes)");
}
let args = args.rest()?;
let puzzle_hash = atom(args.first()?, "coinid")?;
if puzzle_hash.len() != 32 {
return args.err("coinid: invalid puzzle hash (must be 32 bytes)");
}
let args = args.rest()?;
let amount = atom(args.first()?, "coinid")?;
if !amount.is_empty() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use uint_atom::<8> here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I will make this change

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, in this case I want to preserve the buffer, to pass into the Sha256 context, not actually parse the number. I just want to ensure the constraints are met. I could use uint_atom() just to check the constraints, and ignore its return value though, but then there would be wasted work in producing the integer

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might consider checking the assembly generated... it's possible the optimizer might realize you don't use the return value. Also, since it likely just goes into a register anyway, it's probably not particularly expensive.

(I may be being a bit naïvely optimistic about how good the optimizers are.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into splitting the validation and the parsing of the integer and realized that another important difference is that coinid requires the amount to be in canonical integer form. i.e. it does not allow any redundant leading zero. This is an important rule since it affects the hash.

The only shared check is really that the value is not negative. There are some utility functions to sanitize integers in chia_rs, for parsing conditions. Perhaps in the future (especially as we have more operators in here) it might make sense to move those into this repo and have some stricter conventions for validating arguments. I don't think there are enough places we do this in right now though.

Even your suggestion of calling uint_atom::<8>() just for the validation won't work, because redundant leading zeros would still have to be checked.

if (amount[0] & 0x80) != 0 {
return args.err("coinid: invalid amount (may not be negative");
}
if amount == [0_u8] || (amount.len() > 1 && amount[0] == 0 && (amount[1] & 0x80) == 0) {
return args.err("coinid: invalid amount (may not have redundant leading zero)");
}
// the only valid coin value that's 9 bytes is when a leading zero is
// required to not have the value interpreted as negative
if amount.len() > 9 || (amount.len() == 9 && amount[0] != 0) {
return args.err("coinid: invalid amount (may not exceed max coin amount)");
}
}

let mut hasher = Sha256::new();
hasher.update(parent_coin);
hasher.update(puzzle_hash);
hasher.update(amount);
let ret: [u8; 32] = hasher
.finalize()
.as_slice()
.try_into()
.expect("sha256 hash is not 32 bytes");
let ret = a.new_atom(&ret)?;
Ok(Reduction(COINID_COST, ret))
}
Loading