diff --git a/lightning-invoice/Cargo.toml b/lightning-invoice/Cargo.toml index 67c4044c2..8780e1b79 100644 --- a/lightning-invoice/Cargo.toml +++ b/lightning-invoice/Cargo.toml @@ -24,7 +24,7 @@ serde = { version = "1.0", optional = true, default-features = false, features = bitcoin = { version = "0.32.2", default-features = false, features = ["secp-recovery"] } # RGB and related -rgb-lib = { version = "=0.3.0-beta.6", features = [ +rgb-lib = { version = "=0.3.0-beta.7", features = [ "electrum", "esplora", ] } diff --git a/lightning/Cargo.toml b/lightning/Cargo.toml index 5cf228ed1..e281c5b58 100644 --- a/lightning/Cargo.toml +++ b/lightning/Cargo.toml @@ -53,7 +53,7 @@ inventory = { version = "0.3", optional = true } # RGB and related futures = "0.3" -rgb-lib = { version = "=0.3.0-beta.6", features = [ +rgb-lib = { version = "=0.3.0-beta.7", features = [ "electrum", "esplora", ] } diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 0558442ba..bf0936ca8 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -30,7 +30,7 @@ use bitcoin::secp256k1::{ecdsa::Signature, Secp256k1}; use bitcoin::secp256k1::{PublicKey, SecretKey}; use bitcoin::{secp256k1, sighash, FeeRate, Sequence, TxIn}; -use rgb_lib::{ContractId, RgbTransport}; +use rgb_lib::ContractId; use crate::blinded_path::message::BlindedMessagePath; use crate::chain::chaininterface::{ @@ -79,8 +79,8 @@ use crate::ln::LN_MAX_MSG_LEN; use crate::offers::static_invoice::StaticInvoice; use crate::rgb_utils::{ color_closing, color_commitment, color_htlc, get_rgb_channel_info_path, - get_rgb_channel_info_pending, parse_rgb_channel_info, rename_rgb_files, - update_rgb_channel_amount_pending, + get_rgb_channel_info_pending, is_asset_known, parse_rgb_channel_info, rename_rgb_files, + set_counterparty_knows_asset, update_rgb_channel_amount_pending, }; use crate::routing::gossip::NodeId; use crate::sign::ecdsa::EcdsaChannelSigner; @@ -2358,11 +2358,9 @@ pub(crate) struct FundingScope { /// [`ChannelContext::minimum_depth`]. minimum_depth_override: Option, - /// The consignment endpoint used to exchange the RGB consignment. - pub(super) consignment_endpoint: Option, - - /// The RGB asset amount to push to the counterparty on channel open. - pub(super) push_asset_amount: Option, + /// The RGB asset this channel is for, if it is a colored channel: the contract ID of the asset + /// plus the amount of it pushed to the counterparty on channel open, if any. + pub(super) rgb_asset: Option<(ContractId, Option)>, } impl Writeable for FundingScope { @@ -2377,8 +2375,7 @@ impl Writeable for FundingScope { (13, self.funding_tx_confirmation_height, required), (15, self.short_channel_id, option), (17, self.minimum_depth_override, option), - (19, self.consignment_endpoint, option), - (21, self.push_asset_amount, option), + (21, self.rgb_asset, option), }); Ok(()) } @@ -2396,8 +2393,7 @@ impl Readable for FundingScope { let mut funding_tx_confirmation_height = RequiredWrapper(None); let mut short_channel_id = None; let mut minimum_depth_override = None; - let mut consignment_endpoint = None; - let mut push_asset_amount = None; + let mut rgb_asset: Option<(ContractId, Option)> = None; read_tlv_fields!(reader, { (1, value_to_self_msat, required), @@ -2409,8 +2405,7 @@ impl Readable for FundingScope { (13, funding_tx_confirmation_height, required), (15, short_channel_id, option), (17, minimum_depth_override, option), - (19, consignment_endpoint, option), - (21, push_asset_amount, option), + (21, rgb_asset, option), }); Ok(Self { @@ -2427,8 +2422,7 @@ impl Readable for FundingScope { funding_tx_confirmation_height: funding_tx_confirmation_height.0.unwrap(), short_channel_id, minimum_depth_override, - consignment_endpoint, - push_asset_amount, + rgb_asset, #[cfg(any(test, fuzzing))] next_local_fee: Mutex::new(PredictedNextFee::default()), #[cfg(any(test, fuzzing))] @@ -2446,6 +2440,21 @@ impl FundingScope { self.value_to_self_msat } + /// Whether this is an RGB (colored) channel. + pub(crate) fn is_colored(&self) -> bool { + self.rgb_asset.is_some() + } + + /// The RGB contract ID of the asset this channel is for, if it is a colored channel. + pub(crate) fn contract_id(&self) -> Option { + self.rgb_asset.map(|(contract_id, _)| contract_id) + } + + /// The RGB asset amount pushed to the counterparty on channel open, if any. + pub(crate) fn push_asset_amount(&self) -> Option { + self.rgb_asset.and_then(|(_, push_asset_amount)| push_asset_amount) + } + pub fn get_holder_counterparty_selected_channel_reserve_satoshis(&self) -> (u64, Option) { ( self.holder_selected_channel_reserve_satoshis, @@ -2616,8 +2625,7 @@ impl FundingScope { funding_tx_confirmed_in: None, minimum_depth_override: None, short_channel_id: None, - consignment_endpoint: None, - push_asset_amount: None, + rgb_asset: None, } } @@ -3415,7 +3423,7 @@ where msg_channel_reserve_satoshis: u64, msg_push_msat: u64, open_channel_fields: msgs::CommonOpenChannelFields, - push_asset_amount: Option, + rgb_asset: Option<(ContractId, Option)>, ldk_data_dir: PathBuf, ) -> Result<(FundingScope, ChannelContext), ChannelError> where @@ -3624,8 +3632,7 @@ where funding_tx_confirmation_height: 0, short_channel_id: None, minimum_depth_override: None, - consignment_endpoint: open_channel_fields.consignment_endpoint.clone(), - push_asset_amount, + rgb_asset, }; let channel_context = ChannelContext { user_id, @@ -3739,7 +3746,7 @@ where interactive_tx_signing_session: None, - is_colored: funding.consignment_endpoint.is_some(), + is_colored: funding.is_colored(), ldk_data_dir, }; @@ -3764,9 +3771,8 @@ where channel_keys_id: [u8; 32], holder_signer: ::EcdsaSigner, _logger: L, - consignment_endpoint: Option, + rgb_asset: Option<(ContractId, Option)>, ldk_data_dir: PathBuf, - push_asset_amount: Option, ) -> Result<(FundingScope, ChannelContext), APIError> where ES::Target: EntropySource, @@ -3872,8 +3878,7 @@ where funding_tx_confirmation_height: 0, short_channel_id: None, minimum_depth_override: None, - consignment_endpoint: consignment_endpoint.clone(), - push_asset_amount, + rgb_asset, }; let channel_context = Self { user_id, @@ -3985,7 +3990,7 @@ where interactive_tx_signing_session: None, - is_colored: funding.consignment_endpoint.is_some(), + is_colored: funding.is_colored(), ldk_data_dir, }; @@ -13578,7 +13583,7 @@ where pub fn new( fee_estimator: &LowerBoundedFeeEstimator, entropy_source: &ES, signer_provider: &SP, counterparty_node_id: PublicKey, their_features: &InitFeatures, channel_value_satoshis: u64, push_msat: u64, user_id: u128, config: &UserConfig, current_chain_height: u32, - outbound_scid_alias: u64, temporary_channel_id: Option, logger: L, consignment_endpoint: Option, ldk_data_dir: PathBuf, push_asset_amount: Option, + outbound_scid_alias: u64, temporary_channel_id: Option, logger: L, rgb_asset: Option<(ContractId, Option)>, ldk_data_dir: PathBuf, ) -> Result, APIError> where ES::Target: EntropySource, F::Target: FeeEstimator, @@ -13616,9 +13621,8 @@ where channel_keys_id, holder_signer, logger, - consignment_endpoint, + rgb_asset, ldk_data_dir, - push_asset_amount, )?; let unfunded_context = UnfundedChannelContext { unfunded_channel_age_ticks: 0, @@ -13796,11 +13800,10 @@ where None => Builder::new().into_script(), }), channel_type: Some(self.funding.get_channel_type().clone()), - consignment_endpoint: self.funding.consignment_endpoint.clone(), }, push_msat: self.funding.get_value_satoshis() * 1000 - self.funding.value_to_self_msat, channel_reserve_satoshis: self.funding.holder_selected_channel_reserve_satoshis, - push_asset_amount: self.funding.push_asset_amount, + rgb_asset: self.funding.rgb_asset, }) } @@ -13815,7 +13818,13 @@ where their_features, &msg.common_fields, msg.channel_reserve_satoshis, - ) + )?; + + if self.funding.is_colored() && msg.known_asset { + set_counterparty_knows_asset(&self.context.channel_id, &self.context.ldk_data_dir); + } + + Ok(()) } /// Handles a funding_signed message from the remote end. @@ -13996,7 +14005,7 @@ where msg.channel_reserve_satoshis, msg.push_msat, msg.common_fields.clone(), - msg.push_asset_amount, + msg.rgb_asset, ldk_data_dir, )?; let unfunded_context = UnfundedChannelContext { @@ -14053,6 +14062,11 @@ where }; let keys = self.funding.get_holder_pubkeys(); + let known_asset = match self.funding.contract_id() { + Some(contract_id) => is_asset_known(contract_id, &self.context.ldk_data_dir), + None => false, + }; + Some(msgs::AcceptChannel { common_fields: msgs::CommonAcceptChannelFields { temporary_channel_id: self.context.channel_id, @@ -14075,6 +14089,7 @@ where channel_type: Some(self.funding.get_channel_type().clone()), }, channel_reserve_satoshis: self.funding.holder_selected_channel_reserve_satoshis, + known_asset, #[cfg(taproot)] next_local_nonce: None, }) @@ -14238,10 +14253,9 @@ where channel_keys_id, holder_signer, logger, - // ok to pass consignment_endpoint as None since this method is unused + // ok to pass rgb_asset as None since this method is unused None, ldk_data_dir, - None, )?; let unfunded_context = UnfundedChannelContext { unfunded_channel_age_ticks: 0, @@ -14333,7 +14347,6 @@ where None => Builder::new().into_script(), }), channel_type: Some(self.funding.get_channel_type().clone()), - consignment_endpoint: self.funding.consignment_endpoint.clone(), }, funding_feerate_sat_per_1000_weight: self.context.feerate_per_kw, second_per_commitment_point, @@ -15081,8 +15094,7 @@ where (65, self.quiescent_action, option), // Added in 0.2 (67, pending_outbound_held_htlc_flags, optional_vec), // Added in 0.2 (69, holding_cell_held_htlc_flags, optional_vec), // Added in 0.2 - (71, self.funding.consignment_endpoint, option), - (73, self.funding.push_asset_amount, option), + (73, self.funding.rgb_asset, option), }); Ok(()) @@ -15418,8 +15430,7 @@ where let mut channel_keys_id = [0u8; 32]; let mut temporary_channel_id: Option = None; let mut holder_max_accepted_htlcs: Option = None; - let mut consignment_endpoint: Option = None; - let mut push_asset_amount: Option = None; + let mut rgb_asset: Option<(ContractId, Option)> = None; let mut blocked_monitor_updates = Some(Vec::new()); @@ -15502,8 +15513,7 @@ where (65, quiescent_action, upgradable_option), // Added in 0.2 (67, pending_outbound_held_htlc_flags_opt, optional_vec), // Added in 0.2 (69, holding_cell_held_htlc_flags_opt, optional_vec), // Added in 0.2 - (71, consignment_endpoint, option), - (73, push_asset_amount, option), + (73, rgb_asset, option), }); let holder_signer = signer_provider.derive_channel_signer(channel_keys_id); @@ -15782,8 +15792,7 @@ where funding_tx_confirmation_height, short_channel_id, minimum_depth_override, - consignment_endpoint: consignment_endpoint.clone(), - push_asset_amount, + rgb_asset, }, context: ChannelContext { user_id, @@ -15895,7 +15904,7 @@ where is_manual_broadcast: is_manual_broadcast.unwrap_or(false), interactive_tx_signing_session, - is_colored: consignment_endpoint.is_some(), + is_colored: rgb_asset.is_some(), ldk_data_dir, }, holder_commitment_point, @@ -17886,8 +17895,7 @@ mod tests { funding_tx_confirmation_height: 0, short_channel_id: None, minimum_depth_override: None, - consignment_endpoint: None, - push_asset_amount: None, + rgb_asset: None, }; let post_channel_value = funding.compute_post_splice_value(our_funding_contribution, their_funding_contribution); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 1454a6a6a..6a9a2cc32 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -36,7 +36,7 @@ use bitcoin::secp256k1::Secp256k1; use bitcoin::secp256k1::{PublicKey, SecretKey}; use bitcoin::{secp256k1, Sequence, SignedAmount}; -use rgb_lib::{ContractId, RgbTransport}; +use rgb_lib::ContractId; use crate::blinded_path::message::{ AsyncPaymentsContext, BlindedMessagePath, MessageForwardNode, OffersContext, @@ -4144,7 +4144,7 @@ where /// [`Event::FundingGenerationReady::temporary_channel_id`]: events::Event::FundingGenerationReady::temporary_channel_id /// [`Event::ChannelClosed::channel_id`]: events::Event::ChannelClosed::channel_id #[rustfmt::skip] - pub fn create_channel(&self, their_network_key: PublicKey, channel_value_satoshis: u64, push_msat: u64, user_channel_id: u128, temporary_channel_id: Option, override_config: Option, consignment_endpoint: Option, push_asset_amount: Option) -> Result { + pub fn create_channel(&self, their_network_key: PublicKey, channel_value_satoshis: u64, push_msat: u64, user_channel_id: u128, temporary_channel_id: Option, override_config: Option, rgb_asset: Option<(ContractId, Option)>) -> Result { if channel_value_satoshis < 1000 { return Err(APIError::APIMisuseError { err: format!("Channel value must be at least 1000 satoshis. It was {}", channel_value_satoshis) }); } @@ -4180,7 +4180,7 @@ where }; match OutboundV1Channel::new(&self.fee_estimator, &self.entropy_source, &self.signer_provider, their_network_key, their_features, channel_value_satoshis, push_msat, user_channel_id, config, - self.best_block.read().unwrap().height, outbound_scid_alias, temporary_channel_id, &*self.logger, consignment_endpoint, self.ldk_data_dir.clone(), push_asset_amount) + self.best_block.read().unwrap().height, outbound_scid_alias, temporary_channel_id, &*self.logger, rgb_asset, self.ldk_data_dir.clone()) { Ok(res) => res, Err(e) => { @@ -10463,8 +10463,8 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ { Some(Ok(inbound_chan)) => { let logger = WithChannelContext::from(&self.logger, &inbound_chan.context, None); - if let Some(consignment_endpoint) = &inbound_chan.funding.consignment_endpoint { - match handle_funding(&msg.temporary_channel_id, msg.funding_txid.to_string(), &self.ldk_data_dir, consignment_endpoint.clone(), inbound_chan.funding.push_asset_amount) { + if inbound_chan.funding.is_colored() { + match handle_funding(&msg.temporary_channel_id, msg.funding_txid.to_string(), &self.ldk_data_dir, inbound_chan.funding.push_asset_amount()) { Ok(()) => (), Err(e) => { // at this point the channel initiator already transitioned its channel to the funded channel ID diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 3f92baefa..bbd8522f7 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -31,7 +31,7 @@ use bitcoin::secp256k1::ecdsa::Signature; use bitcoin::secp256k1::PublicKey; use bitcoin::{secp256k1, Transaction, Witness}; -use rgb_lib::{ContractId, RgbTransport}; +use rgb_lib::ContractId; use crate::blinded_path::message::BlindedMessagePath; use crate::blinded_path::payment::{ @@ -247,8 +247,6 @@ pub struct CommonOpenChannelFields { /// The channel type that this channel will represent. As defined in the latest /// specification, this field is required. However, it is an `Option` for legacy reasons. pub channel_type: Option, - /// The consignment endpoint used to exchange the RGB consignment - pub consignment_endpoint: Option, } impl CommonOpenChannelFields { @@ -299,8 +297,9 @@ pub struct OpenChannel { pub push_msat: u64, /// The minimum value unencumbered by HTLCs for the counterparty to keep in the channel pub channel_reserve_satoshis: u64, - /// The amount of RGB assets to push to the counterparty as part of the open. - pub push_asset_amount: Option, + /// The RGB asset this channel is for, if it is a colored channel: the contract ID of the asset + /// plus the amount of it to push to the counterparty as part of the open, if any. + pub rgb_asset: Option<(ContractId, Option)>, } /// An [`open_channel2`] message to be sent by or received from the channel initiator. @@ -379,6 +378,9 @@ pub struct AcceptChannel { pub common_fields: CommonAcceptChannelFields, /// The minimum value unencumbered by HTLCs for the counterparty to keep in the channel pub channel_reserve_satoshis: u64, + /// Whether we already know the RGB asset offered in [`OpenChannel::contract_id`], i.e. we hold + /// its contract and every media file it declares. + pub known_asset: bool, #[cfg(taproot)] /// Next nonce the channel initiator should use to create a funding output signature against pub next_local_nonce: Option, @@ -2664,15 +2666,18 @@ impl Writeable for AcceptChannel { self.common_fields.delayed_payment_basepoint.write(w)?; self.common_fields.htlc_basepoint.write(w)?; self.common_fields.first_per_commitment_point.write(w)?; + let known_asset_marker = if self.known_asset { Some(()) } else { None }; #[cfg(not(taproot))] encode_tlv_stream!(w, { (0, self.common_fields.shutdown_scriptpubkey.as_ref().map(|s| WithoutLength(s)), option), // Don't encode length twice. (1, self.common_fields.channel_type, option), + (3, known_asset_marker, option), }); #[cfg(taproot)] encode_tlv_stream!(w, { (0, self.common_fields.shutdown_scriptpubkey.as_ref().map(|s| WithoutLength(s)), option), // Don't encode length twice. (1, self.common_fields.channel_type, option), + (3, known_asset_marker, option), (4, self.next_local_nonce, option), }); Ok(()) @@ -2698,10 +2703,12 @@ impl LengthReadable for AcceptChannel { let mut shutdown_scriptpubkey: Option = None; let mut channel_type: Option = None; + let mut known_asset_marker: Option<()> = None; #[cfg(not(taproot))] decode_tlv_stream!(r, { (0, shutdown_scriptpubkey, (option, encoding: (ScriptBuf, WithoutLength))), (1, channel_type, option), + (3, known_asset_marker, option), }); #[cfg(taproot)] let mut next_local_nonce: Option = None; @@ -2709,6 +2716,7 @@ impl LengthReadable for AcceptChannel { decode_tlv_stream!(r, { (0, shutdown_scriptpubkey, (option, encoding: (ScriptBuf, WithoutLength))), (1, channel_type, option), + (3, known_asset_marker, option), (4, next_local_nonce, option), }); @@ -2731,6 +2739,7 @@ impl LengthReadable for AcceptChannel { channel_type, }, channel_reserve_satoshis, + known_asset: known_asset_marker.is_some(), #[cfg(taproot)] next_local_nonce, }) @@ -3136,8 +3145,7 @@ impl Writeable for OpenChannel { encode_tlv_stream!(w, { (0, self.common_fields.shutdown_scriptpubkey.as_ref().map(|s| WithoutLength(s)), option), // Don't encode length twice. (1, self.common_fields.channel_type, option), - (2, self.common_fields.consignment_endpoint, option), - (3, self.push_asset_amount, option), + (3, self.rgb_asset, option), }); Ok(()) } @@ -3166,13 +3174,11 @@ impl LengthReadable for OpenChannel { let mut shutdown_scriptpubkey: Option = None; let mut channel_type: Option = None; - let mut consignment_endpoint: Option = None; - let mut push_asset_amount: Option = None; + let mut rgb_asset: Option<(ContractId, Option)> = None; decode_tlv_stream!(r, { (0, shutdown_scriptpubkey, (option, encoding: (ScriptBuf, WithoutLength))), (1, channel_type, option), - (2, consignment_endpoint, option), - (3, push_asset_amount, option), + (3, rgb_asset, option), }); Ok(OpenChannel { common_fields: CommonOpenChannelFields { @@ -3194,11 +3200,10 @@ impl LengthReadable for OpenChannel { channel_flags, shutdown_scriptpubkey, channel_type, - consignment_endpoint, }, push_msat, channel_reserve_satoshis, - push_asset_amount, + rgb_asset, }) } } @@ -3228,7 +3233,6 @@ impl Writeable for OpenChannelV2 { (0, self.common_fields.shutdown_scriptpubkey.as_ref().map(|s| WithoutLength(s)), option), // Don't encode length twice. (1, self.common_fields.channel_type, option), (2, self.require_confirmed_inputs, option), - (3, self.common_fields.consignment_endpoint, option), }); Ok(()) } @@ -3259,12 +3263,10 @@ impl LengthReadable for OpenChannelV2 { let mut shutdown_scriptpubkey: Option = None; let mut channel_type: Option = None; let mut require_confirmed_inputs: Option<()> = None; - let mut consignment_endpoint: Option = None; decode_tlv_stream!(r, { (0, shutdown_scriptpubkey, (option, encoding: (ScriptBuf, WithoutLength))), (1, channel_type, option), (2, require_confirmed_inputs, option), - (3, consignment_endpoint, option), }); Ok(OpenChannelV2 { common_fields: CommonOpenChannelFields { @@ -3286,7 +3288,6 @@ impl LengthReadable for OpenChannelV2 { channel_flags, shutdown_scriptpubkey, channel_type, - consignment_endpoint, }, funding_feerate_sat_per_1000_weight, locktime, @@ -3296,28 +3297,6 @@ impl LengthReadable for OpenChannelV2 { } } -impl Readable for RgbTransport { - fn read(r: &mut R) -> Result { - let sz: usize = ::read(r)? as usize; - let mut consignment_endpoint_str_vec = Vec::with_capacity(sz); - consignment_endpoint_str_vec.resize(sz, 0); - r.read_exact(&mut consignment_endpoint_str_vec)?; - match String::from_utf8(consignment_endpoint_str_vec) { - Ok(s) => return Ok(RgbTransport::from_str(&s).unwrap()), - Err(_) => return Err(DecodeError::InvalidValue), - } - } -} - -impl Writeable for RgbTransport { - fn write(&self, w: &mut W) -> Result<(), io::Error> { - let consignment_endpoint_str = format!("{self}"); - (consignment_endpoint_str.len() as u16).write(w)?; - w.write_all(consignment_endpoint_str.as_bytes())?; - Ok(()) - } -} - #[cfg(not(taproot))] impl_writeable_msg!(RevokeAndACK, { channel_id, diff --git a/lightning/src/rgb_utils/mod.rs b/lightning/src/rgb_utils/mod.rs index 52d79e38a..e3c99aa6a 100644 --- a/lightning/src/rgb_utils/mod.rs +++ b/lightning/src/rgb_utils/mod.rs @@ -12,41 +12,33 @@ use crate::types::features::ChannelTypeFeatures; use crate::types::payment::PaymentHash; use bitcoin::blockdata::transaction::Transaction; +use bitcoin::hashes::{sha256, Hash}; use bitcoin::hex::DisplayHex; use bitcoin::psbt::{ExtractTxError, Psbt}; use bitcoin::secp256k1::PublicKey; use bitcoin::TxOut; use rgb_lib::{ bitcoin::psbt::Psbt as RgbLibPsbt, - keys::WitnessVersion, wallet::{ rust_only::{AssetColoringInfo, ColoringInfo}, - DatabaseType, OnlineOptions, SinglesigKeys, Wallet, WalletData, + OnlineOptions, RgbWalletOpsOffline, Wallet, }, - AssetSchema, Assignment, BitcoinNetwork, ConsignmentExt, ContractId, Error as RgbLibError, - FileContent, RgbTransfer, RgbTransport, WitnessOrd, + AssetSchema, Assignment, ConsignmentExt, ContractId, Error as RgbLibError, RgbTransfer, + WitnessOrd, }; use serde::{Deserialize, Serialize}; use tokio::runtime::Handle; use core::ops::Deref; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fs; use std::path::{Path, PathBuf}; use std::str::FromStr; /// Static blinding costant (will be removed in the future) pub const STATIC_BLINDING: u64 = 777; -/// Name of the file containing the bitcoin network -pub const BITCOIN_NETWORK_FNAME: &str = "bitcoin_network"; /// Name of the file containing the electrum URL pub const INDEXER_URL_FNAME: &str = "indexer_url"; -/// Name of the file containing the wallet fingerprint -pub const WALLET_FINGERPRINT_FNAME: &str = "wallet_fingerprint"; -/// Name of the file containing the account-level xPub of the vanilla-side of the wallet -pub const WALLET_ACCOUNT_XPUB_VANILLA_FNAME: &str = "wallet_account_xpub_vanilla"; -/// Name of the file containing the account-level xPub of the colored-side of the wallet -pub const WALLET_ACCOUNT_XPUB_COLORED_FNAME: &str = "wallet_account_xpub_colored"; /// Name of the file containing the master fingerprint of the wallet pub const WALLET_MASTER_FINGERPRINT_FNAME: &str = "wallet_master_fingerprint"; const INBOUND_EXT: &str = "inbound"; @@ -68,6 +60,9 @@ pub struct RgbInfo { /// Batch transfer index from rgb-lib (set after rgb_send_begin) #[serde(default, skip_serializing_if = "Option::is_none")] pub batch_transfer_idx: Option, + /// Whether the channel acceptor told us (in `accept_channel`) that it already knows the asset + #[serde(default)] + pub counterparty_knows_asset: bool, } /// RGB payment info @@ -127,24 +122,6 @@ fn _read_file_in_parent(ldk_data_dir: &Path, fname: &str) -> String { fs::read_to_string(_get_file_in_parent(ldk_data_dir, fname)).unwrap() } -fn _get_rgb_wallet_dir(ldk_data_dir: &Path) -> PathBuf { - let fingerprint = _read_file_in_parent(ldk_data_dir, WALLET_FINGERPRINT_FNAME); - _get_file_in_parent(ldk_data_dir, &fingerprint) -} - -fn _get_bitcoin_network(ldk_data_dir: &Path) -> BitcoinNetwork { - let bitcoin_network = _read_file_in_parent(ldk_data_dir, BITCOIN_NETWORK_FNAME); - BitcoinNetwork::from_str(&bitcoin_network).unwrap() -} - -fn _get_account_xpub_colored(ldk_data_dir: &Path) -> String { - _read_file_in_parent(ldk_data_dir, WALLET_ACCOUNT_XPUB_COLORED_FNAME) -} - -fn _get_account_xpub_vanilla(ldk_data_dir: &Path) -> String { - _read_file_in_parent(ldk_data_dir, WALLET_ACCOUNT_XPUB_VANILLA_FNAME) -} - fn _get_master_fingerprint(ldk_data_dir: &Path) -> String { _read_file_in_parent(ldk_data_dir, WALLET_MASTER_FINGERPRINT_FNAME) } @@ -153,87 +130,53 @@ fn _get_indexer_url(ldk_data_dir: &Path) -> String { _read_file_in_parent(ldk_data_dir, INDEXER_URL_FNAME) } -fn _new_rgb_wallet( - data_dir: String, bitcoin_network: BitcoinNetwork, account_xpub_vanilla: String, - account_xpub_colored: String, master_fingerprint: String, -) -> Wallet { - let keys = SinglesigKeys { - account_xpub_vanilla, - account_xpub_colored, - vanilla_keychain: None, - master_fingerprint, - mnemonic: None, - witness_version: WitnessVersion::Taproot, - }; - Wallet::new( - WalletData { - data_dir, - bitcoin_network, - database_type: DatabaseType::Sqlite, - max_allocations_per_utxo: 1, - supported_schemas: vec![ - AssetSchema::Nia, - AssetSchema::Cfa, - AssetSchema::Uda, - AssetSchema::Ifa, - ], - }, - keys, - ) - .expect("valid rgb-lib wallet") +fn _load_rgb_wallet(data_dir: String, master_fingerprint: String) -> Wallet { + Wallet::load(&data_dir, &master_fingerprint, None).expect("valid rgb-lib wallet") } -fn _get_wallet_data(ldk_data_dir: &Path) -> (String, BitcoinNetwork, String, String, String) { +fn _get_wallet_data(ldk_data_dir: &Path) -> (String, String) { let data_dir = ldk_data_dir.parent().unwrap().to_string_lossy().to_string(); - let bitcoin_network = _get_bitcoin_network(ldk_data_dir); - let account_xpub_vanilla = _get_account_xpub_vanilla(ldk_data_dir); - let account_xpub_colored = _get_account_xpub_colored(ldk_data_dir); let master_fingerprint = _get_master_fingerprint(ldk_data_dir); - (data_dir, bitcoin_network, account_xpub_vanilla, account_xpub_colored, master_fingerprint) + (data_dir, master_fingerprint) } async fn _get_rgb_wallet(ldk_data_dir: &Path) -> Wallet { - let (data_dir, bitcoin_network, account_xpub_vanilla, account_xpub_colored, master_fingerprint) = - _get_wallet_data(ldk_data_dir); - tokio::task::spawn_blocking(move || { - _new_rgb_wallet( - data_dir, - bitcoin_network, - account_xpub_vanilla, - account_xpub_colored, - master_fingerprint, - ) - }) - .await - .unwrap() + let (data_dir, master_fingerprint) = _get_wallet_data(ldk_data_dir); + tokio::task::spawn_blocking(move || _load_rgb_wallet(data_dir, master_fingerprint)) + .await + .unwrap() +} + +pub(crate) fn is_asset_known(contract_id: ContractId, ldk_data_dir: &Path) -> bool { + let handle = Handle::current(); + let _ = handle.enter(); + let wallet = futures::executor::block_on(_get_rgb_wallet(ldk_data_dir)); + wallet.is_asset_known(contract_id).unwrap_or(false) } async fn _accept_transfer( - ldk_data_dir: &Path, funding_txid: String, consignment_endpoint: RgbTransport, -) -> Result<(RgbTransfer, Vec), RgbLibError> { + ldk_data_dir: &Path, funding_txid: String, +) -> Result<(RgbTransfer, Vec, HashSet, PathBuf), RgbLibError> { let funding_vout = 1; - let (data_dir, bitcoin_network, account_xpub_vanilla, account_xpub_colored, master_fingerprint) = - _get_wallet_data(ldk_data_dir); + let (data_dir, master_fingerprint) = _get_wallet_data(ldk_data_dir); let indexer_url = _get_indexer_url(ldk_data_dir); + // the consignment is received from the channel counterparty over the p2p link and written to disk + let consignment_path = ldk_data_dir.join(format!("consignment_{funding_txid}")); tokio::task::spawn_blocking(move || { - let mut wallet = _new_rgb_wallet( - data_dir, - bitcoin_network, - account_xpub_vanilla, - account_xpub_colored, - master_fingerprint, - ); - wallet.go_online(OnlineOptions { + let mut wallet = _load_rgb_wallet(data_dir, master_fingerprint); + let online = wallet.go_online(OnlineOptions { indexer_url, skip_consistency_check: true, vanilla_sync_lookback: VANILLA_SYNC_LOOKBACK, })?; - wallet.accept_transfer( + let (consignment, assignments, media_digests) = wallet.accept_transfer_consignment( + online, + consignment_path, funding_txid.clone(), funding_vout, - consignment_endpoint, STATIC_BLINDING, - ) + )?; + Ok((consignment, assignments, media_digests, wallet.get_media_dir())) }) .await .unwrap() @@ -645,27 +588,23 @@ pub(crate) fn rename_rgb_files( get_rgb_channel_info_path(&chan_id, ldk_data_dir, true), ) .expect("rename ok"); +} - let funding_consignment_tmp = ldk_data_dir.join(format!("consignment_{}", temp_chan_id)); - if funding_consignment_tmp.exists() { - let funding_consignment = ldk_data_dir.join(format!("consignment_{}", chan_id)); - fs::rename(funding_consignment_tmp, funding_consignment).expect("rename ok"); - } +/// Directory holding the media received for a funding, before the contract has vouched for it. +pub fn get_media_staging_dir(ldk_data_dir: &Path, funding_txid: &str) -> PathBuf { + ldk_data_dir.join(format!("media_staging_{funding_txid}")) } /// Handle funding on the receiver side pub(crate) fn handle_funding( temporary_channel_id: &ChannelId, funding_txid: String, ldk_data_dir: &Path, - consignment_endpoint: RgbTransport, push_asset_amount: Option, + push_asset_amount: Option, ) -> Result<(), ChannelError> { let handle = Handle::current(); let _ = handle.enter(); - let accept_res = futures::executor::block_on(_accept_transfer( - ldk_data_dir, - funding_txid.clone(), - consignment_endpoint, - )); - let (consignment, remote_rgb_assignments) = match accept_res { + let accept_res = + futures::executor::block_on(_accept_transfer(ldk_data_dir, funding_txid.clone())); + let (consignment, remote_rgb_assignments, media_digests, media_dir) = match accept_res { Ok(res) => res, Err(RgbLibError::InvalidConsignment) => { return Err(ChannelError::close("Invalid RGB consignment for funding".to_owned())) @@ -687,11 +626,31 @@ pub(crate) fn handle_funding( Err(e) => return Err(ChannelError::close(format!("Unexpected error: {e}"))), }; - let consignment_path = ldk_data_dir.join(format!("consignment_{}", funding_txid)); - consignment.save_file(consignment_path).expect("unable to write file"); - let consignment_path = - ldk_data_dir.join(format!("consignment_{}", temporary_channel_id.0.as_hex())); - consignment.save_file(consignment_path).expect("unable to write file"); + let staging_dir = get_media_staging_dir(ldk_data_dir, &funding_txid); + for digest in media_digests { + let media_path = media_dir.join(&digest); + if media_path.exists() { + continue; + } + let staged_path = staging_dir.join(&digest); + let Ok(media_bytes) = fs::read(&staged_path) else { + return Err(ChannelError::close(format!( + "Missing RGB media file {digest} for funding" + ))); + }; + if sha256::Hash::hash(&media_bytes).to_string() != digest { + return Err(ChannelError::close(format!( + "Corrupt RGB media file {digest} for funding" + ))); + } + if let Err(e) = fs::rename(&staged_path, &media_path) { + return Err(ChannelError::close(format!( + "Failed to store RGB media file {digest} for funding: {e}" + ))); + } + } + // on the error paths above the staging directory is left for the file transfer handler's sweep + let _ = fs::remove_dir_all(&staging_dir); if remote_rgb_assignments.len() != 1 { return Err(ChannelError::close(format!( @@ -711,6 +670,8 @@ pub(crate) fn handle_funding( local_rgb_amount: push_amount, remote_rgb_amount: channel_rgb_amount - push_amount, batch_transfer_idx: None, + // only meaningful on the initiator side, which is the one that sends media + counterparty_knows_asset: false, }; let temporary_channel_id_str = temporary_channel_id.0.as_hex().to_string(); write_rgb_channel_info( @@ -725,6 +686,19 @@ pub(crate) fn handle_funding( Ok(()) } +pub(crate) fn set_counterparty_knows_asset(channel_id: &ChannelId, ldk_data_dir: &Path) { + let channel_id = channel_id.0.as_hex().to_string(); + for pending in [true, false] { + let info_file_path = get_rgb_channel_info_path(&channel_id, ldk_data_dir, pending); + if !info_file_path.exists() { + continue; + } + let mut rgb_info = parse_rgb_channel_info(&info_file_path); + rgb_info.counterparty_knows_asset = true; + write_rgb_channel_info(&info_file_path, &rgb_info); + } +} + /// Update RGB channel amount pub fn update_rgb_channel_amount( channel_id: &str, rgb_offered_htlc: u64, rgb_received_htlc: u64, ldk_data_dir: &Path,