Skip to content

Commit

Permalink
Merge pull request #350 from Chia-Network/program-run
Browse files Browse the repository at this point in the history
fix argument conversion for `Program::_run()`
  • Loading branch information
arvidn authored Dec 17, 2023
2 parents a341985 + 556b4e1 commit 7186de0
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 3 deletions.
57 changes: 56 additions & 1 deletion chia-protocol/src/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,52 @@ fn clvm_convert(a: &mut Allocator, o: &PyAny) -> PyResult<NodePtr> {
}
}

#[cfg(feature = "py-bindings")]
fn clvm_serialize(a: &mut Allocator, o: &PyAny) -> PyResult<NodePtr> {
/*
When passing arguments to run(), there's some special treatment, before falling
back on the regular python -> CLVM conversion (implemented by clvm_convert
above). This function mimics the _serialize() function in python:
def _serialize(node: object) -> bytes:
if isinstance(node, list):
serialized_list = bytearray()
for a in node:
serialized_list += b"\xff"
serialized_list += _serialize(a)
serialized_list += b"\x80"
return bytes(serialized_list)
if type(node) is SerializedProgram:
return bytes(node)
if type(node) is Program:
return bytes(node)
else:
ret: bytes = SExp.to(node).as_bin()
return ret
*/

// List
if let Ok(list) = o.downcast::<PyList>() {
let mut rev = Vec::<&PyAny>::new();
for py_item in list.iter() {
rev.push(py_item);
}
let mut ret = a.null();
for py_item in rev.into_iter().rev() {
let item = clvm_serialize(a, py_item)?;
ret = a
.new_pair(item, ret)
.map_err(|e| PyMemoryError::new_err(e.to_string()))?;
}
Ok(ret)
// Program itself
} else if let Ok(prg) = o.extract::<Program>() {
Ok(node_from_bytes_backrefs(a, prg.0.as_slice())?)
} else {
clvm_convert(a, o)
}
}

#[cfg(feature = "py-bindings")]
fn to_program(py: Python<'_>, node: LazyNode) -> PyResult<&PyAny> {
use pyo3::types::PyDict;
Expand Down Expand Up @@ -252,7 +298,16 @@ impl Program {
use std::rc::Rc;

let mut a = Allocator::new_limited(500000000, 62500000, 62500000);
let clvm_args = clvm_convert(&mut a, args)?;
// The python behavior here is a bit messy, and is best not emulated
// on the rust side. We must be able to pass a Program as an argument,
// and it being treated as the CLVM structure it represents. In python's
// SerializedProgram, we have a hack where we interpret the first
// "layer" of SerializedProgram, or lists of SerializedProgram this way.
// But if we encounter an Optional or tuple, we defer to the clvm
// wheel's conversion function to SExp. This level does not have any
// special treatment for SerializedProgram (as that would cause a
// circular dependency).
let clvm_args = clvm_serialize(&mut a, args)?;

let r: Response = (|| -> PyResult<Response> {
let program = node_from_bytes_backrefs(&mut a, self.0.as_ref())?;
Expand Down
23 changes: 21 additions & 2 deletions tests/test_program_fidelity.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import string
import chia_rs
from chia.types.blockchain_format.program import Program as ChiaProgram
from chia.types.blockchain_format.serialized_program import SerializedProgram
from random import Random

def rand_bytes(rnd: Random) -> bytes:
Expand Down Expand Up @@ -41,18 +42,36 @@ def rand_object(rnd: Random) -> object:
types = [rand_optional, rand_int, rand_string, rand_bytes, rand_program, rand_list, rand_rust_program]
return rnd.sample(types, 1)[0](rnd)

def recursive_replace(o: object) -> object:
if isinstance(o, list):
ret = []
for i in o:
ret.append(recursive_replace(i))
return ret
elif isinstance(o, chia_rs.Program):
return SerializedProgram.from_bytes(o.to_bytes())
else:
return o

def test_run_program() -> None:

rust_identity = chia_rs.Program.from_bytes(b"\x01")
py_identity = ChiaProgram.from_bytes(b"\x01")
py_identity = SerializedProgram.from_bytes(b"\x01")

rnd = Random()
for _ in range(10000):
args = rand_object(rnd)

py_ret = py_identity._run(10000, 0, args)
# the python SerializedProgram treats itself specially, the rust
# Program treats itself specially, but they don't recognize each other,
# so they will produce different results in this regard
rust_ret = rust_identity._run(10000, 0, args)

# Replace rust Program with the python SerializedProgram.
args = recursive_replace(args)

py_ret = py_identity._run(10000, 0, args)

assert rust_ret == py_ret

def test_tree_hash() -> None:
Expand Down

0 comments on commit 7186de0

Please sign in to comment.