Skip to content

Commit 37b632f

Browse files
committed
api/firmware: add simulator tests for P2WSH multisig
Receive (verify address) and spend.
1 parent aaf0e94 commit 37b632f

File tree

2 files changed

+382
-2
lines changed

2 files changed

+382
-2
lines changed

api/firmware/btc_test.go

Lines changed: 258 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package firmware
1818
import (
1919
"bytes"
2020
"errors"
21+
"slices"
2122
"testing"
2223

2324
"github.com/BitBoxSwiss/bitbox02-api-go/api/firmware/messages"
@@ -74,6 +75,44 @@ func p2shPkScript(redeemScript []byte) []byte {
7475
return pkScript
7576
}
7677

78+
// P2WSH multisig witnessScript and pubkeyScript from these xpubs, derived at /<0;1>/*.
79+
// The pubkeys will be sorted lexicographically.
80+
func multisigP2WSH(threshold int, xpubs []string, change bool, index uint32) ([]byte, []byte) {
81+
pubkeys := make([]*btcutil.AddressPubKey, len(xpubs))
82+
for i, xpubStr := range xpubs {
83+
changeIndex := uint32(0)
84+
if change {
85+
changeIndex = 1
86+
}
87+
xpub := mustXpub(xpubStr, changeIndex, index)
88+
pubKey, err := xpub.ECPubKey()
89+
if err != nil {
90+
panic(err)
91+
}
92+
addrPubKey, err := btcutil.NewAddressPubKey(pubKey.SerializeCompressed(), &chaincfg.MainNetParams)
93+
if err != nil {
94+
panic(err)
95+
}
96+
pubkeys[i] = addrPubKey
97+
}
98+
slices.SortFunc(pubkeys, func(a, b *btcutil.AddressPubKey) int {
99+
return bytes.Compare(a.ScriptAddress(), b.ScriptAddress())
100+
})
101+
witnessScript, err := txscript.MultiSigScript(pubkeys, threshold)
102+
if err != nil {
103+
panic(err)
104+
}
105+
addr, err := btcutil.NewAddressWitnessScriptHash(chainhash.HashB(witnessScript), &chaincfg.MainNetParams)
106+
if err != nil {
107+
panic(err)
108+
}
109+
pkScript, err := txscript.PayToAddrScript(addr)
110+
if err != nil {
111+
panic(err)
112+
}
113+
return witnessScript, pkScript
114+
}
115+
77116
//nolint:unparam
78117
func mustOutpoint(s string) *wire.OutPoint {
79118
outPoint, err := wire.NewOutPointFromString(s)
@@ -210,14 +249,27 @@ func TestSimulatorBTCAddress(t *testing.T) {
210249
})
211250
}
212251

252+
func mustXpub(xpubStr string, keypath ...uint32) *hdkeychain.ExtendedKey {
253+
xpub, err := hdkeychain.NewKeyFromString(xpubStr)
254+
if err != nil {
255+
panic(err)
256+
}
257+
for _, childIndex := range keypath {
258+
xpub, err = xpub.Derive(childIndex)
259+
if err != nil {
260+
panic(err)
261+
}
262+
}
263+
return xpub
264+
}
265+
213266
func simulatorPub(t *testing.T, device *Device, keypath ...uint32) *btcec.PublicKey {
214267
t.Helper()
215268

216269
xpubStr, err := device.BTCXPub(messages.BTCCoin_BTC, keypath, messages.BTCPubRequest_XPUB, false)
217270
require.NoError(t, err)
218271

219-
xpub, err := hdkeychain.NewKeyFromString(xpubStr)
220-
require.NoError(t, err)
272+
xpub := mustXpub(xpubStr)
221273
pubKey, err := xpub.ECPubKey()
222274
require.NoError(t, err)
223275
return pubKey
@@ -937,3 +989,207 @@ func TestSimulatorSignBTCTransactionSendSelfDifferentAccount(t *testing.T) {
937989
)
938990
})
939991
}
992+
993+
// 1-of-3 multisig registration and address display/verification.
994+
func TestSimulatorBTCAddressMultisig(t *testing.T) {
995+
testInitializedSimulators(t, func(t *testing.T, device *Device, stdOut *bytes.Buffer) {
996+
t.Helper()
997+
998+
coin := messages.BTCCoin_BTC
999+
keypathAccount := []uint32{
1000+
48 + hardenedKeyStart,
1001+
0 + hardenedKeyStart,
1002+
0 + hardenedKeyStart,
1003+
2 + hardenedKeyStart,
1004+
}
1005+
1006+
receiveKeypath := append(append([]uint32{}, keypathAccount...), 0, 0)
1007+
1008+
ourXPub, err := device.BTCXPub(coin, keypathAccount, messages.BTCPubRequest_XPUB, false)
1009+
require.NoError(t, err)
1010+
1011+
xpubs := []string{
1012+
ourXPub,
1013+
"xpub6Esa6esRHkbuXtbdDKqu3uWjQ1GpK39WW2hxbUAN4L3TxrwDyghEwBtUYZ8uK8LZh3tJ3pjWEpxng9tjfo7RT9BaZKV2T3EPvmZ6N1LgSdj",
1014+
"xpub6FJ6FAAFUzuWQAKyT98Ngs6UwsoPfPCdmepqX2aLLPT54M85ARsWzPciFd49foStMwhWgfiHP6PnMgPrWLrBJpUHgqw8vZPd5ov8uSfW2vo",
1015+
}
1016+
ourXPubIndex := uint32(0)
1017+
threshold := 1
1018+
1019+
scriptConfig, err := NewBTCScriptConfigMultisig(uint32(threshold), xpubs, ourXPubIndex)
1020+
require.NoError(t, err)
1021+
1022+
// The multisig account has to be registered if not already.
1023+
registered, err := device.BTCIsScriptConfigRegistered(coin, scriptConfig, keypathAccount)
1024+
require.NoError(t, err)
1025+
require.False(t, registered)
1026+
1027+
err = device.BTCRegisterScriptConfig(coin, scriptConfig, keypathAccount, "My multisig account")
1028+
require.NoError(t, err)
1029+
1030+
address, err := device.BTCAddress(
1031+
coin,
1032+
receiveKeypath,
1033+
scriptConfig,
1034+
true,
1035+
)
1036+
require.NoError(t, err)
1037+
require.Equal(t, "bc1qdhqnu2arm9al7uv687amuesk5det5nxx0k9ed30x2u8zjsfnsfyqzlsrsu", address)
1038+
if device.Version().AtLeast(semver.NewSemVer(9, 20, 0)) {
1039+
// Before simulator v9.20, address confirmation data was not written to stdout.
1040+
require.Contains(t,
1041+
stdOut.String(),
1042+
`TITLE: Register
1043+
BODY: 1-of-3
1044+
Bitcoin multisig
1045+
CONFIRM SCREEN END
1046+
CONFIRM SCREEN START
1047+
TITLE: Register
1048+
BODY: My multisig account
1049+
CONFIRM SCREEN END
1050+
CONFIRM SCREEN START
1051+
TITLE: Register
1052+
BODY: p2wsh
1053+
at
1054+
m/48'/0'/0'/2'
1055+
CONFIRM SCREEN END
1056+
CONFIRM SCREEN START
1057+
TITLE: Register
1058+
BODY: Cosigner 1/3 (this device): Zpub74CYNJGx5QwGYeXket9qEbWbEhMNCL1d1Za3eXpABKXMWNVDhTZmovUkzBa74SCrZruMQLGQ6Zce9HzJUaLoF8QPkRU7CVfSSqNZ7Qy2BB5
1059+
CONFIRM SCREEN END
1060+
CONFIRM SCREEN START
1061+
TITLE: Register
1062+
BODY: Cosigner 2/3: Zpub75SBqDwhA5FEf49Epht8JA3YTjbyQdp6eXQ55XDgC7ddhF8bFQQeGS4gPg1YsNsJjoBtRMvk3N4PZtjdQR6QC6fT8TzH2GLNMwxFj3Rnnzx
1063+
CONFIRM SCREEN END
1064+
CONFIRM SCREEN START
1065+
TITLE: Register
1066+
BODY: Cosigner 3/3: Zpub75rhyjEXMKYqXKsb4XAbw7dJ1c8YkysDv9Wx15deUB3EnjKSS9avKdnv6jvoE3ydQh174CuXBdVPFREkExqA3mxAFzSPVnVbWzKJGVWvYXJ
1067+
CONFIRM SCREEN END
1068+
STATUS SCREEN START
1069+
TITLE: Multisig account
1070+
registered
1071+
STATUS SCREEN END
1072+
CONFIRM SCREEN START
1073+
TITLE: Receive to
1074+
BODY: 1-of-3
1075+
Bitcoin multisig
1076+
CONFIRM SCREEN END
1077+
CONFIRM SCREEN START
1078+
TITLE: Receive to
1079+
BODY: My multisig account
1080+
CONFIRM SCREEN END
1081+
CONFIRM SCREEN START
1082+
TITLE: Receive to
1083+
BODY: bc1qdhqnu2arm9al7uv687amuesk5det5nxx0k9ed30x2u8zjsfnsfyqzlsrsu
1084+
CONFIRM SCREEN END
1085+
`,
1086+
)
1087+
}
1088+
})
1089+
}
1090+
1091+
// 1-of-3 P2WSH multisig spend
1092+
func TestSimulatorBTCSignMultisig(t *testing.T) {
1093+
testInitializedSimulators(t, func(t *testing.T, device *Device, stdOut *bytes.Buffer) {
1094+
t.Helper()
1095+
coin := messages.BTCCoin_BTC
1096+
1097+
keypathAccount := []uint32{
1098+
48 + hardenedKeyStart,
1099+
0 + hardenedKeyStart,
1100+
0 + hardenedKeyStart,
1101+
2 + hardenedKeyStart,
1102+
}
1103+
1104+
changeKeypath := append(append([]uint32{}, keypathAccount...), 1, 0)
1105+
inputKeypath := append(append([]uint32{}, keypathAccount...), 0, 0)
1106+
1107+
ourXPub, err := device.BTCXPub(coin, keypathAccount, messages.BTCPubRequest_XPUB, false)
1108+
require.NoError(t, err)
1109+
1110+
xpubs := []string{
1111+
ourXPub,
1112+
"xpub6Esa6esRHkbuXtbdDKqu3uWjQ1GpK39WW2hxbUAN4L3TxrwDyghEwBtUYZ8uK8LZh3tJ3pjWEpxng9tjfo7RT9BaZKV2T3EPvmZ6N1LgSdj",
1113+
"xpub6FJ6FAAFUzuWQAKyT98Ngs6UwsoPfPCdmepqX2aLLPT54M85ARsWzPciFd49foStMwhWgfiHP6PnMgPrWLrBJpUHgqw8vZPd5ov8uSfW2vo",
1114+
}
1115+
ourXPubIndex := uint32(0)
1116+
threshold := 1
1117+
1118+
_, inputPkScript := multisigP2WSH(threshold, xpubs, false, 0)
1119+
1120+
scriptConfig, err := NewBTCScriptConfigMultisig(uint32(threshold), xpubs, ourXPubIndex)
1121+
require.NoError(t, err)
1122+
1123+
// The multisig account has to be registered if not already.
1124+
registered, err := device.BTCIsScriptConfigRegistered(coin, scriptConfig, keypathAccount)
1125+
require.NoError(t, err)
1126+
require.False(t, registered)
1127+
1128+
err = device.BTCRegisterScriptConfig(coin, scriptConfig, keypathAccount, "My multisig account")
1129+
require.NoError(t, err)
1130+
1131+
prevTx := &wire.MsgTx{
1132+
Version: 2,
1133+
TxIn: []*wire.TxIn{
1134+
{
1135+
PreviousOutPoint: *mustOutpoint("3131313131313131313131313131313131313131313131313131313131313131:0"),
1136+
Sequence: 0xFFFFFFFF,
1137+
},
1138+
},
1139+
TxOut: []*wire.TxOut{
1140+
{
1141+
Value: 100_000_000,
1142+
PkScript: inputPkScript,
1143+
},
1144+
},
1145+
LockTime: 0,
1146+
}
1147+
convertedPrevTx := NewBTCPrevTxFromBtcd(prevTx)
1148+
1149+
scriptConfigs := []*messages.BTCScriptConfigWithKeypath{
1150+
{
1151+
ScriptConfig: scriptConfig,
1152+
Keypath: keypathAccount,
1153+
},
1154+
}
1155+
require.True(t, BTCSignNeedsPrevTxs(scriptConfigs))
1156+
1157+
prevTxHash := prevTx.TxHash()
1158+
_, err = device.BTCSign(
1159+
coin,
1160+
scriptConfigs,
1161+
nil,
1162+
&BTCTx{
1163+
Version: 2,
1164+
Inputs: []*BTCTxInput{
1165+
{
1166+
Input: &messages.BTCSignInputRequest{
1167+
PrevOutHash: prevTxHash[:],
1168+
PrevOutIndex: 0,
1169+
PrevOutValue: uint64(prevTx.TxOut[0].Value),
1170+
Sequence: 0xFFFFFFFF,
1171+
Keypath: inputKeypath,
1172+
ScriptConfigIndex: 0,
1173+
},
1174+
PrevTx: convertedPrevTx,
1175+
},
1176+
},
1177+
Outputs: []*messages.BTCSignOutputRequest{
1178+
{
1179+
Ours: true,
1180+
Value: 70_000_000,
1181+
Keypath: changeKeypath,
1182+
},
1183+
{
1184+
Value: 20_000_000,
1185+
Payload: []byte("11111111111111111111111111111111"),
1186+
Type: messages.BTCOutputType_P2WSH,
1187+
},
1188+
},
1189+
Locktime: 0,
1190+
},
1191+
messages.BTCSignInitRequest_DEFAULT,
1192+
)
1193+
require.NoError(t, err)
1194+
})
1195+
}

0 commit comments

Comments
 (0)