Skip to content

Commit

Permalink
Merge pull request #64 from hyperledger/tx-recover
Browse files Browse the repository at this point in the history
Add functions for round trip decoding of transaction payloads
  • Loading branch information
peterbroadhurst authored May 10, 2024
2 parents 39fe567 + 3e5403b commit a85f6e1
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 53 deletions.
8 changes: 7 additions & 1 deletion internal/signermsgs/en_error_messges.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Kaleido, Inc.
// Copyright © 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
Expand Down Expand Up @@ -97,4 +97,10 @@ var (
MsgEIP712ValueNotArray = ffe("FF22078", "Value for '%s' not an array (%T)")
MsgEIP712InvalidArrayLen = ffe("FF22079", "Value for '%s' must have %d entries (found %d)")
MsgEIP712PrimaryTypeRequired = ffe("FF22080", "Primary type must be specified")
MsgEmptyTransactionBytes = ffe("FF22081", "Transaction payload is empty")
MsgUnsupportedTransactionType = ffe("FF22082", "Unsupported transaction type 0x%02x")
MsgInvalidLegacyTransaction = ffe("FF22083", "Transaction payload invalid (legacy): %v")
MsgInvalidEIP1559Transaction = ffe("FF22084", "Transaction payload invalid (EIP-1559): %v")
MsgInvalidEIP155TransactionV = ffe("FF22085", "Invalid V value from EIP-155 transaction (chainId=%d)")
MsgInvalidChainID = ffe("FF22086", "Invalid chainId expected=%d actual=%d")
)
137 changes: 135 additions & 2 deletions pkg/ethsigner/transaction.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Kaleido, Inc.
// Copyright © 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
Expand All @@ -23,6 +23,7 @@ import (
"math/big"

"github.com/hyperledger/firefly-common/pkg/i18n"
"github.com/hyperledger/firefly-common/pkg/log"
"github.com/hyperledger/firefly-signer/internal/signermsgs"
"github.com/hyperledger/firefly-signer/pkg/ethtypes"
"github.com/hyperledger/firefly-signer/pkg/rlp"
Expand Down Expand Up @@ -74,14 +75,18 @@ func (t *Transaction) BuildLegacy() rlp.List {
return rlpList
}

func (t *Transaction) AddEIP155HashValues(rlpList rlp.List, chainID int64) rlp.List {
func AddEIP155HashValuesToRLPList(rlpList rlp.List, chainID int64) rlp.List {
// These values go into the hash of the transaction
rlpList = append(rlpList, rlp.WrapInt(big.NewInt(chainID)))
rlpList = append(rlpList, rlp.WrapInt(big.NewInt(0)))
rlpList = append(rlpList, rlp.WrapInt(big.NewInt(0)))
return rlpList
}

func (t *Transaction) AddEIP155HashValues(rlpList rlp.List, chainID int64) rlp.List {
return AddEIP155HashValuesToRLPList(rlpList, chainID)
}

func (t *Transaction) Build1559(chainID int64) rlp.List {
rlpList := make(rlp.List, 0, 9)
rlpList = append(rlpList, rlp.WrapInt(big.NewInt(chainID)))
Expand Down Expand Up @@ -211,6 +216,134 @@ func (t *Transaction) SignEIP1559(signer secp256k1.Signer, chainID int64) ([]byt
return append([]byte{TransactionType1559}, rlpList.Encode()...), nil
}

func RecoverLegacyRawTransaction(ctx context.Context, rawTx ethtypes.HexBytes0xPrefix, chainID int64) (*ethtypes.Address0xHex, *Transaction, error) {

decoded, _, err := rlp.Decode(rawTx)
if err != nil {
log.L(ctx).Errorf("Invalid legacy transaction data '%s': %s", rawTx, err)
return nil, nil, i18n.NewError(ctx, signermsgs.MsgInvalidLegacyTransaction, err)
}

if decoded == nil || len(decoded.(rlp.List)) < 9 {
log.L(ctx).Errorf("Invalid legacy transaction data '%s': EOF", rawTx)
return nil, nil, i18n.NewError(ctx, signermsgs.MsgInvalidLegacyTransaction, "EOF")
}
rlpList := decoded.(rlp.List)

tx := &Transaction{
Nonce: (*ethtypes.HexInteger)(rlpList[0].ToData().Int()),
GasPrice: (*ethtypes.HexInteger)(rlpList[1].ToData().Int()),
GasLimit: (*ethtypes.HexInteger)(rlpList[2].ToData().Int()),
To: rlpList[3].ToData().Address(),
Value: (*ethtypes.HexInteger)(rlpList[4].ToData().Int()),
Data: ethtypes.HexBytes0xPrefix(rlpList[5].ToData()),
}

vValue := rlpList[6].ToData().Int().Int64()
rValue := rlpList[7].ToData().BytesNotNil()
sValue := rlpList[8].ToData().BytesNotNil()

var message []byte
if vValue != 27 && vValue != 28 {
// Legacy with EIP155 extensions
vValue = vValue - (chainID * 2) - 8
if vValue != 27 && vValue != 28 {
return nil, nil, i18n.NewError(ctx, signermsgs.MsgInvalidEIP155TransactionV, chainID)
}

signedRLPList := make(rlp.List, 6, 9)
copy(signedRLPList, rlpList[0:6])
signedRLPList = AddEIP155HashValuesToRLPList(signedRLPList, chainID)
message = signedRLPList.Encode()
} else {
// Legacy original transaction
message = (rlpList[0:6]).Encode()
}

return recoverCommon(tx, message, chainID, vValue, rValue, sValue)

}

func recoverCommon(tx *Transaction, message []byte, chainID int64, v int64, r, s []byte) (*ethtypes.Address0xHex, *Transaction, error) {
foundSig := &secp256k1.SignatureData{
V: new(big.Int),
R: new(big.Int),
S: new(big.Int),
}
foundSig.V.SetInt64(v)
foundSig.R.SetBytes(r)
foundSig.S.SetBytes(s)

signer, err := foundSig.Recover(message, chainID)
if err != nil {
return nil, nil, err
}

return signer, tx, nil
}

func RecoverEIP1559Transaction(ctx context.Context, rawTx ethtypes.HexBytes0xPrefix, chainID int64) (*ethtypes.Address0xHex, *Transaction, error) {

if len(rawTx) == 0 || rawTx[0] != TransactionType1559 {
return nil, nil, i18n.NewError(ctx, signermsgs.MsgInvalidEIP1559Transaction, "TransactionType")
}

rawTx = rawTx[1:]
decoded, _, err := rlp.Decode(rawTx)
if err != nil {
log.L(ctx).Errorf("Invalid EIP-1559 transaction data '%s': %s", rawTx, err)
return nil, nil, i18n.NewError(ctx, signermsgs.MsgInvalidEIP1559Transaction, err)
}

if decoded == nil || len(decoded.(rlp.List)) < 12 {
log.L(ctx).Errorf("Invalid EIP-1559 transaction data '%s': EOF", rawTx)
return nil, nil, i18n.NewError(ctx, signermsgs.MsgInvalidEIP1559Transaction, "EOF")
}
rlpList := decoded.(rlp.List)

encodedChainID := rlpList[0].ToData().IntOrZero().Int64()
if encodedChainID != chainID {
return nil, nil, i18n.NewError(ctx, signermsgs.MsgInvalidChainID, chainID, encodedChainID)
}
tx := &Transaction{
Nonce: (*ethtypes.HexInteger)(rlpList[1].ToData().Int()),
MaxPriorityFeePerGas: (*ethtypes.HexInteger)(rlpList[2].ToData().Int()),
MaxFeePerGas: (*ethtypes.HexInteger)(rlpList[3].ToData().Int()),
GasLimit: (*ethtypes.HexInteger)(rlpList[4].ToData().Int()),
To: rlpList[5].ToData().Address(),
Value: (*ethtypes.HexInteger)(rlpList[6].ToData().Int()),
Data: ethtypes.HexBytes0xPrefix(rlpList[7].ToData()),
// No access list support
}

return recoverCommon(tx,
append([]byte{TransactionType1559}, (rlpList[0:9]).Encode()...),
chainID,
rlpList[9].ToData().Int().Int64(),
rlpList[10].ToData().BytesNotNil(),
rlpList[11].ToData().BytesNotNil(),
)
}

func RecoverRawTransaction(ctx context.Context, rawTx ethtypes.HexBytes0xPrefix, chainID int64) (*ethtypes.Address0xHex, *Transaction, error) {

// The first byte of the payload (per EIP-2718) is either `>= 0xc0` for legacy transactions,
// or a transaction type selector (up to `0x7f`).
if len(rawTx) == 0 {
return nil, nil, i18n.NewError(ctx, signermsgs.MsgEmptyTransactionBytes)
}
txTypeByte := rawTx[0]
switch {
case txTypeByte >= 0xc7:
return RecoverLegacyRawTransaction(ctx, rawTx, chainID)
case txTypeByte == TransactionType1559:
return RecoverEIP1559Transaction(ctx, rawTx, chainID)
default:
return nil, nil, i18n.NewError(ctx, signermsgs.MsgUnsupportedTransactionType, txTypeByte)
}

}

func (t *Transaction) addSignature(rlpList rlp.List, sig *secp256k1.SignatureData) rlp.List {
rlpList = append(rlpList, rlp.WrapInt(sig.V))
rlpList = append(rlpList, rlp.WrapInt(sig.R))
Expand Down
149 changes: 104 additions & 45 deletions pkg/ethsigner/transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
package ethsigner

import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"math/big"
"testing"
Expand Down Expand Up @@ -100,7 +102,7 @@ func TestEncodeExistingEIP1559(t *testing.T) {

}

func TestSignAutoEIP155(t *testing.T) {
func TestSignLegacyEIP155(t *testing.T) {

inputData, err := hex.DecodeString(
"3674e15c00000000000000000000000000000000000000000000000000000000000000a03f04a4e93ded4d2aaa1a41d617e55c59ac5f1b28a47047e2a526e76d45eb9681d19642e9120d63a9b7f5f537565a430d8ad321ef1bc76689a4b3edc861c640fc00000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000966665f73797374656d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e516d58747653456758626265506855684165364167426f3465796a7053434b437834515a4c50793548646a6177730000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a1f7502c8f8797999c0c6b9c2da653ea736598ed0daa856c47ae71411aa8fea2")
Expand All @@ -117,26 +119,17 @@ func TestSignAutoEIP155(t *testing.T) {
keypair, err := secp256k1.GenerateSecp256k1KeyPair()
assert.NoError(t, err)

raw, err := txn.Sign(keypair, 1001)
raw, err := txn.SignLegacyEIP155(keypair, 1001)
assert.NoError(t, err)

rlpList, _, err := rlp.Decode(raw)
signer, txr, err := RecoverRawTransaction(context.Background(), raw, 1001)
assert.NoError(t, err)
foundSig := &secp256k1.SignatureData{
V: new(big.Int),
R: new(big.Int),
S: new(big.Int),
}
foundSig.V.SetBytes([]byte(rlpList.(rlp.List)[6].(rlp.Data)))
foundSig.R.SetBytes([]byte(rlpList.(rlp.List)[7].(rlp.Data)))
foundSig.S.SetBytes([]byte(rlpList.(rlp.List)[8].(rlp.Data)))
assert.Equal(t, keypair.Address.String(), signer.String())
jsonCompare(t, txn, *txr)

signaturePayload := txn.SignaturePayload(1001)
addr, err := foundSig.Recover(signaturePayload.Bytes(), 1001)
assert.NoError(t, err)
assert.Equal(t, keypair.Address.String(), addr.String())
_, _, err = RecoverRawTransaction(context.Background(), raw, 1002)
assert.Regexp(t, "FF22085", err)

assert.Equal(t, "0x4524b8ac39ace2a3a2c061b73125c19c76daf0d25d44a4d88799f3c2ba686fe6", signaturePayload.Hash().String())
}

func TestSignAutoEIP1559(t *testing.T) {
Expand All @@ -160,22 +153,10 @@ func TestSignAutoEIP1559(t *testing.T) {
raw, err := txn.Sign(keypair, 1001)
assert.NoError(t, err)

assert.Equal(t, TransactionType1559, raw[0])
rlpList, _, err := rlp.Decode(raw[1:])
assert.NoError(t, err)
foundSig := &secp256k1.SignatureData{
V: new(big.Int),
R: new(big.Int),
S: new(big.Int),
}
foundSig.V.SetBytes([]byte(rlpList.(rlp.List)[9].(rlp.Data)))
foundSig.R.SetBytes([]byte(rlpList.(rlp.List)[10].(rlp.Data)))
foundSig.S.SetBytes([]byte(rlpList.(rlp.List)[11].(rlp.Data)))

signaturePayload := txn.SignaturePayload(1001)
addr, err := foundSig.Recover(signaturePayload.Bytes(), 1001)
signer, txr, err := RecoverRawTransaction(context.Background(), raw, 1001)
assert.NoError(t, err)
assert.Equal(t, keypair.Address.String(), addr.String())
assert.Equal(t, keypair.Address.String(), signer.String())
jsonCompare(t, txn, *txr)

}

Expand All @@ -199,21 +180,10 @@ func TestSignLegacyOriginal(t *testing.T) {
raw, err := txn.SignLegacyOriginal(keypair)
assert.NoError(t, err)

rlpList, _, err := rlp.Decode(raw)
signer, txr, err := RecoverRawTransaction(context.Background(), raw, 1001)
assert.NoError(t, err)
foundSig := &secp256k1.SignatureData{
V: new(big.Int),
R: new(big.Int),
S: new(big.Int),
}
foundSig.V.SetBytes([]byte(rlpList.(rlp.List)[6].(rlp.Data)))
foundSig.R.SetBytes([]byte(rlpList.(rlp.List)[7].(rlp.Data)))
foundSig.S.SetBytes([]byte(rlpList.(rlp.List)[8].(rlp.Data)))

signaturePayload := txn.SignaturePayloadLegacyOriginal()
addr, err := foundSig.Recover(signaturePayload.Bytes(), 0)
assert.NoError(t, err)
assert.Equal(t, keypair.Address.String(), addr.String())
assert.Equal(t, keypair.Address.String(), signer.String())
jsonCompare(t, txn, *txr)

}

Expand Down Expand Up @@ -270,3 +240,92 @@ func TestSignEIP1559Error(t *testing.T) {
func TestEthTXDocumented(t *testing.T) {
ffapi.CheckObjectDocumented(&Transaction{})
}

func jsonCompare(t *testing.T, expected, actual interface{}) {
expectedJSON, err := json.Marshal(expected)
assert.NoError(t, err)
actualJSON, err := json.Marshal(actual)
assert.NoError(t, err)
assert.JSONEq(t, (string)(expectedJSON), (string)(actualJSON))

}

func TestRecoverRawTransactionEmpty(t *testing.T) {
_, _, err := RecoverRawTransaction(context.Background(), []byte{}, 1001)
assert.Regexp(t, "FF22081", err)
}

func TestRecoverRawTransactionInvalidType(t *testing.T) {
_, _, err := RecoverRawTransaction(context.Background(), []byte{0x03}, 1001)
assert.Regexp(t, "FF22082.*0x03", err)
}

func TestRecoverLegacyTransactionEmpty(t *testing.T) {
_, _, err := RecoverLegacyRawTransaction(context.Background(), []byte{}, 1001)
assert.Regexp(t, "FF22083", err)
}

func TestRecoverLegacyBadData(t *testing.T) {
_, _, err := RecoverLegacyRawTransaction(context.Background(), []byte{0xff}, 1001)
assert.Regexp(t, "FF22083", err)
}

func TestRecoverLegacyBadStructure(t *testing.T) {
_, _, err := RecoverLegacyRawTransaction(context.Background(), (rlp.List{
rlp.WrapInt(big.NewInt(12345)),
}).Encode(), 1001)
assert.Regexp(t, "FF22083.*EOF", err)
}

func TestRecoverEIP1559TransactionEmpty(t *testing.T) {
_, _, err := RecoverEIP1559Transaction(context.Background(), []byte{}, 1001)
assert.Regexp(t, "FF22084.*TransactionType", err)
}

func TestRecoverEIP1559BadData(t *testing.T) {
_, _, err := RecoverEIP1559Transaction(context.Background(), []byte{TransactionType1559, 0xff}, 1001)
assert.Regexp(t, "FF22084", err)
}

func TestRecoverEIP1559BadStructure(t *testing.T) {
_, _, err := RecoverEIP1559Transaction(context.Background(), append([]byte{TransactionType1559}, (rlp.List{
rlp.WrapInt(big.NewInt(12345)),
}).Encode()...), 1001)
assert.Regexp(t, "FF22084.*EOF", err)
}

func TestRecoverEIP1559BadChainID(t *testing.T) {
_, _, err := RecoverEIP1559Transaction(context.Background(), append([]byte{TransactionType1559}, (rlp.List{
rlp.WrapInt(big.NewInt(111)),
rlp.WrapInt(big.NewInt(222)),
rlp.WrapInt(big.NewInt(333)),
rlp.WrapInt(big.NewInt(444)),
rlp.WrapInt(big.NewInt(555)),
rlp.WrapInt(big.NewInt(666)),
rlp.WrapInt(big.NewInt(777)),
rlp.WrapInt(big.NewInt(888)),
rlp.WrapInt(big.NewInt(999)),
rlp.WrapInt(big.NewInt(111)),
rlp.WrapInt(big.NewInt(223)),
rlp.WrapInt(big.NewInt(333)),
}).Encode()...), 1001)
assert.Regexp(t, "FF22086.*1,001.*111", err)
}

func TestRecoverEIP1559Signature(t *testing.T) {
_, _, err := RecoverEIP1559Transaction(context.Background(), append([]byte{TransactionType1559}, (rlp.List{
rlp.WrapInt(big.NewInt(1001)),
rlp.WrapInt(big.NewInt(222)),
rlp.WrapInt(big.NewInt(333)),
rlp.WrapInt(big.NewInt(444)),
rlp.WrapInt(big.NewInt(555)),
rlp.WrapInt(big.NewInt(666)),
rlp.WrapInt(big.NewInt(777)),
rlp.WrapInt(big.NewInt(888)),
rlp.WrapInt(big.NewInt(999)),
rlp.WrapInt(big.NewInt(111)),
rlp.WrapInt(big.NewInt(223)),
rlp.WrapInt(big.NewInt(333)),
}).Encode()...), 1001)
assert.Regexp(t, "invalid", err)
}
2 changes: 1 addition & 1 deletion pkg/ethsigner/typed_data.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Kaleido, Inc.
// Copyright © 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
Expand Down
2 changes: 1 addition & 1 deletion pkg/ethsigner/wallet.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Kaleido, Inc.
// Copyright © 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
Expand Down
2 changes: 1 addition & 1 deletion pkg/rlp/decode.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2022 Kaleido, Inc.
// Copyright © 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
Expand Down
Loading

0 comments on commit a85f6e1

Please sign in to comment.