-
Notifications
You must be signed in to change notification settings - Fork 57
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
Conversation
65253be
to
7f5df6c
Compare
} | ||
let args = args.rest()?; | ||
let amount = atom(args.first()?, "coinid")?; | ||
if !amount.is_empty() { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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.
// this represents the state we were in before entering a soft-fork guard. We | ||
// may need this to long-jump out of the guard, and also to validate the cost | ||
// when exiting the guard | ||
struct SoftforkGuard { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still need this when there is no long-jump capabilities?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, 2 main reasons:
- we need to record the expected cost that was specified when we entered the softfork guard. Once we exit, we need to compare against it. Now, this expected cost could be a field in the
Softfork
operation that checks this, but that would bloat that type from 1 byte to (probably) 16 bytes. - keep track of which extension is currently enabled.
resetting the allocator is the current 3rd use, but that's not strictly necessary. If we would use a Dialect
object instead of an extension enum, this is where that object would go, as I understand it.
src/dialect.rs
Outdated
@@ -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 Extension { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm trying to understand why we need this new structure. I'd always figured the Dialect
object would be the thing that changes based on extension. So you have the base dialect, which is as we see, then a dialect level 0 which adds coinid
and the bls12 ops. Is there something that makes Extension
necessary? Isn't it just sort of an add-on to the Dialect
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it is specifying which operators should be available. It's simple and efficient and it restricts extensions to just add new operators, not change whether we're in mempool mode, the heap or stack limits or the opcodes for apply, quote or softfork.
Having this enum saves us from having multiple dialect objects (with a lot of repeated code, or code shared via inheritance)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in a way Dialect
might be a pretty good name for this enum, but it's taken
@@ -66,6 +88,7 @@ struct RunProgramContext<'a, D> { | |||
val_stack: Vec<NodePtr>, | |||
env_stack: Vec<NodePtr>, | |||
op_stack: Vec<Operation>, | |||
softfork_stack: Vec<SoftforkGuard>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per my comment above, why not just use a stack of dialects, and at the end of the softfork eval process, pop off the top dialect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we would also need to record the expected cost in those stack frames. It would also need dynamic dispatch for the operators, unless of course this stack is of Dialect objects of the same type. In that case, the only state held by the dialect object would be the Extension
field, for it to know which operators are supposed to be available. But that would avoid the dynamic dispatch. It would also make the existing RuntimeDialect
quite expensive, because its (immutable) state would be duplicated in each stack frame.
@@ -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) | |||
pub const ENABLE_BLS_OPS: u32 = 0x0010; |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
src/chia_dialect.rs
Outdated
@@ -56,12 +60,15 @@ impl Dialect for ChiaDialect { | |||
o: NodePtr, | |||
argument_list: NodePtr, | |||
max_cost: Cost, | |||
extensions: Extension, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extensions: Extension, | |
extension: Extension, |
src/chia_dialect.rs
Outdated
&[36] | ||
} | ||
|
||
fn softfork_extension(&self, ext: u32) -> Extension { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about passing in the Extension
directly instead of using flags?
// differently depending on whether we allow unknown ops or not | ||
let (ext, prg, env) = match self.parse_softfork_arguments(&operand_list) { | ||
Ok(ret_values) => ret_values, | ||
Err(err) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a potentially temporary workaround here?
src/run_program.rs
Outdated
self.apply_op(cost, effective_max_cost - cost), | ||
max_cost_ptr, | ||
)?, | ||
Operation::Softfork => self.exit_softfork_guard(cost)?, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The names are a bit inconsistent here. Maybe rename the operator to match the name of the function it calls? exit_guard
& ExitGuard
, or EndSoftfork
and end_soft_fork
, or whatever.
src/test_ops.rs
Outdated
@@ -878,6 +856,27 @@ pubkey_for_exp 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00f0000 | |||
pubkey_for_exp 0x8c1258acd66282b7ccc627f7f65e27faac425bfd0001a40100000000fffffffe => 0xb7f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb | 1327426 | |||
pubkey_for_exp 0x8c1258acd66282b7ccc627f7f65e27faac425bfd0001a40100000000ffffffff => 0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 | 1327426 | |||
pubkey_for_exp 0x8c1258acd66282b7ccc627f7f65e27faac425bfd0001a4010000000000000000 => 0x847f5fcce0b9aa0f2bb3de6847337c9ed1bc2184a125c232721e1c81b0f0fee78506790a78c98abff2dd4b01a0756352 | 1327426 | |||
|
|||
; BLS extensions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
; BLS extensions | |
; BLS extensions could go here |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Extension
is a bit vague and could do with a different name. To me, it seems like it represents the maximum soft fork version number known to this current runtime. Maybe AdditionalOpcodeSet
or something like that?
allocator_state: Checkpoint, | ||
|
||
// this specifies which new operators are available | ||
extension: Operators, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider renaming to operators
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems good enough for now. If you want to keep working on it, that's fine too. Let me know if you need further approvals.
One comment I want to make is that coinid
probably isn't really that useful of a thing to be behind the softfork guard... probably repurposing an unused operator is a better way to do it (although it would have to be assert_coinid
). It'd take the three fields along with a hash, and it'd assert the field sizes and that the hash is correct. Since coinid
is stuck behind the guard, either the whole puzzle has to be there or the hash has to be passed in so it can confirm it's right, effectively making it assert_coinid
. We should probably discuss if there's any use to adding this operator as a guarded softfork op. For now, it's a decent test case.
There are two parts to this PR, best reviewed seperately;
softfork
operator.test_ops
intorun_program
(since the former doesn't run the interpreter)coinid
operator as part of softfork extension 0