Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions contracts/satoshi-bridge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand All @@ -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"]
9 changes: 7 additions & 2 deletions contracts/satoshi-bridge/src/api/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<OutPoint>, output: Vec<TxOut>) {
pub fn active_utxo_management(
&mut self,
input: Vec<OutPoint>,
output: Vec<TxOut>,
orchard_bundle: Option<Vec<u8>>,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And for what reasons might we need orchard_bundle in active UTXO management? I suggest removing it from here.

) {
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.
Expand Down
10 changes: 7 additions & 3 deletions contracts/satoshi-bridge/src/api/token_receiver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ 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<OutPoint>,
output: Vec<TxOut>,
max_gas_fee: Option<U128>,
orchard_bundle_bytes: Option<String>,
Copy link

@daira daira Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming of this is confusing. It's a String containing the hex bytes of the Orchard section of the transaction. This should probably be documented I think (if it's what you intend). We normally use "bundle" to refer to the in-memory OrchardBundle structure — which is why at first I posted a comment about memory safety, misunderstanding it a hex encoding of that structure.

},
}

Expand Down Expand Up @@ -51,14 +53,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()),
),
}
}
Expand Down Expand Up @@ -94,7 +98,7 @@ impl Contract {
&vutxos,
amount,
withdraw_fee,
max_gas_fee
max_gas_fee,
);

let need_signature_num = psbt.get_input_num();
Expand Down
2 changes: 1 addition & 1 deletion contracts/satoshi-bridge/src/psbt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Next come the checks of the amount the user will receive. In shielded transactions we don’t know this amount and don’t know how much was spent on gas.

Expand Down
31 changes: 27 additions & 4 deletions contracts/satoshi-bridge/src/zcash_utils/contract_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,24 @@ impl Contract {
input: Vec<OutPoint>,
output: Vec<TxOut>,
max_gas_fee: Option<U128>,
orchard_bundle: Option<Vec<u8>>,
#[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)
}
Expand All @@ -108,11 +121,18 @@ impl Contract {
account_id: AccountId,
input: Vec<OutPoint>,
output: Vec<TxOut>,
orchard_bundle: Option<Vec<u8>>,
#[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);
}
Expand Down Expand Up @@ -145,6 +165,7 @@ impl Contract {
input: Vec<OutPoint>,
output: Vec<TxOut>,
max_gas_fee: Option<U128>,
orchard_bundle: Option<Vec<u8>>,
) -> PromiseOrValue<U128> {
PromiseOrValue::Promise(
self.get_last_block_height_promise().then(
Expand All @@ -157,6 +178,7 @@ impl Contract {
input,
output,
max_gas_fee,
orchard_bundle,
),
),
)
Expand All @@ -167,11 +189,12 @@ impl Contract {
account_id: AccountId,
input: Vec<OutPoint>,
output: Vec<TxOut>,
orchard_bundle: Option<Vec<u8>>,
) {
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),
);
}

Expand Down
35 changes: 32 additions & 3 deletions contracts/satoshi-bridge/src/zcash_utils/psbt_wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,12 +23,14 @@ pub struct PsbtWrapper {
vin: Vec<ZcashTxIn<Authorized>>,
vout: Vec<ZcashTxOut>,
inputs_utxo: Vec<ZcashTxOut>,
orchard_bundle: Option<orchard::Bundle<orchard::bundle::Authorized, ZatBalance>>,
}

impl PsbtWrapper {
pub fn new(
input: Vec<OutPoint>,
output: Vec<TxOut>,
orchard_bundle_bytes: Option<Vec<u8>>,
expiry_height: u32,
config: &Config,
) -> Self {
Expand Down Expand Up @@ -61,12 +64,28 @@ impl PsbtWrapper {
vin.len()
];

// TODO: pass the recipient address and amount to verify the orchard output
// let recipient_address = "<SOME ZCASH ADDRESS>";
// let value = "<Amount of the output>";

// TODO: verify orchard bundle
// How to verify orchard bundle value and recipient?
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't verify that just based on the bytes of the Orchard section of the transaction. You need to have the data that was encrypted and encoded to produce those bytes. That is available in the PCZT. I think @str4d will need to explain this.

Copy link
Collaborator Author

@karim-en karim-en Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@daira, we can't create the zkproof on-chain over PCZT, it is very gas-intensive. We planned to generate it off-chain and then pass it and verify it.
cc @str4d

// 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()
} else {
None
};

Self {
branch_id: get_branch_id(expiry_height, config),
expiry_height,
vout,
vin,
inputs_utxo: inputs,
orchard_bundle,
}
}

Expand Down Expand Up @@ -95,6 +114,7 @@ impl PsbtWrapper {
vin: original_psbt.vin,
vout,
inputs_utxo: original_psbt.inputs_utxo,
orchard_bundle: original_psbt.orchard_bundle,
}
}

Expand Down Expand Up @@ -140,7 +160,7 @@ impl PsbtWrapper {

pub fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::<u8>::new();
let version: u8 = 2;
let version: u8 = 3;
buf.push(version);
match self.branch_id {
BranchId::Nu6 => buf.write_all(&[7u8; 1]).unwrap(),
Expand Down Expand Up @@ -211,12 +231,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,
}
}

Expand All @@ -231,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,
Expand All @@ -239,7 +267,7 @@ impl PsbtWrapper {
Some(transparent_bundle),
None,
None,
None,
self.orchard_bundle.clone(),
)
.freeze()
.unwrap();
Expand All @@ -260,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;
Expand Down
7 changes: 6 additions & 1 deletion contracts/satoshi-bridge/src/zcash_utils/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -85,6 +86,7 @@ impl Transaction {
expiry_height: u32,
public_key: &bitcoin::PublicKey,
branch_id: BranchId,
orchard_bundle: &Option<orchard::Bundle<orchard::bundle::Authorized, ZatBalance>>,
) -> TransactionData<Unauthorized> {
let transparent_bundle = Self::get_transparent_builder(vin, vout, input, public_key)
.build()
Expand All @@ -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,
Expand All @@ -101,7 +106,7 @@ impl Transaction {
Some(transparent_bundle),
None,
None,
None,
None, // orchard_bundle
);

inner_tx
Expand Down