Skip to content
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

Add functions for round trip decoding of transaction payloads #64

Merged
merged 3 commits into from
May 10, 2024
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
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
Loading