@@ -3279,6 +3279,12 @@ impl crate::psbt_ops::PsbtAccess for BitGoPsbt {
32793279 BitGoPsbt :: Zcash ( ref mut zcash_psbt, _) => & mut zcash_psbt. psbt ,
32803280 }
32813281 }
3282+ fn unsigned_tx_id ( & self ) -> String {
3283+ // Use the network-aware method so Zcash PSBTs hash the full Zcash wire
3284+ // format (including versionGroupId, expiryHeight, and sapling fields)
3285+ // rather than the stripped inner Bitcoin transaction.
3286+ self . unsigned_txid ( ) . to_string ( )
3287+ }
32823288}
32833289
32843290/// All 6 orderings of a 3-element array, used to brute-force the
@@ -4855,6 +4861,59 @@ mod tests {
48554861 assert ! ( serialized. is_ok( ) , "Serialization should succeed" ) ;
48564862 }
48574863
4864+ // Verify that the PsbtAccess::unsigned_tx_id implementation for BitGoPsbt
4865+ // returns the same txid as BitGoPsbt::unsigned_txid for all networks.
4866+ //
4867+ // Regression test for a bug introduced in 4.2.0 where the PsbtAccess trait's
4868+ // default unsigned_tx_id was used for Zcash PSBTs, which hashes only the
4869+ // stripped inner Bitcoin transaction rather than the full Zcash wire format
4870+ // (including versionGroupId, expiryHeight, and empty sapling vectors).
4871+ crate :: test_psbt_fixtures!(
4872+ test_psbt_access_txid_matches_unsigned_txid,
4873+ network,
4874+ format,
4875+ {
4876+ use crate :: psbt_ops:: PsbtAccess ;
4877+
4878+ for sig_state in [
4879+ fixtures:: SignatureState :: Unsigned ,
4880+ fixtures:: SignatureState :: Halfsigned ,
4881+ fixtures:: SignatureState :: Fullsigned ,
4882+ ] {
4883+ let fixture = fixtures:: load_psbt_fixture_with_format_and_namespace(
4884+ network. to_utxolib_name( ) ,
4885+ sig_state,
4886+ format,
4887+ fixtures:: FixtureNamespace :: UtxolibCompat ,
4888+ )
4889+ . unwrap( ) ;
4890+ let bytes = BASE64_STANDARD
4891+ . decode( & fixture. psbt_base64)
4892+ . expect( "Failed to decode base64" ) ;
4893+ let psbt =
4894+ BitGoPsbt :: deserialize( & bytes, network) . expect( "Failed to deserialize PSBT" ) ;
4895+
4896+ let txid_via_trait = PsbtAccess :: unsigned_tx_id( & psbt) ;
4897+ let txid_via_method = psbt. unsigned_txid( ) . to_string( ) ;
4898+
4899+ assert_eq!(
4900+ txid_via_trait, txid_via_method,
4901+ "PsbtAccess::unsigned_tx_id must equal BitGoPsbt::unsigned_txid for {:?}" ,
4902+ network
4903+ ) ;
4904+
4905+ // For Zcash, also verify it differs from the naive Bitcoin txid (stripped bytes).
4906+ if network == Network :: Zcash {
4907+ let stripped_txid = psbt. psbt( ) . unsigned_tx. compute_txid( ) . to_string( ) ;
4908+ assert_ne!(
4909+ txid_via_trait, stripped_txid,
4910+ "Zcash txid must NOT equal the stripped Bitcoin txid (versionGroupId etc. must be included)"
4911+ ) ;
4912+ }
4913+ }
4914+ }
4915+ ) ;
4916+
48584917 /// Test reconstructing PSBTs from fixture data using builder methods
48594918 fn test_psbt_reconstruction_for_network ( network : Network , format : fixtures:: TxFormat ) {
48604919 use crate :: fixed_script_wallet:: bitgo_psbt:: psbt_wallet_input:: InputScriptType ;
0 commit comments