Skip to content

Commit

Permalink
btc: verify and return silent payment outputs
Browse files Browse the repository at this point in the history
Silent Payment outputs are generated and returned by the firmware to
be integrated into a transaction. This commit verifies the correctness
of the generated output using a DLEQ proof and returns the output.

The DLEQ verificaiton function is a quick port of

https://github.com/BlockstreamResearch/secp256k1-zkp/blob/6152622613fdf1c5af6f31f74c427c4e9ee120ce/src/modules/ecdsa_adaptor/dleq_impl.h#L129

A DLEQ (discrete log equivalence) proof proves that the discrete log
of P1 to the secp256k1 base G is the same as the discrete log of P2 to
another base.
  • Loading branch information
benma committed Sep 1, 2024
1 parent a9a0733 commit 005c709
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://github.com/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, 20, 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 005c709

Please sign in to comment.