From d228f047b91315c407283444aac4b4144b535070 Mon Sep 17 00:00:00 2001 From: ABHAY PANDEY Date: Fri, 27 Feb 2026 23:22:42 +0530 Subject: [PATCH] Replace serde_cbor with minicbor Signed-off-by: ABHAY PANDEY --- Cargo.toml | 2 +- src/bin/maker-cli.rs | 2 +- src/error.rs | 17 +- src/maker/error.rs | 4 +- src/maker/rpc/messages.rs | 103 ++++++- src/maker/rpc/server.rs | 2 +- src/maker/server.rs | 2 +- src/maker/server2.rs | 2 +- src/protocol/messages.rs | 413 +++++++++++++++++++++++++--- src/protocol/messages2.rs | 186 +++++++++++-- src/security.rs | 52 ++-- src/taker/api2.rs | 2 +- src/taker/error.rs | 4 +- src/taker/offers.rs | 55 +++- src/taker/routines.rs | 10 +- src/utill.rs | 51 ++-- src/wallet/backup.rs | 38 ++- src/wallet/error.rs | 6 +- src/wallet/ffi.rs | 2 +- src/wallet/fidelity.rs | 89 +++++- src/wallet/storage.rs | 94 ++++++- src/wallet/swapcoin.rs | 199 +++++++++++++- src/watch_tower/registry_storage.rs | 120 +++++++- src/watch_tower/watcher_error.rs | 10 +- 24 files changed, 1312 insertions(+), 153 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e8005ad04..a61cdcdb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,6 @@ bip39 = { version = "2.1.0", features = ["rand"] } bitcoin = "0.32" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -serde_cbor = "0.11.2" log = "^0.4" dirs = "3.0.1" socks = "0.3.4" @@ -37,6 +36,7 @@ zmq = "0.10.0" tungstenite = { version = "0.28.0", features = ["native-tls"]} nostr = "0.44.2" crossbeam-channel = "0.5.15" +minicbor = { version = "0.24.4", features = ["derive", "alloc", "std"] } [dev-dependencies] flate2 = {version = "1.0.35"} diff --git a/src/bin/maker-cli.rs b/src/bin/maker-cli.rs index 75fd5bd25..c55b07a9a 100644 --- a/src/bin/maker-cli.rs +++ b/src/bin/maker-cli.rs @@ -138,7 +138,7 @@ fn send_rpc_req(mut stream: TcpStream, req: RpcMsgReq) -> Result<(), MakerError> send_message(&mut stream, &req)?; let response_bytes = read_message(&mut stream)?; - let response: RpcMsgResp = serde_cbor::from_slice(&response_bytes)?; + let response: RpcMsgResp = minicbor::decode(&response_bytes)?; if matches!(response, RpcMsgResp::Pong) { println!("success"); diff --git a/src/error.rs b/src/error.rs index 320362362..718361f0f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -21,8 +21,11 @@ pub enum NetError { /// Error related to CBOR (Concise Binary Object Representation) serialization or deserialization. /// - /// This variant wraps a [`serde_cbor::Error`] to provide details about the issue. - Cbor(serde_cbor::Error), + /// This variant wraps a [`minicbor::decode::Error`] to provide details about the issue. + Cbor(minicbor::decode::Error), + + /// Error related to CBOR serialization. + Encode(String), /// Error indicating an invalid CLI application network. InvalidAppNetwork, @@ -46,8 +49,8 @@ impl From for NetError { } } -impl From for NetError { - fn from(value: serde_cbor::Error) -> Self { +impl From for NetError { + fn from(value: minicbor::decode::Error) -> Self { Self::Cbor(value) } } @@ -80,3 +83,9 @@ impl From for FeeEstimatorError { FeeEstimatorError::HttpError(err) } } + +impl From> for NetError { + fn from(err: minicbor::encode::Error) -> Self { + NetError::Encode(format!("Encode error: {:?}", err)) + } +} diff --git a/src/maker/error.rs b/src/maker/error.rs index c24024395..51aca9166 100644 --- a/src/maker/error.rs +++ b/src/maker/error.rs @@ -59,8 +59,8 @@ impl From for MakerError { } } -impl From for MakerError { - fn from(value: serde_cbor::Error) -> Self { +impl From for MakerError { + fn from(value: minicbor::decode::Error) -> Self { Self::Net(NetError::Cbor(value)) } } diff --git a/src/maker/rpc/messages.rs b/src/maker/rpc/messages.rs index b722a3d9d..cda9c8a0f 100644 --- a/src/maker/rpc/messages.rs +++ b/src/maker/rpc/messages.rs @@ -12,40 +12,56 @@ use crate::wallet::Balances; /// /// These messages are used for various operations in the Maker-rpc communication. /// Each variant corresponds to a specific action or query in the RPC protocol. -#[derive(Serialize, Deserialize, Debug)] +#[derive(minicbor::Encode, minicbor::Decode, Serialize, Deserialize, Debug)] pub enum RpcMsgReq { /// Ping request to check connectivity. + #[n(0)] Ping, /// Request to fetch all utxos in the wallet. + #[n(1)] Utxo, /// Request to fetch only swap utxos in the wallet. + #[n(2)] SwapUtxo, /// Request to fetch UTXOs in the contract pool. + #[n(3)] ContractUtxo, /// Request to fetch UTXOs in the fidelity pool. + #[n(4)] FidelityUtxo, /// Request to retrieve the total wallet balances of different categories. + #[n(5)] Balances, /// Request for generating a new wallet address. + #[n(6)] NewAddress, /// Request to send funds to a specific address. + #[n(7)] SendToAddress { /// The recipient's address. + #[n(0)] address: String, /// The amount to send. + #[n(1)] amount: u64, /// The transaction fee to include. + #[n(2)] feerate: f64, }, /// Request to retrieve the Tor address of the Maker. + #[n(8)] GetTorAddress, /// Request to retrieve the data directory path. + #[n(9)] GetDataDir, /// Request to stop the Maker server. + #[n(10)] Stop, /// Request to list all active and past fidelity bonds. + #[n(11)] ListFidelity, /// Request to sync the internal wallet with blockchain. + #[n(12)] SyncWallet, } @@ -53,48 +69,82 @@ pub enum RpcMsgReq { /// /// These messages are sent in response to RPC requests and carry the results /// of the corresponding actions or queries. -#[derive(Serialize, Deserialize, Debug)] +#[derive(minicbor::Encode, minicbor::Decode, Serialize, Deserialize, Debug)] pub enum RpcMsgResp { /// Response to a Ping request. + #[n(0)] Pong, /// Response containing all spendable UTXOs + #[n(1)] UtxoResp { /// List of spendable UTXOs in the wallet. + #[n(0)] + #[cbor(encode_with = "encode_json_bytes", decode_with = "decode_json_bytes")] utxos: Vec, }, /// Response containing UTXOs in the swap pool. + #[n(2)] SwapUtxoResp { /// List of UTXOs in the swap pool. + #[n(0)] + #[cbor(encode_with = "encode_json_bytes", decode_with = "decode_json_bytes")] utxos: Vec, }, /// Response containing UTXOs in the fidelity pool. + #[n(3)] FidelityUtxoResp { /// List of UTXOs in the fidelity pool. + #[n(0)] + #[cbor(encode_with = "encode_json_bytes", decode_with = "decode_json_bytes")] utxos: Vec, }, /// Response containing UTXOs in the contract pool. + #[n(4)] ContractUtxoResp { /// List of UTXOs in the contract pool. + #[n(0)] + #[cbor(encode_with = "encode_json_bytes", decode_with = "decode_json_bytes")] utxos: Vec, }, /// Response containing the total wallet balances of different categories. - TotalBalanceResp(Balances), + #[n(5)] + TotalBalanceResp( + #[n(0)] + #[cbor(encode_with = "encode_json_bytes", decode_with = "decode_json_bytes")] + Balances, + ), /// Response containing a newly generated wallet address. - NewAddressResp(String), + #[n(6)] + NewAddressResp(#[n(0)] String), /// Response to a send-to-address request. - SendToAddressResp(String), + #[n(7)] + SendToAddressResp(#[n(0)] String), /// Response containing the Tor address of the Maker. - GetTorAddressResp(String), + #[n(8)] + GetTorAddressResp(#[n(0)] String), /// Response containing the path to the data directory. - GetDataDirResp(PathBuf), + #[n(9)] + GetDataDirResp( + #[n(0)] + #[cbor(encode_with = "encode_path_buf", decode_with = "decode_path_buf")] + PathBuf, + ), /// Response indicating the server has been shut down. + #[n(10)] Shutdown, /// Response with the fidelity spending txid. - FidelitySpend(Txid), + #[n(11)] + FidelitySpend( + #[n(0)] + #[cbor(encode_with = "encode_json_bytes", decode_with = "decode_json_bytes")] + Txid, + ), /// Response with the internal server error. - ServerError(String), + #[n(12)] + ServerError(#[n(0)] String), /// Response listing all current and past fidelity bonds. - ListBonds(String), + #[n(13)] + ListBonds(#[n(0)] String), } impl Display for RpcMsgResp { @@ -136,3 +186,36 @@ impl Display for RpcMsgResp { } } } + +// Inline minicbor helpers +#[allow(dead_code)] +fn encode_json_bytes( + x: &T, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.bytes(&serde_json::to_vec(x).map_err(|_| minicbor::encode::Error::message("json error"))?)?; + Ok(()) +} +fn decode_json_bytes( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + serde_json::from_slice(d.bytes()?) + .map_err(|_| minicbor::decode::Error::message("json decode error")) +} + +fn encode_path_buf( + x: &std::path::PathBuf, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.encode(x.to_str().unwrap_or(""))?; + Ok(()) +} +fn decode_path_buf( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + Ok(std::path::PathBuf::from(d.decode::()?)) +} diff --git a/src/maker/rpc/server.rs b/src/maker/rpc/server.rs index 52cbd2901..87609d32d 100644 --- a/src/maker/rpc/server.rs +++ b/src/maker/rpc/server.rs @@ -28,7 +28,7 @@ pub trait MakerRpc { fn handle_request(maker: &Arc, socket: &mut TcpStream) -> Result<(), MakerError> { let msg_bytes = read_message(socket)?; - let rpc_request: RpcMsgReq = serde_cbor::from_slice(&msg_bytes)?; + let rpc_request: RpcMsgReq = minicbor::decode(&msg_bytes)?; log::info!("RPC request received: {rpc_request:?}"); let resp = match rpc_request { diff --git a/src/maker/server.rs b/src/maker/server.rs index 495021ff2..36cb741cf 100644 --- a/src/maker/server.rs +++ b/src/maker/server.rs @@ -372,7 +372,7 @@ fn handle_client(maker: &Arc, stream: &mut TcpStream) -> Result<(), Maker } } } - let taker_msg = serde_cbor::from_slice::(&bytes)?; + let taker_msg = minicbor::decode::(&bytes)?; log::info!("[{}] <=== {}", maker.config.network_port, taker_msg); diff --git a/src/maker/server2.rs b/src/maker/server2.rs index 5ed8b7c3a..3b4e5157a 100644 --- a/src/maker/server2.rs +++ b/src/maker/server2.rs @@ -435,7 +435,7 @@ fn handle_client_taproot(maker: &Arc, stream: &mut TcpStream) -> Result<( message_bytes.len(), ip ); - let message = match serde_cbor::from_slice::(&message_bytes) { + let message = match minicbor::decode::(&message_bytes) { Ok(msg) => { log::info!("[{}] <=== {}", maker.config.network_port, msg); msg diff --git a/src/protocol/messages.rs b/src/protocol/messages.rs index 2a0af154d..e1d33c60d 100644 --- a/src/protocol/messages.rs +++ b/src/protocol/messages.rs @@ -75,74 +75,120 @@ pub(crate) const PREIMAGE_LEN: usize = 32; pub(crate) type Preimage = [u8; PREIMAGE_LEN]; /// Represents the initial handshake message sent from Taker to Maker. -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) struct TakerHello { + #[n(0)] pub(crate) protocol_version_min: u32, + #[n(1)] pub(crate) protocol_version_max: u32, } /// Represents a request to give an offer. -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) struct GiveOffer; /// Contract Sigs requesting information for the Sender side of the hop. -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) struct ContractTxInfoForSender { + #[n(0)] + #[cbor(encode_with = "encode_secret_key", decode_with = "decode_secret_key")] pub(crate) multisig_nonce: SecretKey, + #[n(1)] + #[cbor(encode_with = "encode_secret_key", decode_with = "decode_secret_key")] pub(crate) hashlock_nonce: SecretKey, + #[n(2)] + #[cbor(encode_with = "encode_public_key", decode_with = "decode_public_key")] pub(crate) timelock_pubkey: PublicKey, + #[n(3)] + #[cbor(encode_with = "encode_transaction", decode_with = "decode_transaction")] pub(crate) senders_contract_tx: Transaction, + #[n(4)] + #[cbor(encode_with = "encode_script_buf", decode_with = "decode_script_buf")] pub(crate) multisig_redeemscript: ScriptBuf, + #[n(5)] + #[cbor(encode_with = "encode_amount", decode_with = "decode_amount")] pub(crate) funding_input_value: Amount, } /// Request for Contract Sigs **for** the Sender side of the hop. -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) struct ReqContractSigsForSender { + #[n(0)] pub(crate) txs_info: Vec, + #[n(1)] + #[cbor( + encode_with = "encode_hash160_hash", + decode_with = "decode_hash160_hash" + )] pub(crate) hashvalue: Hash160, + #[n(2)] pub(crate) locktime: u16, } /// Contract Sigs requesting information for the Receiver side of the hop. -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) struct ContractTxInfoForRecvr { + #[n(0)] + #[cbor(encode_with = "encode_script_buf", decode_with = "decode_script_buf")] pub(crate) multisig_redeemscript: ScriptBuf, + #[n(1)] + #[cbor(encode_with = "encode_transaction", decode_with = "decode_transaction")] pub(crate) contract_tx: Transaction, } /// Request for Contract Sigs **for** the Receiver side of the hop. -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) struct ReqContractSigsForRecvr { + #[n(0)] pub(crate) txs: Vec, } /// Confirmed Funding Tx with extra metadata. -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize, Clone)] pub(crate) struct FundingTxInfo { + #[n(0)] + #[cbor(encode_with = "encode_transaction", decode_with = "decode_transaction")] pub(crate) funding_tx: Transaction, + #[n(1)] pub(crate) funding_tx_merkleproof: String, + #[n(2)] + #[cbor(encode_with = "encode_script_buf", decode_with = "decode_script_buf")] pub(crate) multisig_redeemscript: ScriptBuf, + #[n(3)] + #[cbor(encode_with = "encode_secret_key", decode_with = "decode_secret_key")] pub(crate) multisig_nonce: SecretKey, + #[n(4)] + #[cbor(encode_with = "encode_script_buf", decode_with = "decode_script_buf")] pub(crate) contract_redeemscript: ScriptBuf, + #[n(5)] + #[cbor(encode_with = "encode_secret_key", decode_with = "decode_secret_key")] pub(crate) hashlock_nonce: SecretKey, } /// PublicKey information for the next hop of Coinswap. -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) struct NextHopInfo { + #[n(0)] + #[cbor(encode_with = "encode_public_key", decode_with = "decode_public_key")] pub(crate) next_multisig_pubkey: PublicKey, + #[n(1)] + #[cbor(encode_with = "encode_public_key", decode_with = "decode_public_key")] pub(crate) next_hashlock_pubkey: PublicKey, } /// Message sent to the Coinswap Receiver that funding transaction has been confirmed. /// Including information for the next hop of the coinswap. -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) struct ProofOfFunding { + #[n(0)] pub(crate) confirmed_funding_txes: Vec, + #[n(1)] pub(crate) next_coinswap_info: Vec, + #[n(2)] pub(crate) refund_locktime: u16, + #[n(3)] pub(crate) contract_feerate: f64, + #[n(4)] pub(crate) id: String, } @@ -153,59 +199,96 @@ pub(crate) struct ProofOfFunding { /// This message from Maker2 will contain the signatures as below: /// `receivers_sigs`: Signatures from Maker1. Maker1 is Sender, and Maker2 is Receiver. /// `senders_sigs`: Signatures from Maker3. Maker3 is Receiver and Maker2 is Sender. -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) struct ContractSigsForRecvrAndSender { /// Sigs from previous peer for Contract Tx of previous hop, (coinswap received by this Maker). + #[n(0)] + #[cbor( + encode_with = "encode_vec_bitcoin_signature", + decode_with = "decode_vec_bitcoin_signature" + )] pub(crate) receivers_sigs: Vec, /// Sigs from the next peer for Contract Tx of next hop, (coinswap sent by this Maker). + #[n(1)] + #[cbor( + encode_with = "encode_vec_bitcoin_signature", + decode_with = "decode_vec_bitcoin_signature" + )] pub(crate) senders_sigs: Vec, /// Unique ID for a swap + #[n(2)] pub(crate) id: String, } /// Message to Transfer [`HashPreimage`] from Taker to Makers. -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) struct HashPreimage { + #[n(0)] + #[cbor( + encode_with = "encode_vec_script_buf", + decode_with = "decode_vec_script_buf" + )] pub(crate) senders_multisig_redeemscripts: Vec, + #[n(1)] + #[cbor( + encode_with = "encode_vec_script_buf", + decode_with = "decode_vec_script_buf" + )] pub(crate) receivers_multisig_redeemscripts: Vec, + #[n(2)] pub(crate) preimage: [u8; 32], } /// Multisig Privatekeys used in the last step of coinswap to perform privatekey handover. -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize, Clone)] pub(crate) struct MultisigPrivkey { + #[n(0)] + #[cbor(encode_with = "encode_script_buf", decode_with = "decode_script_buf")] pub(crate) multisig_redeemscript: ScriptBuf, + #[n(1)] + #[cbor(encode_with = "encode_secret_key", decode_with = "decode_secret_key")] pub(crate) key: SecretKey, } /// Message to perform the final Privatekey Handover. This is the last message of the Coinswap Protocol. -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) struct PrivKeyHandover { + #[n(0)] pub(crate) multisig_privkeys: Vec, /// Unique ID to remove the connection state for a completed swap so watchtowers are not triggered. + #[n(1)] pub(crate) id: String, } /// All messages sent from Taker to Maker. -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) enum TakerToMakerMessage { /// Protocol Handshake. - TakerHello(TakerHello), + #[n(0)] + TakerHello(#[n(0)] TakerHello), /// Request the Maker's Offer advertisement. - ReqGiveOffer(GiveOffer), + #[n(1)] + ReqGiveOffer(#[n(0)] GiveOffer), /// Request Contract Sigs **for** the Sender side of the hop. The Maker receiving this message is the Receiver of the hop. - ReqContractSigsForSender(ReqContractSigsForSender), + #[n(2)] + ReqContractSigsForSender(#[n(0)] ReqContractSigsForSender), /// Respond with the [`ProofOfFunding`] message. This is sent when the funding transaction gets confirmed. - RespProofOfFunding(ProofOfFunding), + #[n(3)] + RespProofOfFunding(#[n(0)] ProofOfFunding), /// Respond with Contract Sigs **for** the Receiver and Sender side of the Hop. - RespContractSigsForRecvrAndSender(ContractSigsForRecvrAndSender), + #[n(4)] + RespContractSigsForRecvrAndSender(#[n(0)] ContractSigsForRecvrAndSender), /// Request Contract Sigs **for** the Receiver side of the hop. The Maker receiving this message is the Sender of the hop. - ReqContractSigsForRecvr(ReqContractSigsForRecvr), + #[n(5)] + ReqContractSigsForRecvr(#[n(0)] ReqContractSigsForRecvr), /// Respond with the hash preimage. This settles the HTLC contract. The Receiver side will use this preimage unlock the HTLC. - RespHashPreimage(HashPreimage), + #[n(6)] + RespHashPreimage(#[n(0)] HashPreimage), /// Respond by handing over the Private Keys of coinswap multisig. This denotes the completion of the whole swap. - RespPrivKeyHandover(PrivKeyHandover), - WaitingFundingConfirmation(String), + #[n(7)] + RespPrivKeyHandover(#[n(0)] PrivKeyHandover), + #[n(8)] + WaitingFundingConfirmation(#[n(0)] String), } impl Display for TakerToMakerMessage { @@ -227,59 +310,92 @@ impl Display for TakerToMakerMessage { } /// Represents the initial handshake message sent from Maker to Taker. -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) struct MakerHello { + #[n(0)] pub(crate) protocol_version_min: u32, + #[n(1)] pub(crate) protocol_version_max: u32, } /// Contains proof data related to fidelity bond. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct FidelityProof { /// Details for Fidelity Bond + #[n(0)] pub bond: FidelityBond, /// Double SHA256 hash of certificate message proving bond ownership and binding to maker address + #[n(1)] + #[cbor( + encode_with = "encode_sha256d_hash", + decode_with = "decode_sha256d_hash" + )] pub cert_hash: Hash, /// ECDSA signature over cert_hash using the bond's private key + #[n(2)] + #[cbor(encode_with = "encode_signature", decode_with = "decode_signature")] pub cert_sig: bitcoin::secp256k1::ecdsa::Signature, } /// Represents an offer in the context of the Coinswap protocol. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Offer { /// Base fee charged per swap in satoshis (fixed cost component) + #[n(0)] pub base_fee: u64, // base fee in sats /// Percentage fee relative to swap amount + #[n(1)] pub amount_relative_fee_pct: f64, // % fee on total amount /// Percentage fee for time-locked funds + #[n(2)] pub time_relative_fee_pct: f64, // amount * refund_locktime * TRF% = fees for locking the fund. /// Minimum confirmations required before proceeding with swap + #[n(3)] pub required_confirms: u32, /// Minimum timelock duration in blocks for contract transactions + #[n(4)] pub minimum_locktime: u16, /// Maximum swap amount accepted in sats + #[n(5)] pub max_size: u64, /// Minimum swap amount accepted in sats + #[n(6)] pub min_size: u64, /// Displayed public key of makers, for receiving swaps. /// Actual swap addresses are derived from this public key using unique nonces per swap. + #[n(7)] + #[cbor(encode_with = "encode_public_key", decode_with = "decode_public_key")] pub tweakable_point: PublicKey, /// Cryptographic proof of fidelity bond for Sybil resistance + #[n(8)] pub fidelity: FidelityProof, } /// Contract Tx signatures provided by a Sender of a Coinswap. -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) struct ContractSigsForSender { + #[n(0)] + #[cbor( + encode_with = "encode_vec_bitcoin_signature", + decode_with = "decode_vec_bitcoin_signature" + )] pub(crate) sigs: Vec, } /// Contract Tx and extra metadata from a Sender of a Coinswap -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) struct SenderContractTxInfo { + #[n(0)] + #[cbor(encode_with = "encode_transaction", decode_with = "decode_transaction")] pub(crate) contract_tx: Transaction, + #[n(1)] + #[cbor(encode_with = "encode_public_key", decode_with = "decode_public_key")] pub(crate) timelock_pubkey: PublicKey, + #[n(2)] + #[cbor(encode_with = "encode_script_buf", decode_with = "decode_script_buf")] pub(crate) multisig_redeemscript: ScriptBuf, + #[n(3)] + #[cbor(encode_with = "encode_amount", decode_with = "decode_amount")] pub(crate) funding_amount: Amount, } @@ -287,35 +403,52 @@ pub(crate) struct SenderContractTxInfo { /// for the Maker as both Sender and Receiver of Coinswaps. /// /// This message is sent by a Maker after a [`ProofOfFunding`] has been received. -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) struct ContractSigsAsRecvrAndSender { /// Contract Tx by which this maker is receiving Coinswap. + #[n(0)] + #[cbor( + encode_with = "encode_vec_transaction", + decode_with = "decode_vec_transaction" + )] pub(crate) receivers_contract_txs: Vec, /// Contract Tx info by which this maker is sending Coinswap. + #[n(1)] pub(crate) senders_contract_txs_info: Vec, } /// Contract Tx signatures a Maker sends as a Receiver of CoinSwap. -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) struct ContractSigsForRecvr { + #[n(0)] + #[cbor( + encode_with = "encode_vec_bitcoin_signature", + decode_with = "decode_vec_bitcoin_signature" + )] pub(crate) sigs: Vec, } /// All messages sent from Maker to Taker. -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) enum MakerToTakerMessage { /// Protocol Handshake. - MakerHello(MakerHello), + #[n(0)] + MakerHello(#[n(0)] MakerHello), /// Send the Maker's offer advertisement. - RespOffer(Box), // Add box as Offer has large size due to fidelity bond + #[n(1)] + RespOffer(#[n(0)] Box), // Add box as Offer has large size due to fidelity bond /// Send Contract Sigs **for** the Sender side of the hop. The Maker sending this message is the Receiver of the hop. - RespContractSigsForSender(ContractSigsForSender), + #[n(2)] + RespContractSigsForSender(#[n(0)] ContractSigsForSender), /// Request Contract Sigs, **as** both the Sending and Receiving side of the hop. - ReqContractSigsAsRecvrAndSender(ContractSigsAsRecvrAndSender), + #[n(3)] + ReqContractSigsAsRecvrAndSender(#[n(0)] ContractSigsAsRecvrAndSender), /// Send Contract Sigs **for** the Receiver side of the hop. The Maker sending this message is the Sender of the hop. - RespContractSigsForRecvr(ContractSigsForRecvr), + #[n(4)] + RespContractSigsForRecvr(#[n(0)] ContractSigsForRecvr), /// Send the multisig private keys of the swap, declaring completion of the contract. - RespPrivKeyHandover(PrivKeyHandover), + #[n(5)] + RespPrivKeyHandover(#[n(0)] PrivKeyHandover), } impl Display for MakerToTakerMessage { @@ -334,3 +467,209 @@ impl Display for MakerToTakerMessage { } } } + +// Inline minicbor helpers +#[allow(dead_code)] +fn encode_transaction( + x: &bitcoin::Transaction, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.bytes(&bitcoin::consensus::serialize(x))?; + Ok(()) +} +fn decode_transaction( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + bitcoin::consensus::deserialize(d.bytes()?) + .map_err(|_| minicbor::decode::Error::message("invalid transaction")) +} + +fn encode_amount( + x: &bitcoin::Amount, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.u64(x.to_sat())?; + Ok(()) +} +fn decode_amount( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + Ok(bitcoin::Amount::from_sat(d.u64()?)) +} + +fn encode_hash160_hash( + x: &bitcoin::hashes::hash160::Hash, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.bytes(&x[..])?; + Ok(()) +} +fn decode_hash160_hash( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + use bitcoin::hashes::Hash; + bitcoin::hashes::hash160::Hash::from_slice(d.bytes()?) + .map_err(|_| minicbor::decode::Error::message("invalid hash")) +} + +fn encode_public_key( + x: &bitcoin::PublicKey, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.encode(x.to_string())?; + Ok(()) +} +fn decode_public_key( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + let s = d.decode::()?; + std::str::FromStr::from_str(&s) + .map_err(|_| minicbor::decode::Error::message("invalid public key")) +} + +fn encode_vec_bitcoin_signature( + x: &Vec, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.array(x.len() as u64)?; + for item in x { + e.bytes(&item.serialize())?; + } + Ok(()) +} +fn decode_vec_bitcoin_signature( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result, minicbor::decode::Error> { + let n = d.array()?.unwrap_or(0) as usize; + let mut v = Vec::with_capacity(n); + for _ in 0..n { + v.push( + bitcoin::ecdsa::Signature::from_slice(d.bytes()?) + .map_err(|_| minicbor::decode::Error::message("invalid sig"))?, + ); + } + Ok(v) +} + +fn encode_secret_key( + x: &bitcoin::secp256k1::SecretKey, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.bytes(&x.secret_bytes())?; + Ok(()) +} +fn decode_secret_key( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + bitcoin::secp256k1::SecretKey::from_slice(d.bytes()?) + .map_err(|_| minicbor::decode::Error::message("invalid secret key")) +} + +fn encode_vec_transaction( + x: &Vec, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.array(x.len() as u64)?; + for item in x { + e.bytes(&bitcoin::consensus::serialize(item))?; + } + Ok(()) +} +fn decode_vec_transaction( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result, minicbor::decode::Error> { + let n = d.array()?.unwrap_or(0) as usize; + let mut v = Vec::with_capacity(n); + for _ in 0..n { + v.push( + bitcoin::consensus::deserialize(d.bytes()?) + .map_err(|_| minicbor::decode::Error::message("invalid tx"))?, + ); + } + Ok(v) +} + +fn encode_sha256d_hash( + x: &bitcoin::hashes::sha256d::Hash, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.bytes(&x[..])?; + Ok(()) +} +fn decode_sha256d_hash( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + use bitcoin::hashes::Hash; + bitcoin::hashes::sha256d::Hash::from_slice(d.bytes()?) + .map_err(|_| minicbor::decode::Error::message("invalid hash")) +} + +fn encode_signature( + x: &bitcoin::secp256k1::ecdsa::Signature, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.bytes(&x.serialize_der())?; + Ok(()) +} +fn decode_signature( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + bitcoin::secp256k1::ecdsa::Signature::from_der(d.bytes()?) + .map_err(|_| minicbor::decode::Error::message("invalid signature")) +} + +fn encode_script_buf( + x: &bitcoin::ScriptBuf, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.bytes(x.as_bytes())?; + Ok(()) +} +fn decode_script_buf( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + Ok(bitcoin::ScriptBuf::from(d.bytes()?.to_vec())) +} + +fn encode_vec_script_buf( + x: &Vec, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.array(x.len() as u64)?; + for item in x { + e.bytes(item.as_bytes())?; + } + Ok(()) +} +fn decode_vec_script_buf( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result, minicbor::decode::Error> { + let n = d.array()?.unwrap_or(0) as usize; + let mut v = Vec::with_capacity(n); + for _ in 0..n { + v.push(bitcoin::ScriptBuf::from(d.bytes()?.to_vec())); + } + Ok(v) +} diff --git a/src/protocol/messages2.rs b/src/protocol/messages2.rs index f209bfc9b..e4862fe09 100644 --- a/src/protocol/messages2.rs +++ b/src/protocol/messages2.rs @@ -92,98 +92,158 @@ impl From for PartialSignature { /// After contract transactions are established and broadcasted, parties exchange /// their outgoing contract private keys to enable independent sweeping without /// requiring MuSig2 coordination. -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize, Clone)] pub struct PrivateKeyHandover { /// Unique 8 byte ID to identify each swap process separately. Always generated by the Takers. + #[n(0)] pub(crate) id: String, /// The outgoing contract private key + #[n(1)] + #[cbor(encode_with = "encode_secret_key", decode_with = "decode_secret_key")] pub(crate) secret_key: bitcoin::secp256k1::SecretKey, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Clone, Serialize, Deserialize)] #[allow(clippy::large_enum_variant)] pub(crate) enum TakerToMakerMessage { - GetOffer(GetOffer), - SwapDetails(SwapDetails), - SendersContract(SendersContract), - PrivateKeyHandover(PrivateKeyHandover), + #[n(0)] + GetOffer(#[n(0)] GetOffer), + #[n(1)] + SwapDetails(#[n(0)] SwapDetails), + #[n(2)] + SendersContract(#[n(0)] SendersContract), + #[n(3)] + PrivateKeyHandover(#[n(0)] PrivateKeyHandover), } -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) enum MakerToTakerMessage { - RespOffer(Box), - AckResponse(AckResponse), - SenderContractFromMaker(SenderContractFromMaker), - PrivateKeyHandover(PrivateKeyHandover), + #[n(0)] + RespOffer(#[n(0)] Box), + #[n(1)] + AckResponse(#[n(0)] AckResponse), + #[n(2)] + SenderContractFromMaker(#[n(0)] SenderContractFromMaker), + #[n(3)] + PrivateKeyHandover(#[n(0)] PrivateKeyHandover), } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Clone, Serialize, Deserialize)] pub(crate) struct GetOffer { + #[n(0)] pub(crate) protocol_version_min: u32, + #[n(1)] pub(crate) protocol_version_max: u32, + #[n(2)] pub(crate) number_of_transactions: u32, } /// An offer from a maker to participate in a coinswap -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize, Clone)] pub struct Offer { /// The tweakable public key for the maker + #[n(0)] + #[cbor(encode_with = "encode_public_key", decode_with = "decode_public_key")] pub tweakable_point: PublicKey, /// Base fee charged by the maker (in satoshis) + #[n(1)] pub base_fee: u64, /// Fee as a percentage relative to the swap amount + #[n(2)] pub amount_relative_fee: f64, /// Fee as a percentage relative to the time lock duration + #[n(3)] pub time_relative_fee: f64, /// Minimum time lock duration required by the maker + #[n(4)] pub minimum_locktime: u16, /// Fidelity proof demonstrating the maker's commitment + #[n(5)] pub fidelity: FidelityProof, /// Minimum swap amount the maker will accept (in satoshis) + #[n(6)] pub min_size: u64, /// Maximum swap amount the maker can handle (in satoshis) + #[n(7)] pub max_size: u64, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Clone, Serialize, Deserialize)] pub(crate) struct SwapDetails { + #[n(0)] pub(crate) id: String, + #[n(1)] + #[cbor(encode_with = "encode_amount", decode_with = "decode_amount")] pub(crate) amount: Amount, + #[n(2)] pub(crate) no_of_tx: u8, + #[n(3)] pub(crate) timelock: u16, } /// `tweakable_point = Some(_)` → swap accepted (Ack) /// `tweakable_point = None` → swap rejected (Nack) -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) struct AckResponse { + #[n(0)] + #[cbor( + encode_with = "encode_opt_public_key", + decode_with = "decode_opt_public_key" + )] pub tweakable_point: Option, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize, Clone)] pub(crate) struct SendersContract { + #[n(0)] pub(crate) id: String, + #[n(1)] + #[cbor(encode_with = "encode_json_bytes", decode_with = "decode_json_bytes")] // quick fix pub(crate) contract_txs: Vec, // Below data is used to verify transaction + #[n(2)] + #[cbor(encode_with = "encode_json_bytes", decode_with = "decode_json_bytes")] pub(crate) pubkeys_a: Vec, + #[n(3)] + #[cbor(encode_with = "encode_json_bytes", decode_with = "decode_json_bytes")] pub(crate) hashlock_scripts: Vec, + #[n(4)] + #[cbor(encode_with = "encode_json_bytes", decode_with = "decode_json_bytes")] pub(crate) timelock_scripts: Vec, // Tweakable point for allowing maker to create next contract + #[n(5)] + #[cbor(encode_with = "encode_public_key", decode_with = "decode_public_key")] pub(crate) next_party_tweakable_point: bitcoin::PublicKey, // MuSig2 data for cooperative spending + #[n(6)] + #[cbor(encode_with = "encode_json_bytes", decode_with = "decode_json_bytes")] pub(crate) internal_key: Option, + #[n(7)] + #[cbor(encode_with = "encode_json_bytes", decode_with = "decode_json_bytes")] pub(crate) tap_tweak: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Serialize, Deserialize)] pub(crate) struct SenderContractFromMaker { + #[n(1)] + #[cbor(encode_with = "encode_json_bytes", decode_with = "decode_json_bytes")] // quick fix pub(crate) contract_txs: Vec, // Below data is used to verify transaction + #[n(2)] + #[cbor(encode_with = "encode_json_bytes", decode_with = "decode_json_bytes")] pub(crate) pubkeys_a: Vec, + #[n(3)] + #[cbor(encode_with = "encode_json_bytes", decode_with = "decode_json_bytes")] pub(crate) hashlock_scripts: Vec, + #[n(4)] + #[cbor(encode_with = "encode_json_bytes", decode_with = "decode_json_bytes")] pub(crate) timelock_scripts: Vec, // MuSig2 data for cooperative spending + #[n(6)] + #[cbor(encode_with = "encode_json_bytes", decode_with = "decode_json_bytes")] pub(crate) internal_key: Option, + #[n(7)] + #[cbor(encode_with = "encode_json_bytes", decode_with = "decode_json_bytes")] pub(crate) tap_tweak: Option, } @@ -195,3 +255,95 @@ pub struct MempoolTx { /// Hex encoded raw transaction pub rawtx: String, } + +// Inline minicbor helpers +#[allow(dead_code)] +fn encode_json_bytes( + x: &T, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.bytes(&serde_json::to_vec(x).map_err(|_| minicbor::encode::Error::message("json error"))?)?; + Ok(()) +} +fn decode_json_bytes( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + serde_json::from_slice(d.bytes()?) + .map_err(|_| minicbor::decode::Error::message("json decode error")) +} + +fn encode_amount( + x: &bitcoin::Amount, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.u64(x.to_sat())?; + Ok(()) +} +fn decode_amount( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + Ok(bitcoin::Amount::from_sat(d.u64()?)) +} + +fn encode_public_key( + x: &bitcoin::PublicKey, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.encode(x.to_string())?; + Ok(()) +} +fn decode_public_key( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + let s = d.decode::()?; + std::str::FromStr::from_str(&s) + .map_err(|_| minicbor::decode::Error::message("invalid public key")) +} + +fn encode_secret_key( + x: &bitcoin::secp256k1::SecretKey, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.bytes(&x.secret_bytes())?; + Ok(()) +} +fn decode_secret_key( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + bitcoin::secp256k1::SecretKey::from_slice(d.bytes()?) + .map_err(|_| minicbor::decode::Error::message("invalid secret key")) +} + +fn encode_opt_public_key( + x: &Option, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + if let Some(pk) = x { + e.encode(pk.to_string())?; + } else { + e.null()?; + } + Ok(()) +} +fn decode_opt_public_key( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result, minicbor::decode::Error> { + if d.datatype()? == minicbor::data::Type::Null { + d.null()?; + return Ok(None); + } + let s = d.decode::()?; + let pk = std::str::FromStr::from_str(&s) + .map_err(|_| minicbor::decode::Error::message("invalid opt public key"))?; + Ok(Some(pk)) +} diff --git a/src/security.rs b/src/security.rs index cd80102dc..54acc0dbc 100644 --- a/src/security.rs +++ b/src/security.rs @@ -25,7 +25,7 @@ use std::{fs, path::Path}; #[derive(Debug)] pub enum EncryptError { /// Error occurred during CBOR serialization of the input struct. - Serialization(serde_cbor::Error), + Serialization(String), /// Error occurred during AES-GCM encryption. /// /// Note: This error type carries no additional information because @@ -33,9 +33,15 @@ pub enum EncryptError { Encryption, } -impl From for EncryptError { - fn from(err: serde_cbor::Error) -> Self { - EncryptError::Serialization(err) +impl From for EncryptError { + fn from(err: minicbor::decode::Error) -> Self { + EncryptError::Serialization(err.to_string()) + } +} + +impl From> for EncryptError { + fn from(err: minicbor::encode::Error) -> Self { + EncryptError::Serialization(format!("{:?}", err)) } } @@ -54,7 +60,7 @@ impl From for EncryptError { /// the file in both formats. /// /// **Note on CBOR:** -/// Due to potential trailing bytes left by `serde_cbor`, the CBOR variant delegates +/// Due to potential trailing bytes left by `crate::utill::cbor`, the CBOR variant delegates /// parsing to [`utill::deserialize_from_cbor`] that strips extra data before deserialization. /// /// This trait is **not** intended for general-purpose serialization or format negotiation. @@ -67,7 +73,9 @@ pub trait SerdeFormat { type Error: std::error::Error + Send + Sync + 'static; #[allow(missing_docs)] - fn from_slice(input: &[u8]) -> Result; + fn from_slice minicbor::Decode<'a, ()>>( + input: &[u8], + ) -> Result; } /// JSON implementation of `SerdeFormat`, using `serde_json`. /// @@ -76,21 +84,25 @@ pub struct SerdeJson; impl SerdeFormat for SerdeJson { type Error = serde_json::Error; - fn from_slice(input: &[u8]) -> Result { + fn from_slice minicbor::Decode<'a, ()>>( + input: &[u8], + ) -> Result { serde_json::from_slice(input) } } /// CBOR implementation of `SerdeFormat`, using a utility wrapper /// that handles CBOR trailing data properly. /// -/// `serde_cbor` may leave trailing data behind, which can cause +/// `crate::utill::cbor` may leave trailing data behind, which can cause /// parsing errors. /// This wrapper [`utill::deserialize_from_cbor`] utility method to cleanly deserialize. pub struct SerdeCbor; impl SerdeFormat for SerdeCbor { - type Error = serde_cbor::Error; - fn from_slice(input: &[u8]) -> Result { + type Error = minicbor::decode::Error; + fn from_slice minicbor::Decode<'a, ()>>( + input: &[u8], + ) -> Result { utill::deserialize_from_cbor::(input.to_vec()) } } @@ -208,20 +220,23 @@ impl KeyMaterial { /// refers to the same value as the nonce. They are conceptually the same in this context. /// /// This wrapper itself is then serialized to CBOR and written to disk. -#[derive(Serialize, Deserialize, Debug)] +#[derive(minicbor::Encode, minicbor::Decode, Serialize, Deserialize, Debug)] pub struct EncryptedData { /// Nonce used for AES-GCM encryption (must match during decryption). + #[n(0)] nonce: EncryptionNonce, /// AES-GCM-encrypted CBOR-serialized plaintext struct data. + #[n(1)] encrypted_payload: Vec, /// Salt for the PBKDF2 key generation + #[n(2)] pbkdf2_salt: PBKDF2Salt, } /// Encrypts a serializable struct using AES-256-GCM encryption and CBOR serialization. /// /// This function applies the following transformation pipeline: -/// `Struct -> serde_cbor::ser::to_vec(Struct) -> AES-GCM(encrypted_bytes) = encrypted_payload -> EncryptedData { encrypted_payload, nonce }` +/// `Struct -> minicbor::to_vec(Struct) -> AES-GCM(encrypted_bytes) = encrypted_payload -> EncryptedData { encrypted_payload, nonce }` /// /// /// The struct is first serialized into CBOR bytes, then encrypted using AES-GCM @@ -230,12 +245,12 @@ pub struct EncryptedData { /// /// The resulting `EncryptedData` can be serialized and stored to disk. To decrypt it later, /// use [`decrypt_struct`]. -pub fn encrypt_struct( +pub fn encrypt_struct>( plain_struct: T, enc_material: &KeyMaterial, ) -> Result { // Serialize wallet data to bytes. - let packed_store = serde_cbor::ser::to_vec(&plain_struct)?; + let packed_store = minicbor::to_vec(&plain_struct)?; // Extract nonce and key for AES-GCM. let material_nonce = enc_material.nonce; @@ -264,10 +279,10 @@ pub fn encrypt_struct( /// /// It uses the AES-256-GCM key and nonce in [`KeyMaterial`] to decrypt the /// encrypted CBOR payload, then deserializes it into the original struct. -pub fn decrypt_struct( +pub fn decrypt_struct minicbor::Decode<'a, ()>>( encrypted_struct: EncryptedData, enc_material: &KeyMaterial, -) -> Result { +) -> Result { // Deserialize the outer EncryptedWalletStore wrapper. let nonce_vec = encrypted_struct.nonce; @@ -299,7 +314,10 @@ pub fn decrypt_struct( /// # Type Parameters /// - `T`: The struct type to load. /// - `F`: A type implementing [`SerdeFormat`] (`SerdeCbor` or `SerdeJson`). -pub fn load_sensitive_struct( +pub fn load_sensitive_struct< + T: DeserializeOwned + for<'a> minicbor::Decode<'a, ()>, + F: SerdeFormat, +>( file: &Path, password: Option, ) -> (T, Option) { diff --git a/src/taker/api2.rs b/src/taker/api2.rs index d69105c41..e74dc9e03 100644 --- a/src/taker/api2.rs +++ b/src/taker/api2.rs @@ -832,7 +832,7 @@ impl Taker { // Read response let response_bytes = read_message(&mut socket)?; - let response: MakerToTakerMessage = serde_cbor::from_slice(&response_bytes)?; + let response: MakerToTakerMessage = minicbor::decode(&response_bytes)?; log::info!("<=== {} | {maker_addr}", response); Ok(response) diff --git a/src/taker/error.rs b/src/taker/error.rs index 693196ed6..768b649dd 100644 --- a/src/taker/error.rs +++ b/src/taker/error.rs @@ -48,8 +48,8 @@ impl From for TakerError { } } -impl From for TakerError { - fn from(value: serde_cbor::Error) -> Self { +impl From for TakerError { + fn from(value: minicbor::decode::Error) -> Self { Self::Deserialize(value.to_string()) } } diff --git a/src/taker/offers.rs b/src/taker/offers.rs index 4a5c5d4a3..3d67b96a4 100644 --- a/src/taker/offers.rs +++ b/src/taker/offers.rs @@ -64,15 +64,19 @@ const UNRESPONSIVE_MAKER_BACKOFF_STEP: Duration = Duration::from_secs(30 * 60); const UNRESPONSIVE_MAKER_BACKOFF_STEP: Duration = Duration::from_secs(10); /// Represents an offer along with the corresponding maker address. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct OfferAndAddress { /// Details for Maker Offer + #[n(0)] pub offer: Offer, /// Maker Address: onion_addr:port + #[n(1)] pub address: MakerAddress, /// Current state of maker + #[n(2)] pub state: MakerState, /// Supporting protocol (Legacy or Taproot) + #[n(3)] pub protocol: MakerProtocol, } @@ -139,27 +143,35 @@ impl MakerOfferCandidate { } /// Represents the Maker connection state -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive( + minicbor::Encode, minicbor::Decode, Debug, Clone, Serialize, Deserialize, PartialEq, Eq, +)] pub enum MakerState { /// Maker is responding to offer calls. + #[n(0)] Good, /// Maker is not responding to offer calls. + #[n(1)] Unresponsive { /// We allow only 10 retries before marking /// a maker as bad. + #[n(0)] retries: u8, }, /// Maker either explicitly or because not responding /// is marked bad. + #[n(2)] Bad, } /// Protocol which maker follows -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum MakerProtocol { /// Legacy + #[n(0)] Legacy, /// Taproot + #[n(1)] Taproot, } @@ -172,15 +184,42 @@ impl fmt::Display for MakerProtocol { } } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive( + minicbor::Encode, + minicbor::Decode, + Clone, + Debug, + Serialize, + Deserialize, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, +)] struct OnionAddress { + #[n(0)] port: String, + #[n(1)] onion_addr: String, } /// Enum representing maker addresses. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)] -pub struct MakerAddress(OnionAddress); +#[derive( + minicbor::Encode, + minicbor::Decode, + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + Hash, +)] +#[cbor(transparent)] +pub struct MakerAddress(#[n(0)] OnionAddress); impl fmt::Display for MakerAddress { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -788,7 +827,7 @@ impl MakerAddress { send_message(&mut socket, &TakerToMakerMessage::ReqGiveOffer(GiveOffer))?; let msg_bytes = read_message(&mut socket)?; - let msg: MakerToTakerMessage = serde_cbor::from_slice(&msg_bytes)?; + let msg: MakerToTakerMessage = minicbor::decode(&msg_bytes)?; let offer = match msg { MakerToTakerMessage::RespOffer(offer) => offer, msg => { @@ -823,7 +862,7 @@ impl MakerAddress { let response_bytes = read_message(&mut socket)?; - let response: messages2::MakerToTakerMessage = serde_cbor::from_slice(&response_bytes)?; + let response: messages2::MakerToTakerMessage = minicbor::decode(&response_bytes)?; let taproot_offer = match response { messages2::MakerToTakerMessage::RespOffer(offer) => *offer, diff --git a/src/taker/routines.rs b/src/taker/routines.rs index bc0c7f51a..fe13a2093 100644 --- a/src/taker/routines.rs +++ b/src/taker/routines.rs @@ -44,7 +44,7 @@ pub(crate) fn handshake_maker(socket: &mut TcpStream) -> Result<(), TakerError> }), )?; let msg_bytes = read_message(socket)?; - let msg: MakerToTakerMessage = serde_cbor::from_slice(&msg_bytes)?; + let msg: MakerToTakerMessage = minicbor::decode(&msg_bytes)?; // Check that protocol version is always 1. match msg { @@ -107,7 +107,7 @@ pub(crate) fn req_sigs_for_sender_once( )?; let msg_bytes = read_message(socket)?; - let msg: MakerToTakerMessage = serde_cbor::from_slice(&msg_bytes)?; + let msg: MakerToTakerMessage = minicbor::decode(&msg_bytes)?; let contract_sigs_for_sender = match msg { MakerToTakerMessage::RespContractSigsForSender(m) => { if m.sigs.len() != outgoing_swapcoins.len() { @@ -162,7 +162,7 @@ pub(crate) fn req_sigs_for_recvr_once( )?; let msg_bytes = read_message(socket)?; - let msg: MakerToTakerMessage = serde_cbor::from_slice(&msg_bytes)?; + let msg: MakerToTakerMessage = minicbor::decode(&msg_bytes)?; let contract_sigs_for_recvr = match msg { MakerToTakerMessage::RespContractSigsForRecvr(m) => { if m.sigs.len() != incoming_swapcoins.len() { @@ -244,7 +244,7 @@ pub(crate) fn send_proof_of_funding_and_init_next_hop( // Recv ContractSigsAsRecvrAndSender. let msg_bytes = read_message(socket)?; - let msg: MakerToTakerMessage = serde_cbor::from_slice(&msg_bytes)?; + let msg: MakerToTakerMessage = minicbor::decode(&msg_bytes)?; let contract_sigs_as_recvr_and_sender = match msg { MakerToTakerMessage::ReqContractSigsAsRecvrAndSender(m) => { if m.receivers_contract_txs.len() != tmi.funding_tx_infos.len() { @@ -382,7 +382,7 @@ pub(crate) fn send_hash_preimage_and_get_private_keys( send_message(socket, &hash_preimage_msg)?; let msg_bytes = read_message(socket)?; - let msg: MakerToTakerMessage = serde_cbor::from_slice(&msg_bytes)?; + let msg: MakerToTakerMessage = minicbor::decode(&msg_bytes)?; let privkey_handover = match msg { MakerToTakerMessage::RespPrivKeyHandover(m) => { if m.multisig_privkeys.len() != receivers_multisig_redeemscripts.len() { diff --git a/src/utill.rs b/src/utill.rs index 7f6358c52..96185f1b1 100644 --- a/src/utill.rs +++ b/src/utill.rs @@ -204,10 +204,10 @@ pub fn setup_logger(filter: LevelFilter, data_dir: Option) { /// The first byte sent is the length of the actual message. pub fn send_message( socket_writer: &mut TcpStream, - message: &impl serde::Serialize, + message: &(impl serde::Serialize + minicbor::Encode<()>), ) -> Result<(), NetError> { let mut writer = BufWriter::new(socket_writer); - let msg_bytes = serde_cbor::ser::to_vec(message)?; + let msg_bytes = minicbor::to_vec(message)?; let msg_len = (msg_bytes.len() as u32).to_be_bytes(); let mut to_send = Vec::with_capacity(msg_bytes.len() + msg_len.len()); to_send.extend(msg_len); @@ -482,7 +482,9 @@ pub enum TorError { /// Generic error General(String), /// Cbor error - Serde(serde_cbor::Error), + Serde(minicbor::decode::Error), + /// Metadata mismatch between actual value and expected value + MetadataMismatch(String), } impl From for TorError { @@ -491,8 +493,14 @@ impl From for TorError { } } -impl From for TorError { - fn from(value: serde_cbor::Error) -> Self { +impl From> for TorError { + fn from(err: minicbor::encode::Error) -> Self { + TorError::General(format!("Encode error: {:?}", err)) + } +} + +impl From for TorError { + fn from(value: minicbor::decode::Error) -> Self { TorError::Serde(value) } } @@ -681,10 +689,10 @@ pub(crate) fn get_tor_hostname( if tor_config_path.exists() { if let Ok(tor_metadata) = fs::read(&tor_config_path) { - let data: [&str; 2] = serde_cbor::de::from_slice(&tor_metadata)?; + let data: [String; 2] = minicbor::decode(&tor_metadata)?; - let hostname_data = data[1]; - let private_key_data = data[0]; + let hostname_data = &data[1]; + let private_key_data = &data[0]; let (hostname, private_key) = get_emphemeral_address( control_port, @@ -694,8 +702,19 @@ pub(crate) fn get_tor_hostname( Some(hostname_data.replace(".onion", "").as_str()), )?; - assert_eq!(hostname, hostname_data); - assert_eq!(private_key, private_key_data); + if hostname != *hostname_data { + return Err(TorError::MetadataMismatch(format!( + "Hostname mismatch: expected {}, got {}", + hostname_data, hostname + ))); + } + + if private_key != *private_key_data { + return Err(TorError::MetadataMismatch(format!( + "Private key mismatch: expected {}, got {}", + private_key_data, private_key + ))); + } log::info!("Generated existing Tor Hidden Service Hostname: {hostname}"); @@ -712,7 +731,7 @@ pub(crate) fn get_tor_hostname( fs::write( &tor_config_path, - serde_cbor::ser::to_vec(&[private_key, hostname.clone()])?, + minicbor::to_vec(&[private_key, hostname.clone()])?, )?; log::info!("Generated new Tor Hidden Service Hostname: {hostname}"); @@ -721,11 +740,11 @@ pub(crate) fn get_tor_hostname( } /// Deserialize any generic type from a CBOR file. The type should impl [serde::de::Deserialize]. -pub fn deserialize_from_cbor(mut reader: Vec) -> Result +pub fn deserialize_from_cbor(mut reader: Vec) -> Result where - T: serde::de::DeserializeOwned, + T: serde::de::DeserializeOwned + for<'a> minicbor::Decode<'a, ()>, { - match serde_cbor::from_slice::(&reader) { + match minicbor::decode::(&reader) { Ok(store) => Ok(store), Err(e) => { let err_string = format!("{e:?}"); @@ -734,7 +753,7 @@ where log::info!("Wallet file has trailing data, trying to restore"); loop { reader.pop(); - match serde_cbor::from_slice::(&reader) { + match minicbor::decode::(&reader) { Ok(store) => break Ok(store), Err(_) => continue, } @@ -1000,7 +1019,7 @@ mod tests { thread::spawn(move || { let (mut socket, _) = listener.accept().unwrap(); let msg_bytes = read_message(&mut socket).unwrap(); - let msg: MakerToTakerMessage = serde_cbor::from_slice(&msg_bytes).unwrap(); + let msg: MakerToTakerMessage = minicbor::decode(&msg_bytes).unwrap(); if let MakerToTakerMessage::MakerHello(hello) = msg { assert!(hello.protocol_version_min == 1 && hello.protocol_version_max == 100); diff --git a/src/wallet/backup.rs b/src/wallet/backup.rs index 58ded7c7e..b00bafa73 100644 --- a/src/wallet/backup.rs +++ b/src/wallet/backup.rs @@ -17,15 +17,19 @@ use std::path::Path; /// This struct captures the essential elements of a wallet's state, including /// its network, master key, creation time, and file name. It is serializable /// and can be persisted to disk or transferred for backup purposes. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, Clone, Serialize, Deserialize)] pub struct WalletBackup { /// Network the wallet operates on. + #[cbor(n(0), encode_with = "encode_network", decode_with = "decode_network")] pub(crate) network: Network, //Can be asked to the user, but is nice to save /// The master key for the wallet. + #[cbor(n(1), encode_with = "encode_xpriv", decode_with = "decode_xpriv")] pub(super) master_key: Xpriv, + #[n(2)] pub(super) wallet_birthday: Option, //Avoid scanning from genesis block /// The file name associated with the wallet store. + #[n(3)] pub file_name: String, //Can be asked to user, or stored for convenience } impl From<&Wallet> for WalletBackup { @@ -208,3 +212,35 @@ impl Wallet { } } } + +// Inline minicbor helpers +pub(crate) fn encode_network( + x: &Network, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.encode(x.to_string())?; + Ok(()) +} +pub(crate) fn decode_network( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + let s = d.decode::()?; + std::str::FromStr::from_str(&s).map_err(|_| minicbor::decode::Error::message("invalid network")) +} +pub(crate) fn encode_xpriv( + x: &Xpriv, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.encode(x.to_string())?; + Ok(()) +} +pub(crate) fn decode_xpriv( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + let s = d.decode::()?; + std::str::FromStr::from_str(&s).map_err(|_| minicbor::decode::Error::message("invalid xpriv")) +} diff --git a/src/wallet/error.rs b/src/wallet/error.rs index bf88d624d..6a5b8c379 100644 --- a/src/wallet/error.rs +++ b/src/wallet/error.rs @@ -18,7 +18,7 @@ pub enum WalletError { /// Represents an error during CBOR (Concise Binary Object Representation) serialization or deserialization. /// /// This is used for encoding/decoding data structures. - Cbor(serde_cbor::Error), + Cbor(minicbor::decode::Error), /// Represents an error during JSON serialization or deserialization. /// @@ -114,8 +114,8 @@ impl From for WalletError { Self::Protocol(value) } } -impl From for WalletError { - fn from(value: serde_cbor::Error) -> Self { +impl From for WalletError { + fn from(value: minicbor::decode::Error) -> Self { Self::Cbor(value) } } diff --git a/src/wallet/ffi.rs b/src/wallet/ffi.rs index 6aae0bdcd..92165fb83 100644 --- a/src/wallet/ffi.rs +++ b/src/wallet/ffi.rs @@ -115,7 +115,7 @@ impl Wallet { // Try to deserialize as EncryptedData using CBOR // If it succeeds, the wallet is encrypted // If it fails, the wallet is plaintext - match serde_cbor::from_slice::(&content) { + match minicbor::decode::(&content) { Ok(_) => Ok(true), // Successfully parsed as EncryptedData = encrypted Err(_) => Ok(false), // Failed to parse as EncryptedData = plaintext } diff --git a/src/wallet/fidelity.rs b/src/wallet/fidelity.rs index d1d34a6c5..8d69dd88b 100644 --- a/src/wallet/fidelity.rs +++ b/src/wallet/fidelity.rs @@ -206,19 +206,41 @@ fn calculate_fidelity_value( /// Structure describing a Fidelity Bond. /// Fidelity Bonds are described here : -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Hash)] +#[derive( + minicbor::Encode, + minicbor::Decode, + Debug, + Clone, + PartialEq, + Eq, + Serialize, + Deserialize, + PartialOrd, + Hash, +)] pub struct FidelityBond { + #[n(0)] + #[cbor(encode_with = "encode_outpoint", decode_with = "decode_outpoint")] pub(crate) outpoint: OutPoint, /// Fidelity Amount + #[n(1)] + #[cbor(encode_with = "encode_amount", decode_with = "decode_amount")] pub amount: Amount, /// Fidelity Locktime + #[n(2)] + #[cbor(encode_with = "encode_lock_time", decode_with = "decode_lock_time")] pub lock_time: LockTime, + #[n(3)] + #[cbor(encode_with = "encode_public_key", decode_with = "decode_public_key")] pub(crate) pubkey: PublicKey, // Height at which the bond was confirmed. + #[n(4)] pub(crate) conf_height: Option, // Cert expiry denoted in multiple of difficulty adjustment period (2016 blocks) + #[n(5)] pub(crate) cert_expiry: Option, /// Whether this bond is spent or not. + #[n(6)] pub(crate) is_spent: bool, } @@ -749,3 +771,68 @@ fn test_fidleity_redeemscripts() { assert_eq!(lt, read_locktime_from_fidelity_script(&script).unwrap()); } } + +// Inline minicbor helpers +#[allow(dead_code)] +fn encode_outpoint( + x: &bitcoin::OutPoint, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.encode(x.to_string())?; + Ok(()) +} +fn decode_outpoint( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + std::str::FromStr::from_str(&d.decode::()?) + .map_err(|_| minicbor::decode::Error::message("invalid outpoint")) +} + +fn encode_amount( + x: &bitcoin::Amount, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.u64(x.to_sat())?; + Ok(()) +} +fn decode_amount( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + Ok(bitcoin::Amount::from_sat(d.u64()?)) +} + +fn encode_public_key( + x: &bitcoin::PublicKey, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.encode(x.to_string())?; + Ok(()) +} +fn decode_public_key( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + let s = d.decode::()?; + std::str::FromStr::from_str(&s) + .map_err(|_| minicbor::decode::Error::message("invalid public key")) +} + +fn encode_lock_time( + x: &bitcoin::absolute::LockTime, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.u32(x.to_consensus_u32())?; + Ok(()) +} +fn decode_lock_time( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + Ok(bitcoin::absolute::LockTime::from_consensus(d.u32()?)) +} diff --git a/src/wallet/storage.rs b/src/wallet/storage.rs index 54dd0cde2..a86284a91 100644 --- a/src/wallet/storage.rs +++ b/src/wallet/storage.rs @@ -14,7 +14,6 @@ use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, HashSet}, fs::{self, File}, - io::BufWriter, path::Path, }; @@ -36,38 +35,93 @@ pub enum AddressType { } /// Represents the internal data store for a Bitcoin wallet. -#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[derive(minicbor::Encode, minicbor::Decode, Debug, PartialEq, Serialize, Deserialize)] pub(crate) struct WalletStore { /// The file name associated with the wallet store. + #[n(0)] pub(crate) file_name: String, /// Network the wallet operates on. + #[cbor( + n(1), + encode_with = "crate::wallet::backup::encode_network", + decode_with = "crate::wallet::backup::decode_network" + )] pub(crate) network: Network, /// The master key for the wallet. + #[cbor( + n(2), + encode_with = "crate::wallet::backup::encode_xpriv", + decode_with = "crate::wallet::backup::decode_xpriv" + )] pub(super) master_key: Xpriv, /// The external index for the wallet. + #[n(3)] pub(super) external_index: u32, /// The maximum size for an offer in the wallet. + #[n(4)] pub(crate) offer_maxsize: u64, /// Map of multisig redeemscript to incoming swapcoins. + #[cbor( + n(5), + encode_with = "encode_json_bytes", + decode_with = "decode_json_bytes" + )] pub(super) incoming_swapcoins: HashMap, /// Map of multisig redeemscript to outgoing swapcoins. + #[cbor( + n(6), + encode_with = "encode_json_bytes", + decode_with = "decode_json_bytes" + )] pub(super) outgoing_swapcoins: HashMap, /// Map of taproot contract txid to incoming taproot swapcoins. + #[cbor( + n(7), + encode_with = "encode_json_bytes", + decode_with = "decode_json_bytes" + )] pub(super) incoming_swapcoins_v2: HashMap, /// Map of taproot contract txid to outgoing taproot swapcoins. + #[cbor( + n(8), + encode_with = "encode_json_bytes", + decode_with = "decode_json_bytes" + )] pub(super) outgoing_swapcoins_v2: HashMap, /// Map of prevout to contract redeemscript. + #[cbor( + n(9), + encode_with = "encode_json_bytes", + decode_with = "decode_json_bytes" + )] pub(super) prevout_to_contract_map: HashMap, /// Set of swept incoming swap coin scriptpubkeys to prevent mixing with regular UTXOs + #[cbor( + n(10), + encode_with = "encode_json_bytes", + decode_with = "decode_json_bytes" + )] pub(crate) swept_incoming_swapcoins: HashSet, /// Map for all the fidelity bond information. + #[cbor( + n(11), + encode_with = "encode_json_bytes", + decode_with = "decode_json_bytes" + )] pub(crate) fidelity_bond: HashMap, + #[n(12)] pub(super) last_synced_height: Option, + #[n(13)] pub(super) wallet_birthday: Option, /// Maps transaction outpoints to their associated UTXO and spend information. #[serde(default)] // Ensures deserialization works if `utxo_cache` is missing + #[cbor( + n(14), + encode_with = "encode_json_bytes", + decode_with = "decode_json_bytes" + )] pub(super) utxo_cache: HashMap, } @@ -115,12 +169,6 @@ impl WalletStore { path: &Path, store_enc_material: &Option, ) -> Result<(), WalletError> { - let wallet_file = fs::OpenOptions::new() - .write(true) - .truncate(true) - .open(path)?; - let writer = BufWriter::new(wallet_file); - match store_enc_material { Some(material) => { // Encryption branch: encrypt the serialized wallet before writing. @@ -128,11 +176,18 @@ impl WalletStore { let encrypted = encrypt_struct(self, material).unwrap(); // Write encrypted wallet data to disk. - serde_cbor::to_writer(writer, &encrypted)?; + fs::write( + path, + minicbor::to_vec(&encrypted) + .map_err(|e| WalletError::General(e.to_string()))?, + )?; } None => { // No encryption: serialize and write the wallet directly. - serde_cbor::to_writer(writer, &self)?; + fs::write( + path, + minicbor::to_vec(self).map_err(|e| WalletError::General(e.to_string()))?, + )?; } } Ok(()) @@ -152,6 +207,25 @@ impl WalletStore { } } +// Inline minicbor helpers +#[allow(dead_code)] +fn encode_json_bytes( + x: &T, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.bytes(&serde_json::to_vec(x).map_err(|_| minicbor::encode::Error::message("json error"))?)?; + Ok(()) +} +#[allow(dead_code)] +fn decode_json_bytes( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + serde_json::from_slice(d.bytes()?) + .map_err(|_| minicbor::decode::Error::message("json decode error")) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/wallet/swapcoin.rs b/src/wallet/swapcoin.rs index 83db643a8..14c056ae8 100644 --- a/src/wallet/swapcoin.rs +++ b/src/wallet/swapcoin.rs @@ -41,16 +41,48 @@ use crate::protocol::{ /// The coin that Bob receives from Alice is referred to as `Incoming` from Bob's perspective. /// This designation applies regardless of the swap's status—whether /// it is still in progress or has been finalized. -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive( + minicbor::Encode, + minicbor::Decode, + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, +)] pub(crate) struct IncomingSwapCoin { + #[n(0)] + #[cbor(encode_with = "encode_secret_key", decode_with = "decode_secret_key")] pub(crate) my_privkey: SecretKey, + #[n(1)] + #[cbor(encode_with = "encode_public_key", decode_with = "decode_public_key")] pub(crate) other_pubkey: PublicKey, + #[n(2)] + #[cbor( + encode_with = "encode_opt_secret_key", + decode_with = "decode_opt_secret_key" + )] pub(crate) other_privkey: Option, + #[n(3)] + #[cbor(encode_with = "encode_transaction", decode_with = "decode_transaction")] pub(crate) contract_tx: Transaction, + #[n(4)] + #[cbor(encode_with = "encode_script_buf", decode_with = "decode_script_buf")] pub(crate) contract_redeemscript: ScriptBuf, + #[n(5)] + #[cbor(encode_with = "encode_secret_key", decode_with = "decode_secret_key")] pub(crate) hashlock_privkey: SecretKey, + #[n(6)] + #[cbor(encode_with = "encode_amount", decode_with = "decode_amount")] pub(crate) funding_amount: Amount, + #[n(7)] + #[cbor( + encode_with = "encode_opt_signature", + decode_with = "decode_opt_signature" + )] pub(crate) others_contract_sig: Option, + #[n(8)] pub(crate) hash_preimage: Option, } @@ -67,15 +99,42 @@ pub(crate) struct IncomingSwapCoin { /// the coin that Alice sends to Bob is referred to as `Outgoing` from Alice's perspective. /// This terminology reflects the direction of the asset transfer, /// regardless of whether the swap is still ongoing or has been completed. -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive( + minicbor::Encode, + minicbor::Decode, + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, +)] pub(crate) struct OutgoingSwapCoin { + #[n(0)] + #[cbor(encode_with = "encode_secret_key", decode_with = "decode_secret_key")] pub(crate) my_privkey: SecretKey, + #[n(1)] + #[cbor(encode_with = "encode_public_key", decode_with = "decode_public_key")] pub(crate) other_pubkey: PublicKey, + #[n(2)] + #[cbor(encode_with = "encode_transaction", decode_with = "decode_transaction")] pub(crate) contract_tx: Transaction, + #[n(3)] + #[cbor(encode_with = "encode_script_buf", decode_with = "decode_script_buf")] pub(crate) contract_redeemscript: ScriptBuf, + #[n(4)] + #[cbor(encode_with = "encode_secret_key", decode_with = "decode_secret_key")] pub(crate) timelock_privkey: SecretKey, + #[n(5)] + #[cbor(encode_with = "encode_amount", decode_with = "decode_amount")] pub(crate) funding_amount: Amount, + #[n(6)] + #[cbor( + encode_with = "encode_opt_signature", + decode_with = "decode_opt_signature" + )] pub(crate) others_contract_sig: Option, + #[n(7)] pub(crate) hash_preimage: Option, } @@ -1006,3 +1065,139 @@ mod tests { assert!(final_return.is_ok()); } } + +// Inline minicbor helpers +#[allow(dead_code)] +fn encode_transaction( + x: &bitcoin::Transaction, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.bytes(&bitcoin::consensus::serialize(x))?; + Ok(()) +} +fn decode_transaction( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + bitcoin::consensus::deserialize(d.bytes()?) + .map_err(|_| minicbor::decode::Error::message("invalid transaction")) +} + +fn encode_amount( + x: &bitcoin::Amount, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.u64(x.to_sat())?; + Ok(()) +} +fn decode_amount( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + Ok(bitcoin::Amount::from_sat(d.u64()?)) +} + +fn encode_public_key( + x: &bitcoin::PublicKey, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.encode(x.to_string())?; + Ok(()) +} +fn decode_public_key( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + let s = d.decode::()?; + std::str::FromStr::from_str(&s) + .map_err(|_| minicbor::decode::Error::message("invalid public key")) +} + +fn encode_secret_key( + x: &bitcoin::secp256k1::SecretKey, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.bytes(&x.secret_bytes())?; + Ok(()) +} +fn decode_secret_key( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + bitcoin::secp256k1::SecretKey::from_slice(d.bytes()?) + .map_err(|_| minicbor::decode::Error::message("invalid secret key")) +} + +fn encode_script_buf( + x: &bitcoin::ScriptBuf, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.bytes(x.as_bytes())?; + Ok(()) +} +fn decode_script_buf( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + Ok(bitcoin::ScriptBuf::from(d.bytes()?.to_vec())) +} + +#[allow(dead_code)] +fn encode_opt_secret_key( + x: &Option, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + if let Some(sk) = x { + e.bytes(&sk.secret_bytes())?; + } else { + e.null()?; + } + Ok(()) +} +#[allow(dead_code)] +fn decode_opt_secret_key( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result, minicbor::decode::Error> { + if d.datatype()? == minicbor::data::Type::Null { + d.null()?; + Ok(None) + } else { + SecretKey::from_slice(d.bytes()?) + .map(Some) + .map_err(|_| minicbor::decode::Error::message("invalid secret key")) + } +} +#[allow(dead_code)] +fn encode_opt_signature( + x: &Option, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + if let Some(sig) = x { + e.bytes(&sig.serialize())?; + } else { + e.null()?; + } + Ok(()) +} +#[allow(dead_code)] +fn decode_opt_signature( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result, minicbor::decode::Error> { + if d.datatype()? == minicbor::data::Type::Null { + d.null()?; + Ok(None) + } else { + Signature::from_slice(d.bytes()?) + .map(Some) + .map_err(|_| minicbor::decode::Error::message("invalid signature")) + } +} diff --git a/src/watch_tower/registry_storage.rs b/src/watch_tower/registry_storage.rs index d141ca3ff..f81f39d6f 100644 --- a/src/watch_tower/registry_storage.rs +++ b/src/watch_tower/registry_storage.rs @@ -42,13 +42,82 @@ pub struct Checkpoint { pub hash: BlockHash, } -#[derive(Serialize, Deserialize, Default)] +#[derive(Default)] struct RegistryData { watches: HashMap, fidelity: HashSet, checkpoint: Option, } +impl minicbor::Encode<()> for RegistryData { + fn encode( + &self, + e: &mut minicbor::Encoder, + _ctx: &mut (), + ) -> Result<(), minicbor::encode::Error> { + e.map(self.watches.len() as u64)?; + for (k, v) in &self.watches { + encode_outpoint(k, e, &mut ())?; + let v_bytes = serde_json::to_vec(v) + .map_err(|_| minicbor::encode::Error::message("failed to encode watch request"))?; + e.bytes(&v_bytes)?; + } + e.array(self.fidelity.len() as u64)?; + for f in &self.fidelity { + let f_bytes = serde_json::to_vec(f) + .map_err(|_| minicbor::encode::Error::message("failed to encode watch fidelity"))?; + e.bytes(&f_bytes)?; + } + if let Some(cp) = &self.checkpoint { + e.array(2)?; + e.u64(cp.height)?; + encode_sha256d_hash(cp.hash.as_raw_hash(), e, &mut ())?; + } else { + e.null()?; + } + Ok(()) + } +} + +impl<'b> minicbor::Decode<'b, ()> for RegistryData { + fn decode(d: &mut minicbor::Decoder<'b>, _: &mut ()) -> Result { + let mut watches = HashMap::new(); + if let Ok(Some(n)) = d.map() { + for _ in 0..n { + let k = decode_outpoint(d, &mut ())?; + let v: WatchRequest = serde_json::from_slice(d.bytes()?) + .map_err(|_| minicbor::decode::Error::message("invalid wrap"))?; + watches.insert(k, v); + } + } + let mut fidelity = HashSet::new(); + if let Ok(Some(n)) = d.array() { + for _ in 0..n { + let f: Fidelity = serde_json::from_slice(d.bytes()?) + .map_err(|_| minicbor::decode::Error::message("invalid wrap"))?; + fidelity.insert(f); + } + } + let checkpoint = if d.datatype()? == minicbor::data::Type::Null { + d.null()?; + None + } else { + d.array()?; + let height = d.u64()?; + let hash = decode_sha256d_hash(d, &mut ())?; + Some(Checkpoint { + height, + hash: hash.into(), + }) + }; + Ok(RegistryData { + watches, + fidelity, + checkpoint, + }) + } +} + /// Registry used by the watcher. #[derive(Clone)] pub struct FileRegistry { @@ -62,9 +131,13 @@ impl FileRegistry { let path = path.into(); let data = if path.exists() { match std::fs::read(&path) { - Ok(bytes) => Arc::new(Mutex::new( - serde_cbor::from_slice(&bytes).unwrap_or_default(), - )), + Ok(bytes) => match minicbor::decode(&bytes) { + Ok(decoded) => Arc::new(Mutex::new(decoded)), + Err(e) => { + log::error!("Failed to deserialize registry file {:?}: {}", path, e); + Arc::new(Mutex::new(RegistryData::default())) + } + }, Err(e) => { log::error!("Failed to read registry file {:?}: {}", path, e); Arc::new(Mutex::new(RegistryData::default())) @@ -79,7 +152,7 @@ impl FileRegistry { } } - match serde_cbor::to_vec(&RegistryData::default()) { + match minicbor::to_vec(RegistryData::default()) { Ok(bytes) => { if let Err(e) = std::fs::write(&path, bytes) { log::error!("Failed to write initial registry file {:?}: {}", path, e); @@ -112,7 +185,7 @@ impl FileRegistry { Err(_) => return, }; - let bytes = match serde_cbor::to_vec(&*data) { + let bytes = match minicbor::to_vec(&*data) { Ok(bytes) => bytes, Err(e) => { log::error!("Failed to serialize registry data: {}", e); @@ -323,3 +396,38 @@ mod tests { assert_eq!(reg2.load_checkpoint().unwrap(), cp); } } + +// Inline minicbor helpers +#[allow(dead_code)] +fn encode_outpoint( + x: &bitcoin::OutPoint, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.encode(x.to_string())?; + Ok(()) +} +fn decode_outpoint( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + std::str::FromStr::from_str(&d.decode::()?) + .map_err(|_| minicbor::decode::Error::message("invalid outpoint")) +} + +fn encode_sha256d_hash( + x: &bitcoin::hashes::sha256d::Hash, + e: &mut minicbor::Encoder, + _ctx: &mut C, +) -> Result<(), minicbor::encode::Error> { + e.bytes(&x[..])?; + Ok(()) +} +fn decode_sha256d_hash( + d: &mut minicbor::Decoder<'_>, + _ctx: &mut C, +) -> Result { + use bitcoin::hashes::Hash; + bitcoin::hashes::sha256d::Hash::from_slice(d.bytes()?) + .map_err(|_| minicbor::decode::Error::message("invalid hash")) +} diff --git a/src/watch_tower/watcher_error.rs b/src/watch_tower/watcher_error.rs index 549e713e3..a6af3a862 100644 --- a/src/watch_tower/watcher_error.rs +++ b/src/watch_tower/watcher_error.rs @@ -33,7 +33,7 @@ pub enum WatcherError { /// Bitcoin consensus encoding/decoding error. BitcoinEncodingError(bitcoin::consensus::encode::Error), /// Serialization/deserialization error for CBOR. - SerdeCbor(serde_cbor::Error), + CborError(minicbor::decode::Error), /// WebSocket error from tungstenite WebSocket(tungstenite::Error), /// Nostr message parsing error @@ -80,9 +80,9 @@ impl std::fmt::Display for WatcherError { } } -impl From for WatcherError { - fn from(value: serde_cbor::Error) -> Self { - Self::SerdeCbor(value) +impl From for WatcherError { + fn from(value: minicbor::decode::Error) -> Self { + Self::CborError(value) } } @@ -133,7 +133,7 @@ impl WatcherError { WatcherError::HttpStatus { .. } => "HttpStatus", WatcherError::JsonError(_) => "JsonError", WatcherError::BitcoinEncodingError(_) => "BitcoinEncodingError", - WatcherError::SerdeCbor(_) => "SerdeCbor", + WatcherError::CborError(_) => "CborError", WatcherError::WebSocket(_) => "WebSocket", WatcherError::NostrParsingError(_) => "NostrParsingError", WatcherError::MutexPoison => "MutexPoison",