From e778abcd241f2d162a3eb9a7f2a4ee4126ea25d0 Mon Sep 17 00:00:00 2001 From: karim-en Date: Fri, 12 Sep 2025 14:23:29 +0100 Subject: [PATCH 1/4] Add orchard support --- Cargo.lock | 1 + contracts/satoshi-bridge/Cargo.toml | 5 +-- contracts/satoshi-bridge/src/api/bridge.rs | 9 ++++-- .../satoshi-bridge/src/api/token_receiver.rs | 9 ++++-- .../src/zcash_utils/contract_methods.rs | 31 ++++++++++++++++--- .../src/zcash_utils/psbt_wrapper.rs | 26 ++++++++++++++-- 6 files changed, 67 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be6de1c..fbae2b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3728,6 +3728,7 @@ dependencies = [ "near-plugins", "near-sdk", "near-workspaces", + "orchard", "tokio", "zcash_address", "zcash_primitives", diff --git a/contracts/satoshi-bridge/Cargo.toml b/contracts/satoshi-bridge/Cargo.toml index cc7f540..be9e862 100644 --- a/contracts/satoshi-bridge/Cargo.toml +++ b/contracts/satoshi-bridge/Cargo.toml @@ -45,6 +45,7 @@ ed25519-dalek = "2.1.0" crypto-shared = { git = "https://github.com/near/mpc_old", rev = "0afee9004a1b1c3386940e60c28cff7cf1b5978c" } zcash_primitives = { version = "0.24.0", default-features = false, features = ["circuits", "transparent-inputs"], optional = true } zcash_transparent = { version = "0.4.0", features = ["transparent-inputs"], optional = true } +orchard = { version = "0.11.0", default-features = false, optional = true } zcash_protocol = { version = "0.6.1" } core2 = { version = "0.3", optional = true } zcash_address = { version = "0.9.0" } @@ -57,5 +58,5 @@ near-workspaces = { version = "0.20", features = ["unstable"] } tokio = { version = "1.12.0", features = ["full"] } [features] -default = [] -zcash = ["zcash_primitives", "zcash_transparent", "core2"] +default = ["zcash"] +zcash = ["zcash_primitives", "zcash_transparent", "orchard", "core2"] diff --git a/contracts/satoshi-bridge/src/api/bridge.rs b/contracts/satoshi-bridge/src/api/bridge.rs index f96fa0a..b6ca3d5 100644 --- a/contracts/satoshi-bridge/src/api/bridge.rs +++ b/contracts/satoshi-bridge/src/api/bridge.rs @@ -207,10 +207,15 @@ impl Contract { #[payable] #[access_control_any(roles(Role::DAO, Role::Operator))] #[pause(except(roles(Role::DAO)))] - pub fn active_utxo_management(&mut self, input: Vec, output: Vec) { + pub fn active_utxo_management( + &mut self, + input: Vec, + output: Vec, + orchard_bundle: Option>, + ) { assert_one_yocto(); let account_id = env::predecessor_account_id(); - self.active_utxo_management_chain_specific(account_id, input, output); + self.active_utxo_management_chain_specific(account_id, input, output, orchard_bundle); } /// The initiator of active UTXO management accelerates the transaction by increasing the gas fee. diff --git a/contracts/satoshi-bridge/src/api/token_receiver.rs b/contracts/satoshi-bridge/src/api/token_receiver.rs index f0a261c..354d109 100644 --- a/contracts/satoshi-bridge/src/api/token_receiver.rs +++ b/contracts/satoshi-bridge/src/api/token_receiver.rs @@ -13,6 +13,7 @@ pub enum TokenReceiverMessage { input: Vec, output: Vec, max_gas_fee: Option, + orchard_bundle_bytes: Option, }, } @@ -51,14 +52,16 @@ impl FungibleTokenReceiver for Contract { target_btc_address, input, output, - max_gas_fee + max_gas_fee, + orchard_bundle_bytes, } => self.ft_on_transfer_withdraw_chain_specific( sender_id, amount, target_btc_address, input, output, - max_gas_fee + max_gas_fee, + orchard_bundle_bytes.map(|b| hex::decode(b).unwrap()), ), } } @@ -94,7 +97,7 @@ impl Contract { &vutxos, amount, withdraw_fee, - max_gas_fee + max_gas_fee, ); let need_signature_num = psbt.get_input_num(); diff --git a/contracts/satoshi-bridge/src/zcash_utils/contract_methods.rs b/contracts/satoshi-bridge/src/zcash_utils/contract_methods.rs index 79137d9..5006f27 100644 --- a/contracts/satoshi-bridge/src/zcash_utils/contract_methods.rs +++ b/contracts/satoshi-bridge/src/zcash_utils/contract_methods.rs @@ -93,11 +93,24 @@ impl Contract { input: Vec, output: Vec, max_gas_fee: Option, + orchard_bundle: Option>, #[callback_unwrap] last_block_height: u32, ) -> U128 { let expiry_height = last_block_height + self.get_config().expiry_height_gap; - let mut psbt = PsbtWrapper::new(input, output, expiry_height, self.internal_config()); - self.create_btc_pending_info(sender_id, amount.0, target_btc_address, &mut psbt, max_gas_fee); + let mut psbt = PsbtWrapper::new( + input, + output, + orchard_bundle, + expiry_height, + self.internal_config(), + ); + self.create_btc_pending_info( + sender_id, + amount.0, + target_btc_address, + &mut psbt, + max_gas_fee, + ); U128(0) } @@ -108,11 +121,18 @@ impl Contract { account_id: AccountId, input: Vec, output: Vec, + orchard_bundle: Option>, #[callback_unwrap] last_block_height: u32, ) { let expiry_height = last_block_height + self.get_config().expiry_height_gap; - let mut psbt = PsbtWrapper::new(input, output, expiry_height, self.internal_config()); + let mut psbt = PsbtWrapper::new( + input, + output, + orchard_bundle, + expiry_height, + self.internal_config(), + ); self.create_active_utxo_management_pending_info(account_id, &mut psbt); } @@ -145,6 +165,7 @@ impl Contract { input: Vec, output: Vec, max_gas_fee: Option, + orchard_bundle: Option>, ) -> PromiseOrValue { PromiseOrValue::Promise( self.get_last_block_height_promise().then( @@ -157,6 +178,7 @@ impl Contract { input, output, max_gas_fee, + orchard_bundle, ), ), ) @@ -167,11 +189,12 @@ impl Contract { account_id: AccountId, input: Vec, output: Vec, + orchard_bundle: Option>, ) { self.get_last_block_height_promise().then( Self::ext(env::current_account_id()) .with_static_gas(GAS_FOR_ACTIVE_UTXO_MANAGMENT_CALLBACK) - .active_utxo_management_callback(account_id, input, output), + .active_utxo_management_callback(account_id, input, output, orchard_bundle), ); } diff --git a/contracts/satoshi-bridge/src/zcash_utils/psbt_wrapper.rs b/contracts/satoshi-bridge/src/zcash_utils/psbt_wrapper.rs index ba5b5df..66a75d2 100644 --- a/contracts/satoshi-bridge/src/zcash_utils/psbt_wrapper.rs +++ b/contracts/satoshi-bridge/src/zcash_utils/psbt_wrapper.rs @@ -6,11 +6,12 @@ use crate::zcash_utils::transaction::Transaction; use bitcoin::hashes::Hash; use bitcoin::{OutPoint, TxOut}; use near_sdk::require; +use zcash_primitives::transaction::components::orchard::read_v5_bundle; use zcash_primitives::transaction::fees::transparent::{InputSize, OutputView}; use zcash_primitives::transaction::fees::FeeRule; use zcash_primitives::transaction::{TransactionData, TxVersion}; use zcash_protocol::consensus::{BlockHeight, BranchId}; -use zcash_protocol::value::Zatoshis; +use zcash_protocol::value::{ZatBalance, Zatoshis}; use zcash_transparent::bundle::Authorized; use zcash_transparent::bundle::TxIn as ZcashTxIn; use zcash_transparent::bundle::TxOut as ZcashTxOut; @@ -22,12 +23,14 @@ pub struct PsbtWrapper { vin: Vec>, vout: Vec, inputs_utxo: Vec, + orchard_bundle: Option>, } impl PsbtWrapper { pub fn new( input: Vec, output: Vec, + orchard_bundle_bytes: Option>, expiry_height: u32, config: &Config, ) -> Self { @@ -61,12 +64,21 @@ impl PsbtWrapper { vin.len() ]; + // todo:: verify orchard bundle value + let orchard_bundle = if let Some(orchard_bundle_bytes) = orchard_bundle_bytes { + let mut reader = Cursor::new(orchard_bundle_bytes); + read_v5_bundle(&mut reader).unwrap() + } else { + None + }; + Self { branch_id: get_branch_id(expiry_height, config), expiry_height, vout, vin, inputs_utxo: inputs, + orchard_bundle, } } @@ -95,6 +107,7 @@ impl PsbtWrapper { vin: original_psbt.vin, vout, inputs_utxo: original_psbt.inputs_utxo, + orchard_bundle: original_psbt.orchard_bundle, } } @@ -140,7 +153,7 @@ impl PsbtWrapper { pub fn to_bytes(&self) -> Vec { let mut buf = Vec::::new(); - let version: u8 = 2; + let version: u8 = 3; buf.push(version); match self.branch_id { BranchId::Nu6 => buf.write_all(&[7u8; 1]).unwrap(), @@ -211,12 +224,19 @@ impl PsbtWrapper { inputs.push(ZcashTxOut::read(&mut rdr).unwrap()); } + let orchard_bundle = if version >= 3 { + read_v5_bundle(&mut rdr).unwrap() + } else { + None + }; + Self { branch_id, expiry_height, vin, vout, inputs_utxo: inputs, + orchard_bundle, } } @@ -239,7 +259,7 @@ impl PsbtWrapper { Some(transparent_bundle), None, None, - None, + self.orchard_bundle.clone(), ) .freeze() .unwrap(); From 2c4dc0a7166010e974a61c7409e4f8e669939294 Mon Sep 17 00:00:00 2001 From: karim-en Date: Mon, 15 Sep 2025 16:16:24 +0100 Subject: [PATCH 2/4] Add comments --- .../satoshi-bridge/src/zcash_utils/psbt_wrapper.rs | 11 ++++++++++- .../satoshi-bridge/src/zcash_utils/transaction.rs | 7 ++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/contracts/satoshi-bridge/src/zcash_utils/psbt_wrapper.rs b/contracts/satoshi-bridge/src/zcash_utils/psbt_wrapper.rs index 66a75d2..61ef681 100644 --- a/contracts/satoshi-bridge/src/zcash_utils/psbt_wrapper.rs +++ b/contracts/satoshi-bridge/src/zcash_utils/psbt_wrapper.rs @@ -64,7 +64,14 @@ impl PsbtWrapper { vin.len() ]; - // todo:: verify orchard bundle value + // TODO: pass the recipient address and amount to verify the orchard output + // let recipient_address = ""; + // let value = ""; + + // TODO: verify orchard bundle + // How to verify orchard bundle value and recipient? + // Should we call orchard_bundle.unwrap().verify_proof(vk) here? what is the vk? + // We have to take into account the gas cost and limits let orchard_bundle = if let Some(orchard_bundle_bytes) = orchard_bundle_bytes { let mut reader = Cursor::new(orchard_bundle_bytes); read_v5_bundle(&mut reader).unwrap() @@ -251,6 +258,7 @@ impl PsbtWrapper { authorization: zcash_transparent::bundle::Authorized, }; + // Here we encode the Zcash transaction with orchard bundle so it can be submited to the network let inner_tx = TransactionData::from_parts( TxVersion::V5, self.branch_id, @@ -280,6 +288,7 @@ impl PsbtWrapper { self.expiry_height, public_key, self.branch_id, + &self.orchard_bundle, ); let txid_parts = tx_data.digest(zcash_primitives::transaction::txid::TxIdDigester); let script = &self.inputs_utxo[vin].script_pubkey; diff --git a/contracts/satoshi-bridge/src/zcash_utils/transaction.rs b/contracts/satoshi-bridge/src/zcash_utils/transaction.rs index 021c2bb..ded2332 100644 --- a/contracts/satoshi-bridge/src/zcash_utils/transaction.rs +++ b/contracts/satoshi-bridge/src/zcash_utils/transaction.rs @@ -5,6 +5,7 @@ use zcash_primitives::consensus::{BlockHeight, BranchId}; use zcash_primitives::transaction::{ Transaction as ZCashTransaction, TransactionData, TxVersion, Unauthorized, }; +use zcash_protocol::value::ZatBalance; use zcash_transparent::builder::TransparentBuilder; use zcash_transparent::bundle::Authorized; @@ -85,6 +86,7 @@ impl Transaction { expiry_height: u32, public_key: &bitcoin::PublicKey, branch_id: BranchId, + orchard_bundle: &Option>, ) -> TransactionData { let transparent_bundle = Self::get_transparent_builder(vin, vout, input, public_key) .build() @@ -93,6 +95,9 @@ impl Transaction { let lock_time = 0; let expiry_height = BlockHeight::from_u32(expiry_height); + // TODO: pass the orchard_bundle properly + // How to convert orchard_bundle from Authorized to Unauthorized? + // NOTE: the `TransactionData` is needed just to call `zcash_primitives::transaction::sighash::signature_hash` let inner_tx = TransactionData::from_parts( TxVersion::V5, branch_id, @@ -101,7 +106,7 @@ impl Transaction { Some(transparent_bundle), None, None, - None, + None, // orchard_bundle ); inner_tx From 852f39fa4177950b82963989d9d039cc682bffe7 Mon Sep 17 00:00:00 2001 From: karim-en Date: Mon, 15 Sep 2025 17:40:49 +0100 Subject: [PATCH 3/4] Add comment --- contracts/satoshi-bridge/src/api/token_receiver.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/satoshi-bridge/src/api/token_receiver.rs b/contracts/satoshi-bridge/src/api/token_receiver.rs index 354d109..49d79f6 100644 --- a/contracts/satoshi-bridge/src/api/token_receiver.rs +++ b/contracts/satoshi-bridge/src/api/token_receiver.rs @@ -8,6 +8,7 @@ pub const GAS_FOR_FT_ON_TRANSFER_CALL_BACK: Gas = Gas::from_tgas(100); #[near(serializers = [json])] pub enum TokenReceiverMessage { DepositProtocolFee, + // Here is the withdraw message structure that will be sent from user or dApp to the btc/zcash connector Withdraw { target_btc_address: String, input: Vec, From c8114f5145d3f82dbfa6cbc9d2e17246edafe62c Mon Sep 17 00:00:00 2001 From: Olga Kunyavskaya Date: Mon, 29 Sep 2025 22:33:28 +0100 Subject: [PATCH 4/4] fix outputs checks --- contracts/satoshi-bridge/src/psbt.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/satoshi-bridge/src/psbt.rs b/contracts/satoshi-bridge/src/psbt.rs index c2f5218..6ddcaa1 100644 --- a/contracts/satoshi-bridge/src/psbt.rs +++ b/contracts/satoshi-bridge/src/psbt.rs @@ -185,7 +185,7 @@ impl Contract { } }); require!( - actual_received_amounts.len() == 1, + actual_received_amounts.len() <= 1, "only one user output is allowed." ); let actual_received_amount = actual_received_amounts[0];