From 171d9e822694e9d38e8adfc721a52e8211be5f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zoe=20Faltib=C3=A0?= Date: Mon, 9 Jun 2025 16:14:01 +0200 Subject: [PATCH 1/4] rework TransitionBundle --- Cargo.lock | 9 +++------ Cargo.toml | 4 ++++ psbt/src/rgb.rs | 19 +++++++++++-------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3ff16a5..afb3226 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1457,8 +1457,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rgb-core" version = "0.11.1-alpha.3+unreviewed" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ee6739c9704151ca82f9346f73b1d80189637c8291565866fee6859511e2008" +source = "git+https://github.com/zoedberg/rgb-core?branch=0.11.1-4#128d0eac5b42094ac29748a24778245d736fac98" dependencies = [ "aluvm", "amplify", @@ -1479,8 +1478,7 @@ dependencies = [ [[package]] name = "rgb-invoice" version = "0.11.1-alpha.3+unreviewed" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a53ec3ab57e6727c702b53afb4310b4832e6e93100e6afb52a20b3d9dd0cf7f2" +source = "git+https://github.com/zoedberg/rgb-std?branch=0.11.1-4#ac1d2bfc793cc70c50309b82cb9438181ecd2ffd" dependencies = [ "amplify", "baid64", @@ -1542,8 +1540,7 @@ dependencies = [ [[package]] name = "rgb-std" version = "0.11.1-alpha.3+unreviewed" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df04db909a75c8e4090091cfa7ca2b67f5ccec7ee26e847d377cbc1a1bfc0a49" +source = "git+https://github.com/zoedberg/rgb-std?branch=0.11.1-4#ac1d2bfc793cc70c50309b82cb9438181ecd2ffd" dependencies = [ "aluvm", "amplify", diff --git a/Cargo.toml b/Cargo.toml index a7eb7ff..968cd1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,3 +94,7 @@ serde = ["serde_crate", "bp-std/serde", "rgb-psbt/serde"] [package.metadata.docs.rs] features = ["all"] + +[patch.crates-io] +rgb-core = { git = "https://github.com/zoedberg/rgb-core", branch = "0.11.1-4" } +rgb-std = { git = "https://github.com/zoedberg/rgb-std", branch = "0.11.1-4" } diff --git a/psbt/src/rgb.rs b/psbt/src/rgb.rs index b311b8e..5028b4a 100644 --- a/psbt/src/rgb.rs +++ b/psbt/src/rgb.rs @@ -29,7 +29,7 @@ use bpstd::psbt; use bpstd::psbt::{KeyMap, MpcPsbtError, PropKey, Psbt}; use commit_verify::mpc; use rgbstd::{ - ContractId, InputOpids, MergeReveal, MergeRevealError, OpId, Operation, Transition, + ContractId, InputOpid, MergeReveal, MergeRevealError, OpId, Operation, Opout, Transition, TransitionBundle, Vin, }; use strict_encoding::{DeserializeError, StrictDeserialize, StrictSerialize}; @@ -184,6 +184,9 @@ pub enum RgbPsbtError { /// the size of transition {0} exceeds 16 MB. TransitionTooBig(OpId), + /// a transition {0} specified as contract consumer has not been pushed. + MissingTransition(OpId), + /// state transition data in PSBT are invalid. Details: {0} #[from] InvalidTransition(DeserializeError), @@ -219,7 +222,7 @@ pub trait RgbExt { fn rgb_bundles(&self) -> Result, RgbPsbtError> { let mut map = BTreeMap::new(); for contract_id in self.rgb_contract_ids()? { - let mut input_map: SmallOrdMap = SmallOrdMap::new(); + let mut input_map: SmallOrdMap = SmallOrdMap::new(); let mut known_transitions: SmallOrdMap = SmallOrdMap::new(); let contract_consumers = self.rgb_contract_consumers(contract_id)?; if contract_consumers.is_empty() { @@ -227,17 +230,17 @@ pub trait RgbExt { } for (opids, vin) in contract_consumers { for opid in &opids { + let input_opid = InputOpid { opid: *opid, vin }; let transition = self.rgb_transition(*opid)?; if let Some(transition) = transition { + for opout in transition.inputs() { + input_map.insert(opout, input_opid.clone())?; + } known_transitions.insert(*opid, transition)?; + } else { + return Err(RgbPsbtError::MissingTransition(*opid)); } } - let opids_len = opids.len(); - let opids = - InputOpids::from(Confined::try_from(opids).map_err(|_| { - RgbPsbtError::InvalidTransitionsNumber(contract_id, opids_len) - })?); - input_map.insert(vin, opids)?; } let input_map_len = input_map.len(); let known_transitions_len = known_transitions.len(); From 2109c8b3ff2bacbb89a1fb190f2d1860bd1aad5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zoe=20Faltib=C3=A0?= Date: Mon, 16 Jun 2025 15:33:45 +0200 Subject: [PATCH 2/4] remove InputOpid --- Cargo.lock | 2 +- psbt/src/rgb.rs | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index afb3226..e8f4fc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1457,7 +1457,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rgb-core" version = "0.11.1-alpha.3+unreviewed" -source = "git+https://github.com/zoedberg/rgb-core?branch=0.11.1-4#128d0eac5b42094ac29748a24778245d736fac98" +source = "git+https://github.com/zoedberg/rgb-core?branch=0.11.1-4#3c54f70c2f19038fc431c65ecf7a2436c24323e4" dependencies = [ "aluvm", "amplify", diff --git a/psbt/src/rgb.rs b/psbt/src/rgb.rs index 5028b4a..ed1a722 100644 --- a/psbt/src/rgb.rs +++ b/psbt/src/rgb.rs @@ -29,7 +29,7 @@ use bpstd::psbt; use bpstd::psbt::{KeyMap, MpcPsbtError, PropKey, Psbt}; use commit_verify::mpc; use rgbstd::{ - ContractId, InputOpid, MergeReveal, MergeRevealError, OpId, Operation, Opout, Transition, + ContractId, MergeReveal, MergeRevealError, OpId, Operation, Opout, Transition, TransitionBundle, Vin, }; use strict_encoding::{DeserializeError, StrictDeserialize, StrictSerialize}; @@ -222,19 +222,18 @@ pub trait RgbExt { fn rgb_bundles(&self) -> Result, RgbPsbtError> { let mut map = BTreeMap::new(); for contract_id in self.rgb_contract_ids()? { - let mut input_map: SmallOrdMap = SmallOrdMap::new(); + let mut input_map: SmallOrdMap = SmallOrdMap::new(); let mut known_transitions: SmallOrdMap = SmallOrdMap::new(); let contract_consumers = self.rgb_contract_consumers(contract_id)?; if contract_consumers.is_empty() { return Err(RgbPsbtError::NoContractConsumers); } - for (opids, vin) in contract_consumers { + for (opids, _vin) in contract_consumers { for opid in &opids { - let input_opid = InputOpid { opid: *opid, vin }; let transition = self.rgb_transition(*opid)?; if let Some(transition) = transition { for opout in transition.inputs() { - input_map.insert(opout, input_opid.clone())?; + input_map.insert(opout, *opid)?; } known_transitions.insert(*opid, transition)?; } else { From c07356de16bd8e253050fea2e25567bede2e26af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zoe=20Faltib=C3=A0?= Date: Fri, 20 Jun 2025 12:09:05 +0200 Subject: [PATCH 3/4] rework contract consumers to allow concealed transitions --- Cargo.lock | 6 +- psbt/src/lib.rs | 23 +---- psbt/src/rgb.rs | 258 ++++++++++++++++++++++-------------------------- src/errors.rs | 10 +- src/pay.rs | 15 +-- 5 files changed, 131 insertions(+), 181 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8f4fc4..0482081 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1457,7 +1457,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rgb-core" version = "0.11.1-alpha.3+unreviewed" -source = "git+https://github.com/zoedberg/rgb-core?branch=0.11.1-4#3c54f70c2f19038fc431c65ecf7a2436c24323e4" +source = "git+https://github.com/zoedberg/rgb-core?branch=0.11.1-4#3420162db3c049f216cbcae5494445a3fbd9e5ed" dependencies = [ "aluvm", "amplify", @@ -1478,7 +1478,7 @@ dependencies = [ [[package]] name = "rgb-invoice" version = "0.11.1-alpha.3+unreviewed" -source = "git+https://github.com/zoedberg/rgb-std?branch=0.11.1-4#ac1d2bfc793cc70c50309b82cb9438181ecd2ffd" +source = "git+https://github.com/zoedberg/rgb-std?branch=0.11.1-4#6eccb880a92494e5056a7c15975f3e4f446505c4" dependencies = [ "amplify", "baid64", @@ -1540,7 +1540,7 @@ dependencies = [ [[package]] name = "rgb-std" version = "0.11.1-alpha.3+unreviewed" -source = "git+https://github.com/zoedberg/rgb-std?branch=0.11.1-4#ac1d2bfc793cc70c50309b82cb9438181ecd2ffd" +source = "git+https://github.com/zoedberg/rgb-std?branch=0.11.1-4#6eccb880a92494e5056a7c15975f3e4f446505c4" dependencies = [ "aluvm", "amplify", diff --git a/psbt/src/lib.rs b/psbt/src/lib.rs index e460af2..8b1b462 100644 --- a/psbt/src/lib.rs +++ b/psbt/src/lib.rs @@ -32,17 +32,13 @@ pub use rgb::*; use rgbstd::containers::{Batch, Fascia, PubWitness, SealWitness}; pub use self::rgb::{ - Opids, ProprietaryKeyRgb, RgbExt, RgbInExt, RgbPsbtError, PSBT_GLOBAL_RGB_TRANSITION, - PSBT_IN_RGB_CONSUMED_BY, PSBT_RGB_PREFIX, + ProprietaryKeyRgb, RgbExt, RgbPsbtError, PSBT_GLOBAL_RGB_CONSUMED_BY, + PSBT_GLOBAL_RGB_TRANSITION, PSBT_RGB_PREFIX, }; #[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)] #[display(doc_comments)] pub enum EmbedError { - /// provided transaction batch references inputs which are absent from the - /// PSBT. Possible it was created for a different PSBT. - AbsentInputs, - #[from] Rgb(RgbPsbtError), } @@ -71,19 +67,8 @@ pub trait RgbPsbt { impl RgbPsbt for Psbt { fn rgb_embed(&mut self, batch: Batch) -> Result<(), EmbedError> { - for info in batch { - let contract_id = info.transition.contract_id; - let mut inputs = info.inputs.release(); - for input in self.inputs_mut() { - if inputs.remove(&input.prevout().outpoint()) { - input.set_rgb_consumer(contract_id, info.id)?; - } - } - if !inputs.is_empty() { - return Err(EmbedError::AbsentInputs); - } - self.push_rgb_transition(info.transition) - .expect("transitions are unique since they are in BTreeMap indexed by opid"); + for transition in batch { + self.push_rgb_transition(transition)?; } Ok(()) } diff --git a/psbt/src/rgb.rs b/psbt/src/rgb.rs index ed1a722..61e1c84 100644 --- a/psbt/src/rgb.rs +++ b/psbt/src/rgb.rs @@ -19,18 +19,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use amplify::confinement::{Confined, SmallOrdMap, U24}; use amplify::{confinement, FromSliceError, Wrapper}; use bp::dbc::Method; use bp::seals::txout::CloseMethod; -use bpstd::psbt; use bpstd::psbt::{KeyMap, MpcPsbtError, PropKey, Psbt}; use commit_verify::mpc; use rgbstd::{ - ContractId, MergeReveal, MergeRevealError, OpId, Operation, Opout, Transition, - TransitionBundle, Vin, + AssignmentType, ContractId, MergeReveal, MergeRevealError, OpId, Operation, Opout, Transition, + TransitionBundle, }; use strict_encoding::{DeserializeError, StrictDeserialize, StrictSerialize}; @@ -51,23 +50,24 @@ pub const PSBT_GLOBAL_RGB_TRANSITION: u64 = 0x01; pub const PSBT_GLOBAL_RGB_CLOSE_METHOD: u64 = 0x02; /// Proprietary key subtype to signal that tapret host has been put on change. pub const PSBT_GLOBAL_RGB_TAP_HOST_CHANGE: u64 = 0x03; -/// Proprietary key subtype for storing RGB state transition operation id which -/// consumes this input. -pub const PSBT_IN_RGB_CONSUMED_BY: u64 = 0x01; +/// Proprietary key subtype for storing RGB input allocation and ID of the +/// transition spending it. +pub const PSBT_GLOBAL_RGB_CONSUMED_BY: u64 = 0x04; -#[derive(Wrapper, WrapperMut, Clone, PartialEq, Eq, Hash, Debug, From)] +#[derive(Wrapper, WrapperMut, Clone, PartialEq, Eq, Debug, From)] #[wrapper(Deref)] #[wrapper_mut(DerefMut)] -pub struct Opids(Vec); +pub struct OpoutAndOpids(HashMap); -impl Opids { - pub fn new(opids: Vec) -> Self { Self(opids) } +impl OpoutAndOpids { + pub fn new(items: HashMap) -> Self { Self(items) } pub fn serialize(&self) -> Vec { - let opid_size = std::mem::size_of::(); - let op_ids = &self.0; - let mut bytes = Vec::with_capacity(op_ids.len() * opid_size); - for opid in op_ids { + let mut bytes = Vec::new(); + for (opout, opid) in &self.0 { + bytes.extend(opout.op.to_byte_array()); + bytes.extend(opout.ty.to_le_bytes()); + bytes.extend(opout.no.to_le_bytes()); bytes.extend(opid.to_byte_array()); } bytes @@ -76,21 +76,41 @@ impl Opids { #[allow(clippy::result_large_err)] pub fn deserialize(bytes: &[u8]) -> Result { let opid_size = std::mem::size_of::(); + let assignment_type_size = std::mem::size_of::(); + let u16_size = std::mem::size_of::(); + let item_size = opid_size + assignment_type_size + u16_size + opid_size; let bytes_len = bytes.len(); - if bytes_len % opid_size != 0 { - return Err(RgbPsbtError::InvalidOpidsData(format!( - "Input data length {bytes_len} is not a multiple of {opid_size}" + if bytes_len % item_size != 0 { + return Err(RgbPsbtError::InvalidOpoutAndOpidsData(format!( + "Input data length {bytes_len} is not a multiple of {item_size}" ))); } - let len = bytes.len() / opid_size; - let mut op_ids = Vec::with_capacity(len); - for chunk in bytes.chunks_exact(opid_size) { - let opid = OpId::copy_from_slice(chunk).map_err(|e| { - RgbPsbtError::InvalidOpidsData(format!("Error deserializing an OpId: {:?}", e)) + let len = bytes.len() / item_size; + let mut items = HashMap::with_capacity(len); + for chunk in bytes.chunks_exact(item_size) { + let mut cursor = 0; + let op = OpId::copy_from_slice(&chunk[cursor..cursor + opid_size]).map_err(|e| { + RgbPsbtError::InvalidOpoutAndOpidsData(format!( + "Error deserializing Opout.op: {e:?}", + )) + })?; + cursor += opid_size; + let ty_bytes = &chunk[cursor..cursor + assignment_type_size]; + let ty_u16 = u16::from_le_bytes([ty_bytes[0], ty_bytes[1]]); + let ty = AssignmentType::with(ty_u16); + cursor += assignment_type_size; + let no_bytes = &chunk[cursor..cursor + u16_size]; + let no = u16::from_le_bytes([no_bytes[0], no_bytes[1]]); + cursor += u16_size; + let opid = OpId::copy_from_slice(&chunk[cursor..cursor + opid_size]).map_err(|e| { + RgbPsbtError::InvalidOpoutAndOpidsData(format!( + "Error deserializing consuming OpId: {e:?}" + )) })?; - op_ids.push(opid); + let opout = Opout::new(op, ty, no); + items.insert(opout, opid); } - Ok(Opids::new(op_ids)) + Ok(OpoutAndOpids::new(items)) } } @@ -114,11 +134,11 @@ pub trait ProprietaryKeyRgb { } } - /// Constructs [`PSBT_IN_RGB_CONSUMED_BY`] proprietary key. - fn rgb_in_consumed_by(contract_id: ContractId) -> PropKey { + /// Constructs [`PSBT_GLOBAL_RGB_CONSUMED_BY`] proprietary key. + fn rgb_consumed_by(contract_id: ContractId) -> PropKey { PropKey { identifier: PSBT_RGB_PREFIX.to_owned(), - subtype: PSBT_IN_RGB_CONSUMED_BY, + subtype: PSBT_GLOBAL_RGB_CONSUMED_BY, data: contract_id.to_vec().into(), } } @@ -142,6 +162,9 @@ pub enum RgbPsbtError { /// the key is already present in PSBT, but has a different value AlreadySet, + /// the opout is already signalled as spent by a different opid + DoubleSpend, + /// state transition {0} already present in PSBT is not related to the state /// transition {1} which has to be added to RGB UnrelatedTransitions(OpId, OpId, MergeRevealError), @@ -162,8 +185,8 @@ pub enum RgbPsbtError { #[from(FromSliceError)] InvalidContractId, - /// invalid opids data: {0}. - InvalidOpidsData(String), + /// invalid opout and opids data: {0}. + InvalidOpoutAndOpidsData(String), /// PSBT doesn't provide information about close method. NoCloseMethod, @@ -184,9 +207,6 @@ pub enum RgbPsbtError { /// the size of transition {0} exceeds 16 MB. TransitionTooBig(OpId), - /// a transition {0} specified as contract consumer has not been pushed. - MissingTransition(OpId), - /// state transition data in PSBT are invalid. Details: {0} #[from] InvalidTransition(DeserializeError), @@ -203,9 +223,7 @@ pub trait RgbExt { fn rgb_contract_consumers( &self, contract_id: ContractId, - ) -> Result, Vin)>, RgbPsbtError>; - - fn rgb_op_ids(&self, contract_id: ContractId) -> Result, RgbPsbtError>; + ) -> Result, RgbPsbtError>; fn rgb_transition(&self, opid: OpId) -> Result, RgbPsbtError>; @@ -217,6 +235,25 @@ pub trait RgbExt { fn set_rgb_tapret_host_on_change(&mut self); + /// Adds information about an RGB input allocation and the ID of the state + /// transition spending it. + /// + /// # Returns + /// + /// `Ok(false)`, if the same opout under the same contract was already + /// present with the provided state transition ID. `Ok(true)`, if the + /// opout was successfully added. + /// + /// # Errors + /// + /// If the [`Opout`] already exists but it's referencing a different [`OpId`]. + fn set_rgb_contract_consumer( + &mut self, + contract_id: ContractId, + opout: Opout, + opid: OpId, + ) -> Result; + fn push_rgb_transition(&mut self, transition: Transition) -> Result; fn rgb_bundles(&self) -> Result, RgbPsbtError> { @@ -228,17 +265,10 @@ pub trait RgbExt { if contract_consumers.is_empty() { return Err(RgbPsbtError::NoContractConsumers); } - for (opids, _vin) in contract_consumers { - for opid in &opids { - let transition = self.rgb_transition(*opid)?; - if let Some(transition) = transition { - for opout in transition.inputs() { - input_map.insert(opout, *opid)?; - } - known_transitions.insert(*opid, transition)?; - } else { - return Err(RgbPsbtError::MissingTransition(*opid)); - } + for (opout, opid) in contract_consumers { + input_map.insert(opout, opid)?; + if let Some(transition) = self.rgb_transition(opid)? { + known_transitions.insert(opid, transition)?; } } let input_map_len = input_map.len(); @@ -262,41 +292,25 @@ pub trait RgbExt { impl RgbExt for Psbt { fn rgb_contract_ids(&self) -> Result, FromSliceError> { - self.inputs() - .flat_map(|input| { - input - .proprietary - .keys() - .filter(|prop_key| { - prop_key.identifier == PSBT_RGB_PREFIX - && prop_key.subtype == PSBT_IN_RGB_CONSUMED_BY - }) - .map(|prop_key| prop_key.data.as_slice()) - .map(ContractId::copy_from_slice) + self.proprietary + .keys() + .filter(|prop_key| { + prop_key.identifier == PSBT_RGB_PREFIX + && prop_key.subtype == PSBT_GLOBAL_RGB_CONSUMED_BY }) + .map(|prop_key| prop_key.data.as_slice()) + .map(ContractId::copy_from_slice) .collect() } fn rgb_contract_consumers( &self, contract_id: ContractId, - ) -> Result, Vin)>, RgbPsbtError> { - let mut consumers: BTreeSet<(BTreeSet, Vin)> = bset! {}; - for (no, input) in self.inputs().enumerate() { - if let Some(opids) = input.rgb_consumer(contract_id)? { - consumers.insert((BTreeSet::from_iter(opids), Vin::from_u32(no as u32))); - } - } - Ok(consumers) - } - - fn rgb_op_ids(&self, contract_id: ContractId) -> Result, RgbPsbtError> { - self.inputs().try_fold(BTreeSet::new(), |mut set, input| { - if let Some(ids) = input.rgb_consumer(contract_id)? { - set.extend(ids); - } - Ok(set) - }) + ) -> Result, RgbPsbtError> { + let Some(data) = self.proprietary.get(&PropKey::rgb_consumed_by(contract_id)) else { + return Ok(HashMap::new()); + }; + Ok(OpoutAndOpids::deserialize(data)?.into_inner()) } fn rgb_transition(&self, opid: OpId) -> Result, RgbPsbtError> { @@ -332,6 +346,30 @@ impl RgbExt for Psbt { let _ = self.push_proprietary(PropKey::rgb_tapret_host_on_change(), vec![]); } + fn set_rgb_contract_consumer( + &mut self, + contract_id: ContractId, + opout: Opout, + opid: OpId, + ) -> Result { + let key = PropKey::rgb_consumed_by(contract_id); + if let Some(existing_data) = self.proprietary(&key) { + let mut items = OpoutAndOpids::deserialize(existing_data)?; + if let Some(existing_opid) = items.get(&opout) { + if *existing_opid != opid { + return Err(RgbPsbtError::DoubleSpend); + } + return Ok(false); + } + items.insert(opout, opid); + self.insert_proprietary(key, items.serialize().into()); + } else { + let items = OpoutAndOpids::new(map![opout => opid]); + let _ = self.push_proprietary(key, items.serialize()); + } + Ok(true) + } + fn push_rgb_transition(&mut self, mut transition: Transition) -> Result { let opid = transition.id(); @@ -349,6 +387,11 @@ impl RgbExt for Psbt { // existed let _ = self.push_proprietary(PropKey::rgb_transition(opid), serialized_transition.release()); + + for opout in transition.inputs() { + self.set_rgb_contract_consumer(transition.contract_id, opout, opid)?; + } + Ok(prev_transition.is_none()) } @@ -380,68 +423,3 @@ impl RgbExt for Psbt { Ok(map) } } - -#[allow(clippy::result_large_err)] -pub trait RgbInExt { - /// Returns information which state transition consumes this PSBT input. - /// - /// We do not error on invalid data in order to support future update of - /// this proprietary key to a standard one. In this case, the invalid - /// data will be filtered at the moment of PSBT deserialization and this - /// function will return `None` only in situations when the key is absent. - fn rgb_consumer(&self, contract_id: ContractId) -> Result>, RgbPsbtError>; - - /// Adds information about state transition consuming this PSBT input. - /// - /// # Returns - /// - /// `Ok(false)`, if the same node id under the same contract was already - /// present in the input. `Ok(true)`, if the id node was successfully - /// added to the input. - /// - /// # Errors - /// - /// If the input already contains [`PSBT_IN_RGB_NODE_ID`] key with the given - /// `contract_id` but referencing different [`OpId`]. - #[allow(clippy::result_large_err)] - fn set_rgb_consumer( - &mut self, - contract_id: ContractId, - opid: OpId, - ) -> Result; -} - -impl RgbInExt for psbt::Input { - fn rgb_consumer(&self, contract_id: ContractId) -> Result>, RgbPsbtError> { - let Some(data) = self - .proprietary - .get(&PropKey::rgb_in_consumed_by(contract_id)) - else { - return Ok(None); - }; - let opids = Opids::deserialize(data)?.into_inner(); - Ok(Some(opids)) - } - - fn set_rgb_consumer( - &mut self, - contract_id: ContractId, - opid: OpId, - ) -> Result { - let key = PropKey::rgb_in_consumed_by(contract_id); - Ok(match self.rgb_consumer(contract_id)? { - None => { - let opids = Opids::new(vec![opid]); - let _ = self.push_proprietary(key, opids.serialize()); - true - } - Some(ids) if ids.contains(&opid) => false, - Some(mut opids) => { - opids.push(opid); - let opids = Opids::new(opids); - self.insert_proprietary(key, opids.serialize().into()); - true - } - }) - } -} diff --git a/src/errors.rs b/src/errors.rs index 6b0e4ad..7bb7b2f 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -28,7 +28,7 @@ use amplify::IoError; use bpstd::Psbt; use nonasync::persistence::PersistenceError; use psrgbt::{CommitError, ConstructionError, EmbedError, TapretKeyError}; -use rgbstd::containers::{LoadError, TransitionInfoError}; +use rgbstd::containers::LoadError; use rgbstd::contract::{BuilderError, ContractError}; use rgbstd::persistence::{ ComposeError, ConsignError, FasciaError, Stock, StockError, StockErrorAll, StockErrorMem, @@ -180,14 +180,6 @@ pub enum CompositionError { /// beneficiary output number is given when secret seal is used. BeneficiaryVout, - /// the spent UTXOs contain too many seals which can't fit the state - /// transition input limit. - TooManyInputs, - - #[from] - #[display(inner)] - Transition(TransitionInfoError), - /// the operation produces too many extra state transitions which can't fit /// the container requirements. TooManyExtras, diff --git a/src/pay.rs b/src/pay.rs index 38b85ad..5ccbe2b 100644 --- a/src/pay.rs +++ b/src/pay.rs @@ -37,7 +37,7 @@ use psrgbt::{ Beneficiary as BpBeneficiary, Psbt, PsbtConstructor, PsbtMeta, RgbExt, RgbPsbt, TapretKeyError, TxParams, }; -use rgbstd::containers::{Batch, BuilderSeal, IndexedConsignment, Transfer, TransitionInfo}; +use rgbstd::containers::{Batch, BuilderSeal, IndexedConsignment, Transfer}; use rgbstd::contract::{AllocatedState, AssignmentsFilter, BuilderError}; use rgbstd::invoice::{Amount, Beneficiary, InvoiceState, RgbInvoice}; use rgbstd::persistence::{IndexProvider, StashInconsistency, StashProvider, StateProvider, Stock}; @@ -374,14 +374,12 @@ where Self::Descr: DescriptorRgb .map_err(|e| e.to_string())?; let prev_outputs = prev_outputs.into_iter().collect::>(); - let mut main_inputs = Vec::::new(); let mut sum_inputs = Amount::ZERO; let mut data_inputs = vec![]; - for (output, list) in stock + for (_output, list) in stock .contract_assignments_for(contract_id, prev_outputs.iter().copied()) .map_err(|e| e.to_string())? { - main_inputs.push(output); for (opout, state) in list { main_builder = main_builder.add_input(opout, state.clone())?; if opout.ty != *assignment_type { @@ -463,7 +461,7 @@ where Self::Descr: DescriptorRgb .contract_schema(id) .map_err(|_| BuilderError::Inconsistency(StashInconsistency::ContractAbsent(id)))?; - for (output, assigns) in seal_map { + for (_output, assigns) in seal_map { for (opout, state) in assigns { let transition_type = schema.default_transition_for_assignment(&opout.ty); @@ -480,10 +478,8 @@ where Self::Descr: DescriptorRgb continue; } let transition = extra_builder.complete_transition()?; - let info = TransitionInfo::new(transition, [output]) - .map_err(|_| CompositionError::TooManyInputs)?; extras - .push(info) + .push(transition) .map_err(|_| CompositionError::TooManyExtras)?; } } @@ -493,8 +489,7 @@ where Self::Descr: DescriptorRgb return Err(CompositionError::InsufficientState); } - let main = TransitionInfo::new(main_builder.complete_transition()?, main_inputs) - .map_err(|_| CompositionError::TooManyInputs)?; + let main = main_builder.complete_transition()?; let mut batch = Batch { main, extras }; batch.set_priority(u64::MAX); From 62423fbbb2d10abe279c567d1cf3592f3ac53831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zoe=20Faltib=C3=A0?= Date: Wed, 25 Jun 2025 14:14:46 +0200 Subject: [PATCH 4/4] known_transitions from map to vec --- Cargo.lock | 6 +-- psbt/src/rgb.rs | 100 +++++++++++++++++++++++++++++++++++++----------- 2 files changed, 81 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0482081..ce92d3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1457,7 +1457,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rgb-core" version = "0.11.1-alpha.3+unreviewed" -source = "git+https://github.com/zoedberg/rgb-core?branch=0.11.1-4#3420162db3c049f216cbcae5494445a3fbd9e5ed" +source = "git+https://github.com/zoedberg/rgb-core?branch=0.11.1-4#dcd9d92072b8136149bc892589acb39ec3ba83db" dependencies = [ "aluvm", "amplify", @@ -1478,7 +1478,7 @@ dependencies = [ [[package]] name = "rgb-invoice" version = "0.11.1-alpha.3+unreviewed" -source = "git+https://github.com/zoedberg/rgb-std?branch=0.11.1-4#6eccb880a92494e5056a7c15975f3e4f446505c4" +source = "git+https://github.com/zoedberg/rgb-std?branch=0.11.1-4#b940e6d5fbf7ed21d3f3186de61420f7aebebb2d" dependencies = [ "amplify", "baid64", @@ -1540,7 +1540,7 @@ dependencies = [ [[package]] name = "rgb-std" version = "0.11.1-alpha.3+unreviewed" -source = "git+https://github.com/zoedberg/rgb-std?branch=0.11.1-4#6eccb880a92494e5056a7c15975f3e4f446505c4" +source = "git+https://github.com/zoedberg/rgb-std?branch=0.11.1-4#b940e6d5fbf7ed21d3f3186de61420f7aebebb2d" dependencies = [ "aluvm", "amplify", diff --git a/psbt/src/rgb.rs b/psbt/src/rgb.rs index 61e1c84..d7e4895 100644 --- a/psbt/src/rgb.rs +++ b/psbt/src/rgb.rs @@ -19,17 +19,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; -use amplify::confinement::{Confined, SmallOrdMap, U24}; +use amplify::confinement::{Confined, NonEmptyOrdMap, SmallVec, U24}; use amplify::{confinement, FromSliceError, Wrapper}; use bp::dbc::Method; use bp::seals::txout::CloseMethod; use bpstd::psbt::{KeyMap, MpcPsbtError, PropKey, Psbt}; use commit_verify::mpc; use rgbstd::{ - AssignmentType, ContractId, MergeReveal, MergeRevealError, OpId, Operation, Opout, Transition, - TransitionBundle, + AssignmentType, ContractId, KnownTransition, MergeReveal, MergeRevealError, OpId, Operation, + Opout, Transition, TransitionBundle, }; use strict_encoding::{DeserializeError, StrictDeserialize, StrictSerialize}; @@ -57,10 +57,10 @@ pub const PSBT_GLOBAL_RGB_CONSUMED_BY: u64 = 0x04; #[derive(Wrapper, WrapperMut, Clone, PartialEq, Eq, Debug, From)] #[wrapper(Deref)] #[wrapper_mut(DerefMut)] -pub struct OpoutAndOpids(HashMap); +pub struct OpoutAndOpids(BTreeMap); impl OpoutAndOpids { - pub fn new(items: HashMap) -> Self { Self(items) } + pub fn new(items: BTreeMap) -> Self { Self(items) } pub fn serialize(&self) -> Vec { let mut bytes = Vec::new(); @@ -85,8 +85,7 @@ impl OpoutAndOpids { "Input data length {bytes_len} is not a multiple of {item_size}" ))); } - let len = bytes.len() / item_size; - let mut items = HashMap::with_capacity(len); + let mut items = BTreeMap::new(); for chunk in bytes.chunks_exact(item_size) { let mut cursor = 0; let op = OpId::copy_from_slice(&chunk[cursor..cursor + opid_size]).map_err(|e| { @@ -114,6 +113,57 @@ impl OpoutAndOpids { } } +#[allow(clippy::result_large_err)] +fn insert_transitions_sorted( + transitions: &HashMap, + known_transitions: &mut SmallVec, +) -> Result<(), RgbPsbtError> { + #[allow(clippy::result_large_err)] + fn visit_and_insert( + opid: OpId, + transitions: &HashMap, + known_transitions: &mut SmallVec, + visited: &mut HashSet, + visiting: &mut HashSet, + ) -> Result<(), RgbPsbtError> { + if visited.contains(&opid) { + return Ok(()); + } + if visiting.contains(&opid) { + return Err(RgbPsbtError::KnownTransitionsInconsistency); + } + if let Some(transition) = transitions.get(&opid) { + visiting.insert(opid); + for input in transition.inputs() { + if transitions.contains_key(&input.op) { + visit_and_insert(input.op, transitions, known_transitions, visited, visiting)?; + } + } + visiting.remove(&opid); + visited.insert(opid); + known_transitions + .push(KnownTransition { + opid, + transition: transition.clone(), + }) + .map_err(|_| { + RgbPsbtError::InvalidTransitionsNumber( + transition.contract_id, + transitions.len(), + ) + })?; + } + Ok(()) + } + + let mut visited = HashSet::new(); + let mut visiting = HashSet::new(); + for &opid in transitions.keys() { + visit_and_insert(opid, transitions, known_transitions, &mut visited, &mut visiting)?; + } + Ok(()) +} + /// Extension trait for static functions returning RGB-related proprietary keys. pub trait ProprietaryKeyRgb { /// Constructs [`PSBT_GLOBAL_RGB_TRANSITION`] proprietary key. @@ -188,6 +238,9 @@ pub enum RgbPsbtError { /// invalid opout and opids data: {0}. InvalidOpoutAndOpidsData(String), + /// Unable to sort bundle's known transitions because of data inconsistency. + KnownTransitionsInconsistency, + /// PSBT doesn't provide information about close method. NoCloseMethod, @@ -223,7 +276,7 @@ pub trait RgbExt { fn rgb_contract_consumers( &self, contract_id: ContractId, - ) -> Result, RgbPsbtError>; + ) -> Result, RgbPsbtError>; fn rgb_transition(&self, opid: OpId) -> Result, RgbPsbtError>; @@ -259,23 +312,26 @@ pub trait RgbExt { fn rgb_bundles(&self) -> Result, RgbPsbtError> { let mut map = BTreeMap::new(); for contract_id in self.rgb_contract_ids()? { - let mut input_map: SmallOrdMap = SmallOrdMap::new(); - let mut known_transitions: SmallOrdMap = SmallOrdMap::new(); let contract_consumers = self.rgb_contract_consumers(contract_id)?; if contract_consumers.is_empty() { return Err(RgbPsbtError::NoContractConsumers); } - for (opout, opid) in contract_consumers { - input_map.insert(opout, opid)?; - if let Some(transition) = self.rgb_transition(opid)? { - known_transitions.insert(opid, transition)?; + let inputs_len = contract_consumers.len(); + let input_map = NonEmptyOrdMap::try_from(contract_consumers) + .map_err(|_| RgbPsbtError::InvalidInputsNumber(inputs_len))?; + let mut transitions_map: HashMap = HashMap::new(); + for opid in input_map.values() { + if let Some(transition) = self.rgb_transition(*opid)? { + transitions_map.insert(*opid, transition); } } - let input_map_len = input_map.len(); - let known_transitions_len = known_transitions.len(); + let known_transitions_len = transitions_map.values().len(); + let mut known_transitions: SmallVec = + SmallVec::with_capacity(known_transitions_len); + insert_transitions_sorted(&transitions_map, &mut known_transitions)?; + let bundle = TransitionBundle { - input_map: Confined::try_from(input_map.release()) - .map_err(|_| RgbPsbtError::InvalidInputsNumber(input_map_len))?, + input_map, known_transitions: Confined::try_from(known_transitions.release()).map_err( |_| RgbPsbtError::InvalidTransitionsNumber(contract_id, known_transitions_len), )?, @@ -306,9 +362,9 @@ impl RgbExt for Psbt { fn rgb_contract_consumers( &self, contract_id: ContractId, - ) -> Result, RgbPsbtError> { + ) -> Result, RgbPsbtError> { let Some(data) = self.proprietary.get(&PropKey::rgb_consumed_by(contract_id)) else { - return Ok(HashMap::new()); + return Ok(BTreeMap::new()); }; Ok(OpoutAndOpids::deserialize(data)?.into_inner()) } @@ -364,7 +420,7 @@ impl RgbExt for Psbt { items.insert(opout, opid); self.insert_proprietary(key, items.serialize().into()); } else { - let items = OpoutAndOpids::new(map![opout => opid]); + let items = OpoutAndOpids::new(bmap![opout => opid]); let _ = self.push_proprietary(key, items.serialize()); } Ok(true)