|
3 | 3 | //! This module contains the business logic for BIP-0322 message signing |
4 | 4 | //! with BitGo fixed-script wallets. |
5 | 5 |
|
| 6 | +use crate::fixed_script_wallet::bitgo_psbt::p2tr_musig2_input::Musig2Participants; |
6 | 7 | use crate::fixed_script_wallet::bitgo_psbt::{ |
7 | 8 | create_bip32_derivation, create_tap_bip32_derivation, find_kv, BitGoKeyValue, BitGoPsbt, |
8 | 9 | ProprietaryKeySubtype, |
@@ -129,6 +130,8 @@ pub fn add_bip322_input( |
129 | 130 | inner_psbt.inputs[input_index].bip32_derivation = |
130 | 131 | create_bip32_derivation(wallet_keys, chain, index); |
131 | 132 | inner_psbt.inputs[input_index].redeem_script = Some(script.redeem_script.clone()); |
| 133 | + // Legacy P2SH sighash requires the full previous transaction, not just the output. |
| 134 | + inner_psbt.inputs[input_index].non_witness_utxo = Some(to_spend); |
132 | 135 | } |
133 | 136 | WalletScripts::P2shP2wsh(script) => { |
134 | 137 | inner_psbt.inputs[input_index].bip32_derivation = |
@@ -231,6 +234,18 @@ pub fn add_bip322_input( |
231 | 234 | &[signer_idx, cosigner_idx], |
232 | 235 | None, |
233 | 236 | ); |
| 237 | + // Write Musig2Participants so is_musig2_input() returns true and the |
| 238 | + // nonce-generation and signing routines engage the musig2 protocol. |
| 239 | + let tap_output_key = script.spend_info.output_key().to_x_only_public_key(); |
| 240 | + let musig2_participants = Musig2Participants { |
| 241 | + tap_output_key, |
| 242 | + tap_internal_key: internal_key, |
| 243 | + participant_pub_keys: [pub_triple[0], pub_triple[2]], |
| 244 | + }; |
| 245 | + let (key, value) = musig2_participants.to_key_value().to_key_value(); |
| 246 | + inner_psbt.inputs[input_index] |
| 247 | + .proprietary |
| 248 | + .insert(key, value); |
234 | 249 | } |
235 | 250 | } |
236 | 251 | } |
@@ -712,3 +727,360 @@ pub fn verify_bip322_tx_input_with_pubkeys( |
712 | 727 | // TODO: Parse witness to determine actual signers if needed |
713 | 728 | Ok(vec![0, 1, 2]) |
714 | 729 | } |
| 730 | + |
| 731 | +#[cfg(test)] |
| 732 | +mod tests { |
| 733 | + use super::*; |
| 734 | + use crate::fixed_script_wallet::bitgo_psbt::p2tr_musig2_input::{Musig2Context, Musig2Input}; |
| 735 | + use crate::fixed_script_wallet::test_utils::fixtures::XprvTriple; |
| 736 | + use crate::Network; |
| 737 | + use miniscript::bitcoin::bip32::Xpriv; |
| 738 | + use miniscript::bitcoin::hashes::{sha256, Hash}; |
| 739 | + |
| 740 | + fn make_xprv_triple(seed: &str) -> XprvTriple { |
| 741 | + let get_xpriv = |s: &str| { |
| 742 | + let seed_hash = sha256::Hash::hash(s.as_bytes()).to_byte_array(); |
| 743 | + Xpriv::new_master(miniscript::bitcoin::Network::Testnet, &seed_hash) |
| 744 | + .expect("could not create xpriv from seed") |
| 745 | + }; |
| 746 | + XprvTriple::new([ |
| 747 | + get_xpriv(&format!("{}.0", seed)), |
| 748 | + get_xpriv(&format!("{}.1", seed)), |
| 749 | + get_xpriv(&format!("{}.2", seed)), |
| 750 | + ]) |
| 751 | + } |
| 752 | + |
| 753 | + fn make_wallet_keys(seed: &str) -> (XprvTriple, RootWalletKeys) { |
| 754 | + let xprivs = make_xprv_triple(seed); |
| 755 | + let wallet_keys = xprivs.to_root_wallet_keys(); |
| 756 | + (xprivs, wallet_keys) |
| 757 | + } |
| 758 | + |
| 759 | + fn make_bip322_psbt(wallet_keys: &RootWalletKeys) -> BitGoPsbt { |
| 760 | + BitGoPsbt::new(Network::Bitcoin, wallet_keys, Some(0), None) |
| 761 | + } |
| 762 | + |
| 763 | + // Sign a p2trMusig2 keypath BIP322 PSBT input using the Musig2Context state machine. |
| 764 | + fn sign_musig2_bip322( |
| 765 | + psbt: &mut BitGoPsbt, |
| 766 | + input_index: usize, |
| 767 | + xprivs: &XprvTriple, |
| 768 | + ) -> Result<(), String> { |
| 769 | + let user_session_id: [u8; 32] = [1u8; 32]; |
| 770 | + let bitgo_session_id: [u8; 32] = [2u8; 32]; |
| 771 | + |
| 772 | + let mut ctx = |
| 773 | + Musig2Context::new(psbt.psbt_mut(), input_index).map_err(|e| e.to_string())?; |
| 774 | + let (user_first_round, _) = ctx |
| 775 | + .generate_nonce_first_round(xprivs.user_key(), user_session_id) |
| 776 | + .map_err(|e| e.to_string())?; |
| 777 | + |
| 778 | + let mut ctx = |
| 779 | + Musig2Context::new(psbt.psbt_mut(), input_index).map_err(|e| e.to_string())?; |
| 780 | + let (bitgo_first_round, _) = ctx |
| 781 | + .generate_nonce_first_round(xprivs.bitgo_key(), bitgo_session_id) |
| 782 | + .map_err(|e| e.to_string())?; |
| 783 | + |
| 784 | + let mut ctx = |
| 785 | + Musig2Context::new(psbt.psbt_mut(), input_index).map_err(|e| e.to_string())?; |
| 786 | + ctx.sign_with_first_round(user_first_round, xprivs.user_key()) |
| 787 | + .map_err(|e| e.to_string())?; |
| 788 | + |
| 789 | + let mut ctx = |
| 790 | + Musig2Context::new(psbt.psbt_mut(), input_index).map_err(|e| e.to_string())?; |
| 791 | + ctx.sign_with_first_round(bitgo_first_round, xprivs.bitgo_key()) |
| 792 | + .map_err(|e| e.to_string())?; |
| 793 | + |
| 794 | + Ok(()) |
| 795 | + } |
| 796 | + |
| 797 | + #[test] |
| 798 | + fn test_add_bip322_input_musig2_sets_participants_field() { |
| 799 | + let (_, wallet_keys) = make_wallet_keys("bip322_musig2_test"); |
| 800 | + let mut psbt = make_bip322_psbt(&wallet_keys); |
| 801 | + let idx = add_bip322_input( |
| 802 | + &mut psbt, |
| 803 | + "test message", |
| 804 | + 40, |
| 805 | + 0, |
| 806 | + &wallet_keys, |
| 807 | + Some((0, 2)), |
| 808 | + None, |
| 809 | + ) |
| 810 | + .unwrap(); |
| 811 | + assert!( |
| 812 | + Musig2Input::is_musig2_input(&psbt.psbt().inputs[idx]), |
| 813 | + "Musig2Participants proprietary field must be present" |
| 814 | + ); |
| 815 | + } |
| 816 | + |
| 817 | + #[test] |
| 818 | + fn test_bip322_musig2_keypath_sign_and_verify() { |
| 819 | + let (xprivs, wallet_keys) = make_wallet_keys("bip322_musig2_sign_verify"); |
| 820 | + let message = "BIP322 p2trMusig2 keypath test"; |
| 821 | + let chain = 40; // p2trMusig2 external |
| 822 | + let index = 0; |
| 823 | + |
| 824 | + let mut psbt = make_bip322_psbt(&wallet_keys); |
| 825 | + add_bip322_input( |
| 826 | + &mut psbt, |
| 827 | + message, |
| 828 | + chain, |
| 829 | + index, |
| 830 | + &wallet_keys, |
| 831 | + Some((0, 2)), |
| 832 | + None, |
| 833 | + ) |
| 834 | + .unwrap(); |
| 835 | + sign_musig2_bip322(&mut psbt, 0, &xprivs).unwrap(); |
| 836 | + |
| 837 | + let signers = |
| 838 | + verify_bip322_psbt_input(&psbt, 0, message, chain, index, &wallet_keys, None).unwrap(); |
| 839 | + assert!(signers.contains(&"user".to_string())); |
| 840 | + assert!(signers.contains(&"bitgo".to_string())); |
| 841 | + } |
| 842 | + |
| 843 | + #[test] |
| 844 | + fn test_bip322_musig2_backup_flow_uses_script_path() { |
| 845 | + // backup+user and backup+bitgo pairs must use script path, not musig2 keypath |
| 846 | + let (_, wallet_keys) = make_wallet_keys("bip322_musig2_backup"); |
| 847 | + let chain = 40; |
| 848 | + let index = 0; |
| 849 | + |
| 850 | + for (signer, cosigner) in [(0usize, 1usize), (1, 2)] { |
| 851 | + let mut psbt = make_bip322_psbt(&wallet_keys); |
| 852 | + add_bip322_input( |
| 853 | + &mut psbt, |
| 854 | + "backup flow", |
| 855 | + chain, |
| 856 | + index, |
| 857 | + &wallet_keys, |
| 858 | + Some((signer, cosigner)), |
| 859 | + None, |
| 860 | + ) |
| 861 | + .unwrap(); |
| 862 | + assert!( |
| 863 | + !Musig2Input::is_musig2_input(&psbt.psbt().inputs[0]), |
| 864 | + "Backup flow must not set Musig2Participants (should use script path)" |
| 865 | + ); |
| 866 | + } |
| 867 | + } |
| 868 | + |
| 869 | + #[test] |
| 870 | + fn test_bip322_musig2_multiple_inputs_verify_each() { |
| 871 | + let (xprivs, wallet_keys) = make_wallet_keys("bip322_musig2_multi"); |
| 872 | + let messages = ["msg one", "msg two"]; |
| 873 | + let chain = 40; |
| 874 | + |
| 875 | + let mut psbt = make_bip322_psbt(&wallet_keys); |
| 876 | + for (i, msg) in messages.iter().enumerate() { |
| 877 | + add_bip322_input( |
| 878 | + &mut psbt, |
| 879 | + msg, |
| 880 | + chain, |
| 881 | + i as u32, |
| 882 | + &wallet_keys, |
| 883 | + Some((0, 2)), |
| 884 | + None, |
| 885 | + ) |
| 886 | + .unwrap(); |
| 887 | + sign_musig2_bip322(&mut psbt, i, &xprivs).unwrap(); |
| 888 | + } |
| 889 | + |
| 890 | + for (i, msg) in messages.iter().enumerate() { |
| 891 | + let signers = |
| 892 | + verify_bip322_psbt_input(&psbt, i, msg, chain, i as u32, &wallet_keys, None) |
| 893 | + .unwrap(); |
| 894 | + assert!(signers.contains(&"user".to_string())); |
| 895 | + assert!(signers.contains(&"bitgo".to_string())); |
| 896 | + } |
| 897 | + } |
| 898 | + |
| 899 | + #[test] |
| 900 | + fn test_bip322_musig2_wrong_message_fails_verification() { |
| 901 | + let (xprivs, wallet_keys) = make_wallet_keys("bip322_musig2_wrong_msg"); |
| 902 | + let message = "correct message"; |
| 903 | + let chain = 40; |
| 904 | + let index = 0; |
| 905 | + |
| 906 | + let mut psbt = make_bip322_psbt(&wallet_keys); |
| 907 | + add_bip322_input( |
| 908 | + &mut psbt, |
| 909 | + message, |
| 910 | + chain, |
| 911 | + index, |
| 912 | + &wallet_keys, |
| 913 | + Some((0, 2)), |
| 914 | + None, |
| 915 | + ) |
| 916 | + .unwrap(); |
| 917 | + sign_musig2_bip322(&mut psbt, 0, &xprivs).unwrap(); |
| 918 | + |
| 919 | + let result = |
| 920 | + verify_bip322_psbt_input(&psbt, 0, "wrong message", chain, index, &wallet_keys, None); |
| 921 | + assert!(result.is_err(), "Verification with wrong message must fail"); |
| 922 | + } |
| 923 | + |
| 924 | + #[test] |
| 925 | + fn test_bip322_p2tr_legacy_sign_and_verify() { |
| 926 | + let (xprivs, wallet_keys) = make_wallet_keys("bip322_p2tr_legacy"); |
| 927 | + let message = "BIP322 p2tr legacy test"; |
| 928 | + let chain = 30; // p2tr external |
| 929 | + let index = 0; |
| 930 | + let secp = miniscript::bitcoin::secp256k1::Secp256k1::new(); |
| 931 | + |
| 932 | + let mut psbt = make_bip322_psbt(&wallet_keys); |
| 933 | + add_bip322_input( |
| 934 | + &mut psbt, |
| 935 | + message, |
| 936 | + chain, |
| 937 | + index, |
| 938 | + &wallet_keys, |
| 939 | + Some((0, 2)), |
| 940 | + None, |
| 941 | + ) |
| 942 | + .unwrap(); |
| 943 | + |
| 944 | + psbt.sign(xprivs.user_key(), &secp).ok(); |
| 945 | + psbt.sign(xprivs.bitgo_key(), &secp).ok(); |
| 946 | + |
| 947 | + let signers = |
| 948 | + verify_bip322_psbt_input(&psbt, 0, message, chain, index, &wallet_keys, None).unwrap(); |
| 949 | + assert!(signers.contains(&"user".to_string())); |
| 950 | + assert!(signers.contains(&"bitgo".to_string())); |
| 951 | + } |
| 952 | + |
| 953 | + // --- helpers --- |
| 954 | + |
| 955 | + fn xprv_for_index(xprivs: &XprvTriple, idx: usize) -> &Xpriv { |
| 956 | + match idx { |
| 957 | + 0 => xprivs.user_key(), |
| 958 | + 1 => xprivs.backup_key(), |
| 959 | + 2 => xprivs.bitgo_key(), |
| 960 | + _ => panic!("invalid key index {}", idx), |
| 961 | + } |
| 962 | + } |
| 963 | + |
| 964 | + fn sign_script_path_pair( |
| 965 | + psbt: &mut BitGoPsbt, |
| 966 | + xprivs: &XprvTriple, |
| 967 | + signer: usize, |
| 968 | + cosigner: usize, |
| 969 | + ) { |
| 970 | + let secp = miniscript::bitcoin::secp256k1::Secp256k1::new(); |
| 971 | + psbt.sign(xprv_for_index(xprivs, signer), &secp).ok(); |
| 972 | + psbt.sign(xprv_for_index(xprivs, cosigner), &secp).ok(); |
| 973 | + } |
| 974 | + |
| 975 | + fn assert_signers_include(signers: &[String], expected: &[&str]) { |
| 976 | + for name in expected { |
| 977 | + assert!( |
| 978 | + signers.contains(&name.to_string()), |
| 979 | + "expected signer '{}' not found in {:?}", |
| 980 | + name, |
| 981 | + signers |
| 982 | + ); |
| 983 | + } |
| 984 | + } |
| 985 | + |
| 986 | + // --- P2sh, P2shP2wsh, P2wsh --- |
| 987 | + |
| 988 | + fn test_bip322_ecdsa_sign_and_verify(chain: u32, seed: &str) { |
| 989 | + let (xprivs, wallet_keys) = make_wallet_keys(seed); |
| 990 | + let message = format!("BIP322 chain-{} test", chain); |
| 991 | + let index = 0; |
| 992 | + let secp = miniscript::bitcoin::secp256k1::Secp256k1::new(); |
| 993 | + |
| 994 | + let mut psbt = make_bip322_psbt(&wallet_keys); |
| 995 | + add_bip322_input(&mut psbt, &message, chain, index, &wallet_keys, None, None).unwrap(); |
| 996 | + |
| 997 | + psbt.sign(xprivs.user_key(), &secp).ok(); |
| 998 | + psbt.sign(xprivs.bitgo_key(), &secp).ok(); |
| 999 | + |
| 1000 | + let signers = |
| 1001 | + verify_bip322_psbt_input(&psbt, 0, &message, chain, index, &wallet_keys, None).unwrap(); |
| 1002 | + assert_signers_include(&signers, &["user", "bitgo"]); |
| 1003 | + } |
| 1004 | + |
| 1005 | + #[test] |
| 1006 | + fn test_bip322_p2sh_sign_and_verify() { |
| 1007 | + test_bip322_ecdsa_sign_and_verify(0, "bip322_p2sh"); |
| 1008 | + } |
| 1009 | + |
| 1010 | + #[test] |
| 1011 | + fn test_bip322_p2sh_p2wsh_sign_and_verify() { |
| 1012 | + test_bip322_ecdsa_sign_and_verify(10, "bip322_p2sh_p2wsh"); |
| 1013 | + } |
| 1014 | + |
| 1015 | + #[test] |
| 1016 | + fn test_bip322_p2wsh_sign_and_verify() { |
| 1017 | + test_bip322_ecdsa_sign_and_verify(20, "bip322_p2wsh"); |
| 1018 | + } |
| 1019 | + |
| 1020 | + // --- P2trLegacy backup flows --- |
| 1021 | + |
| 1022 | + #[test] |
| 1023 | + fn test_bip322_p2tr_legacy_backup_sign_and_verify() { |
| 1024 | + const SIGNER_NAMES: [&str; 3] = ["user", "backup", "bitgo"]; |
| 1025 | + let chain = 30; |
| 1026 | + let index = 0; |
| 1027 | + |
| 1028 | + for (signer, cosigner) in [(0usize, 1usize), (1, 2)] { |
| 1029 | + let (xprivs, wallet_keys) = make_wallet_keys(&format!( |
| 1030 | + "bip322_p2tr_legacy_backup_{}_{}", |
| 1031 | + signer, cosigner |
| 1032 | + )); |
| 1033 | + let message = format!("p2tr legacy backup {}-{}", signer, cosigner); |
| 1034 | + let mut psbt = make_bip322_psbt(&wallet_keys); |
| 1035 | + add_bip322_input( |
| 1036 | + &mut psbt, |
| 1037 | + &message, |
| 1038 | + chain, |
| 1039 | + index, |
| 1040 | + &wallet_keys, |
| 1041 | + Some((signer, cosigner)), |
| 1042 | + None, |
| 1043 | + ) |
| 1044 | + .unwrap(); |
| 1045 | + sign_script_path_pair(&mut psbt, &xprivs, signer, cosigner); |
| 1046 | + let signers = |
| 1047 | + verify_bip322_psbt_input(&psbt, 0, &message, chain, index, &wallet_keys, None) |
| 1048 | + .unwrap(); |
| 1049 | + assert_signers_include(&signers, &[SIGNER_NAMES[signer], SIGNER_NAMES[cosigner]]); |
| 1050 | + } |
| 1051 | + } |
| 1052 | + |
| 1053 | + // --- P2trMusig2 backup flow sign+verify --- |
| 1054 | + |
| 1055 | + #[test] |
| 1056 | + fn test_bip322_p2trmusig2_backup_sign_and_verify() { |
| 1057 | + const SIGNER_NAMES: [&str; 3] = ["user", "backup", "bitgo"]; |
| 1058 | + let chain = 40; |
| 1059 | + let index = 0; |
| 1060 | + |
| 1061 | + for (signer, cosigner) in [(0usize, 1usize), (1, 2)] { |
| 1062 | + let (xprivs, wallet_keys) = make_wallet_keys(&format!( |
| 1063 | + "bip322_musig2_backup_sign_{}_{}", |
| 1064 | + signer, cosigner |
| 1065 | + )); |
| 1066 | + let message = format!("p2trMusig2 backup {}-{}", signer, cosigner); |
| 1067 | + let mut psbt = make_bip322_psbt(&wallet_keys); |
| 1068 | + add_bip322_input( |
| 1069 | + &mut psbt, |
| 1070 | + &message, |
| 1071 | + chain, |
| 1072 | + index, |
| 1073 | + &wallet_keys, |
| 1074 | + Some((signer, cosigner)), |
| 1075 | + None, |
| 1076 | + ) |
| 1077 | + .unwrap(); |
| 1078 | + // Backup pairs use script-path: standard ECDSA signing works |
| 1079 | + sign_script_path_pair(&mut psbt, &xprivs, signer, cosigner); |
| 1080 | + let signers = |
| 1081 | + verify_bip322_psbt_input(&psbt, 0, &message, chain, index, &wallet_keys, None) |
| 1082 | + .unwrap(); |
| 1083 | + assert_signers_include(&signers, &[SIGNER_NAMES[signer], SIGNER_NAMES[cosigner]]); |
| 1084 | + } |
| 1085 | + } |
| 1086 | +} |
0 commit comments