Skip to content

Commit

Permalink
Merge branch 'sp'
Browse files Browse the repository at this point in the history
  • Loading branch information
benma committed Sep 18, 2024
2 parents 4531b11 + 1662e21 commit d02c777
Show file tree
Hide file tree
Showing 21 changed files with 876 additions and 361 deletions.
78 changes: 55 additions & 23 deletions api/firmware/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,16 @@ type BTCTxInput struct {
// PrevTx must be the transaction referenced by Input.PrevOutHash. Can be nil if
// `BTCSignNeedsPrevTxs()` returns false.
PrevTx *BTCPrevTx
// Required for silent payment address verification.
//
// Public key according to
// https:/bitcoin/bips/blob/master/bip-0352.mediawiki#user-content-Inputs_For_Shared_Secret_Derivation.
// Must be 33 bytes for a regular pubkey, and 32 bytes in case of a Taproot x-only output
// pubkey.
//
// IMPORTANT: for Taproot inputs, you must provide the 32 byte x-only pubkey, not a 33 byte
// pubkey, otherwise the parity of the Y coordinate could be wrong.
BIP352Pubkey []byte
}

// BTCTx is the data needed to sign a btc transaction.
Expand All @@ -276,31 +286,41 @@ func (device *Device) BTCSign(
scriptConfigs []*messages.BTCScriptConfigWithKeypath,
tx *BTCTx,
formatUnit messages.BTCSignInitRequest_FormatUnit,
) ([][]byte, error) {
) ([][]byte, map[int][]byte, error) {
generatedOutputs := map[int][]byte{}
if !device.version.AtLeast(semver.NewSemVer(9, 10, 0)) {
for _, sc := range scriptConfigs {
if isTaproot(sc) {
return nil, UnsupportedError("9.10.0")
return nil, nil, UnsupportedError("9.10.0")
}
}
}

supportsAntiklepto := device.version.AtLeast(semver.NewSemVer(9, 4, 0))

containsSilentPaymentOutputs := false
for _, output := range tx.Outputs {
if output.SilentPayment != nil {
containsSilentPaymentOutputs = true
break
}
}

signatures := make([][]byte, len(tx.Inputs))
next, err := device.queryBtcSign(&messages.Request{
Request: &messages.Request_BtcSignInit{
BtcSignInit: &messages.BTCSignInitRequest{
Coin: coin,
ScriptConfigs: scriptConfigs,
Version: tx.Version,
NumInputs: uint32(len(tx.Inputs)),
NumOutputs: uint32(len(tx.Outputs)),
Locktime: tx.Locktime,
FormatUnit: formatUnit,
Coin: coin,
ScriptConfigs: scriptConfigs,
Version: tx.Version,
NumInputs: uint32(len(tx.Inputs)),
NumOutputs: uint32(len(tx.Outputs)),
Locktime: tx.Locktime,
FormatUnit: formatUnit,
ContainsSilentPaymentOutputs: containsSilentPaymentOutputs,
}}})
if err != nil {
return nil, err
return nil, nil, err
}

isInputsPass2 := false
Expand All @@ -319,7 +339,7 @@ func (device *Device) BTCSign(
if performAntiklepto {
nonce, err := generateHostNonce()
if err != nil {
return nil, err
return nil, nil, err
}
hostNonce = nonce
input.HostNonceCommitment = &messages.AntiKleptoHostNonceCommitment{
Expand All @@ -331,12 +351,12 @@ func (device *Device) BTCSign(
BtcSignInput: input,
}})
if err != nil {
return nil, err
return nil, nil, err
}

if performAntiklepto {
if next.Type != messages.BTCSignNextResponse_HOST_NONCE || next.AntiKleptoSignerCommitment == nil {
return nil, errp.New("unexpected response; expected signer nonce commitment")
return nil, nil, errp.New("unexpected response; expected signer nonce commitment")
}
signerCommitment := next.AntiKleptoSignerCommitment.Commitment
next, err = device.nestedQueryBtcSign(
Expand All @@ -348,20 +368,20 @@ func (device *Device) BTCSign(
},
})
if err != nil {
return nil, err
return nil, nil, err
}
err := antikleptoVerify(
hostNonce,
signerCommitment,
next.Signature,
)
if err != nil {
return nil, err
return nil, nil, err
}
}
if isInputsPass2 {
if !next.HasSignature {
return nil, errp.New("unexpected response; expected signature")
return nil, nil, errp.New("unexpected response; expected signature")
}
signatures[inputIndex] = next.Signature
}
Expand All @@ -383,7 +403,7 @@ func (device *Device) BTCSign(
},
})
if err != nil {
return nil, err
return nil, nil, err
}
case messages.BTCSignNextResponse_PREVTX_INPUT:
prevtxInput := tx.Inputs[next.Index].PrevTx.Inputs[next.PrevIndex]
Expand All @@ -394,7 +414,7 @@ func (device *Device) BTCSign(
},
})
if err != nil {
return nil, err
return nil, nil, err
}
case messages.BTCSignNextResponse_PREVTX_OUTPUT:
prevtxOutput := tx.Inputs[next.Index].PrevTx.Outputs[next.PrevIndex]
Expand All @@ -405,7 +425,7 @@ func (device *Device) BTCSign(
},
})
if err != nil {
return nil, err
return nil, nil, err
}
case messages.BTCSignNextResponse_OUTPUT:
outputIndex := next.Index
Expand All @@ -414,12 +434,24 @@ func (device *Device) BTCSign(
BtcSignOutput: tx.Outputs[outputIndex],
}})
if err != nil {
return nil, err
return nil, nil, err
}
if next.GeneratedOutputPkscript != nil {
generatedOutputs[int(outputIndex)] = next.GeneratedOutputPkscript
err := silentPaymentOutputVerify(
tx,
int(outputIndex),
next.SilentPaymentDleqProof,
next.GeneratedOutputPkscript,
)
if err != nil {
return nil, nil, err
}
}
case messages.BTCSignNextResponse_PAYMENT_REQUEST:
paymentRequestIndex := next.Index
if int(paymentRequestIndex) >= len(tx.PaymentRequests) {
return nil, errp.New("payment request index out of bounds")
return nil, nil, errp.New("payment request index out of bounds")
}
paymentRequest := tx.PaymentRequests[paymentRequestIndex]
next, err = device.nestedQueryBtcSign(
Expand All @@ -430,10 +462,10 @@ func (device *Device) BTCSign(
},
)
if err != nil {
return nil, err
return nil, nil, err
}
case messages.BTCSignNextResponse_DONE:
return signatures, nil
return signatures, generatedOutputs, nil
}
}
}
Expand Down
118 changes: 111 additions & 7 deletions api/firmware/btc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/BitBoxSwiss/bitbox02-api-go/util/semver"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
Expand Down Expand Up @@ -330,12 +331,12 @@ func TestBTCSignMessage(t *testing.T) {
})
}

func makeTaprootOutput(t *testing.T, pubkey *btcec.PublicKey) []byte {
func makeTaprootOutput(t *testing.T, pubkey *btcec.PublicKey) (*btcec.PublicKey, []byte) {
t.Helper()
outputKey := txscript.ComputeTaprootKeyNoScript(pubkey)
outputPkScript, err := txscript.PayToTaprootScript(outputKey)
require.NoError(t, err)
return outputPkScript
return outputKey, outputPkScript
}

// Test signing; all inputs are BIP86 Taproot keyspends.
Expand All @@ -348,8 +349,8 @@ func TestSimulatorBTCSignTaprootKeySpend(t *testing.T) {
input2Keypath := []uint32{86 + hardenedKeyStart, 0 + hardenedKeyStart, 0 + hardenedKeyStart, 0, 1}
changeKeypath := []uint32{86 + hardenedKeyStart, 0 + hardenedKeyStart, 0 + hardenedKeyStart, 1, 0}

input1PkScript := makeTaprootOutput(t, simulatorPub(t, device, inputKeypath...))
input2PkScript := makeTaprootOutput(t, simulatorPub(t, device, input2Keypath...))
_, input1PkScript := makeTaprootOutput(t, simulatorPub(t, device, inputKeypath...))
_, input2PkScript := makeTaprootOutput(t, simulatorPub(t, device, input2Keypath...))

prevTx := &wire.MsgTx{
Version: 2,
Expand Down Expand Up @@ -381,7 +382,7 @@ func TestSimulatorBTCSignTaprootKeySpend(t *testing.T) {
require.False(t, BTCSignNeedsPrevTxs(scriptConfigs))

prevTxHash := prevTx.TxHash()
_, err := device.BTCSign(
_, _, err := device.BTCSign(
coin,
scriptConfigs,
&BTCTx{
Expand Down Expand Up @@ -452,7 +453,8 @@ func TestSimulatorBTCSignMixed(t *testing.T) {
{
Value: 100_000_000,
PkScript: func() []byte {
return makeTaprootOutput(t, simulatorPub(t, device, input0Keypath...))
_, script := makeTaprootOutput(t, simulatorPub(t, device, input0Keypath...))
return script
}(),
},
{
Expand Down Expand Up @@ -508,7 +510,7 @@ func TestSimulatorBTCSignMixed(t *testing.T) {
require.True(t, BTCSignNeedsPrevTxs(scriptConfigs))

prevTxHash := prevTx.TxHash()
_, err := device.BTCSign(
_, _, err := device.BTCSign(
coin,
scriptConfigs,
&BTCTx{
Expand Down Expand Up @@ -567,3 +569,105 @@ func TestSimulatorBTCSignMixed(t *testing.T) {
require.NoError(t, err)
})
}

// Test that we can send to a silent payment output (generated by the BitBox) and verify the
// corresponding DLEQ proof on the host that the output was generated correctly.
func TestSimulatorBTCSignSilentPayment(t *testing.T) {
testInitializedSimulators(t, func(t *testing.T, device *Device) {
t.Helper()
coin := messages.BTCCoin_BTC
accountKeypath := []uint32{86 + hardenedKeyStart, 0 + hardenedKeyStart, 0 + hardenedKeyStart}
input1Keypath := []uint32{86 + hardenedKeyStart, 0 + hardenedKeyStart, 0 + hardenedKeyStart, 0, 0}
input2Keypath := []uint32{86 + hardenedKeyStart, 0 + hardenedKeyStart, 0 + hardenedKeyStart, 0, 1}
changeKeypath := []uint32{86 + hardenedKeyStart, 0 + hardenedKeyStart, 0 + hardenedKeyStart, 1, 0}
input1Pubkey := simulatorPub(t, device, input1Keypath...)
input2Pubkey := simulatorPub(t, device, input2Keypath...)
input1OutputKey, input1PkScript := makeTaprootOutput(t, input1Pubkey)
input2OutputKey, input2PkScript := makeTaprootOutput(t, input2Pubkey)

prevTx := &wire.MsgTx{
Version: 2,
TxIn: []*wire.TxIn{
{
PreviousOutPoint: *mustOutpoint("3131313131313131313131313131313131313131313131313131313131313131:0"),
Sequence: 0xFFFFFFFF,
},
},
TxOut: []*wire.TxOut{
{
Value: 60_000_000,
PkScript: input1PkScript,
},
{
Value: 40_000_000,
PkScript: input2PkScript,
},
},
LockTime: 0,
}
prevTxHash := prevTx.TxHash()
_, generatedOutputs, err := device.BTCSign(
coin,
[]*messages.BTCScriptConfigWithKeypath{
{
ScriptConfig: NewBTCScriptConfigSimple(messages.BTCScriptConfig_P2TR),
Keypath: accountKeypath,
},
},
&BTCTx{
Version: 2,
Inputs: []*BTCTxInput{
{
Input: &messages.BTCSignInputRequest{
PrevOutHash: prevTxHash[:],
PrevOutIndex: 0,
PrevOutValue: uint64(prevTx.TxOut[0].Value),
Sequence: 0xFFFFFFFF,
Keypath: input1Keypath,
ScriptConfigIndex: 0,
},
BIP352Pubkey: schnorr.SerializePubKey(input1OutputKey),
},
{
Input: &messages.BTCSignInputRequest{
PrevOutHash: prevTxHash[:],
PrevOutIndex: 1,
PrevOutValue: uint64(prevTx.TxOut[1].Value),
Sequence: 0xFFFFFFFF,
Keypath: input2Keypath,
ScriptConfigIndex: 0,
},
BIP352Pubkey: schnorr.SerializePubKey(input2OutputKey),
},
},
Outputs: []*messages.BTCSignOutputRequest{
{
Ours: true,
Value: 70_000_000,
Keypath: changeKeypath,
},
{
Value: 20_000_000,
SilentPayment: &messages.BTCSignOutputRequest_SilentPayment{
Address: "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv",
},
},
},
Locktime: 0,
},
messages.BTCSignInitRequest_DEFAULT,
)

if device.version.AtLeast(semver.NewSemVer(9, 21, 0)) {
require.NoError(t, err)
require.Equal(t,
map[int][]byte{
1: unhex("5120f99b8e8d97aa7b068dd7b4e7ae31f51784f5c2a0cae280748cfd23832b7dcba7"),
},
generatedOutputs,
)
} else {
require.Error(t, err)
}
})
}
2 changes: 1 addition & 1 deletion api/firmware/messages/antiklepto.pb.go

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

2 changes: 1 addition & 1 deletion api/firmware/messages/backup_commands.pb.go

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

2 changes: 1 addition & 1 deletion api/firmware/messages/bitbox02_system.pb.go

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

Loading

0 comments on commit d02c777

Please sign in to comment.