diff --git a/.gitignore b/.gitignore index b9299ca5..713df3d5 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ Cargo.lock **/*.rs.bk .idea + +*.swp diff --git a/Cargo.toml b/Cargo.toml index 6afde53c..e8219dda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,6 @@ grin_secp256k1zkp = { git = "https://github.com/lnp-bp/rust-secp256k1-zkp", bran # bitcoin = { version = "0.23.0", features = [ "use-serde" ] } rand = "0.5" # Required by grin_secp256k1zkp derive_wrapper = "0.1.3" -num-integer = "0.1.42" num-traits = "0.2.11" num-derive = "0.3.0" tokio = { version = "~0.2", features = ["tcp"], optional = true } @@ -36,6 +35,7 @@ parse_arg = { version = "0.1.4", optional = true } # This strange naming is a workaround for not being able to define required features for a dependency # See https://github.com/rust-lang/api-guidelines/issues/180 for the explanation and references. serde_crate = { package = "serde", version = "1.0.106", features = ["derive"], optional = true } +petgraph = { version = "0.5", optional = true } [features] default = [] @@ -45,7 +45,7 @@ use-log = ["log"] use-tor = ["torut/v3"] use-tokio = ["use-lightning", "tokio/tcp", "lightning-net-tokio"] use-bulletproofs = ["grin_secp256k1zkp"] -use-rgb = ["use-bulletproofs"] +use-rgb = ["use-bulletproofs", "petgraph"] use-api = ["zmq"] use-daemons = ["async-trait", "use-api"] use-lightning = ["lightning"] diff --git a/src/bp/blind.rs b/src/bp/blind.rs index e5be30fa..0ad6335e 100644 --- a/src/bp/blind.rs +++ b/src/bp/blind.rs @@ -18,7 +18,7 @@ use bitcoin::hashes::{Hash, HashEngine, sha256d}; /// Data required to generate or reveal the information about blinded /// transaction outpoint -#[derive(Clone, PartialEq, PartialOrd, Debug, Display, Default)] +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Debug, Display, Default)] #[display_from(Debug)] pub struct OutpointReveal { /// Blinding factor preventing rainbow table bruteforce attack based on diff --git a/src/bp/short_id.rs b/src/bp/short_id.rs index 44f95a7a..9af8ec10 100644 --- a/src/bp/short_id.rs +++ b/src/bp/short_id.rs @@ -238,7 +238,7 @@ impl Descriptor { } -#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Debug, Display)] +#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Debug, Display)] #[display_from(Debug)] pub struct ShortId(u64); diff --git a/src/common/macros.rs b/src/common/macros.rs index a19e4ee4..e483746c 100644 --- a/src/common/macros.rs +++ b/src/common/macros.rs @@ -24,6 +24,12 @@ macro_rules! bytes { #[macro_export] macro_rules! map { + { } => { + { + ::std::collections::HashMap::new() + } + }; + { $($key:expr => $value:expr),+ } => { { let mut m = ::std::collections::HashMap::new(); @@ -59,4 +65,4 @@ macro_rules! hlist { m } } -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index f6e93ab2..ff9a5872 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ #![feature(arbitrary_enum_discriminant)] #![feature(bool_to_option)] #![feature(str_strip)] +#![feature(bindings_after_at)] // Coding conventions #![deny(non_upper_case_globals)] @@ -38,7 +39,6 @@ #[macro_use] pub extern crate derive_wrapper; extern crate rand; -extern crate num_integer; extern crate num_derive; extern crate num_traits; #[macro_use] diff --git a/src/rgb/data.rs b/src/rgb/data.rs index 89879546..46cebde9 100644 --- a/src/rgb/data.rs +++ b/src/rgb/data.rs @@ -11,11 +11,15 @@ // along with this software. // If not, see . + pub mod amount { + use std::ops::Add; use rand; + // We do not import particular modules to keep aware with namespace prefixes that we do not use // the standard secp256k1zkp library use secp256k1zkp::*; + pub use secp256k1zkp::pedersen::Commitment as PedersenCommitment; // TODO: Convert Amount into a wrapper type later //wrapper!(Amount, _AmountPhantom, u64, doc="64-bit data for amounts"); @@ -32,6 +36,14 @@ pub mod amount { #[display_from(Debug)] pub struct Proof(secp256k1zkp::key::SecretKey); + impl std::ops::Deref for Proof { + type Target = secp256k1zkp::key::SecretKey; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + #[derive(Clone, PartialEq, Debug, Display)] #[display_from(Debug)] pub struct Confidential { @@ -57,9 +69,63 @@ pub mod amount { } } } + + pub fn commit_last_item(amount: Amount, blinding_factors: Vec) -> Confidential { + // TODO: refactor duplicated code + + let secp = secp256k1zkp::Secp256k1::with_caps(ContextFlag::Commit); + let blinding = secp.blind_sum(vec![secp256k1zkp::key::ONE_KEY], blinding_factors).unwrap(); // FIXME: that's probably broken, but it works + + let value = amount; + let commitment = secp.commit(value, blinding.clone()) + .expect("Internal inconsistency in Grin secp256k1zkp library Pedersen commitments"); + let bulletproof = secp.bullet_proof( + value, blinding.clone(), + blinding.clone(), blinding.clone(), + None, None + ); + Confidential { + commitment: Commitment { commitment, bulletproof }, + proof: Proof(blinding) + } + } + + pub fn zero_pedersen_commitment() -> PedersenCommitment { + let secp = secp256k1zkp::Secp256k1::with_caps(ContextFlag::Commit); + + secp + .commit_value(0) + .expect("Internal inconsistency in Grin secp256k1zkp library Pedersen commitments") + } + + impl Add for Commitment { + type Output = pedersen::Commitment; + + fn add(self, other: pedersen::Commitment) -> Self::Output { + let secp = secp256k1zkp::Secp256k1::with_caps(ContextFlag::Commit); + + secp + .commit_sum(vec![self.commitment, other], vec![]) + .expect("Failed to add Pedersen commitments") + } + } + + pub fn verify_bullet_proof(commitment: &Commitment) -> Result { + let secp = secp256k1zkp::Secp256k1::with_caps(ContextFlag::Commit); + + secp. + verify_bullet_proof(commitment.commitment.clone(), commitment.bulletproof.clone(), None) + } + + pub fn verify_commit_sum(positive: Vec, negative: Vec) -> bool { + let secp = secp256k1zkp::Secp256k1::with_caps(ContextFlag::Commit); + + secp. + verify_commit_sum(positive, negative) + } } -pub use amount::Amount; +pub use amount::{Amount, PedersenCommitment}; #[non_exhaustive] #[derive(Clone, PartialEq, Debug, Display)] @@ -67,5 +133,6 @@ pub use amount::Amount; pub enum Data { Balance(amount::Commitment), Binary(Box<[u8]>), + None, // TODO: Add other supported bound state types according to the schema } diff --git a/src/rgb/history.rs b/src/rgb/history.rs new file mode 100644 index 00000000..6b09c298 --- /dev/null +++ b/src/rgb/history.rs @@ -0,0 +1,525 @@ +// LNP/BP Rust Library +// Written in 2020 by +// Dr. Maxim Orlovsky +// +// To the extent possible under law, the author(s) have dedicated all +// copyright and related and neighboring rights to this software to +// the public domain worldwide. This software is distributed without +// any warranty. +// +// You should have received a copy of the MIT License +// along with this software. +// If not, see . + + +#![allow(unused_imports)] + +use std::collections::{HashSet, HashMap}; +use std::convert::TryInto; + +use bitcoin::{Txid, Transaction, OutPoint}; + +use petgraph::{Directed, Direction, stable_graph::StableGraph}; +use petgraph::visit::{Bfs, EdgeRef, Reversed}; +use petgraph::graph::{NodeIndex, DefaultIx}; + +use crate::common::Wrapper; + +use super::{Transition, Metadata, State}; +use super::state::{Partial, Bound}; +use super::data; +use super::seal; +use super::validation::{TxFetch, ValidationError}; +use super::schema::Schema; + +#[derive(Debug, Clone)] +pub enum GraphError { + InvalidOpenSeal(NodeIndex), + OpenSealAsParent, + IncompatibleOpenSeals, +} + +#[derive(Debug, Clone)] +pub enum HistoryGraphNode { + Open(Option, usize, seal::Seal), + Transition(Transition, Txid), + Genesis(Transition), +} + +#[derive(Debug, Clone)] +pub struct HistoryGraph { + graph: StableGraph, + open: HashSet>, + genesis: NodeIndex, +} + +impl HistoryGraph { + /// Internal method to add all the open seals created in a transition + fn add_open_seals(&mut self, from_transition: &Transition, from_txid: Option, to_node: NodeIndex) { + for (index, state) in from_transition.state.iter().enumerate() { + if let Partial::State(Bound { seal, .. }) = state { + let open_seal_node = self.graph.add_node(HistoryGraphNode::Open(from_txid, index, seal.clone())); + self.graph.add_edge(open_seal_node, to_node, ()); + + self.open.insert(open_seal_node); + } + } + } + + /// Internal method to find the node indexes among the required open seals + fn find_open_seals(&self, outpoints: Vec) -> Result>, GraphError> { + self + .open + .iter() + .try_fold(HashSet::new(), |mut to_close, node_index| { + if let Some(HistoryGraphNode::Open(prev_txid, _, node_seal)) = self.graph.node_weight(*node_index) { + if outpoints.iter().any(|outpoint| node_seal.compare_to_outpoint(outpoint, *prev_txid, None)) { // TODO: add support for blinding key in transitions + to_close.insert(*node_index); + } + + Ok(to_close) + } else { + Err(GraphError::InvalidOpenSeal(*node_index)) + } + }) + } + + /// Creates a new graph starting from the genesis transition. The graph will contain the + /// genesis itself plus all of its bound seals + pub fn new(genesis: Transition) -> Self { + let mut graph = StableGraph::new(); + let genesis_node = graph.add_node(HistoryGraphNode::Genesis(genesis.clone())); + + let mut graph = HistoryGraph { + graph, + genesis: genesis_node, + open: HashSet::new(), + }; + graph.add_open_seals(&genesis, None, graph.genesis); + + graph + } + + /// Applies a transition to the graph, removing the closed seals and adding the newly created + /// ones + pub fn apply_transition(&mut self, transition: Transition, txid: Txid, closes: Vec) -> Result<(), GraphError> { + // TODO: test with the same seal duplicated a few times + + let closing_indexes = self.find_open_seals(closes)?; + + // remove all the seals we are closing from the `open` vec + self.open.retain(|node_index| !closing_indexes.contains(node_index)); + + let new_node = self.graph.add_node(HistoryGraphNode::Transition(transition.clone(), txid)); + self.add_open_seals(&transition, Some(txid), new_node); + + for to_close in closing_indexes { + // copy the edges connected to the node we are removing + let to_edges = self + .graph + .edges_directed(to_close, Direction::Outgoing) + .map(|edge| edge.target()) + .collect::>(); + + for to in to_edges { + self.graph.add_edge(new_node, to, ()); + } + + // and then remove the node + self.graph.remove_node(to_close); + } + + Ok(()) + } + + /// Strips the part of the history that is not required to validate the requested open seals + pub fn strip_history(&mut self, keep: Vec) -> Result<(), GraphError> { + let mut keep_nodes = HashSet::new(); + + for start in self.find_open_seals(keep)? { + let mut bfs = Bfs::new(&self.graph, start); + while let Some(nx) = bfs.next(&self.graph) { + keep_nodes.insert(nx); + } + } + + self.graph.retain_nodes(|_, node| keep_nodes.contains(&node)); + self.open.retain(|node| keep_nodes.contains(&node)); + + Ok(()) + } + + pub fn merge_history(&mut self, other: Self) -> Result<(), GraphError> { + // TODO: other is probably untrusted at this point, so more checks should be done. like: + // - make sure that there's only one genesis, and that it's == + // - check that only the genesis has no closed seals + // - check the "open seals" and make sure they are really created by the transitions + // - instead of comparing open seals with ==, we should somehow try to interpret them + // and check for equivalence + + let mut transition_index = HashMap::new(); + let mut open_index = HashMap::new(); + + // iterate `self.graph` from the genesis going forward (that's why edges are reversed), and + // create an index of every transition or open seal we see + let reversed_graph = Reversed(&self.graph); + let mut bfs = Bfs::new(&reversed_graph, self.genesis); + while let Some(nx) = bfs.next(&reversed_graph) { + match self.graph.node_weight(nx).expect("Corrupted graph: missing node during BFS") { + HistoryGraphNode::Genesis(_) => continue, + HistoryGraphNode::Transition(_, txid) => { transition_index.insert(txid.clone(), nx); }, + HistoryGraphNode::Open(prev_txid, index, seal) => { open_index.insert((prev_txid.clone(), index.clone()), (nx, seal.clone())); }, + } + } + + // Iterate `other.graph` from the genesis again. whenever we find a node that's missing in + // `self.graph`, we add it and copy all the edges. + // + // Since we do a BFS, it's guaranteed that if we find a missing node, all of its "parent" + // nodes will already be present. + let reversed_other = Reversed(&other.graph); + let mut bfs = Bfs::new(&reversed_other, other.genesis); + while let Some(nx) = bfs.next(&reversed_other) { + let mut add_to_self = |node: HistoryGraphNode, index_in_other: NodeIndex| -> Result, GraphError> { + // add the node to ourselves + let new_node = self.graph.add_node(node); + + // iterate over all the neighbors in the `Outgoing` direction, so towards the + // genesis. i.e., its parent nodes. + for prev_node in other.graph.neighbors_directed(index_in_other, Direction::Outgoing) { + match other.graph.node_weight(prev_node).expect("Corrupted graph: missing node during BFS") { + HistoryGraphNode::Genesis(_) => { self.graph.add_edge(new_node, self.genesis, ()); }, + HistoryGraphNode::Transition(_, prev_txid) => { + let corresponding_index = transition_index.get(prev_txid).expect(&format!("Corrupted graph: missing txid in `transition_index`: {:?}", prev_txid)); + self.graph.add_edge(new_node, *corresponding_index, ()); + }, + HistoryGraphNode::Open(_, _, _) => return Err(GraphError::OpenSealAsParent), + } + } + + Ok(new_node) + }; + + match other.graph.node_weight(nx).expect("Corrupted graph: missing node during BFS") { + HistoryGraphNode::Genesis(_) => continue, + HistoryGraphNode::Transition(transition, txid) => { + if transition_index.contains_key(txid) { + continue; + } + + let new_node = add_to_self(HistoryGraphNode::Transition(transition.clone(), *txid), nx)?; + + // add it to the index now that it's in `self.graph` + transition_index.insert(*txid, new_node); + }, + HistoryGraphNode::Open(prev_txid, index, seal) => { + let key = (*prev_txid, *index); + + if let Some((_, known_seal)) = open_index.get(&key) { + // compare seals. TODO here vvvv + if known_seal != seal { + return Err(GraphError::IncompatibleOpenSeals); + } + + continue; + } + + + let new_node = add_to_self(HistoryGraphNode::Open(*prev_txid, *index, seal.clone()), nx)?; + // add to the list of open seals + self.open.insert(new_node); + + // add it to the index now that it's in `self.graph` + open_index.insert(key, (new_node, seal.clone())); + }, + } + } + + Ok(()) + } + + pub fn validate(&self, schema: &Schema, tx_fetch: &mut T, outpoint: OutPoint) -> Result, ValidationError> + where + T: TxFetch + { + let open_seal_nodes = self.find_open_seals(vec![outpoint])?; + if open_seal_nodes.is_empty() { + return Err(ValidationError::InvalidOutpoint(outpoint)); + } + + // iterate all the seals we are closing + for open_seal in open_seal_nodes { + // start a bfs from that node + let mut bfs = Bfs::new(&self.graph, open_seal); + // for each node... + while let Some(nx) = bfs.next(&self.graph) { + // - if it's a transition, inspect it + // - if it's the genesis, compare the asset id + // - if it's an open seal == the one we are closing, skip it + // - if it's a different open seal, return an error + let (transition, txid) = match self.graph.node_weight(nx).expect("Corrupted graph: missing node during BFS") { + HistoryGraphNode::Transition(transition, txid) => (transition, txid), + HistoryGraphNode::Genesis(_) => continue, // TODO: check genesis + HistoryGraphNode::Open(txid, _, seal) if seal.compare_to_outpoint(&outpoint, *txid, None) => continue, // open seal we are spending. TODO: add support for blinding key + HistoryGraphNode::Open(_, _, _) => return Err(GraphError::OpenSealAsParent.into()), + }; + + // fetch the transaction and check the commitment + let tx = tx_fetch.fetch_from_txid(txid).map_err(ValidationError::TxFetch)?; + // TODO: check that `tx` commits to the transition + + let inputs_set: HashSet<_> = tx.input.iter().map(|i| i.previous_output).collect(); + + // validate the transition against its schema + let partial_validation = schema.validate_transition(transition)?; + + let mut closed_seals = Vec::new(); + + // iterate the parent nodes... + for prev_node in self.graph.neighbors_directed(nx, Direction::Outgoing) { + // - if it's a transition or a genesis look for bound seals == the one we are closing + // - if it's an open seal, return an error + let (prev_transition, prev_txid) = match self.graph.node_weight(prev_node).expect("Corrupted graph: missing node during BFS") { + HistoryGraphNode::Transition(transition, txid) => (transition, Some(*txid)), + HistoryGraphNode::Genesis(transition) => (transition, None), + HistoryGraphNode::Open(_, _, _) => return Err(GraphError::OpenSealAsParent.into()), + }; + + // only take the partial items that are bound to one of the inputs of the + // current transaction + for partial in prev_transition.state.iter() { + match partial { + state @ Partial::State(Bound { id, seal, val }) => { + if inputs_set + .iter() + .any(|op| seal.maybe_as_outpoint(Some(*op), prev_txid, None) == Some(*op)) + { + closed_seals.push(state); + } + }, + Partial::Commitment(_) => unimplemented!(), // TODO + _ => continue, + } + } + } + + // check the closed seals against the schema (`partial_validation.should_close`) + let input_commitments = partial_validation + .should_close + .expect("Transition should close some seals") + .validate(&schema.seals, closed_seals)? + .into_iter() + .map(|cmt| cmt.commitment) + .collect(); + + println!("input_commitments: {:?}", input_commitments); + + // sum the inputs and compare it with the sum of outputs + if !data::amount::verify_commit_sum(input_commitments, partial_validation.output_commitments) { + return Err(ValidationError::TxInNeTxOut); + } + } + } + + Ok(None) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::rgb::data; + + #[test] + fn test_graph_apply_transition() { + let genesis = Transition { + id: 0, + meta: Metadata::from_inner(vec![]), + state: State::from_inner(vec![Partial::State(Bound { + id: seal::Type(0), + seal: seal::Seal::revealed(Default::default(), 5, 0), + val: data::Data::None, + })]), + script: None + }; + + let mut graph = HistoryGraph::new(genesis); + println!("{:#?}", graph); + + let next_trans = Transition { + id: 1, + meta: Metadata::from_inner(vec![]), + state: State::from_inner(vec![Partial::State(Bound { + id: seal::Type(0), + seal: seal::Seal::revealed(Default::default(), 42, 0), + val: data::Data::None, + })]), + script: None, + }; + + graph.apply_transition(next_trans, Default::default(), vec![OutPoint { txid: Default::default(), vout: 5 }]); + println!("{:#?}", graph); + } + + #[test] + fn test_graph_strip_history() { + let state = State::from_inner( + vec![ + Partial::State(Bound { + id: seal::Type(0), + seal: seal::Seal::revealed(Default::default(), 0, 0), + val: data::Data::None, + }), + Partial::State(Bound { + id: seal::Type(0), + seal: seal::Seal::revealed(Default::default(), 1, 0), + val: data::Data::None, + }), + Partial::State(Bound { + id: seal::Type(0), + seal: seal::Seal::revealed(Default::default(), 2, 0), + val: data::Data::None, + }), + ] + ); + let genesis = Transition { + id: 0, + meta: Metadata::from_inner(vec![]), + state, + script: None + }; + + let mut graph = HistoryGraph::new(genesis); + println!("{:#?}", graph); + + graph.strip_history(vec![OutPoint { txid: Default::default(), vout: 0 }]); + + println!("{:#?}", graph); + } + + #[test] + fn test_graph_merge_history() { + let seal_genesis = seal::Seal::revealed(Default::default(), 42, 0); + let genesis = Transition { + id: 0, + meta: Metadata::from_inner(vec![]), + state: State::from_inner(vec![Partial::State(Bound { + id: seal::Type(0), + seal: seal_genesis.clone(), + val: data::Data::None, + })]), + script: None + }; + + let mut graph = HistoryGraph::new(genesis); + + let seal_0 = seal::Seal::revealed(Default::default(), 0, 0); + let seal_1 = seal::Seal::revealed(Default::default(), 1, 0); + let state = State::from_inner( + vec![ + Partial::State(Bound { + id: seal::Type(0), + seal: seal_0.clone(), + val: data::Data::None, + }), + Partial::State(Bound { + id: seal::Type(0), + seal: seal_1.clone(), + val: data::Data::None, + }), + ] + ); + let next_trans = Transition { + id: 1, + meta: Metadata::from_inner(vec![]), + state, + script: None, + }; + graph.apply_transition(next_trans, Txid::default(), vec![seal_genesis.maybe_as_outpoint(None, None, None).unwrap()]); + + println!("initial graph {:#?}", graph); + + let mut history_0 = graph.clone(); + history_0.strip_history(vec![seal_0.maybe_as_outpoint(None, None, None).unwrap()]); + + println!("{:#?}", history_0); + + let mut history_1 = graph.clone(); + history_1.strip_history(vec![seal_1.maybe_as_outpoint(None, None, None).unwrap()]); + + history_0.merge_history(history_1); + + println!("{:#?}", history_0); + } + + #[test] + fn test_history_validate_rgb() { + use std::convert::TryFrom; + use std::ops::Deref; + use bitcoin::{TxIn, Transaction, OutPoint}; + + use crate::rgb::schemata::fungible::Rgb1; + use crate::rgb::schemata::Schemata; + use crate::bp::Network; + use super::data; + + #[derive(Debug)] + struct DummyTxFetch(HashMap); + impl TxFetch for DummyTxFetch { + type Error = (); + + fn fetch_from_txid(&mut self, txid: &Txid) -> Result { + Ok(self.0.get(txid).unwrap().clone()) + } + } + + let genesis_confidential_amount = data::amount::commit_last_item(1000, vec![]); + + let genesis_outpoint = OutPoint { txid: Default::default(), vout: 42 }; + let genesis_open_seal = seal::Seal::maybe_from_outpoint(genesis_outpoint.clone(), 0).unwrap(); + + let balances = map!{ + genesis_outpoint.clone() => genesis_confidential_amount.commitment.clone() + }; + let genesis = Rgb1::issue(Network::Regtest, "ALKS", "Alekos", None, balances, 1, None, None).unwrap(); + println!("{:#?}", genesis); + + let asset_id = genesis.transition_id().unwrap(); + println!("asset_id: {}", asset_id); + + let mut graph = HistoryGraph::new(genesis); + println!("{:#?}", graph); + + let transfer_conf_amount_0 = data::amount::Confidential::from(500); + let transfer_conf_amount_1 = data::amount::commit_last_item(500, vec![transfer_conf_amount_0.proof.deref().clone()]); + + let transfer_outpoint_0 = OutPoint { txid: Default::default(), vout: 100 }; + let transfer_outpoint_1 = OutPoint { txid: Default::default(), vout: 101 }; + let transfer_balances = map!{ + transfer_outpoint_0.clone() => transfer_conf_amount_0.commitment.clone(), + transfer_outpoint_1.clone() => transfer_conf_amount_1.commitment.clone() + }; + + let transfer = Rgb1::transfer(transfer_balances).unwrap(); + println!("{:#?}", transfer); + + let committing_tx = Transaction { + version: 1, + lock_time: 0, + input: vec![TxIn { + previous_output: genesis_outpoint.clone(), + sequence: 0xFFFFFFFF, + ..Default::default() + }], + output: vec![], + }; + // (the commitment is not checked at the moment...) + let mut tx_fetch = DummyTxFetch(map!{ committing_tx.txid() => committing_tx.clone() }); + + graph.apply_transition(transfer, committing_tx.txid(), vec![genesis_outpoint]); + println!("{:#?}", graph); + + let result = graph.validate(Rgb1::get_schema(), &mut tx_fetch, transfer_outpoint_0); + println!("result = {:?}", result); + } +} diff --git a/src/rgb/mod.rs b/src/rgb/mod.rs index ca46e7bf..b1c0181a 100644 --- a/src/rgb/mod.rs +++ b/src/rgb/mod.rs @@ -21,6 +21,8 @@ pub mod seal; pub mod state; pub mod script; pub mod transition; +pub mod history; +pub mod validation; pub mod serialize; pub mod commit; diff --git a/src/rgb/schema/field.rs b/src/rgb/schema/field.rs index 46b09552..e8dc7fd8 100644 --- a/src/rgb/schema/field.rs +++ b/src/rgb/schema/field.rs @@ -15,6 +15,8 @@ use std::io; use super::types::*; +use super::schema::SchemaError; +use crate::rgb::metadata::{Metadata, Type, Value}; use crate::csv::serialize::*; @@ -32,6 +34,35 @@ pub enum FieldFormat { Signature(SignatureAlgorithm), } +impl FieldFormat { + pub fn validate(&self, value: &Value) -> Result<(), SchemaError> { + match (self, value) { + (Self::Unsigned { bits: Bits::Bit256, min: None, max: None }, Value::U256(_)) => Ok(()), + (Self::Unsigned { bits: Bits::Bit256, .. }, Value::U256(_)) => Err(SchemaError::MinMaxBoundsOnLargeInt), + (Self::Unsigned { bits: Bits::Bit128, min: None, max: None }, Value::U128(_)) => Ok(()), + (Self::Unsigned { bits: Bits::Bit128, .. }, Value::U128(_)) => Err(SchemaError::MinMaxBoundsOnLargeInt), + (Self::Unsigned { bits: Bits::Bit64, min, max }, Value::U64(val)) if *val >= min.unwrap_or(0) && *val <= max.unwrap_or(u64::MAX) => Ok(()), + (Self::Unsigned { bits: Bits::Bit32, min, max }, Value::U32(val)) if *val as u64 >= min.unwrap_or(0) && *val as u64 <= max.unwrap_or(u32::MAX as u64) => Ok(()), + (Self::Unsigned { bits: Bits::Bit16, min, max }, Value::U16(val)) if *val as u64 >= min.unwrap_or(0) && *val as u64 <= max.unwrap_or(u16::MAX as u64) => Ok(()), + (Self::Unsigned { bits: Bits::Bit8, min, max }, Value::U8(val)) if *val as u64 >= min.unwrap_or(0) && *val as u64 <= max.unwrap_or(u8::MAX as u64) => Ok(()), + (Self::Integer { bits: Bits::Bit64, min, max }, Value::I64(val)) if *val >= min.unwrap_or(0) && *val <= max.unwrap_or(i64::MAX) => Ok(()), + (Self::Integer { bits: Bits::Bit32, min, max }, Value::I32(val)) if *val as i64 >= min.unwrap_or(0) && *val as i64 <= max.unwrap_or(i32::MAX as i64) => Ok(()), + (Self::Integer { bits: Bits::Bit16, min, max }, Value::I16(val)) if *val as i64 >= min.unwrap_or(0) && *val as i64 <= max.unwrap_or(i16::MAX as i64) => Ok(()), + (Self::Integer { bits: Bits::Bit8, min, max }, Value::I8(val)) if *val as i64 >= min.unwrap_or(0) && *val as i64 <= max.unwrap_or(i8::MAX as i64) => Ok(()), + (Self::Float { bits: Bits::Bit64, min, max }, Value::F64(val)) if *val >= min.unwrap_or(0.0) && *val <= max.unwrap_or(f64::MAX) => Ok(()), + (Self::Float { bits: Bits::Bit32, min, max }, Value::F32(val)) if *val as f64 >= min.unwrap_or(0.0) && *val as f64 <= max.unwrap_or(f32::MAX as f64) => Ok(()), + + (Self::Enum{ values }, Value::U8(val)) if values.contains(val) => Ok(()), + (Self::String(max_len), Value::Str(string)) if string.len() <= *max_len as usize => Ok(()), + (Self::Bytes(max_len), Value::Bytes(bytes)) if bytes.len() <= *max_len as usize => Ok(()), + + // TODO: other types when added to metadata::Value + + _ => Err(SchemaError::InvalidValue(value.clone())) + } + } +} + impl Commitment for FieldFormat { fn commitment_serialize(&self, mut e: E) -> Result { Ok(match self { @@ -58,6 +89,25 @@ impl Commitment for FieldFormat { #[display_from(Debug)] pub struct Field(pub FieldFormat, pub Occurences); +impl Field { + pub fn validate(&self, field_type: Type, metadata: &Metadata) -> Result<(), SchemaError> { + let count = metadata + .iter() + .filter_map(|m| { + if m.id == field_type { + Some(&m.val) + } else { + None + } + }) + .try_fold(0, |acc, val| self.0.validate(&val).and_then(|_| Ok(acc + 1))) + .map_err(|e| SchemaError::InvalidField(field_type, Box::new(e)))?; + + self.1.check_count(count) + .map_err(|e| SchemaError::InvalidField(field_type, Box::new(SchemaError::OccurencesNotMet(e)))) + } +} + impl Commitment for Field { fn commitment_serialize(&self, mut e: E) -> Result { self.0.commitment_serialize(&mut e)?; @@ -71,3 +121,131 @@ impl Commitment for Field { )) } } + +#[cfg(test)] +#[allow(unused_imports)] +mod test { + use super::super::types::*; + use super::{Field, FieldFormat}; + use crate::rgb::metadata::{self, Metadata, Type, Value}; + + #[test] + fn test_validate_unsigned_256() { + let field_format = FieldFormat::Unsigned { bits: Bits::Bit256, min: None, max: None }; + let value = Value::U256(Default::default()); + field_format.validate(&value).unwrap(); + } + #[test] + #[should_panic(expected = "MinMaxBoundsOnLargeInt")] + fn test_validate_unsigned_256_bounds() { + let field_format = FieldFormat::Unsigned { bits: Bits::Bit256, min: Some(0), max: None }; + let value = Value::U256(Default::default()); + field_format.validate(&value).unwrap(); + } + + #[test] + fn test_validate_unsigned_64() { + let field_format = FieldFormat::Unsigned { bits: Bits::Bit64, min: None, max: None }; + let value = Value::U64(42424242); + field_format.validate(&value).unwrap(); + } + #[test] + #[should_panic(expected = "InvalidValue(U64(42))")] + fn test_validate_unsigned_64_min() { + let field_format = FieldFormat::Unsigned { bits: Bits::Bit64, min: Some(69), max: None }; + let value = Value::U64(42); + field_format.validate(&value).unwrap(); + } + #[test] + fn test_validate_unsigned_64_min_max() { + let field_format = FieldFormat::Unsigned { bits: Bits::Bit64, min: Some(42), max: Some(69) }; + let value = Value::U64(50); + field_format.validate(&value).unwrap(); + } + #[test] + #[should_panic(expected = "InvalidValue(U32(42424242))")] + fn test_validate_unsigned_64_wrong_type() { + let field_format = FieldFormat::Unsigned { bits: Bits::Bit64, min: None, max: None }; + let value = Value::U32(42424242); + field_format.validate(&value).unwrap(); + } + + #[test] + fn test_validate_enum() { + let field_format = FieldFormat::Enum { values: vec![0, 1, 2, 3] }; + let value = Value::U8(2); + field_format.validate(&value).unwrap(); + } + #[test] + #[should_panic(expected = "InvalidValue(U8(42))")] + fn test_validate_enum_missing() { + let field_format = FieldFormat::Enum { values: vec![0, 1, 2, 3] }; + let value = Value::U8(42); + field_format.validate(&value).unwrap(); + } + + #[test] + fn test_validate_string() { + let field_format = FieldFormat::String(5); + let value = Value::Str("test".into()); + field_format.validate(&value).unwrap(); + } + #[test] + #[should_panic(expected = "InvalidValue(Str(\"testtest\"))")] + fn test_validate_string_too_long() { + let field_format = FieldFormat::String(5); + let value = Value::Str("testtest".into()); + field_format.validate(&value).unwrap(); + } + + #[test] + fn test_validate_bytes() { + let field_format = FieldFormat::Bytes(5); + let value = Value::Bytes(vec![0x00, 0x11].into_boxed_slice()); + field_format.validate(&value).unwrap(); + } + #[test] + #[should_panic(expected = "InvalidValue(Bytes([0, 0, 0, 0]))")] + fn test_validate_bytes_too_long() { + let field_format = FieldFormat::Bytes(3); + let value = Value::Bytes(vec![0x00; 4].into_boxed_slice()); + field_format.validate(&value).unwrap(); + } + + #[test] + fn test_validate_metadata_empty() { + let field = Field(FieldFormat::Unsigned{ bits: Bits::Bit64, min: None, max: None }, Occurences::NoneOrOnce); + let metadata = Metadata::from_inner(vec![]); + field.validate(Type(0), &metadata).unwrap() + } + + #[test] + fn test_validate_metadata_simple() { + let field = Field(FieldFormat::Unsigned{ bits: Bits::Bit64, min: None, max: None }, Occurences::NoneOrOnce); + let metadata = Metadata::from_inner(vec![ + metadata::Field{ id: Type(0), val: Value::U64(42) } + ]); + field.validate(Type(0), &metadata).unwrap() + } + + #[test] + #[should_panic(expected = "InvalidField(Type(0), OccurencesNotMet(OccurencesError { expected: NoneOrOnce, found: 2 })")] + fn test_validate_metadata_fail_too_many() { + let field = Field(FieldFormat::Unsigned{ bits: Bits::Bit64, min: None, max: None }, Occurences::NoneOrOnce); + let metadata = Metadata::from_inner(vec![ + metadata::Field{ id: Type(0), val: Value::U64(0) }, + metadata::Field{ id: Type(0), val: Value::U64(42) }, + ]); + field.validate(Type(0), &metadata).unwrap() + } + + #[test] + #[should_panic(expected = "InvalidField(Type(0), InvalidValue(U32(42)))")] + fn test_validate_metadata_fail_invalid_value() { + let field = Field(FieldFormat::Unsigned{ bits: Bits::Bit64, min: None, max: None }, Occurences::NoneOrOnce); + let metadata = Metadata::from_inner(vec![ + metadata::Field{ id: Type(0), val: Value::U32(42) }, + ]); + field.validate(Type(0), &metadata).unwrap() + } +} diff --git a/src/rgb/schema/schema.rs b/src/rgb/schema/schema.rs index fb549ed1..0ff2b493 100644 --- a/src/rgb/schema/schema.rs +++ b/src/rgb/schema/schema.rs @@ -22,20 +22,41 @@ use super::{ types::*, transition::* }; +use crate::rgb::{self, metadata, seal, data}; +use crate::rgb::schema::script; use crate::csv::{ConsensusCommit, serialize, Error}; -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Display)] +#[derive(Clone, Debug, Display)] #[display_from(Debug)] -pub struct ValidationError { +pub enum SchemaError { + InvalidValue(metadata::Value), + MinMaxBoundsOnLargeInt, + + OccurencesNotMet(OccurencesError), + + UnknownField(metadata::Type), + InvalidField(metadata::Type, Box), + + InvalidTransitionId(usize), + + InvalidBoundSeal(seal::Type, Box), + InvalidBoundSealId(seal::Type), + InvalidBoundSealValue(seal::Type, StateFormat, data::Data), + InvalidOutputBalanceBulletProof(usize), +} +#[derive(Clone, Debug)] +pub struct PartialValidation { + pub should_close: Option, + pub output_commitments: Vec, } #[derive(Clone, Debug, Display)] #[display_from(Debug)] pub struct Schema { pub seals: HashMap, - pub transitions: Vec, + pub transitions: HashMap, } impl Schema { @@ -43,8 +64,39 @@ impl Schema { self.consensus_commit().expect("Schema with commit failures must nor be serialized") } - pub fn validate(&self, ts: super::transition::Transition) -> Result<(), ValidationError> { - unimplemented!() + pub fn validate_transition(&self, ts: &rgb::Transition) -> Result { + let transition_schema = self.transitions.get(&ts.id).ok_or(SchemaError::InvalidTransitionId(ts.id))?; + + // we only support standard scripting with no extensions at the moment + match transition_schema.scripting { + script::Scripting { validation: script::Procedure::Standard(procedure), extensions: script::Extensions::ScriptsDenied } => procedure.validate(ts.script.as_ref())?, + _ => panic!(format!("Unimplemented validation of: {:?}", transition_schema.scripting)), + } + + // TODO: unsafe casting that will be removed if we switch to maps indexed by u16s + + // find invalid unknown fields + for metadata::Field { id, .. } in ts.meta.iter() { + transition_schema.fields.get(&(id.0 as usize)).ok_or(SchemaError::UnknownField(*id))?; + } + // check known fields + for (field_type, field) in &transition_schema.fields { + field.validate(metadata::Type(*field_type as u16), &ts.meta)?; + } + + let output_commitments = transition_schema + .binds + .validate(&self.seals, ts.state.iter().collect())? + .into_iter() + .map(|cmt| cmt.commitment) + .collect(); + println!("output_commitments = {:?}", output_commitments); + //let total_output_amount = output_commitments.into_iter().fold(data::amount::zero_pedersen_commitment(), |acc, x| x + acc); + + Ok(PartialValidation { + should_close: transition_schema.closes.clone(), + output_commitments, + }) } } @@ -57,7 +109,7 @@ impl serialize::Commitment for Schema { fn commitment_deserialize(mut d: D) -> Result { Ok(Self { seals: >::commitment_deserialize(&mut d)?, - transitions: >::commitment_deserialize(&mut d)?, + transitions: >::commitment_deserialize(&mut d)?, }) } } @@ -74,3 +126,55 @@ tagged_hash!(SchemaId, SchemaIdTag, MIDSTATE_SHEMAID, doc=""); impl ConsensusCommit for Schema { type CommitmentHash = SchemaId; } + +#[cfg(test)] +#[allow(unused_imports)] +mod test { + use crate::rgb; + use crate::rgb::metadata; + use crate::rgb::state::State; + use crate::rgb::schema::*; + use crate::rgb::schema::types::*; + use crate::rgb::schema::script::*; + use crate::rgb::script::*; + use crate::rgb::schema::*; + + #[test] + fn schema_test() { + const TRANSITION_VAL: usize = 0; + const FIELD_VAL: usize = 5; + + let schema_transition = Transition { + closes: None, + fields: map!{ + FIELD_VAL => Field(FieldFormat::String(10), Occurences::Once) + }, + binds: map!{}.into(), + scripting: Scripting { + validation: Procedure::Standard(StandardProcedure::Rgb1Genesis), + extensions: Extensions::ScriptsDenied, + } + }; + let schema = Schema { + seals: map!{}, + transitions: map!{ + TRANSITION_VAL => schema_transition + } + }; + + let meta = metadata::Metadata::from_inner(vec![ + metadata::Field{ id: metadata::Type(FIELD_VAL as u16), val: metadata::Value::Str("test".into()) }, + ]); + let transition = rgb::Transition { + id: 0, + meta, + state: State::from_inner(vec![]), + script: None + }; + + println!("{:#?}", schema); + println!("{:#?}", transition); + + println!("{:?}", schema.validate_transition(&transition)); + } +} diff --git a/src/rgb/schema/script.rs b/src/rgb/schema/script.rs index 479540b2..0ec4580c 100644 --- a/src/rgb/schema/script.rs +++ b/src/rgb/schema/script.rs @@ -16,7 +16,9 @@ use std::io; use num_traits::{ToPrimitive, FromPrimitive}; use num_derive::{ToPrimitive, FromPrimitive}; +use super::SchemaError; use crate::csv::{Commitment, Error}; +use crate::rgb; #[non_exhaustive] #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Display, ToPrimitive, FromPrimitive)] @@ -46,6 +48,13 @@ pub enum StandardProcedure { impl_commitment_enum!(StandardProcedure); +impl StandardProcedure { + pub fn validate(&self, _transition_script: Option<&rgb::Script>) -> Result<(), SchemaError> { + // TODO: validate the script + Ok(()) + } +} + #[non_exhaustive] #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Display)] diff --git a/src/rgb/schema/transition.rs b/src/rgb/schema/transition.rs index 110dbb83..a0edcfcd 100644 --- a/src/rgb/schema/transition.rs +++ b/src/rgb/schema/transition.rs @@ -18,18 +18,19 @@ use std::collections::HashMap; use super::{ types::*, field::*, + schema::SchemaError, script::Scripting, }; - +use crate::rgb::{data, state, seal}; use crate::csv::{serialize::Commitment, Error}; - +use crate::common::wrapper::Wrapper; #[derive(Clone, Debug, Display)] #[display_from(Debug)] pub struct Transition { - pub closes: Option>>, - pub fields: Vec, - pub binds: HashMap>, + pub closes: Option, + pub fields: HashMap, + pub binds: SealsSchema, pub scripting: Scripting, } @@ -52,3 +53,68 @@ impl Commitment for Transition { */ } } + +wrapper!(SealsSchema, _SealsSchemaPhantom, HashMap>, doc=""); + +impl Commitment for SealsSchema { + fn commitment_serialize(&self, mut e: E) -> Result { + self.as_ref().commitment_serialize(&mut e) + } + + fn commitment_deserialize(mut d: D) -> Result { + let data: HashMap> = Commitment::commitment_deserialize(&mut d)?; + Ok(data.into()) + } +} + +impl SealsSchema { + pub fn validate(&self, seals: &HashMap, state: Vec<&state::Partial>) -> Result, SchemaError> { + let mut output_commitments = Vec::new(); + + // find invalid created seals + for (index, partial) in state.iter().enumerate() { + match partial { + state::Partial::State(state::Bound{ id, val, .. }) => { + let usize_id = id.0 as usize; + + // check if it's expected in this transition + self.get(&usize_id).ok_or(SchemaError::InvalidBoundSealId(*id))?; + + // match with the provided data type + match (seals.get(&usize_id), val) { + (Some(StateFormat::NoState), data::Data::None) => {}, + (Some(StateFormat::Amount), data::Data::Balance(commitment)) => { + data::amount::verify_bullet_proof(commitment).map_err(|_| SchemaError::InvalidOutputBalanceBulletProof(index))?; + + output_commitments.push(commitment.clone()); + }, + (Some(StateFormat::Data), data::Data::Binary(_data)) => { + unimplemented!(); // TODO + }, + + (None, data) => return Err(SchemaError::InvalidBoundSealId(*id)), + (Some(state_format), data) => return Err(SchemaError::InvalidBoundSealValue(*id, *state_format, data.clone())), + } + }, + state::Partial::Commitment(_) => unimplemented!(), // TODO + } + } + // check created seals + for (seal_type, occurences) in self.iter() { + let count = state + .iter() + .filter(|m| { + match m { + state::Partial::State(state::Bound { id: seal_type, .. }) => true, + _ => false, + } + }) + .count(); + + occurences.check_count(count as u32) + .map_err(|e| SchemaError::InvalidBoundSeal(seal::Type(*seal_type as u16), Box::new(SchemaError::OccurencesNotMet(e))))?; + } + + Ok(output_commitments) + } +} diff --git a/src/rgb/schema/types.rs b/src/rgb/schema/types.rs index 2bef34b8..31b27950 100644 --- a/src/rgb/schema/types.rs +++ b/src/rgb/schema/types.rs @@ -14,12 +14,22 @@ use std::{io, convert::TryFrom}; -use num_integer::Integer; use num_traits::{ToPrimitive, FromPrimitive}; use num_derive::{ToPrimitive, FromPrimitive}; use crate::csv::serialize::*; +pub trait UnsignedInteger: Clone + Copy + PartialEq + Eq + PartialOrd + Ord + Into + std::fmt::Debug { + fn as_u64(self) -> u64 { + self.into() + } +} + +impl UnsignedInteger for u8 { } +impl UnsignedInteger for u16 { } +impl UnsignedInteger for u32 { } +impl UnsignedInteger for u64 { } + #[non_exhaustive] #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Display, ToPrimitive, FromPrimitive)] #[display_from(Debug)] @@ -87,11 +97,44 @@ impl_commitment_enum!(ECPointSerialization); #[non_exhaustive] #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Display)] #[display_from(Debug)] -pub enum Occurences where MAX: std::fmt::Debug { +pub enum Occurences { Once, NoneOrOnce, - OnceOrUpTo(Option), - NoneOrUpTo(Option), + OnceOrUpTo(Option), + NoneOrUpTo(Option), +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Display)] +#[display_from(Debug)] +pub struct OccurencesError { + pub expected: Occurences, + pub found: u64 +} + +impl Occurences { + pub fn translate_u64(self) -> Occurences { + match self { + Occurences::Once => Occurences::Once, + Occurences::NoneOrOnce => Occurences::NoneOrOnce, + Occurences::OnceOrUpTo(None) => Occurences::OnceOrUpTo(None), + Occurences::OnceOrUpTo(Some(max)) => Occurences::OnceOrUpTo(Some(max.as_u64())), + Occurences::NoneOrUpTo(None) => Occurences::NoneOrUpTo(None), + Occurences::NoneOrUpTo(Some(max)) => Occurences::NoneOrUpTo(Some(max.as_u64())), + _ => panic!("Unknown occurence variant"), + } + } + + pub fn check_count(&self, count: I) -> Result<(), OccurencesError> { + match self { + Occurences::Once if count.as_u64() == 1 => Ok(()), + Occurences::NoneOrOnce if count.as_u64() <= 1 => Ok(()), + Occurences::OnceOrUpTo(None) if count.as_u64() > 0 => Ok(()), + Occurences::OnceOrUpTo(Some(max)) if count.as_u64() > 0 && count <= *max => Ok(()), + Occurences::NoneOrUpTo(None) => Ok(()), + Occurences::NoneOrUpTo(Some(max)) if count <= *max => Ok(()), + _ => Err(OccurencesError { expected: self.clone().translate_u64(), found: count.as_u64() }), + } + } } macro_rules! impl_occurences { @@ -136,3 +179,104 @@ impl_occurences!(u8); impl_occurences!(u16); impl_occurences!(u32); impl_occurences!(u64); + +#[cfg(test)] +mod test { + use super::Occurences; + + #[test] + fn test_once_check_count() { + let occurence: Occurences = Occurences::Once; + occurence.check_count(1).unwrap(); + } + #[test] + #[should_panic(expected = "OccurencesError { expected: Once, found: 0 }")] + fn test_once_check_count_fail_zero() { + let occurence: Occurences = Occurences::Once; + occurence.check_count(0).unwrap(); + } + #[test] + #[should_panic(expected = "OccurencesError { expected: Once, found: 2 }")] + fn test_once_check_count_fail_two() { + let occurence: Occurences = Occurences::Once; + occurence.check_count(2).unwrap(); + } + + #[test] + fn test_none_or_once_check_count() { + let occurence: Occurences = Occurences::NoneOrOnce; + occurence.check_count(1).unwrap(); + } + #[test] + fn test_none_or_once_check_count_zero() { + let occurence: Occurences = Occurences::NoneOrOnce; + occurence.check_count(0).unwrap(); + } + #[test] + #[should_panic(expected = "OccurencesError { expected: NoneOrOnce, found: 2 }")] + fn test_none_or_once_check_count_fail_two() { + let occurence: Occurences = Occurences::NoneOrOnce; + occurence.check_count(2).unwrap(); + } + + #[test] + fn test_once_or_up_to_none() { + let occurence: Occurences = Occurences::OnceOrUpTo(None); + occurence.check_count(1).unwrap(); + } + #[test] + fn test_once_or_up_to_none_large() { + let occurence: Occurences = Occurences::OnceOrUpTo(None); + occurence.check_count(u32::MAX).unwrap(); + } + #[test] + #[should_panic(expected = "OccurencesError { expected: OnceOrUpTo(None), found: 0 }")] + fn test_once_or_up_to_none_fail_zero() { + let occurence: Occurences = Occurences::OnceOrUpTo(None); + occurence.check_count(0).unwrap(); + } + #[test] + fn test_once_or_up_to_42() { + let occurence: Occurences = Occurences::OnceOrUpTo(Some(42)); + occurence.check_count(42).unwrap(); + } + #[test] + #[should_panic(expected = "OccurencesError { expected: OnceOrUpTo(Some(42)), found: 43 }")] + fn test_once_or_up_to_42_large() { + let occurence: Occurences = Occurences::OnceOrUpTo(Some(42)); + occurence.check_count(43).unwrap(); + } + #[test] + #[should_panic(expected = "OccurencesError { expected: OnceOrUpTo(Some(42)), found: 0 }")] + fn test_once_or_up_to_42_fail_zero() { + let occurence: Occurences = Occurences::OnceOrUpTo(Some(42)); + occurence.check_count(0).unwrap(); + } + + #[test] + fn test_none_or_up_to_none_zero() { + let occurence: Occurences = Occurences::NoneOrUpTo(None); + occurence.check_count(0).unwrap(); + } + #[test] + fn test_none_or_up_to_none_large() { + let occurence: Occurences = Occurences::NoneOrUpTo(None); + occurence.check_count(u32::MAX).unwrap(); + } + #[test] + fn test_none_or_up_to_42_zero() { + let occurence: Occurences = Occurences::NoneOrUpTo(Some(42)); + occurence.check_count(0).unwrap(); + } + #[test] + fn test_none_or_up_to_42() { + let occurence: Occurences = Occurences::NoneOrUpTo(Some(42)); + occurence.check_count(42).unwrap(); + } + #[test] + #[should_panic(expected = "OccurencesError { expected: NoneOrUpTo(Some(42)), found: 43 }")] + fn test_none_or_up_to_42_large() { + let occurence: Occurences = Occurences::NoneOrUpTo(Some(42)); + occurence.check_count(43).unwrap(); + } +} diff --git a/src/rgb/schemata/fungible.rs b/src/rgb/schemata/fungible.rs index beab8781..17e350b0 100644 --- a/src/rgb/schemata/fungible.rs +++ b/src/rgb/schemata/fungible.rs @@ -62,14 +62,22 @@ pub struct Rgb1(); impl Rgb1 { const PRIM_ISSUE_TS: usize = 0; - const SEC_ISSUE_TS: usize = 0; - const TRANSFER_TS: usize = 0; - const PRINE_TS: usize = 0; + const SEC_ISSUE_TS: usize = 1; + const TRANSFER_TS: usize = 2; + const PRUNE_TS: usize = 3; const ISSUE_SEAL: usize = 0; const BALANCE_SEAL: usize = 1; const PRUNE_SEAL: usize = 2; + const TICKER_FIELD: usize = 0; + const TITLE_FIELD: usize = 1; + const DESCRIPTION_FIELD: usize = 2; + const TOTAL_SUPPLY_FIELD: usize = 3; + const FRACTIONAL_BITS_FIELD: usize = 4; + const DUST_LIMIT_FIELD: usize = 5; + const NETWORK_FIELD: usize = 6; + fn balances_to_bound_state(balances: Balances) -> Result { let seals_count = balances.len(); Ok(rgb::State::from_inner( @@ -99,36 +107,37 @@ impl Rgb1 { //let ts_schema = &schema.transitions[PRIM_ISSUE_TS]; let mut meta = rgb::Metadata::from_inner(vec![ - metadata::Field { id: metadata::Type(0), val: metadata::Value::Str(String::from(ticker)) }, - metadata::Field { id: metadata::Type(1), val: metadata::Value::Str(String::from(name)) }, - metadata::Field { id: metadata::Type(5), val: metadata::Value::U8(precision) }, - metadata::Field { id: metadata::Type(7), val: metadata::Value::U32(network.into()) }, + metadata::Field { id: metadata::Type(Self::TICKER_FIELD as u16), val: metadata::Value::Str(String::from(ticker)) }, + metadata::Field { id: metadata::Type(Self::TITLE_FIELD as u16), val: metadata::Value::Str(String::from(name)) }, + metadata::Field { id: metadata::Type(Self::FRACTIONAL_BITS_FIELD as u16), val: metadata::Value::U8(precision) }, + metadata::Field { id: metadata::Type(Self::NETWORK_FIELD as u16), val: metadata::Value::U32(network.into()) }, ]); if let Some(descr) = descr { meta.as_mut().push( - metadata::Field { id: metadata::Type(2), val: metadata::Value::Str(String::from(descr)) } + metadata::Field { id: metadata::Type(Self::DESCRIPTION_FIELD as u16), val: metadata::Value::Str(String::from(descr)) } ); } if let Some(supply) = supply { + // TODO: why is this optional? meta.as_mut().push( - metadata::Field { id: metadata::Type(3), val: metadata::Value::U64(supply) } + metadata::Field { id: metadata::Type(Self::TOTAL_SUPPLY_FIELD as u16), val: metadata::Value::U64(supply) } ); } if let Some(dust) = dust { meta.as_mut().push( - metadata::Field { id: metadata::Type(5), val: metadata::Value::U64(dust) } + metadata::Field { id: metadata::Type(Self::DUST_LIMIT_FIELD as u16), val: metadata::Value::U64(dust) } ); } let state = Self::balances_to_bound_state(balances)?; - Ok(rgb::Transition { meta, state, script: None }) + Ok(rgb::Transition { id: Self::PRIM_ISSUE_TS, meta, state, script: None }) } pub fn transfer(balances: Balances) -> Result { let state = Self::balances_to_bound_state(balances)?; - Ok(rgb::Transition { meta: rgb::Metadata::default(), state, script: None }) + Ok(rgb::Transition { id: Self::TRANSFER_TS, meta: rgb::Metadata::default(), state, script: None }) } } @@ -144,81 +153,81 @@ impl Schemata for Rgb1 { Self::BALANCE_SEAL => Amount, Self::PRUNE_SEAL => NoState }, - transitions: vec![ + transitions: map!{ // Genesis state: primary issue - Transition { + Self::PRIM_ISSUE_TS => Transition { closes: None, - fields: vec![ + fields: map!{ // Ticker - Field(FieldFormat::String(16), Once), + Self::TICKER_FIELD => Field(FieldFormat::String(16), Once), // Title - Field(FieldFormat::String(256), Once), + Self::TITLE_FIELD => Field(FieldFormat::String(256), Once), // Description - Field(FieldFormat::String(1024), NoneOrOnce), + Self::DESCRIPTION_FIELD => Field(FieldFormat::String(1024), NoneOrOnce), // Total supply - Field(FieldFormat::Unsigned { bits: Bit64, min: None, max: None }, NoneOrOnce), + Self::TOTAL_SUPPLY_FIELD => Field(FieldFormat::Unsigned { bits: Bit64, min: None, max: None }, NoneOrOnce), // Fractional bits - Field(FieldFormat::Unsigned { bits: Bit8, min: None, max: None }, Once), + Self::FRACTIONAL_BITS_FIELD => Field(FieldFormat::Unsigned { bits: Bit8, min: None, max: None }, Once), // Dust limit - Field(FieldFormat::Unsigned { bits: Bit64, min: None, max: None }, NoneOrOnce), + Self::DUST_LIMIT_FIELD => Field(FieldFormat::Unsigned { bits: Bit64, min: None, max: None }, NoneOrOnce), // Network - Field(FieldFormat::Unsigned { bits: Bit32, min: None, max: None }, Once), - ], + Self::NETWORK_FIELD => Field(FieldFormat::Unsigned { bits: Bit32, min: None, max: None }, Once) + }, binds: map!{ Self::BALANCE_SEAL => OnceOrUpTo(None), Self::ISSUE_SEAL => NoneOrOnce, Self::PRUNE_SEAL => NoneOrOnce - }, + }.into(), scripting: Scripting { validation: Standard(Rgb1Genesis), extensions: ScriptsDenied } }, // Issuance transition: secondary issue - Transition { + Self::SEC_ISSUE_TS => Transition { closes: Some(map! { Self::ISSUE_SEAL => Once - }), - fields: vec![], + }.into()), + fields: map!{}, binds: map!{ Self::BALANCE_SEAL => OnceOrUpTo(None), Self::ISSUE_SEAL => NoneOrUpTo(None) - }, + }.into(), scripting: Scripting { validation: Standard(Rgb1Issue), extensions: ScriptsDenied } }, // Amount transition: asset transfers - Transition { + Self::TRANSFER_TS => Transition { closes: Some(map!{ Self::BALANCE_SEAL => OnceOrUpTo(None) - }), - fields: vec![], + }.into()), + fields: map!{}, binds: map!{ Self::BALANCE_SEAL => NoneOrUpTo(None) - }, + }.into(), scripting: Scripting { validation: Standard(Rgb1Transfer), extensions: ScriptsDenied } }, // Pruning transition: asset re-issue - Transition { + Self::PRUNE_TS => Transition { closes: Some(map!{ Self::PRUNE_SEAL => NoneOrOnce - }), - fields: vec![], + }.into()), + fields: map!{}, binds: map!{ Self::BALANCE_SEAL => OnceOrUpTo(None), Self::PRUNE_SEAL => Once - }, + }.into(), scripting: Scripting { validation: Standard(Rgb1Prune), extensions: ScriptsDenied } } - ] + } }))); }); diff --git a/src/rgb/schemata/mod.rs b/src/rgb/schemata/mod.rs index c8bbfb9e..b24c0f22 100644 --- a/src/rgb/schemata/mod.rs +++ b/src/rgb/schemata/mod.rs @@ -14,10 +14,10 @@ use super::schema::Schema; pub mod fungible; -pub mod collectibles; +// pub mod collectibles; pub use fungible::Rgb1; -pub use collectibles::Rgb2; +// pub use collectibles::Rgb2; pub trait Schemata { diff --git a/src/rgb/seal.rs b/src/rgb/seal.rs index 13d48b6e..c718f349 100644 --- a/src/rgb/seal.rs +++ b/src/rgb/seal.rs @@ -12,19 +12,25 @@ // If not, see . +use bitcoin::OutPoint; use bitcoin::hash_types::Txid; use crate::bp::{ short_id::ShortId, blind::{OutpointReveal, OutpointHash} }; +#[derive(Clone, PartialEq, PartialOrd, Debug, Display)] +#[display_from(Debug)] +pub enum Error { + VoutOverflow, +} #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Display, Default)] #[display_from(Debug)] pub struct Type(pub u16); -#[derive(Clone, PartialEq, PartialOrd, Debug, Display)] +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Debug, Display)] #[display_from(Debug)] pub enum Seal { /// Seal contained within the witness transaction @@ -55,4 +61,24 @@ impl Seal { pub fn blinded(hash: OutpointHash) -> Self { Seal::BlindedTxout(hash) } + + pub fn maybe_as_outpoint(&self, revealed_outpoint: Option, creating_txid: Option, blinding_key: Option) -> Option { + match self { + Seal::WitnessTxout(vout) if creating_txid.is_some() => Some(OutPoint { txid: creating_txid.unwrap(), vout: *vout as u32 }), + Seal::RevealedTxout(revealed, _) => Some(OutPoint { txid: revealed.txid, vout: revealed.vout as u32 }), + Seal::BlindedTxout(hash) if revealed_outpoint.is_some() && blinding_key.is_some() => { + match Seal::maybe_from_outpoint(revealed_outpoint.unwrap(), blinding_key.unwrap()) { + Some(Seal::RevealedTxout(revealed, _)) if revealed.outpoint_hash() == *hash => Some(OutPoint { txid: revealed.txid, vout: revealed.vout as u32 }), + _ => None + } + }, + _ => None + } + } + + pub fn compare_to_outpoint(&self, outpoint: &OutPoint, creating_txid: Option, blinding_key: Option) -> bool { + self + .maybe_as_outpoint(Some(outpoint.clone()), creating_txid, blinding_key) + .map_or(false, |out| out == *outpoint) + } } diff --git a/src/rgb/serialize.rs b/src/rgb/serialize.rs index 8c0e8e1f..bec1df14 100644 --- a/src/rgb/serialize.rs +++ b/src/rgb/serialize.rs @@ -476,6 +476,7 @@ impl csv::serialize::Commitment for rgb::Transition { fn commitment_serialize(&self, mut e: E) -> Result { use crate::rgb::commit::Identifiable; Ok( + self.id.commitment_serialize(&mut e)? + self.meta.commitment()?.commitment_serialize(&mut e)? + self.state.commitment()?.commitment_serialize(&mut e)? + self.script.commitment()?.commitment_serialize(&mut e)? @@ -490,6 +491,7 @@ impl csv::serialize::Commitment for rgb::Transition { impl csv::serialize::Network for rgb::Transition { fn network_serialize(&self, mut e: E) -> Result { Ok( + self.id.network_serialize(&mut e)? + self.meta.network_serialize(&mut e)? + self.state.network_serialize(&mut e)? + self.script.network_serialize(&mut e)? @@ -498,6 +500,7 @@ impl csv::serialize::Network for rgb::Transition { fn network_deserialize(mut d: D) -> Result { Ok(Self { + id: csv::serialize::Network::network_deserialize(&mut d)?, meta: rgb::Metadata::network_deserialize(&mut d)?, state: rgb::State::network_deserialize(&mut d)?, script: Option::::network_deserialize(&mut d)? diff --git a/src/rgb/transition.rs b/src/rgb/transition.rs index d03258cd..c4789f8b 100644 --- a/src/rgb/transition.rs +++ b/src/rgb/transition.rs @@ -12,12 +12,37 @@ // If not, see . +use bitcoin::hashes::{Hash, sha256t}; + use super::{State, Metadata, Script}; +use crate::csv::ConsensusCommit; +use crate::csv::serialize; #[derive(Clone, PartialEq, Debug, Display)] #[display_from(Debug)] pub struct Transition { + pub id: usize, pub meta: Metadata, pub state: State, pub script: Option