diff --git a/Cargo.lock b/Cargo.lock index 564a9c48..686f2354 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -210,8 +210,7 @@ dependencies = [ [[package]] name = "bp-core" version = "0.10.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc600afc6331ba4ec1faf6ef173c56289112895ea7a7452eebc3096828faa236" +source = "git+https://github.com/BP-WG/bp-core#5dddcf2925ea02251beec50ea674f086a74f3b6c" dependencies = [ "amplify", "bp-dbc", @@ -226,8 +225,7 @@ dependencies = [ [[package]] name = "bp-dbc" version = "0.10.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233d822c2a634662211f7f6224eaeb38539ebe266030899c6f07a30c27e1dd44" +source = "git+https://github.com/BP-WG/bp-core#5dddcf2925ea02251beec50ea674f086a74f3b6c" dependencies = [ "amplify", "bp-primitives", @@ -240,8 +238,7 @@ dependencies = [ [[package]] name = "bp-primitives" version = "0.10.0-beta.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75650820410a2c8e151ebb021558b63a531be375d49229cc27cde57731456035" +source = "git+https://github.com/BP-WG/bp-core#5dddcf2925ea02251beec50ea674f086a74f3b6c" dependencies = [ "amplify", "commit_verify", @@ -253,8 +250,7 @@ dependencies = [ [[package]] name = "bp-seals" version = "0.10.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121a8135de21362d6cef4647ae5acb8f4c0a0087d4c673c5edf87b924d7ae48e" +source = "git+https://github.com/BP-WG/bp-core#5dddcf2925ea02251beec50ea674f086a74f3b6c" dependencies = [ "amplify", "baid58", @@ -317,8 +313,7 @@ dependencies = [ [[package]] name = "commit_verify" version = "0.10.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea02b36cdb0d308ca1a1af8075977de0ab5e01e670a79d0b271143b9fcc16c48" +source = "git+https://github.com/LNP-BP/client_side_validation#4d0b97d0a307e4d7751bdf09d0af6105da84e440" dependencies = [ "amplify", "rand", @@ -721,8 +716,7 @@ dependencies = [ [[package]] name = "rgb-core" version = "0.10.0-beta.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f281a29e6d8e853f18da28ef93d146c91ebd76e94b9852e31064d8d806a1543b" +source = "git+https://github.com/RGB-WG/rgb-core#fc4af5cff74fe6e9105a17911ba504f06cc64dc3" dependencies = [ "aluvm", "amplify", diff --git a/Cargo.toml b/Cargo.toml index 860f548d..55f675e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,4 +76,7 @@ wasm-bindgen-test = "0.3" features = [ "all" ] [patch.crates-io] -strict_types = { git = "https://github.com/strict-types/strict-types" } \ No newline at end of file +strict_types = { git = "https://github.com/strict-types/strict-types" } +commit_verify = { git = "https://github.com/LNP-BP/client_side_validation" } +bp-core = { git = "https://github.com/BP-WG/bp-core" } +rgb-core = { git = "https://github.com/RGB-WG/rgb-core" } diff --git a/src/invoice.rs b/src/invoice.rs index 001410d1..c7d532c3 100644 --- a/src/invoice.rs +++ b/src/invoice.rs @@ -22,6 +22,8 @@ use std::num::ParseIntError; use std::str::FromStr; +use bitcoin::{Address, Network}; +use bp::Chain; use fluent_uri::Uri; use indexmap::IndexMap; use rgb::{AttachId, ContractId, SecretSeal}; @@ -53,9 +55,17 @@ pub enum InvoiceState { Attach(AttachId), } +#[derive(Clone, Eq, PartialEq, Hash, Debug, Display)] +#[display(inner)] +pub enum Beneficiary { + BlindedSeal(SecretSeal), + WitnessUtxo(Address), + // TODO: add BifrostNode(), +} + #[derive(Clone, Eq, PartialEq, Debug, Display)] // TODO: Change to custom display impl providing support for optionals & query -#[display("{transport}{contract}/{iface}/{value}@{seal}")] +#[display("{transport}{contract}/{iface}/{value}+{beneficiary}")] pub struct RgbInvoice { pub transport: RgbTransport, pub contract: ContractId, @@ -63,8 +73,9 @@ pub struct RgbInvoice { pub operation: Option, pub assignment: Option, // pub owned_state: Option, - pub seal: SecretSeal, + pub beneficiary: Beneficiary, pub value: u64, // TODO: Change to TypedState + pub chain: Option, pub unknown_query: IndexMap, } @@ -81,6 +92,15 @@ pub enum InvoiceParseError { #[from] Id(baid58::Baid58ParseError), + #[display(doc_comments)] + /// can't recognize beneficiary "": it should be either a bitcoin address or + /// a blinded UTXO seal. + Beneficiary(String), + + #[display(doc_comments)] + /// network {0} is not supported. + UnsupportedNetwork(Network), + #[from] Num(ParseIntError), @@ -102,9 +122,31 @@ impl FromStr for RgbInvoice { .map(|e| e.to_string()) .collect::>(); + let mut chain = None; + let mut assignment = path[2].split('@'); let (seal, value) = match (assignment.next(), assignment.next()) { - (Some(a), Some(b)) => (SecretSeal::from_str(b)?, u64::from_str_radix(a, 10)?), + (Some(a), Some(b)) => { + let value = u64::from_str_radix(a, 10)?; + let benefactor = match (SecretSeal::from_str(b), Address::from_str(b)) { + (Ok(seal), Err(_)) => Beneficiary::BlindedSeal(seal), + (Err(_), Ok(addr)) => { + chain = Some(match addr.network { + Network::Bitcoin => Chain::Bitcoin, + Network::Testnet => Chain::Testnet3, + Network::Signet => Chain::Signet, + Network::Regtest => Chain::Regtest, + unknown => return Err(InvoiceParseError::UnsupportedNetwork(unknown)), + }); + Beneficiary::WitnessUtxo(addr.assume_checked()) + } + (Err(_), Err(_)) => return Err(InvoiceParseError::Beneficiary(a.to_owned())), + (Ok(_), Ok(_)) => panic!( + "found a string which is both valid bitcoin address and UTXO blind seal" + ), + }; + (benefactor, value) + } _ => return Err(InvoiceParseError::Invalid), }; @@ -114,8 +156,9 @@ impl FromStr for RgbInvoice { iface: TypeName::try_from(path[1].clone())?, operation: None, assignment: None, - seal, + beneficiary: seal, value, + chain, unknown_query: Default::default(), }) } diff --git a/src/lib.rs b/src/lib.rs index 197b5817..b7653206 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,11 +61,10 @@ extern crate amplify; mod invoice; +mod pay; pub mod psbt; pub use invoice::{InvoiceParseError, InvoiceState, RgbInvoice, RgbTransport}; -use rgbstd::interface::TransitionBuilder; -use rgbstd::persistence::Stash; // 1. Construct main state transition with transition builder // -- shortcut using invoice to do that construction (like .with_invoice()) @@ -76,14 +75,3 @@ use rgbstd::persistence::Stash; // that output // 3. Extract from PSBT all spent prevouts and construct blank state transitions // for each one of them; embed them into PSBT - -pub enum InvoiceInconsistency {} - -impl RgbInvoice { - pub fn to_builder( - &self, - stash: &impl Stash, - ) -> Result { - todo!() - } -} diff --git a/src/pay.rs b/src/pay.rs new file mode 100644 index 00000000..e41a826d --- /dev/null +++ b/src/pay.rs @@ -0,0 +1,253 @@ +// RGB wallet library for smart contracts on Bitcoin & Lightning network +// +// SPDX-License-Identifier: Apache-2.0 +// +// Written in 2019-2023 by +// Dr Maxim Orlovsky +// +// Copyright (C) 2019-2023 LNP/BP Standards Association. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::{BTreeMap, HashMap}; +use std::error::Error; + +use bitcoin::hashes::Hash; +use bitcoin::psbt::Psbt; +use bp::seals::txout::CloseMethod; +use bp::Outpoint; +use rgb::{AssignmentType, ContractId, GraphSeal, Opout}; +use rgbstd::containers::{Bindle, BuilderSeal, Transfer}; +use rgbstd::interface::{BuilderError, ContractSuppl, TypedState, VelocityClass}; +use rgbstd::persistence::{ConsignerError, Inventory, InventoryError, Stash}; + +use crate::invoice::Beneficiary; +use crate::psbt::{DbcPsbtError, PsbtDbc, RgbExt, RgbPsbtError}; +use crate::RgbInvoice; + +#[derive(Debug, Display, Error, From)] +#[display(inner)] +pub enum PayError +where E1: From +{ + /// not enough PSBT output found to put all required state (can't add + /// assignment {1} for {0}-velocity state). + #[display(doc_comments)] + NoBlankChange(VelocityClass, AssignmentType), + + /// PSBT lacks beneficiary output matching the invoice. + #[display(doc_comments)] + NoBeneficiaryOutput, + + /// state provided via PSBT inputs is not sufficient to cover invoice state + /// requirements. + InsufficientState, + + #[from] + Inventory(InventoryError), + + #[from] + Builder(BuilderError), + + #[from] + Consigner(ConsignerError), + + #[from] + RgbPsbt(RgbPsbtError), + + #[from] + DbcPsbt(DbcPsbtError), +} + +pub trait InventoryWallet: Inventory { + /// # Assumptions + /// + /// 1. If PSBT output has BIP32 derivation information it belongs to our + /// wallet - except when it matches address from the invoice. + fn pay( + &mut self, + invoice: RgbInvoice, + psbt: &mut Psbt, + method: CloseMethod, + ) -> Result, PayError::Error>> + where + Self::Error: From<::Error>, + { + // 1. Prepare the data + let contract_id = invoice.contract; + let mut main_builder = self.transition_builder(contract_id, invoice.iface.clone())?; + + let (beneficiary_output, beneficiary) = match invoice.beneficiary { + Beneficiary::BlindedSeal(seal) => { + let seal = BuilderSeal::Concealed(seal); + (None, seal) + } + Beneficiary::WitnessUtxo(addr) => { + let vout = psbt + .unsigned_tx + .output + .iter() + .enumerate() + .find(|(_, txout)| txout.script_pubkey == addr.script_pubkey()) + .map(|(no, _)| no as u32) + .ok_or(PayError::NoBeneficiaryOutput)?; + let seal = BuilderSeal::Revealed(GraphSeal::new_vout(method, vout)); + (Some(vout), seal) + } + }; + let prev_outputs = psbt + .unsigned_tx + .input + .iter() + .map(|txin| txin.previous_output) + .map(|outpoint| Outpoint::new(outpoint.txid.to_byte_array().into(), outpoint.vout)) + .collect::>(); + + // Classify PSBT outputs which can be used for assignments + let mut out_classes = HashMap::>::new(); + for (no, outp) in psbt.outputs.iter().enumerate() { + if beneficiary_output == Some(no as u32) { + continue; + } + if let Some(class) = outp + // NB: Here we assume that if output has derivation information it belongs to our wallet. + .bip32_derivation + .first_key_value() + .and_then(|(_, src)| src.1.into_iter().rev().skip(1).next()) + .copied() + .map(u32::from) + .and_then(|index| u8::try_from(index).ok()) + .and_then(|index| VelocityClass::try_from(index).ok()) + { + out_classes.entry(class).or_default().push(no as u32); + } + } + let mut out_classes = out_classes + .into_iter() + .map(|(class, indexes)| (class, indexes.into_iter().cycle())) + .collect::>(); + let mut output_for_assignment = |suppl: Option<&ContractSuppl>, + assignment_type: AssignmentType| + -> Result, PayError<_, _>> { + let velocity = suppl + .and_then(|suppl| suppl.owned_state.get(&assignment_type)) + .map(|s| s.velocity_class) + .unwrap_or_default(); + let vout = out_classes + .get_mut(&velocity) + .and_then(|iter| iter.next()) + .ok_or(PayError::NoBlankChange(velocity, assignment_type))?; + let seal = GraphSeal::new_vout(method, vout); + Ok(BuilderSeal::Revealed(seal)) + }; + + // 2. Prepare and self-consume transition + if let Some(op_name) = invoice.operation { + main_builder = main_builder.set_transition_type(op_name)?; + } + let assignment_name = invoice + .assignment + .as_ref() + .or_else(|| main_builder.default_assignment().ok()) + .ok_or(BuilderError::NoDefaultAssignment)?; + let assignment_id = main_builder + .assignments_type(assignment_name) + .ok_or(BuilderError::InvalidStateType(assignment_name.clone()))?; + // TODO: select supplement basing on the signer trust level + let suppl = self + .contract_suppl(contract_id) + .and_then(|set| set.first()) + .cloned(); + let mut sum_inputs = 0u64; + for (opout, state) in self.state_for_outpoints(contract_id, prev_outputs.iter().copied())? { + main_builder = main_builder.add_input(opout)?; + if opout.ty != assignment_id { + let seal = output_for_assignment(suppl.as_ref(), opout.ty)?; + main_builder = main_builder + .add_input(opout)? + .add_raw_state(opout.ty, seal, state)?; + } else if let TypedState::Amount(value) = state { + sum_inputs += value; + } + } + // Add change + if sum_inputs > invoice.value { + let seal = output_for_assignment(suppl.as_ref(), assignment_id)?; + let change = TypedState::Amount(sum_inputs - invoice.value); + main_builder = main_builder.add_raw_state(assignment_id, seal, change)?; + } else if sum_inputs < invoice.value { + return Err(PayError::InsufficientState); + } + let transition = main_builder + .add_raw_state(assignment_id, beneficiary, TypedState::Amount(invoice.value))? + .complete_transition()?; + + // 3. Prepare and self-consume other transitions + let mut spent_state = HashMap::>::new(); + for outpoint in prev_outputs { + for id in self.contracts_by_outpoints([outpoint])? { + if id == contract_id { + continue; + } + spent_state + .entry(id) + .or_default() + .extend(self.state_for_outpoints(id, [outpoint])?); + } + } + // Construct blank transitions, self-consume them + let mut other_transitions = Vec::with_capacity(spent_state.len()); + for (id, opouts) in spent_state { + let mut blank_builder = self + .transition_builder(id, invoice.iface.clone())? + .do_blank_transition()?; + // TODO: select supplement basing on the signer trust level + let suppl = self.contract_suppl(id).and_then(|set| set.first()); + + for (opout, state) in opouts { + let seal = output_for_assignment(suppl, opout.ty)?; + blank_builder = blank_builder + .add_input(opout)? + .add_raw_state(opout.ty, seal, state)?; + } + + other_transitions.push(blank_builder.complete_transition()?); + } + + // 4. Add transitions to PSBT + psbt.push_rgb_transition(transition)?; + for transition in other_transitions { + psbt.push_rgb_transition(transition)?; + } + // Here we assume the provided PSBT is final: its inputs and outputs will not be + // modified after calling this method. + let bundles = psbt.rgb_bundles()?; + // TODO: Make it two-staged, such that PSBT editing will be allowed by other + // participants as required for multiparty protocols like coinjoin. + psbt.rgb_bundle_to_lnpbp4()?; + let anchor = psbt.dbc_conclude(method)?; + // TODO: Ensure that with PSBTv2 we remove flag allowing PSBT modification. + + // 4. Prepare transfer + let witness_txid = psbt.unsigned_tx.txid(); + self.consume_anchor(anchor)?; + for (id, bundle) in bundles { + self.consume_bundle(id, bundle, witness_txid.to_byte_array().into())?; + } + let transfer = self.transfer(contract_id, [beneficiary])?; + + Ok(transfer) + } +} + +impl InventoryWallet for I where I: Inventory {} diff --git a/src/psbt/dbc.rs b/src/psbt/dbc.rs new file mode 100644 index 00000000..e41713ad --- /dev/null +++ b/src/psbt/dbc.rs @@ -0,0 +1,169 @@ +// RGB wallet library for smart contracts on Bitcoin & Lightning network +// +// SPDX-License-Identifier: Apache-2.0 +// +// Written in 2019-2023 by +// Dr Maxim Orlovsky +// +// Copyright (C) 2019-2023 LNP/BP Standards Association. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use amplify::num::u4; +use amplify::RawArray; +use bitcoin::hashes::Hash; +use bitcoin::psbt::Psbt; +use bitcoin::secp256k1::SECP256K1; +use bitcoin::taproot::{TapTree, TaprootBuilder, TaprootBuilderError}; +use bitcoin::ScriptBuf; +use bp::dbc::tapret::{TapretCommitment, TapretPathProof, TapretProof}; +use bp::dbc::Proof; +use bp::seals::txout::CloseMethod; +use bp::TapScript; +use commit_verify::{mpc, CommitVerify, CommitmentId, TryCommitVerify}; +use rgb::Anchor; + +use crate::psbt::lnpbp4::OutputLnpbp4; +use crate::psbt::opret::OutputOpret; +use crate::psbt::tapret::{OutputTapret, TapretKeyError}; +use crate::psbt::{Lnpbp4PsbtError, OpretKeyError, PSBT_OUT_LNPBP4_MIN_TREE_DEPTH}; + +#[derive(Clone, PartialEq, Eq, Debug, Display, Error, From)] +#[display(doc_comments)] +pub enum DbcPsbtError { + /// Using non-empty taptree is not supported in RGB v0.10. Please update. + TapTreeNonEmpty, + + /// taproot output doesn't specify internal key. + NoInternalKey, + + /// none of the outputs is market as a commitment host. + NoHostOutput, + + /// multiple commitment outputs are found + MultipleCommitmentHosts, + + /// commitment method {0} is not supported yet. Please update. + MethodUnsupported(CloseMethod), + + #[from] + #[display(inner)] + Mpc(mpc::Error), + + #[from] + #[display(inner)] + Lnpbp4Psbt(Lnpbp4PsbtError), + + #[from] + #[display(inner)] + TapretKey(TapretKeyError), + + #[from] + #[display(inner)] + OpretKey(OpretKeyError), + + #[from] + #[display(inner)] + TaprootBuilder(TaprootBuilderError), +} + +pub trait PsbtDbc { + fn dbc_conclude( + &mut self, + method: CloseMethod, + ) -> Result, DbcPsbtError>; +} + +impl PsbtDbc for Psbt { + fn dbc_conclude( + &mut self, + method: CloseMethod, + ) -> Result, DbcPsbtError> { + if self + .outputs + .iter() + .map(|output| output.is_tapret_host() | output.is_opret_host()) + .count() > + 1 + { + return Err(DbcPsbtError::MultipleCommitmentHosts); + } + + let (vout, output) = self + .outputs + .iter_mut() + .enumerate() + .find(|(_, output)| { + (output.is_tapret_host() && method == CloseMethod::TapretFirst) | + (output.is_opret_host() && method == CloseMethod::OpretFirst) + }) + .ok_or(DbcPsbtError::NoHostOutput)?; + let txout = &mut self.unsigned_tx.output[vout]; + + let messages = output.lnpbp4_message_map()?; + let min_depth = u4::with( + output + .lnpbp4_min_tree_depth() + .unwrap_or(PSBT_OUT_LNPBP4_MIN_TREE_DEPTH), + ); + let source = mpc::MultiSource { + min_depth, + messages, + }; + let merkle_tree = mpc::MerkleTree::try_commit(&source)?; + let entropy = merkle_tree.entropy(); + output.set_lnpbp4_entropy(entropy)?; + let commitment = merkle_tree.commitment_id(); + + // 2. Depending on the method modify output which is necessary to modify + // TODO: support non-empty tap trees + let proof = if method == CloseMethod::TapretFirst { + if output.tap_tree.is_some() { + return Err(DbcPsbtError::TapTreeNonEmpty); + } + let tapret_commitment = &TapretCommitment::with(commitment, 0); + let script_commitment = + ScriptBuf::from_bytes(TapScript::commit(tapret_commitment).to_vec()); + let builder = TaprootBuilder::with_capacity(1).add_leaf(0, script_commitment)?; + let tap_tree = TapTree::try_from(builder.clone()).expect("builder is complete"); + let internal_pk = output.tap_internal_key.ok_or(DbcPsbtError::NoInternalKey)?; + let tapret_proof = TapretProof { + path_proof: TapretPathProof::root(), + internal_pk: internal_pk.into(), + }; + output.tap_tree = Some(tap_tree); + let spent_info = builder + .finalize(SECP256K1, internal_pk) + .expect("complete tree"); + let merkle_root = spent_info.merkle_root().expect("script tree present"); + + output.set_tapret_commitment(commitment, &tapret_proof.path_proof)?; + txout.script_pubkey = ScriptBuf::new_v1_p2tr(SECP256K1, internal_pk, Some(merkle_root)); + Proof::TapretFirst(tapret_proof) + } else if method == CloseMethod::OpretFirst { + output.set_opret_commitment(commitment)?; + txout.script_pubkey = ScriptBuf::new_op_return(&commitment.to_raw_array()); + Proof::OpretFirst + } else { + return Err(DbcPsbtError::MethodUnsupported(method)); + }; + + let anchor = Anchor { + txid: self.unsigned_tx.txid().to_byte_array().into(), + mpc_proof: mpc::MerkleBlock::from(merkle_tree), + dbc_proof: proof, + }; + + Ok(anchor) + } +} diff --git a/src/psbt/lnpbp4.rs b/src/psbt/lnpbp4.rs index f051af9b..afa47027 100644 --- a/src/psbt/lnpbp4.rs +++ b/src/psbt/lnpbp4.rs @@ -37,7 +37,7 @@ pub const PSBT_OUT_LNPBP4_MESSAGE: u8 = 0x00; pub const PSBT_OUT_LNPBP4_ENTROPY: u8 = 0x01; /// Proprietary key subtype for storing LNPBP4 requirement for a minimal tree /// size. -pub const PSBT_OUT_LNPBP4_MIN_TREE_DEPTH: u8 = 0x02; +pub const PSBT_OUT_LNPBP4_MIN_TREE_DEPTH: u8 = 0x04; /// Extension trait for static functions returning LNPBP4-related proprietary /// keys. diff --git a/src/psbt/mod.rs b/src/psbt/mod.rs index 98639fdd..d8306c31 100644 --- a/src/psbt/mod.rs +++ b/src/psbt/mod.rs @@ -27,11 +27,14 @@ // TODO: Move to BP wallet mod lnpbp4; // TODO: Move to BP wallet +mod dbc; +// TODO: Move to BP wallet mod opret; // TODO: Move to BP wallet mod tapret; mod rgb; +pub use dbc::{DbcPsbtError, PsbtDbc}; pub use lnpbp4::{ Lnpbp4PsbtError, ProprietaryKeyLnpbp4, PSBT_LNPBP4_PREFIX, PSBT_OUT_LNPBP4_ENTROPY, PSBT_OUT_LNPBP4_MESSAGE, PSBT_OUT_LNPBP4_MIN_TREE_DEPTH, diff --git a/std/src/containers/mod.rs b/std/src/containers/mod.rs index d144c3a9..458fa088 100644 --- a/std/src/containers/mod.rs +++ b/std/src/containers/mod.rs @@ -43,5 +43,5 @@ pub use bindle::{LoadError, UniversalBindle}; pub use certs::{Cert, ContentId, ContentSigs, Identity}; pub use consignment::{Consignment, Contract, Transfer}; pub use disclosure::Disclosure; -pub use seal::{TerminalSeal, VoutSeal}; +pub use seal::{BuilderSeal, TerminalSeal, VoutSeal}; pub use util::{ContainerVer, Terminal}; diff --git a/std/src/containers/seal.rs b/std/src/containers/seal.rs index 59f35535..5a55c8e4 100644 --- a/std/src/containers/seal.rs +++ b/std/src/containers/seal.rs @@ -29,9 +29,9 @@ use bp::seals::txout::{CloseMethod, TxPtr}; use bp::secp256k1::rand::{thread_rng, RngCore}; use bp::Vout; use commit_verify::Conceal; -use rgb::{GraphSeal, SecretSeal}; +use rgb::{ExposedSeal, GenesisSeal, GraphSeal, SecretSeal}; -use crate::LIB_NAME_RGB_STD; +use crate::{Outpoint, LIB_NAME_RGB_STD}; /// Seal definition which re-uses witness transaction id of some other seal, /// which is not known at the moment of seal construction. Thus, the definition @@ -187,3 +187,29 @@ impl FromStr for TerminalSeal { .or_else(|_| GraphSeal::from_str(s).map(TerminalSeal::from)) } } + +/// Seal used by operation builder which can be either revealed or concealed. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, From)] +pub enum BuilderSeal { + Revealed(Seal), + #[from] + Concealed(SecretSeal), +} + +impl From for BuilderSeal +where Seal: From +{ + fn from(seal: Outpoint) -> Self { BuilderSeal::Revealed(seal.into()) } +} + +impl From for BuilderSeal +where Seal: From +{ + fn from(seal: GraphSeal) -> Self { BuilderSeal::Revealed(seal.into()) } +} + +impl From for BuilderSeal +where Seal: From +{ + fn from(seal: GenesisSeal) -> Self { BuilderSeal::Revealed(seal.into()) } +} diff --git a/std/src/interface/builder.rs b/std/src/interface/builder.rs index 8502711e..82bda93c 100644 --- a/std/src/interface/builder.rs +++ b/std/src/interface/builder.rs @@ -19,21 +19,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::BTreeMap; +use std::collections::HashMap; use amplify::confinement::{Confined, TinyOrdMap, U8}; use amplify::{confinement, Wrapper}; use bp::secp256k1::rand::thread_rng; -use bp::{Chain, Outpoint}; +use bp::Chain; use rgb::{ - fungible, Assign, AssignmentType, Assignments, ExposedSeal, FungibleType, Genesis, GlobalState, - Opout, PrevOuts, StateSchema, SubSchema, Transition, TransitionType, TypedAssigns, + fungible, Assign, AssignmentType, Assignments, ExposedSeal, FungibleType, Genesis, GenesisSeal, + GlobalState, GraphSeal, Opout, PrevOuts, StateSchema, SubSchema, Transition, TransitionType, + TypedAssigns, BLANK_TRANSITION_ID, }; use strict_encoding::{SerializeError, StrictSerialize, TypeName}; use strict_types::decode; -use crate::containers::Contract; -use crate::interface::{Iface, IfaceImpl, IfacePair}; +use crate::containers::{BuilderSeal, Contract}; +use crate::interface::{Iface, IfaceImpl, IfacePair, TypedState}; #[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)] #[display(doc_comments)] @@ -56,6 +57,9 @@ pub enum BuilderError { /// operation type must be provided with `set_operation_type` method. NoOperationSubtype, + /// interface doesn't have a default assignment type. + NoDefaultAssignment, + #[from] #[display(inner)] StrictEncode(SerializeError), @@ -71,7 +75,7 @@ pub enum BuilderError { #[derive(Clone, Debug)] pub struct ContractBuilder { - builder: OperationBuilder, + builder: OperationBuilder, chain: Chain, } @@ -100,10 +104,12 @@ impl ContractBuilder { pub fn add_fungible_state( mut self, name: impl Into, - seal: impl Into, + seal: impl Into, value: u64, ) -> Result { - self.builder = self.builder.add_fungible_state(name, seal, value)?; + self.builder = + self.builder + .add_fungible_state(name, BuilderSeal::Revealed(seal.into()), value)?; Ok(self) } @@ -131,7 +137,7 @@ impl ContractBuilder { #[derive(Clone, Debug)] pub struct TransitionBuilder { - builder: OperationBuilder, + builder: OperationBuilder, transition_type: Option, inputs: PrevOuts, } @@ -145,11 +151,20 @@ impl TransitionBuilder { }) } + pub fn assignments_type(&self, name: &TypeName) -> Option { + self.builder.iimpl.assignments_type(name) + } + pub fn add_input(mut self, opout: Opout) -> Result { self.inputs.push(opout)?; Ok(self) } + pub fn do_blank_transition(mut self) -> Result { + self.transition_type = Some(BLANK_TRANSITION_ID); + Ok(self) + } + pub fn set_transition_type(mut self, name: impl Into) -> Result { let name = name.into(); let transition_type = self @@ -161,6 +176,25 @@ impl TransitionBuilder { Ok(self) } + pub fn default_assignment(&self) -> Result<&TypeName, BuilderError> { + let transition_type = self.transition_type()?; + let transition_name = self + .builder + .iimpl + .transition_name(transition_type) + .expect("reverse type"); + let tiface = self + .builder + .iface + .transitions + .get(transition_name) + .expect("internal inconsistency"); + tiface + .default_assignment + .as_ref() + .ok_or(BuilderError::NoDefaultAssignment) + } + pub fn add_global_state( mut self, name: impl Into, @@ -170,28 +204,57 @@ impl TransitionBuilder { Ok(self) } + pub fn add_fungible_state_default( + self, + seal: impl Into>, + value: u64, + ) -> Result { + let assignment_name = self.default_assignment()?; + let id = self + .builder + .iimpl + .assignments_type(assignment_name) + .ok_or(BuilderError::InvalidStateType(assignment_name.clone()))?; + + self.add_raw_state(id, seal, TypedState::Amount(value)) + } + pub fn add_fungible_state( mut self, name: impl Into, - seal: impl Into, + seal: impl Into>, value: u64, ) -> Result { self.builder = self.builder.add_fungible_state(name, seal, value)?; Ok(self) } - pub fn complete_transition(self) -> Result { - let (_, pair, global, assignments) = self.builder.complete(); + pub fn add_raw_state( + mut self, + type_id: AssignmentType, + seal: impl Into>, + state: TypedState, + ) -> Result { + self.builder = self.builder.add_raw_state(type_id, seal, state)?; + Ok(self) + } - let transition_type = self - .transition_type + fn transition_type(&self) -> Result { + self.transition_type .or_else(|| { - pair.iface + self.builder + .iface .default_operation .as_ref() - .and_then(|name| pair.transition_type(name)) + .and_then(|name| self.builder.iimpl.transition_type(name)) }) - .ok_or(BuilderError::NoOperationSubtype)?; + .ok_or(BuilderError::NoOperationSubtype) + } + + pub fn complete_transition(self) -> Result { + let transition_type = self.transition_type()?; + + let (_, _, global, assignments) = self.builder.complete(); let transition = Transition { ffv: none!(), @@ -210,20 +273,22 @@ impl TransitionBuilder { } #[derive(Clone, Debug)] -pub struct OperationBuilder { +pub struct OperationBuilder { + // TODO: use references instead of owned values schema: SubSchema, iface: Iface, iimpl: IfaceImpl, global: GlobalState, - // rights: TinyOrdMap, 1, U8>>, - fungible: TinyOrdMap, 1, U8>>, - // data: TinyOrdMap, 1, U8>>, + // rights: TinyOrdMap>, 1, U8>>, + fungible: + TinyOrdMap, fungible::Revealed>, 1, U8>>, + // data: TinyOrdMap, SmallBlob>, 1, U8>>, // TODO: add attachments // TODO: add valencies } -impl OperationBuilder { +impl OperationBuilder { pub fn with(iface: Iface, schema: SubSchema, iimpl: IfaceImpl) -> Result { if iimpl.iface_id != iface.iface_id() { return Err(BuilderError::InterfaceMismatch); @@ -255,20 +320,20 @@ impl OperationBuilder { let serialized = value.to_strict_serialized::<{ u16::MAX as usize }>()?; // Check value matches type requirements - let Some(id) = self.iimpl.global_state.iter().find(|t| t.name == name).map(|t| t.id) else { + let Some(type_id) = self.iimpl.global_state.iter().find(|t| t.name == name).map(|t| t.id) else { return Err(BuilderError::TypeNotFound(name)); }; - let ty_id = self + let sem_id = self .schema .global_types - .get(&id) + .get(&type_id) .expect("schema should match interface: must be checked by the constructor") .sem_id; self.schema .type_system - .strict_deserialize_type(ty_id, &serialized)?; + .strict_deserialize_type(sem_id, &serialized)?; - self.global.add_state(id, serialized.into())?; + self.global.add_state(type_id, serialized.into())?; Ok(self) } @@ -276,41 +341,74 @@ impl OperationBuilder { pub fn add_fungible_state( mut self, name: impl Into, - seal: impl Into, + seal: impl Into>, value: u64, ) -> Result { let name = name.into(); - let Some(id) = self.iimpl.owned_state.iter().find(|t| t.name == name).map(|t| t.id) else { + let Some(type_id) = self.iimpl.assignments.iter().find(|t| t.name == name).map(|t| t.id) else { return Err(BuilderError::TypeNotFound(name)); }; - let ty = self + + let state_schema = self .schema .owned_types - .get(&id) + .get(&type_id) .expect("schema should match interface: must be checked by the constructor"); - if *ty != StateSchema::Fungible(FungibleType::Unsigned64Bit) { + if *state_schema != StateSchema::Fungible(FungibleType::Unsigned64Bit) { return Err(BuilderError::InvalidStateType(name)); } let state = fungible::Revealed::new(value, &mut thread_rng()); - match self.fungible.get_mut(&id) { + match self.fungible.get_mut(&type_id) { Some(assignments) => { assignments.insert(seal.into(), state)?; } None => { self.fungible - .insert(id, Confined::with((seal.into(), state)))?; + .insert(type_id, Confined::with((seal.into(), state)))?; + } + } + Ok(self) + } + + pub fn add_raw_state( + mut self, + type_id: AssignmentType, + seal: impl Into>, + state: TypedState, + ) -> Result { + match state { + TypedState::Void => { + todo!() + } + TypedState::Amount(value) => { + let state = fungible::Revealed::new(value, &mut thread_rng()); + match self.fungible.get_mut(&type_id) { + Some(assignments) => { + assignments.insert(seal.into(), state.into())?; + } + None => { + self.fungible + .insert(type_id, Confined::with((seal.into(), state)))?; + } + } + } + TypedState::Data(_) => { + todo!() + } + TypedState::Attachment(_) => { + todo!() } } Ok(self) } - fn complete(self) -> (SubSchema, IfacePair, GlobalState, Assignments) { + fn complete(self) -> (SubSchema, IfacePair, GlobalState, Assignments) { let owned_state = self.fungible.into_iter().map(|(id, vec)| { - let vec = vec.into_iter().map(|(seal, value)| Assign::Revealed { - seal: seal.into(), - state: value, + let vec = vec.into_iter().map(|(seal, value)| match seal { + BuilderSeal::Revealed(seal) => Assign::Revealed { seal, state: value }, + BuilderSeal::Concealed(seal) => Assign::ConfidentialSeal { seal, state: value }, }); let state = Confined::try_from_iter(vec).expect("at least one element"); let state = TypedAssigns::Fungible(state); @@ -319,7 +417,7 @@ impl OperationBuilder { let owned_state = Confined::try_from_iter(owned_state).expect("same size"); let assignments = Assignments::from_inner(owned_state); - let iface_pair = IfacePair::with(self.iface.clone(), self.iimpl); + let iface_pair = IfacePair::with(self.iface, self.iimpl); (self.schema, iface_pair, self.global, assignments) } diff --git a/std/src/interface/contract.rs b/std/src/interface/contract.rs index a3eaeb0b..a5b0d12d 100644 --- a/std/src/interface/contract.rs +++ b/std/src/interface/contract.rs @@ -19,7 +19,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use amplify::confinement::{LargeOrdMap, LargeVec, SmallVec}; +use amplify::confinement::{LargeOrdMap, LargeVec, SmallBlob, SmallVec}; use bp::Outpoint; use rgb::{attachment, AssignmentType, ContractState, FungibleOutput, SealWitness}; use strict_encoding::TypeName; @@ -44,7 +44,7 @@ pub enum ContractError { pub enum TypedState { Void, Amount(u64), - Data(StrictVal), + Data(SmallBlob), Attachment(attachment::Revealed), } diff --git a/std/src/interface/iimpl.rs b/std/src/interface/iimpl.rs index 64285cc1..89993dbe 100644 --- a/std/src/interface/iimpl.rs +++ b/std/src/interface/iimpl.rs @@ -125,7 +125,7 @@ pub struct IfaceImpl { pub schema_id: SchemaId, pub iface_id: IfaceId, pub global_state: TinyOrdSet>, - pub owned_state: TinyOrdSet>, + pub assignments: TinyOrdSet>, pub valencies: TinyOrdSet>, pub transitions: TinyOrdSet>, pub extensions: TinyOrdSet>, @@ -155,7 +155,7 @@ impl IfaceImpl { } pub fn assignments_type(&self, name: &TypeName) -> Option { - self.owned_state + self.assignments .iter() .find(|nt| &nt.name == name) .map(|nt| nt.id) @@ -167,6 +167,27 @@ impl IfaceImpl { .find(|nt| &nt.name == name) .map(|nt| nt.id) } + + pub fn global_name(&self, id: GlobalStateType) -> Option<&TypeName> { + self.global_state + .iter() + .find(|nt| nt.id == id) + .map(|nt| &nt.name) + } + + pub fn assignment_name(&self, id: AssignmentType) -> Option<&TypeName> { + self.assignments + .iter() + .find(|nt| nt.id == id) + .map(|nt| &nt.name) + } + + pub fn transition_name(&self, id: TransitionType) -> Option<&TypeName> { + self.transitions + .iter() + .find(|nt| nt.id == id) + .map(|nt| &nt.name) + } } // TODO: Implement validation of implementation against interface requirements diff --git a/std/src/interface/suppl.rs b/std/src/interface/suppl.rs index eab9d891..1ad746b8 100644 --- a/std/src/interface/suppl.rs +++ b/std/src/interface/suppl.rs @@ -93,18 +93,20 @@ pub struct OwnedStateSuppl { pub velocity_class: VelocityClass, } -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)] +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Display, Default)] +#[derive(StrictType, StrictEncode, StrictDecode)] #[strict_type(lib = LIB_NAME_RGB_STD, tags = repr, try_from_u8, into_u8)] #[cfg_attr( feature = "serde", derive(Serialize, Deserialize), serde(crate = "serde_crate", rename_all = "camelCase") )] +#[display(lowercase)] pub enum VelocityClass { - #[strict_type(dumb)] HighFrequency = 10, + #[display("20")] Frequent = 20, + #[default] Regular = 30, Episodic = 40, Seldom = 50, diff --git a/std/src/persistence/hoard.rs b/std/src/persistence/hoard.rs index e1e8d7a4..ba4820ff 100644 --- a/std/src/persistence/hoard.rs +++ b/std/src/persistence/hoard.rs @@ -23,7 +23,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::convert::Infallible; use amplify::confinement; -use amplify::confinement::{Confined, LargeOrdMap, SmallOrdMap, TinyOrdMap}; +use amplify::confinement::{Confined, LargeOrdMap, SmallOrdMap, TinyOrdMap, TinyOrdSet}; use bp::dbc::anchor::MergeError; use commit_verify::mpc; use commit_verify::mpc::{MerkleBlock, UnrelatedProof}; @@ -35,7 +35,7 @@ use strict_encoding::TypeName; use crate::accessors::{MergeReveal, MergeRevealError}; use crate::containers::{Cert, Consignment, ContentId, ContentSigs}; -use crate::interface::{rgb20, Iface, IfaceId, IfacePair, SchemaIfaces}; +use crate::interface::{rgb20, ContractSuppl, Iface, IfaceId, IfacePair, SchemaIfaces}; use crate::persistence::{Stash, StashError, StashInconsistency}; use crate::LIB_NAME_RGB_STD; @@ -63,6 +63,7 @@ pub struct Hoard { pub(super) schemata: TinyOrdMap, pub(super) ifaces: TinyOrdMap, pub(super) geneses: TinyOrdMap, + pub(super) suppl: TinyOrdMap>, pub(super) bundles: LargeOrdMap, pub(super) extensions: LargeOrdMap, pub(super) anchors: LargeOrdMap>, @@ -79,6 +80,7 @@ impl Hoard { rgb20_id => rgb20, }, geneses: none!(), + suppl: none!(), bundles: none!(), extensions: none!(), anchors: none!(), @@ -108,7 +110,7 @@ impl Hoard { } // TODO: Move into Stash trait and re-implement using trait accessor methods - pub fn consume( + pub fn consume_consignment( &mut self, consignment: Consignment, ) -> Result<(), ConsumeError> { @@ -138,6 +140,16 @@ impl Hoard { }; } + // TODO: filter most trusted signers + match self.suppl.get_mut(&contract_id) { + Some(entry) => { + entry.extend(consignment.supplements).ok(); + } + None => { + self.suppl.insert(contract_id, consignment.supplements).ok(); + } + } + match self.geneses.get_mut(&contract_id) { Some(genesis) => *genesis = genesis.clone().merge_reveal(consignment.genesis)?, None => { @@ -158,19 +170,8 @@ impl Hoard { for AnchoredBundle { anchor, bundle } in consignment.bundles { let bundle_id = bundle.bundle_id(); let anchor = anchor.into_merkle_block(contract_id, bundle_id.into())?; - let anchor_id = anchor.anchor_id(); - match self.anchors.get_mut(&anchor_id) { - Some(a) => *a = a.clone().merge_reveal(anchor)?, - None => { - self.anchors.insert(anchor_id, anchor)?; - } - } - match self.bundles.get_mut(&bundle_id) { - Some(b) => *b = b.clone().merge_reveal(bundle)?, - None => { - self.bundles.insert(bundle_id, bundle)?; - } - } + self.consume_anchor(anchor)?; + self.consume_bundle(bundle)?; } for (content_id, sigs) in consignment.signatures { @@ -180,16 +181,36 @@ impl Hoard { Ok(()) } + + // TODO: Move into Stash trait and re-implement using trait accessor methods + pub fn consume_bundle(&mut self, bundle: TransitionBundle) -> Result<(), ConsumeError> { + let bundle_id = bundle.bundle_id(); + match self.bundles.get_mut(&bundle_id) { + Some(b) => *b = b.clone().merge_reveal(bundle)?, + None => { + self.bundles.insert(bundle_id, bundle)?; + } + } + Ok(()) + } + + // TODO: Move into Stash trait and re-implement using trait accessor methods + pub fn consume_anchor(&mut self, anchor: Anchor) -> Result<(), ConsumeError> { + let anchor_id = anchor.anchor_id(); + match self.anchors.get_mut(&anchor_id) { + Some(a) => *a = a.clone().merge_reveal(anchor)?, + None => { + self.anchors.insert(anchor_id, anchor)?; + } + } + Ok(()) + } } impl Stash for Hoard { // With in-memory data we have no connectivity or I/O errors type Error = Infallible; - fn schema_ids(&self) -> Result, Self::Error> { - Ok(self.schemata.keys().copied().collect()) - } - fn ifaces(&self) -> Result, Self::Error> { Ok(self .ifaces @@ -198,15 +219,12 @@ impl Stash for Hoard { .collect()) } - fn contract_ids(&self) -> Result, Self::Error> { - Ok(self.geneses.keys().copied().collect()) - } - - fn iface_by_name(&self, name: &str) -> Result<&Iface, StashError> { + fn iface_by_name(&self, name: impl Into) -> Result<&Iface, StashError> { + let name = name.into(); self.ifaces .values() - .find(|iface| iface.name.as_str() == name) - .ok_or_else(|| StashInconsistency::IfaceNameAbsent(name.to_owned()).into()) + .find(|iface| iface.name == name) + .ok_or_else(|| StashInconsistency::IfaceNameAbsent(name).into()) } fn iface_by_id(&self, id: IfaceId) -> Result<&Iface, StashError> { self.ifaces @@ -214,12 +232,24 @@ impl Stash for Hoard { .ok_or_else(|| StashInconsistency::IfaceAbsent(id).into()) } + fn schema_ids(&self) -> Result, Self::Error> { + Ok(self.schemata.keys().copied().collect()) + } + fn schema(&self, schema_id: SchemaId) -> Result<&SchemaIfaces, StashError> { self.schemata .get(&schema_id) .ok_or_else(|| StashInconsistency::SchemaAbsent(schema_id).into()) } + fn contract_ids(&self) -> Result, Self::Error> { + Ok(self.geneses.keys().copied().collect()) + } + + fn contract_suppl(&self, contract_id: ContractId) -> Option<&TinyOrdSet> { + self.suppl.get(&contract_id) + } + fn genesis(&self, contract_id: ContractId) -> Result<&Genesis, StashError> { self.geneses .get(&contract_id) diff --git a/std/src/persistence/inventory.rs b/std/src/persistence/inventory.rs index af01cf4e..aa1f496f 100644 --- a/std/src/persistence/inventory.rs +++ b/std/src/persistence/inventory.rs @@ -27,13 +27,18 @@ use amplify::confinement::{self, Confined}; use bp::Txid; use commit_verify::mpc; use rgb::{ - validation, AnchoredBundle, BundleId, ContractId, OpId, Operation, Opout, SchemaId, SubSchema, - Transition, + validation, Anchor, AnchoredBundle, BundleId, ContractId, ExposedSeal, GraphSeal, OpId, + Operation, Opout, SchemaId, SecretSeal, SubSchema, Transition, TransitionBundle, }; +use strict_encoding::TypeName; use crate::accessors::{BundleExt, MergeRevealError, RevealError}; -use crate::containers::{Bindle, Cert, Consignment, ContentId, Contract, Terminal, Transfer}; -use crate::interface::{ContractIface, Iface, IfaceId, IfaceImpl, IfacePair}; +use crate::containers::{ + Bindle, BuilderSeal, Cert, Consignment, ContentId, Contract, Terminal, Transfer, +}; +use crate::interface::{ + ContractIface, Iface, IfaceId, IfaceImpl, IfacePair, TransitionBuilder, TypedState, +}; use crate::persistence::hoard::ConsumeError; use crate::persistence::stash::StashInconsistency; use crate::persistence::{Stash, StashError}; @@ -97,7 +102,9 @@ where E2: From fn from(err: StashError) -> Self { match err { StashError::Connectivity(err) => Self::Connectivity(err.into()), - StashError::InternalInconsistency(e) => Self::InternalInconsistency(e.into()), + StashError::InternalInconsistency(e) => { + Self::InternalInconsistency(InventoryInconsistency::Stash(e)) + } } } } @@ -155,7 +162,7 @@ pub enum DataError { #[display(inner)] Merge(MergeRevealError), - /// outpoint {0} is not part of the contract {1} + /// outpoint {0} is not part of the contract {1}. OutpointUnknown(Outpoint, ContractId), #[from] @@ -164,6 +171,9 @@ pub enum DataError { #[from] IfaceImpl(IfaceImplError), + /// schema {0} doesn't implement interface {1}. + NoIfaceImpl(SchemaId, IfaceId), + #[from] HeightResolver(Box), } @@ -278,6 +288,18 @@ pub trait Inventory: Deref { where R::Error: 'static; + fn consume_anchor( + &mut self, + anchor: Anchor, + ) -> Result<(), InventoryError>; + + fn consume_bundle( + &mut self, + contract_id: ContractId, + bundle: TransitionBundle, + witness_txid: Txid, + ) -> Result<(), InventoryError>; + /// # Safety /// /// Calling this method may lead to including into the stash asset @@ -298,6 +320,26 @@ pub trait Inventory: Deref { fn anchored_bundle(&self, opid: OpId) -> Result>; + fn transition_builder( + &mut self, + contract_id: ContractId, + iface: impl Into, + ) -> Result> + where + Self::Error: From<::Error>, + { + let schema_ifaces = self.contract_schema(contract_id)?; + let iface = self.iface_by_name(iface)?; + let schema = &schema_ifaces.schema; + let iimpl = schema_ifaces + .iimpls + .get(&iface.iface_id()) + .ok_or(DataError::NoIfaceImpl(schema.schema_id(), iface.iface_id()))?; + let builder = TransitionBuilder::with(iface.clone(), schema.clone(), iimpl.clone()) + .expect("internal inconsistency"); + Ok(builder) + } + fn transition(&self, opid: OpId) -> Result> { Ok(self .anchored_bundle(opid)? @@ -308,17 +350,35 @@ pub trait Inventory: Deref { .expect("Stash::anchored_bundle should guarantee returning revealed transition")) } + fn contracts_by_outpoints( + &mut self, + outpoints: impl IntoIterator>, + ) -> Result, InventoryError>; + fn public_opouts( &mut self, contract_id: ContractId, ) -> Result, InventoryError>; - fn outpoint_opouts( + fn opouts_by_outpoints( &mut self, contract_id: ContractId, outpoints: impl IntoIterator>, ) -> Result, InventoryError>; + fn opouts_by_terminals( + &mut self, + terminals: impl IntoIterator, + ) -> Result, InventoryError>; + + fn state_for_outpoints( + &mut self, + contract_id: ContractId, + outpoints: impl IntoIterator>, + ) -> Result, InventoryError>; + + fn store_seal_secret(&mut self, secret: u64) -> Result<(), InventoryError>; + fn export_contract( &mut self, contract_id: ContractId, @@ -326,40 +386,53 @@ pub trait Inventory: Deref { Bindle, ConsignerError::Target as Stash>::Error>, > { - let mut consignment = self.consign(contract_id, [] as [Outpoint; 0])?; + let mut consignment = + self.consign::(contract_id, [] as [GraphSeal; 0])?; consignment.transfer = false; Ok(consignment.into()) // TODO: Add known sigs to the bindle } - fn store_seal_secret(&mut self, secret: u64) -> Result<(), InventoryError>; - fn transfer( &mut self, contract_id: ContractId, - outpoints: impl IntoIterator>, + seals: impl IntoIterator>>, ) -> Result< Bindle, ConsignerError::Target as Stash>::Error>, > { - let mut consignment = self.consign(contract_id, outpoints)?; + let mut consignment = self.consign(contract_id, seals)?; consignment.transfer = true; Ok(consignment.into()) // TODO: Add known sigs to the bindle } - fn consign( + fn consign( &mut self, contract_id: ContractId, - outpoints: impl IntoIterator>, + seals: impl IntoIterator>>, ) -> Result< Consignment, ConsignerError::Target as Stash>::Error>, > { // 1. Collect initial set of anchored bundles - let outpoints = outpoints.into_iter().map(|o| o.into()); let mut opouts = self.public_opouts(contract_id)?; - opouts.extend(self.outpoint_opouts(contract_id, outpoints)?); + { + let (outpoints, terminals) = seals + .into_iter() + .map(|seal| match seal.into() { + BuilderSeal::Revealed(seal) => (seal.outpoint(), None), + BuilderSeal::Concealed(seal) => (None, Some(seal)), + }) + .unzip::<_, _, Vec<_>, Vec<_>>(); + opouts.extend( + self.opouts_by_outpoints( + contract_id, + outpoints.into_iter().filter_map(|seal| seal), + )?, + ); + opouts.extend(self.opouts_by_terminals(terminals.into_iter().filter_map(|seal| seal))?); + } // 1.1. Get all public transitions // 1.2. Collect all state transitions assigning state to the provided @@ -420,6 +493,7 @@ pub trait Inventory: Deref { consignment.terminals = Confined::try_from(terminals).map_err(|_| ConsignerError::TooManyTerminals)?; + // TODO: Conceal everything we do not need // TODO: Add known sigs to the consignment Ok(consignment) diff --git a/std/src/persistence/mod.rs b/std/src/persistence/mod.rs index 93126c80..986a64a2 100644 --- a/std/src/persistence/mod.rs +++ b/std/src/persistence/mod.rs @@ -39,6 +39,8 @@ pub mod stock; pub mod hoard; pub use hoard::Hoard; -pub use inventory::{Inventory, InventoryDataError, InventoryError, InventoryInconsistency}; +pub use inventory::{ + ConsignerError, Inventory, InventoryDataError, InventoryError, InventoryInconsistency, +}; pub use stash::{Stash, StashError, StashInconsistency}; pub use stock::Stock; diff --git a/std/src/persistence/stash.rs b/std/src/persistence/stash.rs index 12e1d1ca..2faad5d3 100644 --- a/std/src/persistence/stash.rs +++ b/std/src/persistence/stash.rs @@ -24,13 +24,14 @@ use std::collections::{BTreeMap, BTreeSet}; use std::error::Error; +use amplify::confinement::TinyOrdSet; use commit_verify::mpc; use rgb::{ Anchor, AnchorId, BundleId, ContractId, Extension, Genesis, OpId, SchemaId, TransitionBundle, }; use strict_encoding::TypeName; -use crate::interface::{Iface, IfaceId, SchemaIfaces}; +use crate::interface::{ContractSuppl, Iface, IfaceId, SchemaIfaces}; #[derive(Debug, Display, Error, From)] #[display(inner)] @@ -48,7 +49,7 @@ pub enum StashError { #[display(doc_comments)] pub enum StashInconsistency { /// interfae {0} is unknown; you need to import it first. - IfaceNameAbsent(String), + IfaceNameAbsent(TypeName), /// interfae {0} is unknown; you need to import it first. IfaceAbsent(IfaceId), @@ -89,15 +90,27 @@ pub trait Stash { type Error: Error; fn schema_ids(&self) -> Result, Self::Error>; + fn ifaces(&self) -> Result, Self::Error>; - fn contract_ids(&self) -> Result, Self::Error>; - fn iface_by_name(&self, name: &str) -> Result<&Iface, StashError>; + fn iface_by_name(&self, name: impl Into) -> Result<&Iface, StashError>; fn iface_by_id(&self, id: IfaceId) -> Result<&Iface, StashError>; fn schema(&self, schema_id: SchemaId) -> Result<&SchemaIfaces, StashError>; + fn contract_ids(&self) -> Result, Self::Error>; + + fn contract_schema( + &self, + contract_id: ContractId, + ) -> Result<&SchemaIfaces, StashError> { + let genesis = self.genesis(contract_id)?; + self.schema(genesis.schema_id) + } + + fn contract_suppl(&self, contract_id: ContractId) -> Option<&TinyOrdSet>; + fn genesis(&self, contract_id: ContractId) -> Result<&Genesis, StashError>; fn bundle(&self, bundle_id: BundleId) -> Result<&TransitionBundle, StashError>; diff --git a/std/src/persistence/stock.rs b/std/src/persistence/stock.rs index 255e21e0..eeb1c954 100644 --- a/std/src/persistence/stock.rs +++ b/std/src/persistence/stock.rs @@ -19,21 +19,28 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use std::convert::Infallible; use std::ops::{Deref, DerefMut}; -use amplify::confinement::{MediumOrdMap, MediumOrdSet, TinyOrdMap}; +use amplify::confinement::{Confined, MediumOrdMap, MediumOrdSet, TinyOrdMap}; +use amplify::{RawArray, Wrapper}; +use bp::dbc::Anchor; +use bp::Txid; +use commit_verify::mpc::MerkleBlock; use rgb::validation::{Status, Validity, Warning}; use rgb::{ - validation, AnchorId, AnchoredBundle, BundleId, ContractHistory, ContractId, ContractState, - OpId, Opout, SubSchema, + validation, AnchorId, AnchoredBundle, Assign, AssignmentType, BundleId, ContractHistory, + ContractId, ContractState, ExposedState, GraphSeal, OpId, Opout, SecretSeal, SubSchema, + TransitionBundle, TxoSeal, TypedAssigns, }; use strict_encoding::{StrictDeserialize, StrictSerialize}; use crate::accessors::BundleExt; use crate::containers::{Bindle, Cert, Consignment, ContentId, Contract, Transfer}; -use crate::interface::{ContractIface, Iface, IfaceId, IfaceImpl, IfacePair, SchemaIfaces}; +use crate::interface::{ + ContractIface, Iface, IfaceId, IfaceImpl, IfacePair, SchemaIfaces, TypedState, +}; use crate::persistence::inventory::{DataError, IfaceImplError, InventoryInconsistency}; use crate::persistence::{ Hoard, Inventory, InventoryDataError, InventoryError, Stash, StashInconsistency, @@ -70,6 +77,7 @@ pub struct Stock { bundle_op_index: MediumOrdMap, anchor_bundle_index: MediumOrdMap, contract_index: TinyOrdMap, + terminal_index: MediumOrdMap, // secrets seal_secrets: MediumOrdSet, } @@ -82,6 +90,7 @@ impl Default for Stock { bundle_op_index: empty!(), anchor_bundle_index: empty!(), contract_index: empty!(), + terminal_index: empty!(), seal_secrets: empty!(), } } @@ -153,18 +162,80 @@ impl Stock { let bundle_id = bundle.bundle_id(); let anchor_id = anchor.anchor_id(contract_id, bundle_id.into())?; self.anchor_bundle_index.insert(bundle_id, anchor_id)?; - for (opid, item) in bundle.iter() { - if let Some(transition) = &item.transition { - self.bundle_op_index - .insert(*opid, IndexedBundle(contract_id, bundle_id))?; - // TODO: index opouts for self.contract_index + self.index_bundle(contract_id, bundle, anchor.txid)?; + } + + self.hoard.consume_consignment(consignment)?; + + Ok(status) + } + + fn index_bundle( + &mut self, + id: ContractId, + bundle: &TransitionBundle, + witness_txid: Txid, + ) -> Result<(), InventoryError<::Error>> { + let bundle_id = bundle.bundle_id(); + for (opid, item) in bundle.iter() { + if let Some(transition) = &item.transition { + self.bundle_op_index + .insert(*opid, IndexedBundle(id, bundle_id))?; + for (type_id, assign) in transition.assignments.iter() { + match assign { + TypedAssigns::Declarative(vec) => { + self.index_assignments(id, vec, *opid, *type_id, witness_txid)?; + } + TypedAssigns::Fungible(vec) => { + self.index_assignments(id, vec, *opid, *type_id, witness_txid)?; + } + TypedAssigns::Structured(vec) => { + self.index_assignments(id, vec, *opid, *type_id, witness_txid)?; + } + TypedAssigns::Attachment(vec) => { + self.index_assignments(id, vec, *opid, *type_id, witness_txid)?; + } + } } } } - self.hoard.consume(consignment)?; + Ok(()) + } - Ok(status) + fn index_assignments( + &mut self, + contract_id: ContractId, + vec: &[Assign], + opid: OpId, + type_id: AssignmentType, + witness_txid: Txid, + ) -> Result<(), InventoryError<::Error>> { + let index = self + .contract_index + .get_mut(&contract_id) + .ok_or(StashInconsistency::ContractAbsent(contract_id))?; + + for (no, a) in vec.iter().enumerate() { + let opout = Opout::new(opid, type_id, no as u16); + if let Assign::ConfidentialState { seal, .. } | Assign::Revealed { seal, .. } = a { + let outpoint = seal.outpoint_or(witness_txid); + match index.outpoint_opouts.get_mut(&outpoint) { + Some(opouts) => { + opouts.push(opout)?; + } + None => { + index + .outpoint_opouts + .insert(outpoint, confined_bset!(opout))?; + } + } + } + if let Assign::Confidential { seal, .. } | Assign::ConfidentialSeal { seal, .. } = a { + self.terminal_index.insert(*seal, opout)?; + } + } + Ok(()) } } @@ -289,6 +360,30 @@ impl Inventory for Stock { self.consume_consignment(transfer, resolver, false) } + fn consume_anchor( + &mut self, + anchor: Anchor, + ) -> Result<(), InventoryError> { + let anchor_id = anchor.anchor_id(); + for (_, bundle_id) in anchor.mpc_proof.to_known_message_map() { + self.anchor_bundle_index + .insert(bundle_id.to_raw_array().into(), anchor_id)?; + } + self.hoard.consume_anchor(anchor)?; + Ok(()) + } + + fn consume_bundle( + &mut self, + contract_id: ContractId, + bundle: TransitionBundle, + witness_txid: Txid, + ) -> Result<(), InventoryError<::Error>> { + self.index_bundle(contract_id, &bundle, witness_txid)?; + self.hoard.consume_bundle(bundle)?; + Ok(()) + } + unsafe fn import_contract_force( &mut self, contract: Contract, @@ -350,6 +445,25 @@ impl Inventory for Stock { Ok(AnchoredBundle { anchor, bundle }) } + fn contracts_by_outpoints( + &mut self, + outpoints: impl IntoIterator>, + ) -> Result, InventoryError> { + let outpoints = outpoints + .into_iter() + .map(|o| o.into()) + .collect::>(); + let mut selected = BTreeSet::new(); + for (contract_id, index) in &self.contract_index { + for outpoint in &outpoints { + if index.outpoint_opouts.contains_key(outpoint) { + selected.insert(*contract_id); + } + } + } + Ok(selected) + } + fn public_opouts( &mut self, contract_id: ContractId, @@ -361,7 +475,7 @@ impl Inventory for Stock { Ok(index.public_opouts.to_inner()) } - fn outpoint_opouts( + fn opouts_by_outpoints( &mut self, contract_id: ContractId, outpoints: impl IntoIterator>, @@ -381,6 +495,63 @@ impl Inventory for Stock { Ok(opouts) } + fn opouts_by_terminals( + &mut self, + terminals: impl IntoIterator, + ) -> Result, InventoryError> { + let terminals = terminals.into_iter().collect::>(); + Ok(self + .terminal_index + .iter() + .filter(|(seal, _)| terminals.contains(*seal)) + .map(|(_, opout)| *opout) + .collect()) + } + + fn state_for_outpoints( + &mut self, + contract_id: ContractId, + outpoints: impl IntoIterator>, + ) -> Result, InventoryError> { + let outpoints = outpoints + .into_iter() + .map(|o| o.into()) + .collect::>(); + + let history = self + .history + .get(&contract_id) + .ok_or(StashInconsistency::ContractAbsent(contract_id))?; + + let mut res = BTreeMap::new(); + + for output in history.fungibles() { + if outpoints.contains(&output.seal) { + res.insert(output.opout, TypedState::Amount(output.state.value.as_u64())); + } + } + + for output in history.data() { + if outpoints.contains(&output.seal) { + res.insert(output.opout, TypedState::Data(output.state.to_inner())); + } + } + + for output in history.rights() { + if outpoints.contains(&output.seal) { + res.insert(output.opout, TypedState::Void); + } + } + + for output in history.attach() { + if outpoints.contains(&output.seal) { + res.insert(output.opout, TypedState::Attachment(output.state.clone())); + } + } + + Ok(res) + } + fn store_seal_secret(&mut self, secret: u64) -> Result<(), InventoryError> { self.seal_secrets.push(secret)?; Ok(())