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 Aug 23, 2024
1 parent 383ad4b commit ce2feb9
Show file tree
Hide file tree
Showing 10 changed files with 854 additions and 349 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(next.Index)] = 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
112 changes: 105 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,99 @@ 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()
_, _, 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)
} else {
require.Error(t, err)
}
})
}
Loading

0 comments on commit ce2feb9

Please sign in to comment.