diff --git a/.travis.yml b/.travis.yml index ab0587427..0fe5d7632 100644 --- a/.travis.yml +++ b/.travis.yml @@ -71,8 +71,8 @@ matrix: - wasm-pack build - wasm-pack test --firefox --headless - wasm-pack test --firefox --headless --release - - wasm-pack test --chrome --headless - - wasm-pack test --chrome --headless --release + # - wasm-pack test --chrome --headless + # - wasm-pack test --chrome --headless --release - name: JS tests dist: bionic diff --git a/sigma-tree/src/chain/context_extension.rs b/sigma-tree/src/chain/context_extension.rs index 31bd22ab5..ad0218f4a 100644 --- a/sigma-tree/src/chain/context_extension.rs +++ b/sigma-tree/src/chain/context_extension.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use std::io; /// User-defined variables to be put into context -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] pub struct ContextExtension { /// key-value pairs of variable id and it's value diff --git a/sigma-tree/src/chain/data_input.rs b/sigma-tree/src/chain/data_input.rs index 421f624cc..bca6c35ab 100644 --- a/sigma-tree/src/chain/data_input.rs +++ b/sigma-tree/src/chain/data_input.rs @@ -14,11 +14,12 @@ use proptest_derive::Arbitrary; use serde::{Deserialize, Serialize}; /// Inputs, that are used to enrich script context, but won't be spent by the transaction -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] #[cfg_attr(test, derive(Arbitrary))] #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] pub struct DataInput { /// id of the box to add into context (should be in UTXO) + #[cfg_attr(feature = "with-serde", serde(rename = "boxId"))] pub box_id: BoxId, } diff --git a/sigma-tree/src/chain/ergo_box.rs b/sigma-tree/src/chain/ergo_box.rs index c0112ff3c..8d3544231 100644 --- a/sigma-tree/src/chain/ergo_box.rs +++ b/sigma-tree/src/chain/ergo_box.rs @@ -25,6 +25,8 @@ use sigma_ser::vlq_encode; #[cfg(feature = "with-serde")] use std::convert::TryFrom; use std::io; +#[cfg(feature = "with-serde")] +use thiserror::Error; /// Box (aka coin, or an unspent output) is a basic concept of a UTXO-based cryptocurrency. /// In Bitcoin, such an object is associated with some monetary value (arbitrary, @@ -143,9 +145,18 @@ impl ErgoBox { } } +/// Errors on parsing ErgoBox from JSON +#[cfg(feature = "with-serde")] +#[derive(Error, PartialEq, Eq, Debug, Clone)] +pub enum ErgoBoxFromJsonError { + /// Box id parsed from JSON differs from calculated from box serialized bytes + #[error("Box id parsed from JSON differs from calculated from box serialized bytes")] + InvalidBoxId, +} + #[cfg(feature = "with-serde")] impl TryFrom for ErgoBox { - type Error = json::ergo_box::ErgoBoxFromJsonError; + type Error = ErgoBoxFromJsonError; fn try_from(box_json: json::ergo_box::ErgoBoxFromJson) -> Result { let box_with_zero_id = ErgoBox { box_id: BoxId::zero(), @@ -165,7 +176,7 @@ impl TryFrom for ErgoBox { if ergo_box.box_id() == box_json.box_id { Ok(ergo_box) } else { - Err(json::ergo_box::ErgoBoxFromJsonError::InvalidBoxId) + Err(ErgoBoxFromJsonError::InvalidBoxId) } } } @@ -196,7 +207,7 @@ impl SigmaSerializable for ErgoBox { /// Contains the same fields as `ErgoBox`, except if transaction id and index, /// that will be calculated after full transaction formation. -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Clone, Debug)] pub struct ErgoBoxCandidate { /// amount of money associated with the box pub value: BoxValue, @@ -260,6 +271,18 @@ impl SigmaSerializable for ErgoBoxCandidate { } } +impl From for ErgoBoxCandidate { + fn from(b: ErgoBox) -> Self { + ErgoBoxCandidate { + value: b.value, + ergo_tree: b.ergo_tree, + tokens: b.tokens, + additional_registers: b.additional_registers, + creation_height: b.creation_height, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/sigma-tree/src/chain/input.rs b/sigma-tree/src/chain/input.rs index f6dad6b93..5ddce24e5 100644 --- a/sigma-tree/src/chain/input.rs +++ b/sigma-tree/src/chain/input.rs @@ -9,16 +9,31 @@ use super::{box_id::BoxId, prover_result::ProverResult}; use serde::{Deserialize, Serialize}; /// Fully signed transaction input -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] #[cfg_attr(test, derive(proptest_derive::Arbitrary))] #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] pub struct Input { /// id of the box to spent + #[cfg_attr(feature = "with-serde", serde(rename = "boxId"))] pub box_id: BoxId, /// proof of spending correctness + #[cfg_attr(feature = "with-serde", serde(rename = "spendingProof"))] pub spending_proof: ProverResult, } +impl Input { + /// input with an empty proof + pub fn input_to_sign(&self) -> Input { + Input { + box_id: self.box_id.clone(), + spending_proof: ProverResult { + proof: vec![], + extension: self.spending_proof.extension.clone(), + }, + } + } +} + impl SigmaSerializable for Input { fn sigma_serialize(&self, w: &mut W) -> Result<(), io::Error> { self.box_id.sigma_serialize(w)?; diff --git a/sigma-tree/src/chain/json.rs b/sigma-tree/src/chain/json.rs index a99783941..9d5093b2b 100644 --- a/sigma-tree/src/chain/json.rs +++ b/sigma-tree/src/chain/json.rs @@ -44,7 +44,6 @@ pub mod ergo_box { ErgoTree, }; use serde::Deserialize; - use thiserror::Error; #[derive(Deserialize, PartialEq, Eq, Debug, Clone)] pub struct ErgoBoxFromJson { @@ -74,17 +73,33 @@ pub mod ergo_box { #[serde(rename = "index")] pub index: u16, } +} + +pub mod transaction { + use crate::chain::{data_input::DataInput, ErgoBox, Input, TxId}; + use serde::{Deserialize, Serialize}; - #[derive(Error, PartialEq, Eq, Debug, Clone)] - pub enum ErgoBoxFromJsonError { - #[error("Box id parsed from JSON differs from calculated from box serialized bytes")] - InvalidBoxId, + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + pub struct TransactionJson { + #[cfg_attr(feature = "with-serde", serde(rename = "id"))] + pub tx_id: TxId, + /// inputs, that will be spent by this transaction. + #[cfg_attr(feature = "with-serde", serde(rename = "inputs"))] + pub inputs: Vec, + /// inputs, that are not going to be spent by transaction, but will be reachable from inputs + /// scripts. `dataInputs` scripts will not be executed, thus their scripts costs are not + /// included in transaction cost and they do not contain spending proofs. + #[cfg_attr(feature = "with-serde", serde(rename = "dataInputs"))] + pub data_inputs: Vec, + #[cfg_attr(feature = "with-serde", serde(rename = "outputs"))] + pub outputs: Vec, } } #[cfg(test)] mod tests { use super::super::ergo_box::*; + use super::super::transaction::*; use super::*; use proptest::prelude::*; use register::NonMandatoryRegisters; @@ -99,6 +114,15 @@ mod tests { prop_assert_eq![b, b_parsed]; } + #[test] + fn tx_roundtrip(t in any::()) { + let j = serde_json::to_string(&t)?; + // dbg!(j); + eprintln!("{}", j); + let t_parsed: Transaction = serde_json::from_str(&j)?; + prop_assert_eq![t, t_parsed]; + } + } #[test] diff --git a/sigma-tree/src/chain/prover_result.rs b/sigma-tree/src/chain/prover_result.rs index 584f8cb49..2460e1091 100644 --- a/sigma-tree/src/chain/prover_result.rs +++ b/sigma-tree/src/chain/prover_result.rs @@ -9,7 +9,7 @@ use super::context_extension::ContextExtension; use serde::{Deserialize, Serialize}; /// Proof of correctness of tx spending -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] pub struct ProverResult { /// proof that satisfies final sigma proposition diff --git a/sigma-tree/src/chain/transaction.rs b/sigma-tree/src/chain/transaction.rs index 2faa30a0a..2fda65fdb 100644 --- a/sigma-tree/src/chain/transaction.rs +++ b/sigma-tree/src/chain/transaction.rs @@ -1,20 +1,28 @@ //! Ergo transaction +#[cfg(feature = "with-serde")] +use super::json; use super::{ - data_input::DataInput, digest32::Digest32, ergo_box::ErgoBoxCandidate, input::Input, + data_input::DataInput, + digest32::{blake2b256_hash, Digest32}, + ergo_box::ErgoBoxCandidate, + input::Input, token::TokenId, + ErgoBox, }; use indexmap::IndexSet; #[cfg(test)] use proptest_derive::Arbitrary; #[cfg(feature = "with-serde")] -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::{Deserialize, Serialize}; use sigma_ser::serializer::SerializationError; use sigma_ser::serializer::SigmaSerializable; use sigma_ser::vlq_encode; use std::convert::TryFrom; use std::io; use std::iter::FromIterator; +#[cfg(feature = "with-serde")] +use thiserror::Error; /// Transaction id (ModifierId in sigmastate) #[derive(PartialEq, Eq, Hash, Debug, Clone)] @@ -22,6 +30,13 @@ use std::iter::FromIterator; #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] pub struct TxId(pub Digest32); +impl TxId { + /// All zeros + pub fn zero() -> TxId { + TxId(Digest32::zero()) + } +} + impl SigmaSerializable for TxId { fn sigma_serialize(&self, w: &mut W) -> Result<(), io::Error> { self.0.sigma_serialize(w)?; @@ -42,8 +57,17 @@ impl SigmaSerializable for TxId { * Transactions are not encrypted, so it is possible to browse and view every transaction ever * collected into a block. */ -#[derive(PartialEq, Debug)] +#[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "with-serde", + serde( + try_from = "json::transaction::TransactionJson", + into = "json::transaction::TransactionJson" + ) +)] +#[derive(PartialEq, Debug, Clone)] pub struct Transaction { + tx_id: TxId, /// inputs, that will be spent by this transaction. pub inputs: Vec, /// inputs, that are not going to be spent by transaction, but will be reachable from inputs @@ -55,6 +79,51 @@ pub struct Transaction { pub output_candidates: Vec, } +impl Transaction { + /// Creates new transation + pub fn new( + inputs: Vec, + data_inputs: Vec, + output_candidates: Vec, + ) -> Transaction { + let tx_to_sign = Transaction { + tx_id: TxId::zero(), + inputs, + data_inputs, + output_candidates, + }; + let tx_id = tx_to_sign.calc_tx_id(); + Transaction { + tx_id, + ..tx_to_sign + } + } + + /// create ErgoBox from ErgoBoxCandidate with tx id and indices + pub fn outputs(&self) -> Vec { + assert!(self.output_candidates.len() < u16::MAX as usize); + self.output_candidates + .iter() + .enumerate() + .map(|(idx, bc)| ErgoBox::from_box_candidate(bc, self.tx_id.clone(), idx as u16)) + .collect() + } + + fn calc_tx_id(&self) -> TxId { + let bytes = self.bytes_to_sign(); + TxId(blake2b256_hash(&bytes)) + } + + fn bytes_to_sign(&self) -> Vec { + let empty_proof_inputs = self.inputs.iter().map(|i| i.input_to_sign()).collect(); + let tx_to_sign = Transaction { + inputs: empty_proof_inputs, + ..(*self).clone() + }; + tx_to_sign.sigma_serialise_bytes() + } +} + impl SigmaSerializable for Transaction { fn sigma_serialize(&self, w: &mut W) -> Result<(), io::Error> { // reference implementation - https://github.com/ScorexFoundation/sigmastate-interpreter/blob/9b20cb110effd1987ff76699d637174a4b2fb441/sigmastate/src/main/scala/org/ergoplatform/ErgoLikeTransaction.scala#L112-L112 @@ -121,32 +190,52 @@ impl SigmaSerializable for Transaction { )?) } - Ok(Transaction { - inputs, - data_inputs, - output_candidates: outputs, - }) + Ok(Transaction::new(inputs, data_inputs, outputs)) } } #[cfg(feature = "with-serde")] -impl serde::Serialize for Transaction { - fn serialize(&self, s: S) -> Result - where - S: Serializer, - { - // not implmented - s.serialize_str("TBD") +impl Into for Transaction { + fn into(self) -> json::transaction::TransactionJson { + json::transaction::TransactionJson { + tx_id: self.tx_id.clone(), + inputs: self.inputs.clone(), + data_inputs: self.data_inputs.clone(), + outputs: self.outputs(), + } } } +/// Errors on parsing Transaction from JSON +#[cfg(feature = "with-serde")] +#[derive(Error, PartialEq, Eq, Debug, Clone)] +pub enum TransactionFromJsonError { + /// Tx id parsed from JSON differs from calculated from serialized bytes + #[error("Tx id parsed from JSON differs from calculated from serialized bytes")] + InvalidTxId, +} + #[cfg(feature = "with-serde")] -impl<'de> serde::Deserialize<'de> for Transaction { - fn deserialize(_: D) -> Result - where - D: Deserializer<'de>, - { - todo!() +impl TryFrom for Transaction { + type Error = TransactionFromJsonError; + fn try_from(tx_json: json::transaction::TransactionJson) -> Result { + let output_candidates = tx_json.outputs.iter().map(|o| o.clone().into()).collect(); + let tx_to_sign = Transaction { + tx_id: TxId::zero(), + inputs: tx_json.inputs, + data_inputs: tx_json.data_inputs, + output_candidates, + }; + let tx_id = tx_to_sign.calc_tx_id(); + let tx = Transaction { + tx_id, + ..tx_to_sign + }; + if tx.tx_id == tx_json.tx_id { + Ok(tx) + } else { + Err(TransactionFromJsonError::InvalidTxId) + } } } @@ -167,11 +256,7 @@ mod tests { vec(any::(), 0..10), vec(any::(), 1..10), ) - .prop_map(|(inputs, data_inputs, outputs)| Self { - inputs, - data_inputs, - output_candidates: outputs, - }) + .prop_map(|(inputs, data_inputs, outputs)| Self::new(inputs, data_inputs, outputs)) .boxed() } type Strategy = BoxedStrategy;