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

Make clvm_rs a replacement for clvm #253

Draft
wants to merge 46 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
a897e01
checkpoint
richardkiss Sep 21, 2022
01e11d0
Add serialize tests.
richardkiss Sep 21, 2022
f8993f1
Test improvements.
richardkiss Sep 22, 2022
91c2492
Some speed improvements.
richardkiss Sep 28, 2022
62142b9
More improvements.
richardkiss Sep 29, 2022
94080d6
Improve coverage.
richardkiss Oct 5, 2022
e6255a3
Make deserialization time tree-hashing optional.
richardkiss Oct 8, 2022
d797d66
Tree hash on deserialization is now optional.
richardkiss Oct 8, 2022
f7f5eef
Refactor, rename.
richardkiss Oct 17, 2022
a154d7e
First crack at rust tree hashes.
richardkiss Oct 18, 2022
9a2ca75
Use rust parsing if present.
richardkiss Jan 13, 2023
49fe40f
checkpoint
richardkiss Jan 13, 2023
8ecaee4
tests pass
richardkiss Jan 5, 2023
4a11b0f
checkpoint
richardkiss Jan 19, 2023
48b2b74
Rename `base`.
richardkiss Jan 20, 2023
23ab08e
Rename to `eval_error.py`.
richardkiss Jan 20, 2023
a15c2c1
Rename modules.
richardkiss Jan 20, 2023
31af146
Add comments, improve implementations.
richardkiss Jan 20, 2023
0107e8a
Remove `keywords.py`.
richardkiss Jan 20, 2023
f2162a0
Various improvements to python.
richardkiss Jan 24, 2023
d796ee4
More tests.
richardkiss Jan 24, 2023
fbff98c
Improvements to `uncurry`, tests, coverage.
richardkiss Jan 24, 2023
3172fda
Refactor.
richardkiss Jan 24, 2023
f80a0fc
More refactor.
richardkiss Jan 25, 2023
81e54ef
Handle end of stream properly.
richardkiss Jan 25, 2023
3cfbd56
More refactor.
richardkiss Jan 26, 2023
4b8fe5b
Fix name
richardkiss Jan 26, 2023
0acda17
refactor
richardkiss Jan 26, 2023
67dc587
fix benchmarks
richardkiss Jan 26, 2023
edde719
fix benchmark
richardkiss Jan 26, 2023
0c25e04
Support py37
richardkiss Jan 26, 2023
4f5c9dc
Use api
richardkiss Jan 26, 2023
3bc775d
fix comments
richardkiss Jan 27, 2023
0ffa864
lint
richardkiss Feb 1, 2023
c1724d7
Tests pass, coverage seems good, benchmarks seem good.
richardkiss Feb 3, 2023
8f7abca
Speed up `parse` and `__eq__`.
richardkiss Feb 7, 2023
2b7f9cf
Improve benchmarking code.
richardkiss Feb 7, 2023
b6905ea
Factor out `copy_exact` and `skip_bytes`.
richardkiss Feb 7, 2023
c6af427
Add `skip_clvm_object` api.
richardkiss Feb 8, 2023
61146ea
Add more `parse` tests.
richardkiss Feb 8, 2023
3a0183a
Use old error messages.
richardkiss Feb 8, 2023
ac05bc9
Remove a bunch of `.as_*` methods.
richardkiss Feb 10, 2023
818ee69
Don't cast `None`. Add `.at_many`.
richardkiss Feb 10, 2023
c9ece37
Don't cast `None`. Add `.at_many`.
richardkiss Feb 10, 2023
17196e7
Add back `.as_iter()`.
richardkiss Feb 16, 2023
2ba2689
Merge remote-tracking branch 'chia/main' into remove-clvm-dep
richardkiss Apr 18, 2023
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
Prev Previous commit
Next Next commit
lint
  • Loading branch information
richardkiss committed Feb 7, 2023
commit 0ffa864383d7c75e36bf29d324d3493d5b9fa531
4 changes: 2 additions & 2 deletions wheel/python/clvm_rs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from .eval_error import EvalError
from .program import Program
from .eval_error import EvalError # noqa: F401
from .program import Program # noqa: F401
6 changes: 5 additions & 1 deletion wheel/python/clvm_rs/casts.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Some utilities to cast python types to and from clvm.
"""

from typing import Callable, List, SupportsBytes, Tuple, Union
from typing import Callable, List, SupportsBytes, Tuple, Union, cast

from .clvm_storage import CLVMStorage, is_clvm_storage

Expand Down Expand Up @@ -99,6 +99,7 @@ def to_clvm_object(
if op == 0:
v = to_convert.pop()
if is_clvm_storage(v):
v = cast(CLVMStorage, v)
if v.pair is None:
atom = v.atom
assert atom is not None
Expand All @@ -114,6 +115,8 @@ def to_clvm_object(
ll_right = is_clvm_storage(right)
ll_left = is_clvm_storage(left)
if ll_right and ll_left:
left = cast(CLVMStorage, left)
right = cast(CLVMStorage, right)
did_convert.append(to_pair_f(left, right))
else:
ops.append(1) # cons
Expand All @@ -134,6 +137,7 @@ def to_clvm_object(
to_convert.append(_)
ops.append(0) # convert
continue
v = cast(AtomCastableType, v)
did_convert.append(to_atom_f(to_atom_type(v)))
continue
if op == 1: # cons
Expand Down
23 changes: 23 additions & 0 deletions wheel/python/clvm_rs/clvm_rs.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import List, Optional, Tuple

from .clvm_storage import CLVMStorage

def run_serialized_chia_program(
program: bytes, environment: bytes, max_cost: int, flags: int
) -> Tuple[int, CLVMStorage]: ...
def deserialize_as_tree(
blob: bytes, calculate_tree_hashes: bool
) -> Tuple[List[Tuple[int, int, int]], Optional[List[bytes]]]: ...
def serialized_length(blob: bytes) -> int: ...

NO_NEG_DIV: int
NO_UNKNOWN_OPS: int
LIMIT_HEAP: int
LIMIT_STACK: int
MEMPOOL_MODE: int

class LazyNode(CLVMStorage):
atom: Optional[bytes]

@property
def pair(self) -> Optional[Tuple[CLVMStorage, CLVMStorage]]: ...
8 changes: 3 additions & 5 deletions wheel/python/clvm_rs/clvm_storage.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
from typing import Optional, Tuple
from typing import Optional, Tuple, _SpecialForm, cast

# we support py3.7 which doesn't yet have typing.Protocol

try:
from typing import Protocol, runtime_checkable
from typing import Protocol
except ImportError:
Protocol = object
runtime_checkable = lambda arg: arg
Protocol = cast(_SpecialForm, object)


@runtime_checkable
class CLVMStorage(Protocol):
atom: Optional[bytes]

Expand Down
11 changes: 6 additions & 5 deletions wheel/python/clvm_rs/clvm_tree.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from .clvm_storage import CLVMStorage
from .de import deserialize_as_tuples

from typing import List, Optional, Tuple
from typing import List, Optional, Tuple, Union


class CLVMTree(CLVMStorage):
Expand Down Expand Up @@ -48,21 +48,22 @@ def from_bytes(cls, blob: bytes, calculate_tree_hash: bool = True) -> "CLVMTree"

def __init__(
self,
blob: bytes,
blob: Union[memoryview, bytes],
int_tuples: List[Tuple[int, int, int]],
tree_hashes: List[Optional[bytes]],
tree_hashes: Optional[List[bytes]],
index: int,
):
self.blob = blob
self.int_tuples = int_tuples
self.tree_hashes = tree_hashes
self.index = index
self._cached_sha256_treehash = self.tree_hashes[index]
if self.tree_hashes:
self._cached_sha256_treehash = self.tree_hashes[index]
start, end, atom_offset = self.int_tuples[self.index]
if self.blob[start] == 0xFF:
self.atom = None
else:
self.atom = bytes(self.blob[start + atom_offset : end])
self.atom = bytes(self.blob[start + atom_offset:end])
self._pair = None

@property
Expand Down
4 changes: 4 additions & 0 deletions wheel/python/clvm_rs/curry_and_treehash.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ def curry_and_treehash(
`arguments` : tree hashes of arguments to be curried
"""

for arg in hashed_arguments:
if not isinstance(arg, bytes) or len(arg) != 32:
raise ValueError(f"arguments must be bytes of len 32: {arg.hex()}")

curried_values = self.curried_values_tree_hash(list(hashed_arguments))
return shatree_pair(
self.a_kw_treehash,
Expand Down
34 changes: 17 additions & 17 deletions wheel/python/clvm_rs/de.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

from .tree_hash import shatree_atom, shatree_pair

deserialize_as_tree: Optional[
Callable[[bytes, bool], Tuple[List[Tuple[int, int, int]], Optional[List[bytes]]]]
]

try:
from clvm_rs.clvm_rs import deserialize_as_tree
except ImportError:
Expand All @@ -21,15 +25,13 @@

def deserialize_as_tuples(
blob: bytes, cursor: int, calculate_tree_hash: bool
) -> Tuple[List[Tuple[int, int, int]], List[Optional[bytes]]]:
) -> Tuple[List[Tuple[int, int, int]], Optional[List[bytes]]]:

if deserialize_as_tree:
try:
tree, hashes = deserialize_as_tree(blob, calculate_tree_hash)
except OSError as ex:
raise ValueError(ex)
if not calculate_tree_hash:
hashes = [None] * len(tree)
return tree, hashes

def save_cursor(
Expand All @@ -41,17 +43,14 @@ def save_cursor(
) -> int:
blob_index = obj_list[index][0]
assert blob[blob_index] == 0xFF
left_hash = tree_hash_list[index + 1]
hash_index = obj_list[index][2]
right_hash = tree_hash_list[hash_index]
tree_hash_list[index] = None
if calculate_tree_hash:
assert left_hash is not None
assert right_hash is not None
tree_hash_list[index] = shatree_pair(left_hash, right_hash)
v0 = obj_list[index][0]
v2 = obj_list[index][2]
obj_list[index] = (v0, cursor, v2)
if calculate_tree_hash:
left_hash = tree_hash_list[index + 1]
hash_index = obj_list[index][2]
right_hash = tree_hash_list[hash_index]
tree_hash_list[index] = shatree_pair(left_hash, right_hash)
return cursor

def save_index(
Expand All @@ -73,28 +72,29 @@ def parse_obj(

if blob[cursor] == CONS_BOX_MARKER:
index = len(obj_list)
tree_hash_list.append(None)
obj_list.append((cursor, 0, 0))
op_stack.append(lambda *args: save_cursor(index, *args))
op_stack.append(parse_obj)
op_stack.append(lambda *args: save_index(index, *args))
op_stack.append(parse_obj)
if calculate_tree_hash:
tree_hash_list.append(b"")
return cursor + 1
atom_offset, new_cursor = _atom_size_from_cursor(blob, cursor)
my_hash = None
if calculate_tree_hash:
my_hash = shatree_atom(blob[cursor + atom_offset : new_cursor])
tree_hash_list.append(my_hash)
my_hash = shatree_atom(blob[cursor + atom_offset:new_cursor])
tree_hash_list.append(my_hash)
obj_list.append((cursor, new_cursor, atom_offset))
return new_cursor

obj_list: List[Triple] = []
tree_hash_list: List[Optional[bytes]] = []
tree_hash_list: List[bytes] = []
op_stack: List[DeserOp] = [parse_obj]
while op_stack:
f = op_stack.pop()
cursor = f(blob, cursor, obj_list, op_stack)
return obj_list, tree_hash_list
return obj_list, tree_hash_list if calculate_tree_hash else None


def _atom_size_from_cursor(blob, cursor) -> Tuple[int, int]:
Expand All @@ -112,7 +112,7 @@ def _atom_size_from_cursor(blob, cursor) -> Tuple[int, int]:
bit_mask >>= 1
size_blob = bytes([b])
if bit_count > 1:
size_blob += blob[cursor + 1 : cursor + bit_count]
size_blob += blob[cursor + 1:cursor + bit_count]
size = int.from_bytes(size_blob, "big")
new_cursor = cursor + size + bit_count
if new_cursor > len(blob):
Expand Down
47 changes: 32 additions & 15 deletions wheel/python/clvm_rs/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

class Program(CLVMStorage):
"""
A thin wrapper around s-expression data intended to be invoked with "eval".
A wrapper around `CLVMStorage` providing many convenience functions.
"""

curry_treehasher: CurryTreehasher = CHIA_CURRY_TREEHASHER
Expand Down Expand Up @@ -234,10 +234,10 @@ def run_with_cost(
prog_bytes = bytes(self)
args_bytes = bytes(self.to(args))
try:
cost, r = run_serialized_chia_program(
cost, lazy_node = run_serialized_chia_program(
prog_bytes, args_bytes, max_cost, flags
)
r = self.wrap(r)
r = self.wrap(lazy_node)
except ValueError as ve:
raise EvalError(ve.args[0], self.wrap(ve.args[1]))
return cost, r
Expand All @@ -260,7 +260,7 @@ def curry(self, *args) -> "Program":

The resulting argument list is interpreted with apply (2)

(2 (1 . self) rest)
(a (q . self) rest)

Resulting in a function which places its own arguments after those
curried in in the form of a proper list.
Expand All @@ -282,8 +282,32 @@ def uncurry(self) -> Tuple[Program, Optional[List[Program]]]:
p_args = args if args is None else [self.to(_) for _ in args]
return self.to(mod), p_args

def curry_hash(self, *args: bytes32) -> bytes32:
"""
Return a puzzle hash that would be created if you curried this puzzle
with arguments that have the given hashes.

In other words,

```
c1 = self.curry(arg1, arg2, arg3).tree_hash()
c2 = self.curry_hash(arg1.tree_hash(), arg2.tree_hash(), arg3.tree_hash())
assert c1 == c2 # they will be the same
```

This looks useless to the unitiated, but sometimes you'll need a puzzle
hash where you don't actually know the contents of a clvm subtree -- just its
hash. This lets you calculate the puzzle hash with hidden information.
"""
curry_treehasher = self.curry_treehasher
quoted_mod_hash = curry_treehasher.calculate_hash_of_quoted_mod_hash(self.tree_hash())
return curry_treehasher.curry_and_treehash(quoted_mod_hash, *args)

def as_int(self) -> int:
return int_from_bytes(self.as_atom())
v = self.as_atom()
if v is None:
raise ValueError("can't cast pair to int")
return int_from_bytes(v)

def as_iter(self) -> Iterator[Program]:
v = self
Expand All @@ -293,11 +317,8 @@ def as_iter(self) -> Iterator[Program]:

def as_atom_iter(self) -> Iterator[bytes]:
"""
Pretend `self` is a list of atoms. Yield the corresponding atoms.

At each step, we always assume a node to be an atom or a pair.
If the assumption is wrong, we exit early. This way we never fail
and always return SOMETHING.
Pretend `self` is a list of atoms. Yield the corresponding atoms
up until this assumption is wrong.
"""
obj = self
while obj.pair is not None:
Expand All @@ -310,11 +331,7 @@ def as_atom_iter(self) -> Iterator[bytes]:
def as_atom_list(self) -> List[bytes]:
"""
Pretend `self` is a list of atoms. Return the corresponding
python list of atoms.

At each step, we always assume a node to be an atom or a pair.
If the assumption is wrong, we exit early. This way we never fail
and always return SOMETHING.
python list of atoms up until this assumption is wrong.
"""
return list(self.as_atom_iter())

Expand Down
6 changes: 3 additions & 3 deletions wheel/python/clvm_rs/ser.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def sexp_to_byte_iterator(sexp: CLVMStorage) -> Iterator[bytes]:
yield from atom_to_byte_iterator(atom)


def size_blob_for_size(blob: int) -> bytes:
def size_blob_for_blob(blob: bytes) -> bytes:
size = len(blob)
if size < 0x40:
return bytes([0x80 | size])
Expand All @@ -73,7 +73,7 @@ def size_blob_for_size(blob: int) -> bytes:
(size >> 0) & 0xFF,
]
)
raise ValueError("sexp too long %r" % blob)
raise ValueError("blob too long %r" % blob)


def atom_to_byte_iterator(as_atom: bytes) -> Iterator[bytes]:
Expand All @@ -88,7 +88,7 @@ def atom_to_byte_iterator(as_atom: bytes) -> Iterator[bytes]:
if as_atom[0] <= MAX_SINGLE_BYTE:
yield as_atom
return
yield size_blob_for_size(as_atom)
yield size_blob_for_blob(as_atom)
yield as_atom


Expand Down
14 changes: 5 additions & 9 deletions wheel/python/clvm_rs/tree_hash.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@

from hashlib import sha256
from typing import List
from weakref import WeakKeyDictionary

from .bytes32 import bytes32
from .clvm_storage import CLVMStorage

bytes32 = bytes


class Treehasher:
atom_prefix: bytes
Expand All @@ -22,7 +20,6 @@ class Treehasher:
def __init__(self, atom_prefix: bytes, pair_prefix: bytes):
self.atom_prefix = atom_prefix
self.pair_prefix = pair_prefix
self.hash_cache: WeakKeyDictionary[CLVMStorage, bytes32] = WeakKeyDictionary()
self.cache_hits = 0

def shatree_atom(self, atom: bytes) -> bytes32:
Expand All @@ -42,8 +39,6 @@ def sha256_treehash(self, sexp: CLVMStorage) -> bytes32:
def handle_sexp(sexp_stack, hash_stack, op_stack) -> None:
sexp = sexp_stack.pop()
r = getattr(sexp, "_cached_sha256_treehash", None)
if r is None:
r = self.hash_cache.get(sexp)
if r is not None:
self.cache_hits += 1
hash_stack.append(r)
Expand All @@ -59,15 +54,16 @@ def handle_sexp(sexp_stack, hash_stack, op_stack) -> None:
r = shatree_atom(sexp.atom)
hash_stack.append(r)
sexp._cached_sha256_treehash = r
self.hash_cache[sexp] = r

def handle_pair(sexp_stack, hash_stack, op_stack) -> None:
p0 = hash_stack.pop()
p1 = hash_stack.pop()
r = shatree_pair(p0, p1)
hash_stack.append(r)
sexp._cached_sha256_treehash = r
self.hash_cache[sexp] = r
try:
setattr(sexp, "_cached_sha256_treehash", r)
except AttributeError:
pass

sexp_stack = [sexp]
op_stack = [handle_sexp]
Expand Down
Loading