Skip to content

firmware/btc: return struct in BTCSign and BTCSignMessage #121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 56 additions & 32 deletions api/firmware/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,22 +277,29 @@ type BTCTx struct {
PaymentRequests []*messages.BTCPaymentRequestRequest
}

// BTCSignResult is the result of `BTCSign()`.
type BTCSignResult struct {
// Signatures contains the input signatures. One 64 byte signature per input.
Signatures [][]byte
// GeneratedOutputs contains the outputs generated (silent payments). The map key is the input
// index, the map value is the generated pkScript.
GeneratedOutputs map[int][]byte
}

// BTCSign signs a bitcoin or bitcoin-like transaction. The previous transactions of the inputs
// need to be provided if `BTCSignNeedsPrevTxs()` returns true.
//
// Returns one 64 byte signature per input.
func (device *Device) BTCSign(
coin messages.BTCCoin,
scriptConfigs []*messages.BTCScriptConfigWithKeypath,
outputScriptConfigs []*messages.BTCScriptConfigWithKeypath,
tx *BTCTx,
formatUnit messages.BTCSignInitRequest_FormatUnit,
) ([][]byte, map[int][]byte, error) {
) (*BTCSignResult, error) {
generatedOutputs := map[int][]byte{}
if !device.version.AtLeast(semver.NewSemVer(9, 10, 0)) {
for _, sc := range scriptConfigs {
if isTaproot(sc) {
return nil, nil, UnsupportedError("9.10.0")
return nil, UnsupportedError("9.10.0")
}
}
}
Expand All @@ -308,11 +315,11 @@ func (device *Device) BTCSign(
}

if containsSilentPaymentOutputs && !device.version.AtLeast(semver.NewSemVer(9, 21, 0)) {
return nil, nil, UnsupportedError("9.21.0")
return nil, UnsupportedError("9.21.0")
}

if len(outputScriptConfigs) > 0 && !device.version.AtLeast(semver.NewSemVer(9, 22, 0)) {
return nil, nil, UnsupportedError("9.22.0")
return nil, UnsupportedError("9.22.0")
}

signatures := make([][]byte, len(tx.Inputs))
Expand All @@ -330,7 +337,7 @@ func (device *Device) BTCSign(
OutputScriptConfigs: outputScriptConfigs,
}}})
if err != nil {
return nil, nil, err
return nil, err
}

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

if performAntiklepto {
if next.Type != messages.BTCSignNextResponse_HOST_NONCE || next.AntiKleptoSignerCommitment == nil {
return nil, nil, errp.New("unexpected response; expected signer nonce commitment")
return nil, errp.New("unexpected response; expected signer nonce commitment")
}
signerCommitment := next.AntiKleptoSignerCommitment.Commitment
next, err = device.nestedQueryBtcSign(
Expand All @@ -378,20 +385,20 @@ func (device *Device) BTCSign(
},
})
if err != nil {
return nil, nil, err
return nil, err
}
err := antikleptoVerify(
hostNonce,
signerCommitment,
next.Signature,
)
if err != nil {
return nil, nil, err
return nil, err
}
}
if isInputsPass2 {
if !next.HasSignature {
return nil, nil, errp.New("unexpected response; expected signature")
return nil, errp.New("unexpected response; expected signature")
}
signatures[inputIndex] = next.Signature
}
Expand All @@ -413,7 +420,7 @@ func (device *Device) BTCSign(
},
})
if err != nil {
return nil, nil, err
return nil, err
}
case messages.BTCSignNextResponse_PREVTX_INPUT:
prevtxInput := tx.Inputs[next.Index].PrevTx.Inputs[next.PrevIndex]
Expand All @@ -424,7 +431,7 @@ func (device *Device) BTCSign(
},
})
if err != nil {
return nil, nil, err
return nil, err
}
case messages.BTCSignNextResponse_PREVTX_OUTPUT:
prevtxOutput := tx.Inputs[next.Index].PrevTx.Outputs[next.PrevIndex]
Expand All @@ -435,7 +442,7 @@ func (device *Device) BTCSign(
},
})
if err != nil {
return nil, nil, err
return nil, err
}
case messages.BTCSignNextResponse_OUTPUT:
outputIndex := next.Index
Expand All @@ -444,7 +451,7 @@ func (device *Device) BTCSign(
BtcSignOutput: tx.Outputs[outputIndex],
}})
if err != nil {
return nil, nil, err
return nil, err
}
if next.GeneratedOutputPkscript != nil {
generatedOutputs[int(outputIndex)] = next.GeneratedOutputPkscript
Expand All @@ -455,13 +462,13 @@ func (device *Device) BTCSign(
next.GeneratedOutputPkscript,
)
if err != nil {
return nil, nil, err
return nil, err
}
}
case messages.BTCSignNextResponse_PAYMENT_REQUEST:
paymentRequestIndex := next.Index
if int(paymentRequestIndex) >= len(tx.PaymentRequests) {
return nil, nil, errp.New("payment request index out of bounds")
return nil, errp.New("payment request index out of bounds")
}
paymentRequest := tx.PaymentRequests[paymentRequestIndex]
next, err = device.nestedQueryBtcSign(
Expand All @@ -472,10 +479,13 @@ func (device *Device) BTCSign(
},
)
if err != nil {
return nil, nil, err
return nil, err
}
case messages.BTCSignNextResponse_DONE:
return signatures, generatedOutputs, nil
return &BTCSignResult{
Signatures: signatures,
GeneratedOutputs: generatedOutputs,
}, nil
}
}
}
Expand Down Expand Up @@ -542,18 +552,28 @@ func (device *Device) BTCRegisterScriptConfig(
return nil
}

// BTCSignMessageResult is the result of `BTCSignMessage()`.
type BTCSignMessageResult struct {
// Signature is the 64 byte raw signature.
Signature []byte
// RecID is the recoverable ID.
RecID byte
// ElectrumSig65 is the 65 byte signature in Electrum format.
ElectrumSig65 []byte
}

// BTCSignMessage signs a Bitcoin message. The 64 byte raw signature, the recoverable ID and the 65
// byte signature in Electrum format are returned.
func (device *Device) BTCSignMessage(
coin messages.BTCCoin,
scriptConfig *messages.BTCScriptConfigWithKeypath,
message []byte,
) (raw []byte, recID byte, electrum65 []byte, err error) {
) (*BTCSignMessageResult, error) {
if isTaproot(scriptConfig) {
return nil, 0, nil, errp.New("taproot not supported")
return nil, errp.New("taproot not supported")
}
if !device.version.AtLeast(semver.NewSemVer(9, 2, 0)) {
return nil, 0, nil, UnsupportedError("9.2.0")
return nil, UnsupportedError("9.2.0")
}

supportsAntiklepto := device.version.AtLeast(semver.NewSemVer(9, 5, 0))
Expand All @@ -564,7 +584,7 @@ func (device *Device) BTCSignMessage(
var err error
hostNonce, err = generateHostNonce()
if err != nil {
return nil, 0, nil, err
return nil, err
}
hostNonceCommitment = &messages.AntiKleptoHostNonceCommitment{
Commitment: antikleptoHostCommit(hostNonce),
Expand All @@ -583,14 +603,14 @@ func (device *Device) BTCSignMessage(
}
response, err := device.queryBTC(request)
if err != nil {
return nil, 0, nil, err
return nil, err
}

var signature []byte
if supportsAntiklepto {
signerCommitment, ok := response.Response.(*messages.BTCResponse_AntikleptoSignerCommitment)
if !ok {
return nil, 0, nil, errp.New("unexpected response")
return nil, errp.New("unexpected response")
}
response, err := device.queryBTC(&messages.BTCRequest{
Request: &messages.BTCRequest_AntikleptoSignature{
Expand All @@ -600,12 +620,12 @@ func (device *Device) BTCSignMessage(
},
})
if err != nil {
return nil, 0, nil, err
return nil, err
}

signResponse, ok := response.Response.(*messages.BTCResponse_SignMessage)
if !ok {
return nil, 0, nil, errp.New("unexpected response")
return nil, errp.New("unexpected response")
}
signature = signResponse.SignMessage.Signature
err = antikleptoVerify(
Expand All @@ -614,12 +634,12 @@ func (device *Device) BTCSignMessage(
signature[:64],
)
if err != nil {
return nil, 0, nil, err
return nil, err
}
} else {
signResponse, ok := response.Response.(*messages.BTCResponse_SignMessage)
if !ok {
return nil, 0, nil, errp.New("unexpected response")
return nil, errp.New("unexpected response")
}
signature = signResponse.SignMessage.Signature
}
Expand All @@ -628,5 +648,9 @@ func (device *Device) BTCSignMessage(
// See https://github.com/spesmilo/electrum/blob/84dc181b6e7bb20e88ef6b98fb8925c5f645a765/electrum/ecc.py#L521-L523
const compressed = 4 // BitBox02 uses only compressed pubkeys
electrumSig65 := append([]byte{27 + compressed + recID}, sig...)
return sig, recID, electrumSig65, nil
return &BTCSignMessageResult{
Signature: sig,
RecID: recID,
ElectrumSig65: electrumSig65,
}, nil
}
24 changes: 12 additions & 12 deletions api/firmware/btc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ func TestSimulatorBTCSignMessage(t *testing.T) {

pubKey := simulatorPub(t, device, keypath...)

sig, _, _, err := device.BTCSignMessage(
result, err := device.BTCSignMessage(
coin,
&messages.BTCScriptConfigWithKeypath{
ScriptConfig: NewBTCScriptConfigSimple(messages.BTCScriptConfig_P2WPKH_P2SH),
Expand All @@ -171,7 +171,7 @@ func TestSimulatorBTCSignMessage(t *testing.T) {
)
require.NoError(t, err)
sigHash := chainhash.DoubleHashB([]byte("\x18Bitcoin Signed Message:\n\x07message"))
require.True(t, parseECDSASignature(t, sig).Verify(sigHash, pubKey))
require.True(t, parseECDSASignature(t, result.Signature).Verify(sigHash, pubKey))
})
}

Expand Down Expand Up @@ -352,7 +352,7 @@ func TestBTCSignMessage(t *testing.T) {
generateHostNonce = func() ([]byte, error) {
return hostNonce, nil
}
sig, recID, electrumSig65, err := env.device.BTCSignMessage(
result, err := env.device.BTCSignMessage(
messages.BTCCoin_BTC,
&messages.BTCScriptConfigWithKeypath{
ScriptConfig: NewBTCScriptConfigSimple(messages.BTCScriptConfig_P2WPKH_P2SH),
Expand All @@ -362,9 +362,9 @@ func TestBTCSignMessage(t *testing.T) {
)
if env.version.AtLeast(semver.NewSemVer(9, 2, 0)) {
require.NoError(t, err)
require.Equal(t, expectedSig[:64], sig)
require.Equal(t, byte(0), recID)
require.Equal(t, electrumSig65, append([]byte{31}, expectedSig[:64]...))
require.Equal(t, expectedSig[:64], result.Signature)
require.Equal(t, byte(0), result.RecID)
require.Equal(t, result.ElectrumSig65, append([]byte{31}, expectedSig[:64]...))
} else {
require.EqualError(t, err, UnsupportedError("9.2.0").Error())
}
Expand Down Expand Up @@ -422,7 +422,7 @@ func TestSimulatorBTCSignTaprootKeySpend(t *testing.T) {
require.False(t, BTCSignNeedsPrevTxs(scriptConfigs))

prevTxHash := prevTx.TxHash()
_, _, err := device.BTCSign(
_, err := device.BTCSign(
coin,
scriptConfigs,
nil,
Expand Down Expand Up @@ -551,7 +551,7 @@ func TestSimulatorBTCSignMixed(t *testing.T) {
require.True(t, BTCSignNeedsPrevTxs(scriptConfigs))

prevTxHash := prevTx.TxHash()
_, _, err := device.BTCSign(
_, err := device.BTCSign(
coin,
scriptConfigs,
nil,
Expand Down Expand Up @@ -648,7 +648,7 @@ func TestSimulatorBTCSignSilentPayment(t *testing.T) {
LockTime: 0,
}
prevTxHash := prevTx.TxHash()
_, generatedOutputs, err := device.BTCSign(
result, err := device.BTCSign(
coin,
[]*messages.BTCScriptConfigWithKeypath{
{
Expand Down Expand Up @@ -707,7 +707,7 @@ func TestSimulatorBTCSignSilentPayment(t *testing.T) {
map[int][]byte{
1: unhex("5120f99b8e8d97aa7b068dd7b4e7ae31f51784f5c2a0cae280748cfd23832b7dcba7"),
},
generatedOutputs,
result.GeneratedOutputs,
)
} else {
require.EqualError(t, err, UnsupportedError("9.21.0").Error())
Expand Down Expand Up @@ -753,7 +753,7 @@ func TestSimulatorSignBTCTransactionSendSelfSameAccount(t *testing.T) {
}

prevTxHash := prevTx.TxHash()
_, _, err := device.BTCSign(
_, err := device.BTCSign(
coin,
scriptConfigs,
nil,
Expand Down Expand Up @@ -847,7 +847,7 @@ func TestSimulatorSignBTCTransactionSendSelfDifferentAccount(t *testing.T) {
outputScriptConfigIndex := uint32(0)

prevTxHash := prevTx.TxHash()
_, _, err := device.BTCSign(
_, err := device.BTCSign(
coin,
scriptConfigs,
outputScriptConfigs,
Expand Down
6 changes: 3 additions & 3 deletions cmd/miniscript/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ func main() {
}
accountKeypath := keyOriginInfo.Keypath
mp := multipaths[i]
signatures, _, err := device.BTCSign(
result, err := device.BTCSign(
coin,
[]*messages.BTCScriptConfigWithKeypath{
{
Expand Down Expand Up @@ -390,8 +390,8 @@ func main() {
errpanic(err)

// Convert to DER encoding
signaturesDER := make([][]byte, len(signatures))
for sigIndex, signature := range signatures {
signaturesDER := make([][]byte, len(result.Signatures))
for sigIndex, signature := range result.Signatures {
r := new(btcec.ModNScalar)
r.SetByteSlice(signature[:32])
s := new(btcec.ModNScalar)
Expand Down
2 changes: 1 addition & 1 deletion cmd/paymentrequest/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func main() {
errpanic(err)
paymentRequest.Signature = signature[1:]

_, _, err = device.BTCSign(
_, err = device.BTCSign(
messages.BTCCoin_TBTC,
[]*messages.BTCScriptConfigWithKeypath{
{
Expand Down
Loading
Loading