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

add all member functions to Program #291

Merged
merged 2 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:

- name: Build
run: |
python -m pip install clvm_tools colorama blspy
python -m pip install clvm_tools colorama blspy chia-blockchain==2.1.0
maturin develop --release -m wheel/Cargo.toml

- name: python mypy
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ jobs:
source venv/bin/activate
git clone https://github.com/Chia-Network/clvm_tools.git --branch=main --single-branch
pip install ./clvm_tools
pip install colorama maturin pytest
pip install colorama maturin pytest chia-blockchain==2.1.0
maturin develop --release -m wheel/Cargo.toml
pytest tests
grcov . --binary-path target -s . --branch --ignore-not-existing --ignore='*/.cargo/*' --ignore='tests/*' --ignore='venv/*' -o rust_cov.info
Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions chia-protocol/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@ repository = "https://github.com/Chia-Network/chia_rs/chia-protocol/"
py-bindings = ["dep:pyo3", "dep:chia_py_streamable_macro", "chia-traits/py-bindings"]

[dependencies]
pyo3 = { version = "0.19.0", features = ["multiple-pymethods"], optional = true }
pyo3 = { version = "0.19.0", features = ["multiple-pymethods", "num-bigint"], optional = true }
arvidn marked this conversation as resolved.
Show resolved Hide resolved
sha2 = "0.10.8"
hex = "0.4.3"
chia_streamable_macro = { version = "=0.2.12", path = "../chia_streamable_macro" }
chia_py_streamable_macro = { version = "=0.2.13", path = "../chia_py_streamable_macro", optional = true }
clvmr = "=0.3.0"
chia-traits = { version = "=0.2.13", path = "../chia-traits" }
clvm-traits = { version = "=0.2.12", path = "../clvm-traits", features = ["derive"] }
chia-bls = { version = "0.2.13", path = "../chia-bls" }
clvm-utils = { version = "=0.2.12", path = "../clvm-utils" }
chia-bls = { version = "=0.2.13", path = "../chia-bls" }
arbitrary = { version = "1.3.0", features = ["derive"] }

[dev-dependencies]
Expand Down
273 changes: 264 additions & 9 deletions chia-protocol/src/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,6 @@ use clvmr::Allocator;
use sha2::{Digest, Sha256};
use std::io::Cursor;

#[cfg(feature = "py-bindings")]
use chia_traits::{FromJsonDict, ToJsonDict};

#[cfg(feature = "py-bindings")]
use chia_py_streamable_macro::PyStreamable;

#[cfg(feature = "py-bindings")]
use pyo3::prelude::*;

#[cfg_attr(feature = "py-bindings", pyclass, derive(PyStreamable))]
#[derive(Hash, Debug, Clone, Eq, PartialEq)]
pub struct Program(Bytes);
Expand Down Expand Up @@ -59,6 +50,270 @@ impl Program {
}
}

impl Default for Program {
fn default() -> Self {
Self(vec![0x80_u8].into())
}
}

#[cfg(feature = "py-bindings")]
use crate::lazy_node::LazyNode;

#[cfg(feature = "py-bindings")]
use chia_traits::{FromJsonDict, ToJsonDict};

#[cfg(feature = "py-bindings")]
use chia_py_streamable_macro::PyStreamable;

#[cfg(feature = "py-bindings")]
use pyo3::prelude::*;

#[cfg(feature = "py-bindings")]
use pyo3::types::{PyList, PyTuple};

#[cfg(feature = "py-bindings")]
use clvmr::serde::node_from_bytes_backrefs;

#[cfg(feature = "py-bindings")]
use clvmr::allocator::SExp;

#[cfg(feature = "py-bindings")]
use pyo3::exceptions::*;

// TODO: this conversion function should probably be converted to a type holding
// the PyAny object implementing the ToClvm trait. That way, the Program::to()
// function could turn a python structure directly into bytes, without taking
// the detour via Allocator. propagating python errors through ToClvmError is a
// bit tricky though
#[cfg(feature = "py-bindings")]
fn clvm_convert(a: &mut Allocator, o: &PyAny) -> PyResult<NodePtr> {
// None
if o.is_none() {
Ok(a.null())
// Program itself
} else if let Ok(prg) = o.extract::<Program>() {
Ok(node_from_bytes_backrefs(a, prg.0.as_slice())?)
// bytes
} else if let Ok(buffer) = o.extract::<&[u8]>() {
a.new_atom(buffer)
.map_err(|e| PyMemoryError::new_err(e.to_string()))
// str
} else if let Ok(text) = o.extract::<String>() {
a.new_atom(text.as_bytes())
.map_err(|e| PyMemoryError::new_err(e.to_string()))
// int
} else if let Ok(val) = o.extract::<clvmr::number::Number>() {
a.new_number(val)
.map_err(|e| PyMemoryError::new_err(e.to_string()))
// Tuple (SExp-like)
} else if let Ok(pair) = o.downcast::<PyTuple>() {
if pair.len() != 2 {
Err(PyValueError::new_err(format!(
"can't cast tuple of size {}",
pair.len()
)))
} else {
let left = clvm_convert(a, pair.get_item(0)?)?;
let right = clvm_convert(a, pair.get_item(1)?)?;
a.new_pair(left, right)
.map_err(|e| PyMemoryError::new_err(e.to_string()))
}
// List
} else 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_convert(a, py_item)?;
ret = a
.new_pair(item, ret)
.map_err(|e| PyMemoryError::new_err(e.to_string()))?;
}
Ok(ret)
// SExp (such as clvm.SExp)
} else if let (Ok(atom), Ok(pair)) = (o.getattr("atom"), o.getattr("pair")) {
if atom.is_none() {
if pair.is_none() {
Err(PyTypeError::new_err(format!("invalid SExp item {o}")))
} else {
let pair = pair.downcast::<PyTuple>()?;
let left = clvm_convert(a, &pair[0])?;
let right = clvm_convert(a, &pair[1])?;
a.new_pair(left, right)
.map_err(|e| PyMemoryError::new_err(e.to_string()))
}
} else {
a.new_atom(atom.extract::<&[u8]>()?)
.map_err(|e| PyMemoryError::new_err(e.to_string()))
}
// anything convertible to bytes
} else if let Ok(fun) = o.getattr("__bytes__") {
let bytes = fun.call0()?;
let buffer = bytes.extract::<&[u8]>()?;
a.new_atom(buffer)
.map_err(|e| PyMemoryError::new_err(e.to_string()))
} else {
Err(PyTypeError::new_err(format!(
"unknown parameter to run_with_cost() {o}"
)))
}
}

#[cfg(feature = "py-bindings")]
fn to_program(py: Python<'_>, node: LazyNode) -> PyResult<&PyAny> {
arvidn marked this conversation as resolved.
Show resolved Hide resolved
use pyo3::types::PyDict;
let ctx: &PyDict = PyDict::new(py);
ctx.set_item("node", node)?;
py.run(
"from chia.types.blockchain_format.program import Program\n\
ret = Program(node)\n",
None,
Some(ctx),
)?;
Ok(ctx.get_item("ret").unwrap())
}

#[cfg(feature = "py-bindings")]
#[pymethods]
impl Program {
#[pyo3(name = "default")]
#[staticmethod]
fn py_default() -> Self {
Self::default()
}

#[staticmethod]
#[pyo3(name = "to")]
fn py_to(args: &PyAny) -> PyResult<Program> {
let mut a = Allocator::new_limited(500000000, 62500000, 62500000);
let clvm = clvm_convert(&mut a, args)?;
Ok(Program::from_node_ptr(&a, clvm)?)
}

fn get_tree_hash(&self) -> crate::Bytes32 {
let mut cursor = Cursor::new(self.0.as_ref());
clvmr::serde::tree_hash_from_stream(&mut cursor)
.unwrap()
.into()
}

#[staticmethod]
fn from_program(py: Python<'_>, p: PyObject) -> PyResult<Self> {
let buf = p.getattr(py, "__bytes__")?.call0(py)?;
let buf = buf.extract::<&[u8]>(py)?;
Ok(Self(buf.into()))
}

#[staticmethod]
fn fromhex(h: String) -> Result<Self> {
let s = if let Some(st) = h.strip_prefix("0x") {
st
} else {
&h[..]
};
Self::from_bytes(hex::decode(s).map_err(|_| Error::InvalidString)?.as_slice())
}

fn run_mempool_with_cost<'a>(
&self,
py: Python<'a>,
max_cost: u64,
args: &PyAny,
) -> PyResult<(u64, &'a PyAny)> {
use clvmr::MEMPOOL_MODE;
self._run(py, max_cost, MEMPOOL_MODE, args)
}

fn run_with_cost<'a>(
&self,
py: Python<'a>,
max_cost: u64,
args: &PyAny,
) -> PyResult<(u64, &'a PyAny)> {
self._run(py, max_cost, 0, args)
}

fn _run<'a>(
&self,
py: Python<'a>,
max_cost: u64,
flags: u32,
args: &PyAny,
) -> PyResult<(u64, &'a PyAny)> {
use clvmr::reduction::Response;
use clvmr::run_program;
use clvmr::ChiaDialect;
use std::rc::Rc;

let mut a = Allocator::new_limited(500000000, 62500000, 62500000);
let clvm_args = clvm_convert(&mut a, args)?;

let r: Response = (|| -> PyResult<Response> {
let program = node_from_bytes_backrefs(&mut a, self.0.as_ref())?;
let dialect = ChiaDialect::new(flags);

Ok(py.allow_threads(|| run_program(&mut a, &dialect, program, clvm_args, max_cost)))
})()?;
match r {
Ok(reduction) => {
let val = LazyNode::new(Rc::new(a), reduction.1);
Ok((reduction.0, to_program(py, val)?))
}
Err(eval_err) => Err(PyValueError::new_err(eval_err.to_string())),
}
}

fn to_program<'a>(&self, py: Python<'a>) -> PyResult<&'a PyAny> {
use std::rc::Rc;
let mut a = Allocator::new_limited(500000000, 62500000, 62500000);
let prg = node_from_bytes_backrefs(&mut a, self.0.as_ref())?;
let prg = LazyNode::new(Rc::new(a), prg);
to_program(py, prg)
}

fn uncurry<'a>(&self, py: Python<'a>) -> PyResult<(&'a PyAny, &'a PyAny)> {
use clvm_utils::CurriedProgram;
use std::rc::Rc;

let mut a = Allocator::new_limited(500000000, 62500000, 62500000);
let prg = node_from_bytes_backrefs(&mut a, self.0.as_ref())?;
let Ok(uncurried) = CurriedProgram::<NodePtr, NodePtr>::from_node_ptr(&a, prg) else {
let a = Rc::new(a);
let prg = LazyNode::new(a.clone(), prg);
let ret = a.null();
let ret = LazyNode::new(a, ret);
return Ok((to_program(py, prg)?, to_program(py, ret)?));
};

let mut curried_args = Vec::<NodePtr>::new();
let mut args = uncurried.args;
loop {
if let SExp::Atom = a.sexp(args) {
break;
}
// the args of curried puzzles are in the form of:
// (c . ((q . <arg1>) . (<rest> . ())))
let (_, ((_, arg), (rest, _))) =
<(
clvm_traits::MatchByte<4>,
(clvm_traits::match_quote!(NodePtr), (NodePtr, ())),
) as clvm_traits::FromNodePtr>::from_node_ptr(&a, args)?;
curried_args.push(arg);
args = rest;
}
let mut ret = a.null();
for item in curried_args.into_iter().rev() {
ret = a.new_pair(item, ret).map_err(|_e| Error::EndOfBuffer)?;
}
let a = Rc::new(a);
let prg = LazyNode::new(a.clone(), uncurried.program);
let ret = LazyNode::new(a, ret);
Ok((to_program(py, prg)?, to_program(py, ret)?))
}
}

impl Streamable for Program {
fn update_digest(&self, digest: &mut Sha256) {
digest.update(&self.0);
Expand Down
2 changes: 2 additions & 0 deletions clvm-traits/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ features = ["derive"]

[features]
derive = ["dep:clvm-derive"]
py-bindings = ["dep:pyo3"]

[dependencies]
pyo3 = { version = ">=0.19.0", optional = true }
clvm-derive = { version = "0.2.12", path = "../clvm-derive", optional = true }
clvmr = "0.3.0"
num-bigint = "0.4.3"
Expand Down
17 changes: 17 additions & 0 deletions clvm-traits/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,20 @@ pub enum FromClvmError {
#[error("{0}")]
Custom(String),
}

#[cfg(feature = "py-bindings")]
use pyo3::PyErr;

#[cfg(feature = "py-bindings")]
impl std::convert::From<ToClvmError> for PyErr {
fn from(err: ToClvmError) -> PyErr {
pyo3::exceptions::PyValueError::new_err(err.to_string())
}
}

#[cfg(feature = "py-bindings")]
impl std::convert::From<FromClvmError> for PyErr {
fn from(err: FromClvmError) -> PyErr {
pyo3::exceptions::PyValueError::new_err(err.to_string())
}
}
Loading