-
Notifications
You must be signed in to change notification settings - Fork 26
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
Redesign clvm-traits to be more generic #314
Conversation
@arvidn this should be good to review, tests are passing on my end and I finished the last few things. |
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 your first approach (where Allocator
was a template and could be swapped out) was simpler than this approach. As I mention in the comments, I'm not excited about the basic structure of this abstraction being so complex you need a macro to implement the trait.
I'm thinking that maybe it would even make sense to not reuse the ToClvm
and FromClvm
traits. What would you think of, instead add a new trait, something like ClvmSerialize
and ClvmDeserialize
which converts to- and from serialized binary representation. There will be a little bit of overlap with ToClvm
and FromClvm
, but not very much, and I think the resulting code will be a lot more legible.
I'm not convinced that it's important to be able to compute the tree hash directly via this trait. The most efficient tree hash implementations are not recursive anyway, and it seems risky to introduce a sub-optimal one. Perhaps efficient tree-hash of rust types should even be yet another trait. I think that would keep things really simple too
|
||
fuzz_target!(|data: &[u8]| { | ||
let _ = Program::from_bytes(data); | ||
}); |
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.
did you mean to remove this?
/// This is used for building trees with the `ToClvm` and `FromClvm` traits. | ||
pub enum ClvmValue<'a, Node> { | ||
/// An atomic value in CLVM, represented with bytes. | ||
Atom(&'a [u8]), |
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 haven't fully wrapped my head around this patch yet, but it worries me a bit to expose an atom as a reference to a slice. I'm hoping to, in the future, make Allocator
not expose the slices directly as references, but as proxy objects, to allow internal mutability of the Allocator
to convert types lazily (G1, G2 elements, big integers etc.).
The problem is in cases where we need multiple references to atom buffers, and the second one will need to be converted on the fly. The first reference we've already handed out may be invalidated, so the allocator itself isn't mutable at that point.
I have reasons to believe that the execution of CLVM programs could see a material speed-up if the conversions would be lazy (and skipped entirely in many cases)
clvm-traits/src/from_clvm.rs
Outdated
Ok(ptr) | ||
} | ||
#[macro_export] | ||
macro_rules! from_clvm { |
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 can see it being tempting to stick all this in a macro, it makes me a little bit nervous though, since it becomes a bit opaque at the call sites. Did you explore using type aliases to make the signature a bit shorter instead?
Node: Clone, | ||
{ | ||
fn from_clvm<'a>( | ||
f: &mut impl FnMut(&Node) -> ClvmValue<'a, Node>, |
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 f
could use a better name, or at least a comment explaining what it's supposed to do and be used for
let mut sha256 = Sha256::new(); | ||
sha256.update([2]); | ||
sha256.update(first.0); | ||
sha256.update(rest.0); |
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.
this doesn't look right. don't you need to recurse left and right?
I was going to point out that the fact that this trait must be recursive (afaiu) is a security risk when called on input of untrusted structure (e.g. a NodePtr
), but would be fine on an object known at compile time.
Actually, given that the existing tree hash algorithms we have are not recursive, I would think those would be preferable over this one in all cases. The main benefit of this feature you're adding is that we can convert directly between a concrete rust type to serialized binary form. In this case, that we could compute the tree hash for a structure without first turning it into a CLVM tree.
I would think a better approach (rather than implementing tree_hash()
here, would be to improve the existing ones to work with any type implementing ToClvm
let ptr = value.tree_hash().unwrap().0; | ||
assert_eq!( | ||
hex::encode(ptr), | ||
"7c3670f319e07cff6d433e4c22e0895f1f0a10bad5bbcd23c32e3bc5589c23cb" |
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's not obvious that this hash is correct by inspecting this test. I think it would be good to test this implementation of tree_hash against the existing implementations. Maybe even in a fuzzer.
#[derive(Error, Debug, Clone, PartialEq, Eq)] | ||
pub enum ToClvmError { | ||
#[error("limit reached")] | ||
LimitReached, |
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 would be less generic and more concrete (I think) to call this something like out of memory. As far as I can tell, it's only used when the Allocator hits its memory limit
_ => Ok(a.new_atom(bytes).or(Err(ToClvmError::LimitReached))?), | ||
}, | ||
ClvmValue::Pair(first, rest) => { | ||
Ok(a.new_pair(first, rest).or(Err(ToClvmError::LimitReached))?) |
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.
Maybe I misunderstand something, how come you don't need to call to_ptr()
on first
and rest
? Have they already been converted to allocations in a
?
@@ -90,9 +96,9 @@ mod tests { | |||
} | |||
|
|||
#[test] | |||
fn test_proper_list() { | |||
fn test_list() { |
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.
renaming proper_list
-> list
and curried_args
-> args
are not convtroversial. I would land that change right away.
#[error("expected cons")] | ||
ExpectedCons(NodePtr), | ||
#[derive(Error, Debug, Clone, PartialEq, Eq)] | ||
pub enum FromClvmError { |
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 seems reasonable to split up the error codes for these
Pull Request Test Coverage Report for Build 6871666220
💛 - Coveralls |
@@ -110,8 +112,8 @@ pub fn fast_forward_singleton( | |||
return Err(Error::PuzzleHashMismatch); | |||
} | |||
|
|||
let singleton = CurriedProgram::<SingletonArgs>::from_clvm(a, puzzle)?; | |||
let mut new_solution = SingletonSolution::from_clvm(a, solution)?; | |||
let singleton: CurriedProgram<NodePtr, SingletonArgs<NodePtr>> = FromPtr::from_ptr(a, puzzle)?; |
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.
why do you need the NodePtr
template argument here? isn't that implied by FromPtr
?
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 think. I think the Rust compiler has limitations on what it can infer
let puzzle = spend.puzzle_reveal.to_clvm(&mut a).expect("to_clvm"); | ||
let solution = spend.solution.to_clvm(&mut a).expect("to_clvm"); | ||
let puzzle = node_from_bytes(&mut a, spend.puzzle_reveal.as_slice()).expect("to_clvm"); | ||
let solution = node_from_bytes(&mut a, spend.solution.as_slice()).expect("to_clvm"); |
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 we should preserve the ability for Program
to convert to and from NodePtr
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.
Yeah, I agree - I'll make sure this is included
maintainability.
This PR overhauls the
clvm-traits
andclvm-derive
crates, making them more generic.This is to simplify implementing the following functionality when needed in the future:
Allocator
.Task list:
ToClvm
andFromClvm
traits to be more generic, and updateclvm-derive
accordingly.impl Trait
in type aliases.clvm-derive
, and renamecurried_args
andproper_list
tocurry
andlist
, respectively.or
operation).a.value_to_ptr
anda.value_from_ptr
fromAllocatorExt
. Perhaps this should be a top-level function.Out of scope for this PR:
SingletonArgs
andSingletonSolution
split amongst the code base.