Repository: ergoplatform/sigma-rust (language: rust)

Issue:
Skip box id on JSON deserialization (generate from scratch)
Extracted from #50
On JSON deserialization check generated box id with the value in JSON.
 


Discussion / hints:

Fix patch:
diff --git a/sigma-tree/Cargo.toml b/sigma-tree/Cargo.toml
index a77443e1e..f38c86ac2 100644
--- a/sigma-tree/Cargo.toml
+++ b/sigma-tree/Cargo.toml
@@ -12,6 +12,7 @@ sigma-ser = { path = "../sigma-ser" }
 indexmap = "1.3.2"
 base16 = "0.2.1"
 k256 = "0.3.0"
+blake2 = "0.9"
 elliptic-curve = {version = "0.4.0", features = ["getrandom"]}
 serde = { version = "1.0", features = ["derive"], optional = true }
 serde_json = "1.0"
diff --git a/sigma-tree/src/chain.rs b/sigma-tree/src/chain.rs
index bc812f63b..1d1ebaba3 100644
--- a/sigma-tree/src/chain.rs
+++ b/sigma-tree/src/chain.rs
@@ -23,4 +23,5 @@ pub use input::*;
 pub use json::Base16DecodedBytes;
 #[cfg(feature = "with-serde")]
 pub use json::Base16EncodedBytes;
+pub use token::*;
 pub use transaction::*;
diff --git a/sigma-tree/src/chain/address.rs b/sigma-tree/src/chain/address.rs
index 82eef736a..560b3334f 100644
--- a/sigma-tree/src/chain/address.rs
+++ b/sigma-tree/src/chain/address.rs
@@ -86,7 +86,7 @@ impl Address for P2PKAddress {
         AddressTypePrefix::P2PKAddress
     }
     fn script(&self) -> ErgoTree {
-        ErgoTree::from_proposition(Rc::new(Expr::Const(Constant::sigma_prop(SigmaProp::new(
+        ErgoTree::from(Rc::new(Expr::Const(Constant::sigma_prop(SigmaProp::new(
             SigmaBoolean::ProofOfKnowledge(SigmaProofOfKnowledgeTree::ProveDlog(
                 self.pubkey.clone(),
             )),
diff --git a/sigma-tree/src/chain/digest32.rs b/sigma-tree/src/chain/digest32.rs
index 5ad708019..932e18a49 100644
--- a/sigma-tree/src/chain/digest32.rs
+++ b/sigma-tree/src/chain/digest32.rs
@@ -1,5 +1,7 @@
 #[cfg(feature = "with-serde")]
 use crate::chain::json::{Base16DecodedBytes, Base16EncodedBytes};
+use blake2::digest::{Update, VariableOutput};
+use blake2::VarBlake2b;
 #[cfg(test)]
 use proptest_derive::Arbitrary;
 #[cfg(feature = "with-serde")]
@@ -9,7 +11,8 @@ use sigma_ser::{
     vlq_encode,
 };
 #[cfg(feature = "with-serde")]
-use std::convert::{TryFrom, TryInto};
+use std::convert::TryFrom;
+use std::convert::TryInto;
 use std::io;
 use thiserror::Error;
 
@@ -32,6 +35,15 @@ impl Digest32 {
     }
 }
 
+pub fn blake2b256_hash(bytes: &[u8]) -> Digest32 {
+    // unwrap is safe 32 bytes is a valid hash size (<= 512 && 32 % 8 == 0)
+    let mut hasher = VarBlake2b::new(Digest32::SIZE).unwrap();
+    hasher.update(bytes);
+    let hash = hasher.finalize_boxed();
+    // unwrap is safe due to hash size is expected to be Digest32::SIZE
+    Digest32(hash.try_into().unwrap())
+}
+
 impl From<[u8; Digest32::SIZE]> for Digest32 {
     fn from(bytes: [u8; Digest32::SIZE]) -> Self {
         Digest32(Box::new(bytes))
diff --git a/sigma-tree/src/chain/ergo_box.rs b/sigma-tree/src/chain/ergo_box.rs
index 440dded4f..de52e140d 100644
--- a/sigma-tree/src/chain/ergo_box.rs
+++ b/sigma-tree/src/chain/ergo_box.rs
@@ -6,10 +6,15 @@ pub mod register;
 #[cfg(feature = "with-serde")]
 use super::json;
 use super::{
+    digest32::blake2b256_hash,
     token::{TokenAmount, TokenId},
     BoxId, TxId,
 };
-use crate::{ast::Constant, ergo_tree::ErgoTree, ErgoTreeParsingError};
+use crate::{
+    ergo_tree::ErgoTree,
+    serialization::ergo_box::{parse_box_with_indexed_digests, serialize_box_with_indexed_digests},
+    ErgoTreeParsingError,
+};
 use box_value::BoxValue;
 use indexmap::IndexSet;
 use register::NonMandatoryRegisters;
@@ -18,6 +23,7 @@ use serde::{Deserialize, Serialize};
 use sigma_ser::serializer::SerializationError;
 use sigma_ser::serializer::SigmaSerializable;
 use sigma_ser::vlq_encode;
+#[cfg(feature = "with-serde")]
 use std::convert::TryFrom;
 use std::io;
 
@@ -39,6 +45,10 @@ use std::io;
 /// A transaction is unsealing a box. As a box can not be open twice, any further valid transaction
 /// can not be linked to the same box.
 #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))]
+#[cfg_attr(
+    feature = "with-serde",
+    serde(try_from = "json::ergo_box::ErgoBoxFromJson")
+)]
 #[derive(PartialEq, Eq, Debug, Clone)]
 pub struct ErgoBox {
     #[cfg_attr(feature = "with-serde", serde(rename = "boxId"))]
@@ -129,27 +139,64 @@ impl ErgoBox {
     }
 
     fn calc_box_id(&self) -> BoxId {
-        // let _bytes = self.sigma_serialise_bytes();
-        // TODO: use blake2b256 hash
-        BoxId::zero()
+        let bytes = self.sigma_serialise_bytes();
+        BoxId(blake2b256_hash(&bytes))
     }
 }
 
-// impl SigmaSerializable for ErgoBox {
-//     fn sigma_serialize<W: vlq_encode::WriteSigmaVlqExt>(&self, w: &mut W) -> Result<(), io::Error> {
-//         let box_candidate = ErgoBoxCandidate::from(self);
-//         box_candidate.serialize_body_with_indexed_digests(None, w)?;
-//         self.transaction_id.sigma_serialize(w)?;
-//         w.put_u16(self.index)?;
-//         Ok(())
-//     }
-//     fn sigma_parse<R: vlq_encode::ReadSigmaVlqExt>(r: &mut R) -> Result<Self, SerializationError> {
-//         let box_candidate = ErgoBoxCandidate::parse_body_with_indexed_digests(None, r)?;
-//         let tx_id = TxId::sigma_parse(r)?;
-//         let index = r.get_u16()?;
-//         Ok(ErgoBox::from_box_candidate(&box_candidate, tx_id, index))
-//     }
-// }
+#[cfg(feature = "with-serde")]
+impl TryFrom<json::ergo_box::ErgoBoxFromJson> for ErgoBox {
+    type Error = json::ergo_box::ErgoBoxFromJsonError;
+    fn try_from(box_json: json::ergo_box::ErgoBoxFromJson) -> Result<Self, Self::Error> {
+        let box_with_zero_id = ErgoBox {
+            box_id: BoxId::zero(),
+            value: box_json.value,
+            ergo_tree: box_json.ergo_tree,
+            tokens: box_json.tokens,
+            additional_registers: box_json.additional_registers,
+            creation_height: box_json.creation_height,
+            transaction_id: box_json.transaction_id,
+            index: box_json.index,
+        };
+        let box_id = box_with_zero_id.calc_box_id();
+        let ergo_box = ErgoBox {
+            box_id,
+            ..box_with_zero_id
+        };
+        if ergo_box.box_id() == box_json.box_id {
+            Ok(ergo_box)
+        } else {
+            Err(json::ergo_box::ErgoBoxFromJsonError::InvalidBoxId)
+        }
+    }
+}
+
+impl SigmaSerializable for ErgoBox {
+    fn sigma_serialize<W: vlq_encode::WriteSigmaVlqExt>(&self, w: &mut W) -> Result<(), io::Error> {
+        let ergo_tree_bytes = match &self.ergo_tree {
+            Ok(ergo_tree) => ergo_tree.sigma_serialise_bytes(),
+            Err(ErgoTreeParsingError { bytes, .. }) => bytes.clone(),
+        };
+        serialize_box_with_indexed_digests(
+            &self.value,
+            ergo_tree_bytes,
+            &self.tokens,
+            &self.additional_registers,
+            self.creation_height,
+            None,
+            w,
+        )?;
+        self.transaction_id.sigma_serialize(w)?;
+        w.put_u16(self.index)?;
+        Ok(())
+    }
+    fn sigma_parse<R: vlq_encode::ReadSigmaVlqExt>(r: &mut R) -> Result<Self, SerializationError> {
+        let box_candidate = ErgoBoxCandidate::parse_body_with_indexed_digests(None, r)?;
+        let tx_id = TxId::sigma_parse(r)?;
+        let index = r.get_u16()?;
+        Ok(ErgoBox::from_box_candidate(&box_candidate, tx_id, index))
+    }
+}
 
 /// Contains the same fields as `ErgoBox`, except if transaction id and index,
 /// that will be calculated after full transaction formation.
@@ -188,39 +235,15 @@ impl ErgoBoxCandidate {
         token_ids_in_tx: Option<&IndexSet<TokenId>>,
         w: &mut W,
     ) -> Result<(), io::Error> {
-        // reference implementation - https://github.com/ScorexFoundation/sigmastate-interpreter/blob/9b20cb110effd1987ff76699d637174a4b2fb441/sigmastate/src/main/scala/org/ergoplatform/ErgoBoxCandidate.scala#L95-L95
-        self.value.sigma_serialize(w)?;
-        self.ergo_tree.sigma_serialize(w)?;
-        w.put_u32(self.creation_height)?;
-        w.put_u8(u8::try_from(self.tokens.len()).unwrap())?;
-
-        self.tokens.iter().try_for_each(|t| {
-            match token_ids_in_tx {
-                Some(token_ids) => w.put_u32(
-                    u32::try_from(
-                        token_ids
-                            .get_full(&t.token_id)
-                            // this is not a true runtime error it just means that
-                            // calling site messed up the token ids
-                            .expect("failed to find token id in tx's digest index")
-                            .0,
-                    )
-                    .unwrap(),
-                ),
-                None => t.token_id.sigma_serialize(w),
-            }
-            .and_then(|()| w.put_u64(t.amount))
-        })?;
-
-        let regs_num = self.additional_registers.len();
-        w.put_u8(regs_num as u8)?;
-
-        self.additional_registers
-            .get_ordered_values()
-            .iter()
-            .try_for_each(|c| c.sigma_serialize(w))?;
-
-        Ok(())
+        serialize_box_with_indexed_digests(
+            &self.value,
+            self.ergo_tree.sigma_serialise_bytes(),
+            &self.tokens,
+            &self.additional_registers,
+            self.creation_height,
+            token_ids_in_tx,
+            w,
+        )
     }
 
     /// Box deserialization with token ids optionally parsed in transaction
@@ -228,44 +251,7 @@ impl ErgoBoxCandidate {
         digests_in_tx: Option<&IndexSet<TokenId>>,
         r: &mut R,
     ) -> Result<ErgoBoxCandidate, SerializationError> {
-        // reference implementation -https://github.com/ScorexFoundation/sigmastate-interpreter/blob/9b20cb110effd1987ff76699d637174a4b2fb441/sigmastate/src/main/scala/org/ergoplatform/ErgoBoxCandidate.scala#L144-L144
-
-        let value = BoxValue::sigma_parse(r)?;
-        let ergo_tree = ErgoTree::sigma_parse(r)?;
-        let creation_height = r.get_u32()?;
-        let tokens_count = r.get_u8()?;
-        let mut tokens = Vec::with_capacity(tokens_count as usize);
-        for _ in 0..tokens_count {
-            let token_id = match digests_in_tx {
-                None => TokenId::sigma_parse(r)?,
-                Some(digests) => {
-                    let digest_index = r.get_u32()?;
-                    match digests.get_index(digest_index as usize) {
-                        Some(i) => Ok((*i).clone()),
-                        None => Err(SerializationError::Misc(
-                            "failed to find token id in tx digests".to_string(),
-                        )),
-                    }?
-                }
-            };
-            let amount = r.get_u64()?;
-            tokens.push(TokenAmount { token_id, amount })
-        }
-
-        let regs_num = r.get_u8()?;
-        let mut additional_regs = Vec::with_capacity(regs_num as usize);
-        for _ in 0..regs_num {
-            let v = Constant::sigma_parse(r)?;
-            additional_regs.push(v);
-        }
-        let additional_registers = NonMandatoryRegisters::from_ordered_values(additional_regs)?;
-        Ok(ErgoBoxCandidate {
-            value,
-            ergo_tree,
-            tokens,
-            additional_registers,
-            creation_height,
-        })
+        parse_box_with_indexed_digests(digests_in_tx, r)
     }
 }
 
@@ -329,9 +315,9 @@ mod tests {
             prop_assert_eq![sigma_serialize_roundtrip(&v), v];
         }
 
-        // #[test]
-        // fn ergo_box_ser_roundtrip(v in any::<ErgoBox>()) {
-        //     prop_assert_eq![sigma_serialize_roundtrip(&v), v];
-        // }
+        #[test]
+        fn ergo_box_ser_roundtrip(v in any::<ErgoBox>()) {
+            prop_assert_eq![sigma_serialize_roundtrip(&v), v];
+        }
     }
 }
diff --git a/sigma-tree/src/chain/ergo_box/box_value.rs b/sigma-tree/src/chain/ergo_box/box_value.rs
index 7a18618cf..5f9910a20 100644
--- a/sigma-tree/src/chain/ergo_box/box_value.rs
+++ b/sigma-tree/src/chain/ergo_box/box_value.rs
@@ -77,8 +77,7 @@ mod tests {
         type Parameters = ();
         type Strategy = BoxedStrategy<Self>;
         fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
-            // TODO: should be in 1 - i64.max range
-            any::<u64>().prop_map(BoxValue).boxed()
+            (1..i64::MAX).prop_map(|v| BoxValue(v as u64)).boxed()
         }
     }
 }
diff --git a/sigma-tree/src/chain/json.rs b/sigma-tree/src/chain/json.rs
index 41b466265..f2ed286a6 100644
--- a/sigma-tree/src/chain/json.rs
+++ b/sigma-tree/src/chain/json.rs
@@ -30,7 +30,7 @@ pub mod ergo_tree {
         S: Serializer,
     {
         let bytes = match maybe_ergo_tree {
-            Ok(ergo_tree) => ergo_tree.bytes(),
+            Ok(ergo_tree) => ergo_tree.sigma_serialise_bytes(),
             Err(err) => err.bytes.clone(),
         };
         serialize_bytes(&bytes[..], serializer)
@@ -52,6 +52,50 @@ pub mod ergo_tree {
     }
 }
 
+pub mod ergo_box {
+    use crate::{
+        chain::{box_value::BoxValue, register::NonMandatoryRegisters, BoxId, TokenAmount, TxId},
+        ErgoTree, ErgoTreeParsingError,
+    };
+    use serde::Deserialize;
+    use thiserror::Error;
+
+    #[derive(Deserialize, PartialEq, Eq, Debug, Clone)]
+    pub struct ErgoBoxFromJson {
+        #[serde(rename = "boxId")]
+        pub box_id: BoxId,
+        /// amount of money associated with the box
+        #[serde(rename = "value")]
+        pub value: BoxValue,
+        /// guarding script, which should be evaluated to true in order to open this box
+        #[serde(rename = "ergoTree", with = "super::ergo_tree")]
+        pub ergo_tree: Result<ErgoTree, ErgoTreeParsingError>,
+        /// secondary tokens the box contains
+        #[serde(rename = "assets")]
+        pub tokens: Vec<TokenAmount>,
+        ///  additional registers the box can carry over
+        #[serde(rename = "additionalRegisters")]
+        pub additional_registers: NonMandatoryRegisters,
+        /// height when a transaction containing the box was created.
+        /// This height is declared by user and should not exceed height of the block,
+        /// containing the transaction with this box.
+        #[serde(rename = "creationHeight")]
+        pub creation_height: u32,
+        /// id of transaction which created the box
+        #[serde(rename = "transactionId")]
+        pub transaction_id: TxId,
+        /// number of box (from 0 to total number of boxes the transaction with transactionId created - 1)
+        #[serde(rename = "index")]
+        pub index: u16,
+    }
+
+    #[derive(Error, PartialEq, Eq, Debug, Clone)]
+    pub enum ErgoBoxFromJsonError {
+        #[error("Box id parsed from JSON differs from calculated from box serialized bytes")]
+        InvalidBoxId,
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::super::ergo_box::*;
diff --git a/sigma-tree/src/ergo_tree.rs b/sigma-tree/src/ergo_tree.rs
index 02d078aa4..4896d167e 100644
--- a/sigma-tree/src/ergo_tree.rs
+++ b/sigma-tree/src/ergo_tree.rs
@@ -35,33 +35,23 @@ pub struct ErgoTreeParsingError {
 impl ErgoTree {
     const DEFAULT_HEADER: ErgoTreeHeader = ErgoTreeHeader(0);
 
-    // TODO: move to Into and From implementations
     /// get Expr out of ErgoTree
     pub fn proposition(&self) -> Rc<Expr> {
         self.root.clone()
     }
+}
 
-    /// build ErgoTree from an Expr
-    pub fn from_proposition(expr: Rc<Expr>) -> ErgoTree {
+impl From<Rc<Expr>> for ErgoTree {
+    fn from(expr: Rc<Expr>) -> Self {
         match &*expr {
             Expr::Const(c) if c.tpe == SType::SSigmaProp => ErgoTree {
                 header: ErgoTree::DEFAULT_HEADER,
                 constants: Vec::new(),
-                root: expr.clone(),
+                root: expr,
             },
             _ => panic!("not yet supported"),
         }
     }
-
-    /// Serialized ErgoTree
-    pub fn bytes(&self) -> Vec<u8> {
-        // TODO: expensive, store in the struct?
-        let mut data = Vec::new();
-        // since it can only fail from IO error only it's safe to unwrap
-        self.sigma_serialize(&mut data)
-            .expect("serialization failed");
-        data
-    }
 }
 
 impl SigmaSerializable for ErgoTreeHeader {
@@ -120,7 +110,7 @@ mod tests {
         fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
             (any::<SigmaProp>())
                 .prop_map(|p| {
-                    ErgoTree::from_proposition(Rc::new(Expr::Const(Constant {
+                    ErgoTree::from(Rc::new(Expr::Const(Constant {
                         tpe: SType::SSigmaProp,
                         v: ConstantVal::SigmaProp(Box::new(p)),
                     })))
diff --git a/sigma-tree/src/serialization.rs b/sigma-tree/src/serialization.rs
index 66273da13..4a95daff9 100644
--- a/sigma-tree/src/serialization.rs
+++ b/sigma-tree/src/serialization.rs
@@ -6,5 +6,6 @@ mod expr;
 mod fold;
 mod sigmaboolean;
 
+pub mod ergo_box;
 pub mod op_code;
 pub mod types;
diff --git a/sigma-tree/src/serialization/ergo_box.rs b/sigma-tree/src/serialization/ergo_box.rs
new file mode 100644
index 000000000..d82088815
--- /dev/null
+++ b/sigma-tree/src/serialization/ergo_box.rs
@@ -0,0 +1,106 @@
+use crate::{
+    ast::Constant,
+    chain::{
+        box_value::BoxValue, register::NonMandatoryRegisters, ErgoBoxCandidate, TokenAmount,
+        TokenId,
+    },
+    ErgoTree,
+};
+use indexmap::IndexSet;
+use sigma_ser::{
+    serializer::{SerializationError, SigmaSerializable},
+    vlq_encode,
+};
+use std::convert::TryFrom;
+use std::io;
+
+/// Box serialization with token ids optionally saved in transaction
+/// (in this case only token index is saved)
+pub fn serialize_box_with_indexed_digests<W: vlq_encode::WriteSigmaVlqExt>(
+    box_value: &BoxValue,
+    ergo_tree_bytes: Vec<u8>,
+    tokens: &[TokenAmount],
+    additional_registers: &NonMandatoryRegisters,
+    creation_height: u32,
+    token_ids_in_tx: Option<&IndexSet<TokenId>>,
+    w: &mut W,
+) -> Result<(), io::Error> {
+    // reference implementation - https://github.com/ScorexFoundation/sigmastate-interpreter/blob/9b20cb110effd1987ff76699d637174a4b2fb441/sigmastate/src/main/scala/org/ergoplatform/ErgoBoxCandidate.scala#L95-L95
+    box_value.sigma_serialize(w)?;
+    w.write_all(&ergo_tree_bytes[..])?;
+    w.put_u32(creation_height)?;
+    w.put_u8(u8::try_from(tokens.len()).unwrap())?;
+
+    tokens.iter().try_for_each(|t| {
+        match token_ids_in_tx {
+            Some(token_ids) => w.put_u32(
+                u32::try_from(
+                    token_ids
+                        .get_full(&t.token_id)
+                        // this is not a true runtime error it just means that
+                        // calling site messed up the token ids
+                        .expect("failed to find token id in tx's digest index")
+                        .0,
+                )
+                .unwrap(),
+            ),
+            None => t.token_id.sigma_serialize(w),
+        }
+        .and_then(|()| w.put_u64(t.amount))
+    })?;
+
+    let regs_num = additional_registers.len();
+    w.put_u8(regs_num as u8)?;
+
+    additional_registers
+        .get_ordered_values()
+        .iter()
+        .try_for_each(|c| c.sigma_serialize(w))?;
+
+    Ok(())
+}
+
+/// Box deserialization with token ids optionally parsed in transaction
+pub fn parse_box_with_indexed_digests<R: vlq_encode::ReadSigmaVlqExt>(
+    digests_in_tx: Option<&IndexSet<TokenId>>,
+    r: &mut R,
+) -> Result<ErgoBoxCandidate, SerializationError> {
+    // reference implementation -https://github.com/ScorexFoundation/sigmastate-interpreter/blob/9b20cb110effd1987ff76699d637174a4b2fb441/sigmastate/src/main/scala/org/ergoplatform/ErgoBoxCandidate.scala#L144-L144
+
+    let value = BoxValue::sigma_parse(r)?;
+    let ergo_tree = ErgoTree::sigma_parse(r)?;
+    let creation_height = r.get_u32()?;
+    let tokens_count = r.get_u8()?;
+    let mut tokens = Vec::with_capacity(tokens_count as usize);
+    for _ in 0..tokens_count {
+        let token_id = match digests_in_tx {
+            None => TokenId::sigma_parse(r)?,
+            Some(digests) => {
+                let digest_index = r.get_u32()?;
+                match digests.get_index(digest_index as usize) {
+                    Some(i) => Ok((*i).clone()),
+                    None => Err(SerializationError::Misc(
+                        "failed to find token id in tx digests".to_string(),
+                    )),
+                }?
+            }
+        };
+        let amount = r.get_u64()?;
+        tokens.push(TokenAmount { token_id, amount })
+    }
+
+    let regs_num = r.get_u8()?;
+    let mut additional_regs = Vec::with_capacity(regs_num as usize);
+    for _ in 0..regs_num {
+        let v = Constant::sigma_parse(r)?;
+        additional_regs.push(v);
+    }
+    let additional_registers = NonMandatoryRegisters::from_ordered_values(additional_regs)?;
+    Ok(ErgoBoxCandidate {
+        value,
+        ergo_tree,
+        tokens,
+        additional_registers,
+        creation_height,
+    })
+}


Test patch: