From 0e5e96422179aae0608ffaf886d14e1818604c3d Mon Sep 17 00:00:00 2001 From: Tomas Vrba Date: Mon, 20 Apr 2026 13:07:30 +0200 Subject: [PATCH] eth: add streaming for large data transactions and typed data --- api/firmware/eth.go | 350 ++++++++++++++++++++++++++------------- api/firmware/eth_test.go | 201 ++++++++++++++++++++++ 2 files changed, 437 insertions(+), 114 deletions(-) diff --git a/api/firmware/eth.go b/api/firmware/eth.go index ff6ff6f..d8e1563 100644 --- a/api/firmware/eth.go +++ b/api/firmware/eth.go @@ -15,9 +15,14 @@ import ( "github.com/BitBoxSwiss/bitbox02-api-go/util/semver" ) -// queryETH is like query, but nested one level deeper for Ethereum. -func (device *Device) queryETH(request *messages.ETHRequest) (*messages.ETHResponse, error) { - response, err := device.query(&messages.Request{ +// ethStreamingThreshold is the firmware limit above which ETH data/bytes values +// must be streamed instead of sent inline. Typed-message strings larger than +// this limit are rejected instead of streamed. +const ethStreamingThreshold = 6144 + +// nonAtomicQueryETH is like nonAtomicQuery, but nested one level deeper for Ethereum. +func (device *Device) nonAtomicQueryETH(request *messages.ETHRequest) (*messages.ETHResponse, error) { + response, err := device.nonAtomicQuery(&messages.Request{ Request: &messages.Request_Eth{ Eth: request, }, @@ -32,6 +37,13 @@ func (device *Device) queryETH(request *messages.ETHRequest) (*messages.ETHRespo return ethResponse.Eth, nil } +// queryETH is like query, but nested one level deeper for Ethereum. +func (device *Device) queryETH(request *messages.ETHRequest) (*messages.ETHResponse, error) { + return atomicQueriesValue(device, func() (*messages.ETHResponse, error) { + return device.nonAtomicQueryETH(request) + }) +} + // ethCoin the deprecated `coin` enum value for a given chain_id. Only ETH, Ropsten and Rinkeby are // converted, as these were the only supported networks up to v9.10.0. With v9.10.0, the chain ID is // passed directly, and the `coin` field is ignored. @@ -98,12 +110,15 @@ func handleHostNonceCommitment() (*messages.AntiKleptoHostNonceCommitment, []byt return hostNonceCommitment, hostNonce, nil } -func (device *Device) handleSignerNonceCommitment(response *messages.ETHResponse, hostNonce []byte) ([]byte, error) { +func (device *Device) nonAtomicHandleSignerNonceCommitment( + response *messages.ETHResponse, + hostNonce []byte, +) ([]byte, error) { signerCommitment, ok := response.Response.(*messages.ETHResponse_AntikleptoSignerCommitment) if !ok { return nil, errp.New("unexpected response") } - response, err := device.queryETH(&messages.ETHRequest{ + response, err := device.nonAtomicQueryETH(&messages.ETHRequest{ Request: &messages.ETHRequest_AntikleptoSignature{ AntikleptoSignature: &messages.AntiKleptoSignatureRequest{ HostNonce: hostNonce, @@ -129,6 +144,34 @@ func (device *Device) handleSignerNonceCommitment(response *messages.ETHResponse return signature, nil } +func (device *Device) nonAtomicHandleETHDataStreaming( + data []byte, + response *messages.ETHResponse, +) (*messages.ETHResponse, error) { + for { + chunkRequest, ok := response.Response.(*messages.ETHResponse_DataRequestChunk) + if !ok { + return response, nil + } + offset := int(chunkRequest.DataRequestChunk.Offset) + length := int(chunkRequest.DataRequestChunk.Length) + if offset < 0 || length < 0 || offset+length > len(data) { + return nil, errp.New("unexpected response") + } + var err error + response, err = device.nonAtomicQueryETH(&messages.ETHRequest{ + Request: &messages.ETHRequest_DataResponseChunk{ + DataResponseChunk: &messages.ETHSignDataResponseChunkRequest{ + Chunk: data[offset : offset+length], + }, + }, + }) + if err != nil { + return nil, err + } + } +} + // ETHIdentifyCase identifies the case of the recipient address given as hexadecimal string. // This function exists as a convenience to potentially help clients to determine the case of the // recipient address. The output of the function goes to ETHSign and ETHSignEIP1559 as the @@ -145,7 +188,9 @@ func ETHIdentifyCase(recipientAddress string) messages.ETHAddressCase { } } -// ETHSign signs an ethereum transaction. It returns a 65 byte signature (R, S, and 1 byte recID). +// ETHSign signs an ethereum transaction. It returns a 65 byte signature (R, S, +// and 1 byte recID). If len(data) > 6144, firmware v9.26.0 or newer is +// required. func (device *Device) ETHSign( chainID uint64, keypath []uint32, @@ -156,7 +201,37 @@ func (device *Device) ETHSign( value *big.Int, data []byte, recipientAddressCase messages.ETHAddressCase) ([]byte, error) { + return atomicQueriesValue(device, func() ([]byte, error) { + return device.nonAtomicETHSign( + chainID, + keypath, + nonce, + gasPrice, + gasLimit, + recipient, + value, + data, + recipientAddressCase, + ) + }) +} + +func (device *Device) nonAtomicETHSign( + chainID uint64, + keypath []uint32, + nonce uint64, + gasPrice *big.Int, + gasLimit uint64, + recipient [20]byte, + value *big.Int, + data []byte, + recipientAddressCase messages.ETHAddressCase, +) ([]byte, error) { supportsAntiklepto := device.version.AtLeast(semver.NewSemVer(9, 5, 0)) + useStreaming := len(data) > ethStreamingThreshold + if useStreaming && !device.version.AtLeast(semver.NewSemVer(9, 26, 0)) { + return nil, UnsupportedError("9.26.0") + } var hostNonceCommitment *messages.AntiKleptoHostNonceCommitment var err error @@ -174,34 +249,42 @@ func (device *Device) ETHSign( return nil, err } + signReq := &messages.ETHSignRequest{ + Coin: coin, + ChainId: chainID, + Keypath: keypath, + Nonce: new(big.Int).SetUint64(nonce).Bytes(), + GasPrice: gasPrice.Bytes(), + GasLimit: new(big.Int).SetUint64(gasLimit).Bytes(), + Recipient: recipient[:], + Value: value.Bytes(), + Data: data, + HostNonceCommitment: hostNonceCommitment, + AddressCase: recipientAddressCase, + } + if useStreaming { + signReq.Data = nil + signReq.DataLength = uint32(len(data)) + } + request := &messages.ETHRequest{ Request: &messages.ETHRequest_Sign{ - Sign: &messages.ETHSignRequest{ - Coin: coin, - ChainId: chainID, - Keypath: keypath, - Nonce: new(big.Int).SetUint64(nonce).Bytes(), - GasPrice: gasPrice.Bytes(), - GasLimit: new(big.Int).SetUint64(gasLimit).Bytes(), - Recipient: recipient[:], - Value: value.Bytes(), - Data: data, - HostNonceCommitment: hostNonceCommitment, - AddressCase: recipientAddressCase, - }, + Sign: signReq, }, } - response, err := device.queryETH(request) + response, err := device.nonAtomicQueryETH(request) if err != nil { return nil, err } - - if supportsAntiklepto { - signature, err := device.handleSignerNonceCommitment(response, hostNonce) + if useStreaming { + response, err = device.nonAtomicHandleETHDataStreaming(data, response) if err != nil { return nil, err } - return signature, nil + } + + if supportsAntiklepto { + return device.nonAtomicHandleSignerNonceCommitment(response, hostNonce) } signResponse, ok := response.Response.(*messages.ETHResponse_Sign) if !ok { @@ -210,8 +293,9 @@ func (device *Device) ETHSign( return signResponse.Sign.Signature, nil } -// ETHSignEIP1559 signs an ethereum EIP1559 transaction. It returns a 65 byte signature (R, S, and 1 byte recID). -// If paymentRequest is provided, firmware v9.26.0 or newer is required. +// ETHSignEIP1559 signs an ethereum EIP1559 transaction. It returns a 65 byte +// signature (R, S, and 1 byte recID). If paymentRequest is provided or +// len(data) > 6144, firmware v9.26.0 or newer is required. func (device *Device) ETHSignEIP1559( chainID uint64, keypath []uint32, @@ -225,11 +309,41 @@ func (device *Device) ETHSignEIP1559( recipientAddressCase messages.ETHAddressCase, paymentRequest *messages.BTCPaymentRequestRequest, ) ([]byte, error) { + return atomicQueriesValue(device, func() ([]byte, error) { + return device.nonAtomicETHSignEIP1559( + chainID, + keypath, + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + gasLimit, + recipient, + value, + data, + recipientAddressCase, + paymentRequest, + ) + }) +} +func (device *Device) nonAtomicETHSignEIP1559( + chainID uint64, + keypath []uint32, + nonce uint64, + maxPriorityFeePerGas *big.Int, + maxFeePerGas *big.Int, + gasLimit uint64, + recipient [20]byte, + value *big.Int, + data []byte, + recipientAddressCase messages.ETHAddressCase, + paymentRequest *messages.BTCPaymentRequestRequest, +) ([]byte, error) { if !device.version.AtLeast(semver.NewSemVer(9, 16, 0)) { return nil, UnsupportedError("9.16.0") } - if paymentRequest != nil && !device.version.AtLeast(semver.NewSemVer(9, 26, 0)) { + useStreaming := len(data) > ethStreamingThreshold + if (paymentRequest != nil || useStreaming) && !device.version.AtLeast(semver.NewSemVer(9, 26, 0)) { return nil, UnsupportedError("9.26.0") } @@ -238,34 +352,42 @@ func (device *Device) ETHSignEIP1559( return nil, err } + signEIP1559Req := &messages.ETHSignEIP1559Request{ + ChainId: chainID, + Keypath: keypath, + Nonce: new(big.Int).SetUint64(nonce).Bytes(), + MaxPriorityFeePerGas: maxPriorityFeePerGas.Bytes(), + MaxFeePerGas: maxFeePerGas.Bytes(), + GasLimit: new(big.Int).SetUint64(gasLimit).Bytes(), + Recipient: recipient[:], + Value: value.Bytes(), + Data: data, + HostNonceCommitment: hostNonceCommitment, + AddressCase: recipientAddressCase, + PaymentRequest: paymentRequest, + } + if useStreaming { + signEIP1559Req.Data = nil + signEIP1559Req.DataLength = uint32(len(data)) + } + request := &messages.ETHRequest{ Request: &messages.ETHRequest_SignEip1559{ - SignEip1559: &messages.ETHSignEIP1559Request{ - ChainId: chainID, - Keypath: keypath, - Nonce: new(big.Int).SetUint64(nonce).Bytes(), - MaxPriorityFeePerGas: maxPriorityFeePerGas.Bytes(), - MaxFeePerGas: maxFeePerGas.Bytes(), - GasLimit: new(big.Int).SetUint64(gasLimit).Bytes(), - Recipient: recipient[:], - Value: value.Bytes(), - Data: data, - HostNonceCommitment: hostNonceCommitment, - AddressCase: recipientAddressCase, - PaymentRequest: paymentRequest, - }, + SignEip1559: signEIP1559Req, }, } - response, err := device.queryETH(request) + response, err := device.nonAtomicQueryETH(request) if err != nil { return nil, err } - - signature, err := device.handleSignerNonceCommitment(response, hostNonce) - if err != nil { - return nil, err + if useStreaming { + response, err = device.nonAtomicHandleETHDataStreaming(data, response) + if err != nil { + return nil, err + } } - return signature, nil + + return device.nonAtomicHandleSignerNonceCommitment(response, hostNonce) } // ETHSignMessage signs an Ethereum message. The provided msg will be prefixed with "\x19Ethereum @@ -276,6 +398,16 @@ func (device *Device) ETHSignMessage( chainID uint64, keypath []uint32, msg []byte, +) ([]byte, error) { + return atomicQueriesValue(device, func() ([]byte, error) { + return device.nonAtomicETHSignMessage(chainID, keypath, msg) + }) +} + +func (device *Device) nonAtomicETHSignMessage( + chainID uint64, + keypath []uint32, + msg []byte, ) ([]byte, error) { if len(msg) > 1024 { return nil, errp.New("message too large") @@ -311,37 +443,13 @@ func (device *Device) ETHSignMessage( }, }, } - response, err := device.queryETH(request) + response, err := device.nonAtomicQueryETH(request) if err != nil { return nil, err } if supportsAntiklepto { - signerCommitment, ok := response.Response.(*messages.ETHResponse_AntikleptoSignerCommitment) - if !ok { - return nil, errp.New("unexpected response") - } - response, err := device.queryETH(&messages.ETHRequest{ - Request: &messages.ETHRequest_AntikleptoSignature{ - AntikleptoSignature: &messages.AntiKleptoSignatureRequest{ - HostNonce: hostNonce, - }, - }, - }) - if err != nil { - return nil, err - } - - signResponse, ok := response.Response.(*messages.ETHResponse_Sign) - if !ok { - return nil, errp.New("unexpected response") - } - signature := signResponse.Sign.Signature - err = antikleptoVerify( - hostNonce, - signerCommitment.AntikleptoSignerCommitment.Commitment, - signature[:64], - ) + signature, err := device.nonAtomicHandleSignerNonceCommitment(response, hostNonce) if err != nil { return nil, err } @@ -536,7 +644,10 @@ func encodeValue(typ *messages.ETHSignTypedMessageRequest_MemberType, value inte return nil, errp.New("couldn't encode value") } -func getValue(what *messages.ETHTypedMessageValueResponse, msg map[string]interface{}) ([]byte, error) { +func getValue( + what *messages.ETHTypedMessageValueResponse, + msg map[string]interface{}, +) ([]byte, messages.ETHSignTypedMessageRequest_DataType, error) { types := msg["types"].(map[string]interface{}) var value interface{} @@ -548,17 +659,17 @@ func getValue(what *messages.ETHTypedMessageValueResponse, msg map[string]interf var err error typ, err = parseType("EIP712Domain", types) if err != nil { - return nil, err + return nil, messages.ETHSignTypedMessageRequest_UNKNOWN, err } case messages.ETHTypedMessageValueResponse_MESSAGE: value = msg["message"] var err error typ, err = parseType(msg["primaryType"].(string), types) if err != nil { - return nil, err + return nil, messages.ETHSignTypedMessageRequest_UNKNOWN, err } default: - return nil, errp.Newf("unknown root: %v", what.RootObject) + return nil, messages.ETHSignTypedMessageRequest_UNKNOWN, errp.Newf("unknown root: %v", what.RootObject) } for _, element := range what.Path { switch typ.Type { @@ -568,26 +679,43 @@ func getValue(what *messages.ETHTypedMessageValueResponse, msg map[string]interf var err error typ, err = parseType(structMember["type"].(string), types) if err != nil { - return nil, err + return nil, messages.ETHSignTypedMessageRequest_UNKNOWN, err } case messages.ETHSignTypedMessageRequest_ARRAY: value = value.([]interface{})[element] typ = typ.ArrayType default: - return nil, errp.New("path element does not point to struct or array") + return nil, messages.ETHSignTypedMessageRequest_UNKNOWN, errp.New("path element does not point to struct or array") } } - return encodeValue(typ, value) + encoded, err := encodeValue(typ, value) + if err != nil { + return nil, messages.ETHSignTypedMessageRequest_UNKNOWN, err + } + return encoded, typ.Type, nil } -// ETHSignTypedMessage signs an Ethereum EIP-712 typed message. 27 is added to the recID to denote -// an uncompressed pubkey. If useAntiklepto is false, signing is deterministic and requires -// firmware >= 9.26.0. +// ETHSignTypedMessage signs an Ethereum EIP-712 typed message. 27 is added to +// the recID to denote an uncompressed pubkey. If useAntiklepto is false, +// signing is deterministic and requires firmware >= 9.26.0. If a typed-message +// bytes value exceeds 6144 bytes, firmware >= 9.26.0 is required to stream it. +// Typed-message string values larger than 6144 bytes are rejected. func (device *Device) ETHSignTypedMessage( chainID uint64, keypath []uint32, jsonMsg []byte, useAntiklepto bool, +) ([]byte, error) { + return atomicQueriesValue(device, func() ([]byte, error) { + return device.nonAtomicETHSignTypedMessage(chainID, keypath, jsonMsg, useAntiklepto) + }) +} + +func (device *Device) nonAtomicETHSignTypedMessage( + chainID uint64, + keypath []uint32, + jsonMsg []byte, + useAntiklepto bool, ) ([]byte, error) { if !device.version.AtLeast(semver.NewSemVer(9, 12, 0)) { return nil, UnsupportedError("9.12.0") @@ -645,56 +773,50 @@ func (device *Device) ETHSignTypedMessage( }, }, } - response, err := device.queryETH(request) + response, err := device.nonAtomicQueryETH(request) if err != nil { return nil, err } typedMsgValueResponse, ok := response.Response.(*messages.ETHResponse_TypedMsgValue) for ok { - value, err := getValue(typedMsgValueResponse.TypedMsgValue, msg) + value, dataType, err := getValue(typedMsgValueResponse.TypedMsgValue, msg) if err != nil { return nil, err } - response, err = device.queryETH(&messages.ETHRequest{ + useStreaming := len(value) > ethStreamingThreshold + if dataType == messages.ETHSignTypedMessageRequest_STRING && useStreaming { + return nil, errp.New("string value exceeds maximum size") + } + if useStreaming && !device.version.AtLeast(semver.NewSemVer(9, 26, 0)) { + return nil, UnsupportedError("9.26.0") + } + valueRequest := &messages.ETHTypedMessageValueRequest{ + Value: value, + } + if useStreaming { + valueRequest.Value = nil + valueRequest.DataLength = uint32(len(value)) + } + response, err = device.nonAtomicQueryETH(&messages.ETHRequest{ Request: &messages.ETHRequest_TypedMsgValue{ - TypedMsgValue: &messages.ETHTypedMessageValueRequest{ - Value: value, - }, + TypedMsgValue: valueRequest, }, }) if err != nil { return nil, err } + if useStreaming { + response, err = device.nonAtomicHandleETHDataStreaming(value, response) + if err != nil { + return nil, err + } + } typedMsgValueResponse, ok = response.Response.(*messages.ETHResponse_TypedMsgValue) } if useAntiklepto { - signerCommitment, ok := response.Response.(*messages.ETHResponse_AntikleptoSignerCommitment) - if !ok { - return nil, errp.New("unexpected response") - } - response, err = device.queryETH(&messages.ETHRequest{ - Request: &messages.ETHRequest_AntikleptoSignature{ - AntikleptoSignature: &messages.AntiKleptoSignatureRequest{ - HostNonce: hostNonce, - }, - }, - }) - if err != nil { - return nil, err - } - - signResponse, ok := response.Response.(*messages.ETHResponse_Sign) - if !ok { - return nil, errp.New("unexpected response") - } - signature := signResponse.Sign.Signature - err = antikleptoVerify( - hostNonce, - signerCommitment.AntikleptoSignerCommitment.Commitment, - signature[:64], - ) + signature, err := device.nonAtomicHandleSignerNonceCommitment(response, hostNonce) if err != nil { return nil, err } diff --git a/api/firmware/eth_test.go b/api/firmware/eth_test.go index 143f43e..923c70b 100644 --- a/api/firmware/eth_test.go +++ b/api/firmware/eth_test.go @@ -4,10 +4,13 @@ package firmware import ( "bytes" + "encoding/json" "math/big" "testing" + "github.com/BitBoxSwiss/bitbox02-api-go/api/common" "github.com/BitBoxSwiss/bitbox02-api-go/api/firmware/messages" + "github.com/BitBoxSwiss/bitbox02-api-go/api/firmware/mocks" "github.com/BitBoxSwiss/bitbox02-api-go/util/semver" "github.com/stretchr/testify/require" "golang.org/x/crypto/sha3" @@ -283,6 +286,107 @@ func TestEncodeValue(t *testing.T) { require.Equal(t, []byte("\x00\x00\x03\xe8"), encoded) } +func TestHandleETHDataStreamingOutOfBounds(t *testing.T) { + device := &Device{} + _, err := device.nonAtomicHandleETHDataStreaming( + []byte("hello"), + &messages.ETHResponse{ + Response: &messages.ETHResponse_DataRequestChunk{ + DataRequestChunk: &messages.ETHSignDataRequestChunkResponse{ + Offset: 4, + Length: 2, + }, + }, + }, + ) + require.EqualError(t, err, "unexpected response") +} + +func TestGetValueReturnsType(t *testing.T) { + var msg map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(` +{ + "types": { + "EIP712Domain": [{ "name": "name", "type": "string" }], + "Mail": [ + { "name": "contents", "type": "string" }, + { "name": "payload", "type": "bytes" } + ] + }, + "primaryType": "Mail", + "domain": { "name": "Test" }, + "message": { + "contents": "hello", + "payload": "0xaabb" + } +}`), &msg)) + + value, dataType, err := getValue(&messages.ETHTypedMessageValueResponse{ + RootObject: messages.ETHTypedMessageValueResponse_DOMAIN, + Path: []uint32{0}, + }, msg) + require.NoError(t, err) + require.Equal(t, []byte("Test"), value) + require.Equal(t, messages.ETHSignTypedMessageRequest_STRING, dataType) + + value, dataType, err = getValue(&messages.ETHTypedMessageValueResponse{ + RootObject: messages.ETHTypedMessageValueResponse_MESSAGE, + Path: []uint32{1}, + }, msg) + require.NoError(t, err) + require.Equal(t, []byte{0xaa, 0xbb}, value) + require.Equal(t, messages.ETHSignTypedMessageRequest_BYTES, dataType) +} + +func TestETHSignTypedMessageRejectsLargeString(t *testing.T) { + communication := &mocks.Communication{} + device := newDevice( + t, + semver.NewSemVer(9, 26, 0), + common.ProductBitBox02Multi, + communication, + func(request *messages.Request) *messages.Response { + ethRequest, ok := request.Request.(*messages.Request_Eth) + require.True(t, ok) + + switch ethRequest.Eth.Request.(type) { + case *messages.ETHRequest_SignTypedMsg: + return &messages.Response{ + Response: &messages.Response_Eth{ + Eth: &messages.ETHResponse{ + Response: &messages.ETHResponse_TypedMsgValue{ + TypedMsgValue: &messages.ETHTypedMessageValueResponse{ + RootObject: messages.ETHTypedMessageValueResponse_MESSAGE, + Path: []uint32{0}, + }, + }, + }, + }, + } + default: + t.Fatal("unexpected follow-up request") + return nil + } + }, + ) + + _, err := device.ETHSignTypedMessage( + 1, + []uint32{44 + hardenedKeyStart, 60 + hardenedKeyStart, hardenedKeyStart, 0, 10}, + []byte(`{ + "types": { + "EIP712Domain": [{ "name": "name", "type": "string" }], + "Msg": [{ "name": "text", "type": "string" }] + }, + "primaryType": "Msg", + "domain": { "name": "Test" }, + "message": { "text": "`+string(bytes.Repeat([]byte("a"), ethStreamingThreshold+1))+`" } + }`), + false, + ) + require.EqualError(t, err, "string value exceeds maximum size") +} + func TestSimulatorETHPub(t *testing.T) { testInitializedSimulators(t, func(t *testing.T, device *Device, stdOut *bytes.Buffer) { t.Helper() @@ -456,6 +560,36 @@ func TestSimulatorETHSign(t *testing.T) { }) } +func TestSimulatorETHSignStreaming(t *testing.T) { + testInitializedSimulators(t, func(t *testing.T, device *Device, stdOut *bytes.Buffer) { + t.Helper() + if !device.Version().AtLeast(semver.NewSemVer(9, 26, 0)) { + t.Skip("requires firmware >= 9.26.0") + } + + sig, err := device.ETHSign( + 1, + []uint32{ + 44 + hardenedKeyStart, + 60 + hardenedKeyStart, + 0 + hardenedKeyStart, + 0, + 10, + }, + 8156, + new(big.Int).SetUint64(6000000000), + 21000, + [20]byte{0x04, 0xf2, 0x64, 0xcf, 0x34, 0x44, 0x03, 0x13, 0xb4, 0xa0, + 0x19, 0x2a, 0x35, 0x28, 0x14, 0xfb, 0xe9, 0x27, 0xb8, 0x85}, + new(big.Int).SetUint64(530564000000000000), + bytes.Repeat([]byte{0xab}, 10000), + messages.ETHAddressCase_ETH_ADDRESS_CASE_MIXED, + ) + require.NoError(t, err) + require.Len(t, sig, 65) + }) +} + func TestSimulatorETHSignEIP1559(t *testing.T) { testInitializedSimulators(t, func(t *testing.T, device *Device, stdOut *bytes.Buffer) { t.Helper() @@ -493,3 +627,70 @@ func TestSimulatorETHSignEIP1559(t *testing.T) { require.Len(t, sig, 65, "The signature should have exactly 65 bytes") }) } + +func TestSimulatorETHSignEIP1559Streaming(t *testing.T) { + testInitializedSimulators(t, func(t *testing.T, device *Device, stdOut *bytes.Buffer) { + t.Helper() + if !device.Version().AtLeast(semver.NewSemVer(9, 26, 0)) { + t.Skip("requires firmware >= 9.26.0") + } + + sig, err := device.ETHSignEIP1559( + 1, + []uint32{ + 44 + hardenedKeyStart, + 60 + hardenedKeyStart, + 0 + hardenedKeyStart, + 0, + 10, + }, + 8156, + new(big.Int), + new(big.Int).SetUint64(6000000000), + 21000, + [20]byte{0x04, 0xf2, 0x64, 0xcf, 0x34, 0x44, 0x03, 0x13, 0xb4, 0xa0, + 0x19, 0x2a, 0x35, 0x28, 0x14, 0xfb, 0xe9, 0x27, 0xb8, 0x85}, + new(big.Int).SetUint64(530564000000000000), + bytes.Repeat([]byte{0xcd}, 10000), + messages.ETHAddressCase_ETH_ADDRESS_CASE_MIXED, + nil, + ) + require.NoError(t, err) + require.Len(t, sig, 65) + }) +} + +func TestSimulatorETHSignTypedMessageStreamingBytes(t *testing.T) { + testInitializedSimulators(t, func(t *testing.T, device *Device, stdOut *bytes.Buffer) { + t.Helper() + + sig, err := device.ETHSignTypedMessage( + 1, + []uint32{ + 44 + hardenedKeyStart, + 60 + hardenedKeyStart, + 0 + hardenedKeyStart, + 0, + 10, + }, + []byte(`{ + "types": { + "EIP712Domain": [{ "name": "name", "type": "string" }], + "Msg": [{ "name": "data", "type": "bytes" }] + }, + "primaryType": "Msg", + "domain": { "name": "Test" }, + "message": { "data": "0x`+string(bytes.Repeat([]byte("aa"), 10000))+`" } + }`), + false, + ) + + if !device.Version().AtLeast(semver.NewSemVer(9, 26, 0)) { + require.EqualError(t, err, UnsupportedError("9.26.0").Error()) + return + } + + require.NoError(t, err) + require.Len(t, sig, 65) + }) +}