Files
linea-monorepo/prover/backend/ethereum/tx_encoding.go
Arya Tabaie ce964fc0cb Decompressor Library (#229)
* chore bring over changes from other branch

* chore: blocks as byte arrays not lists; tx.S <- 1

* build: update compressor lib release script by jpnovais

* docs and go generate

---------

Co-authored-by: Arya Tabaie <15056835+Tabaie@users.noreply.github.com>
2024-10-22 14:32:14 -05:00

319 lines
9.0 KiB
Go

package ethereum
import (
"bytes"
"errors"
"fmt"
"math/big"
"github.com/consensys/linea-monorepo/prover/utils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rlp"
)
// Returns the transaction hash of the transaction
func GetTxHash(tx *types.Transaction) common.Hash {
if !tx.Protected() {
// The normal signer does not return the right value
return getUnprotectedSigner().Hash(tx)
}
return getSigner(tx.ChainId()).Hash(tx)
}
// Encode the transaction so that the hash of the encoded bytes produces exactly
// the transaction hash.
func EncodeTxForSigning(tx *types.Transaction) (encodedTx []byte) {
// Since there are different types of transactions in Ethereum.
// We encode them differently
var buffer bytes.Buffer
switch {
// LONDON with dynamic fees
case tx.Type() == types.DynamicFeeTxType:
buffer.Write([]byte{tx.Type()})
rlp.Encode(&buffer, []interface{}{
tx.ChainId(),
tx.Nonce(),
tx.GasTipCap(),
tx.GasFeeCap(),
tx.Gas(),
tx.To(),
tx.Value(),
tx.Data(),
tx.AccessList(),
})
// EIP2390 transaction with access-list
case tx.Type() == types.AccessListTxType:
buffer.Write([]byte{tx.Type()})
rlp.Encode(&buffer, []interface{}{
tx.ChainId(),
tx.Nonce(),
tx.GasPrice(),
tx.Gas(),
tx.To(),
tx.Value(),
tx.Data(),
tx.AccessList(),
})
// EIP155 signature with protection against replay
case tx.Type() == types.LegacyTxType && tx.Protected():
rlp.Encode(&buffer, []interface{}{
tx.Nonce(),
tx.GasPrice(),
tx.Gas(),
tx.To(),
tx.Value(),
tx.Data(),
tx.ChainId(), uint(0), uint(0),
})
// Homestead signature
case tx.Type() == types.LegacyTxType && !tx.Protected():
rlp.Encode(&buffer, []interface{}{
tx.Nonce(),
tx.GasPrice(),
tx.Gas(),
tx.To(),
tx.Value(),
tx.Data(),
})
default:
utils.Panic("Unknown type of transaction %v, %++v", tx.Type(), tx)
}
return buffer.Bytes()
}
const (
// Number of rlp encoded field of the transaction
dynFeeNumField int = 9
accessListTxNumField int = 8
legacyTxNumField int = 9
unprotectedTxNumField int = 6
)
// DecodeTxFromBytes from a string of bytes. If the stream of bytes is larger
// than the transaction then the remaining bytes are discarded and only the
// first bytes are used to decode the transaction. The function returns the
// transactions and the number of bytes read.
func DecodeTxFromBytes(b *bytes.Reader) (tx types.TxData, err error) {
var (
firstByte byte
)
if b.Len() == 0 {
return nil, fmt.Errorf("empty buffer")
}
if firstByte, err = b.ReadByte(); err != nil {
return nil, fmt.Errorf("could not read the first byte: %w", err)
}
switch {
case firstByte == types.DynamicFeeTxType:
return decodeDynamicFeeTx(b)
case firstByte == types.AccessListTxType:
return decodeAccessListTx(b)
// According to the RLP rule, `0xc0 + x` or `0xf7` indicates that the current
// item is a list and this is what's used to identify that the transaction is
// a legacy transaction or a EIP-155 transaction.
//
// Note that 0xc0 would indicate an empty list and thus be an invalid tx.
case firstByte > 0xc0:
// Set the byte-reader backward so that we can apply the rlp-decoder
// over it.
b.UnreadByte()
return decodeLegacyTx(b)
default:
return nil, fmt.Errorf("unexpected first byte: %x", firstByte)
}
}
// decodeDynamicFeeTx encodes a [types.DynamicFeeTx] into a [bytes.Reader] and
// returns an error if it did not pass.
func decodeDynamicFeeTx(b *bytes.Reader) (parsedTx *types.DynamicFeeTx, err error) {
decTx := []any{}
if err = rlp.Decode(b, &decTx); err != nil {
return nil, fmt.Errorf("could not rlp decode transaction: %w", err)
}
if len(decTx) != dynFeeNumField {
return nil, fmt.Errorf("invalid number of field for a dynamic transaction")
}
parsedTx = new(types.DynamicFeeTx)
err = errors.Join(
TryCast(&parsedTx.ChainID, decTx[0], "chainID"),
TryCast(&parsedTx.Nonce, decTx[1], "nonce"),
TryCast(&parsedTx.GasTipCap, decTx[2], "gas-tip-cap"),
TryCast(&parsedTx.GasFeeCap, decTx[3], "gas-fee-cap"),
TryCast(&parsedTx.Gas, decTx[4], "gas"),
TryCast(&parsedTx.To, decTx[5], "to"),
TryCast(&parsedTx.Value, decTx[6], "value"),
TryCast(&parsedTx.Data, decTx[7], "data"),
TryCast(&parsedTx.AccessList, decTx[8], "access-list"),
)
return
}
// decodeAccessListTx decodes a [types.AccessListTx] from a [bytes.Reader]
// and returns an error if it did not pass.
func decodeAccessListTx(b *bytes.Reader) (parsedTx *types.AccessListTx, err error) {
decTx := []any{}
if err := rlp.Decode(b, &decTx); err != nil {
return nil, fmt.Errorf("could not rlp decode transaction: %w", err)
}
if len(decTx) != accessListTxNumField {
return nil, fmt.Errorf("invalid number of field for a dynamic transaction")
}
parsedTx = new(types.AccessListTx)
err = errors.Join(
TryCast(&parsedTx.ChainID, decTx[0], "chainID"),
TryCast(&parsedTx.Nonce, decTx[1], "nonce"),
TryCast(&parsedTx.GasPrice, decTx[2], "gas-price"),
TryCast(&parsedTx.Gas, decTx[3], "gas"),
TryCast(&parsedTx.To, decTx[4], "to"),
TryCast(&parsedTx.Value, decTx[5], "value"),
TryCast(&parsedTx.Data, decTx[6], "data"),
TryCast(&parsedTx.AccessList, decTx[7], "access-list"),
)
return
}
// decodeLegacyTx decodes a [types.LegacyTx] from a [bytes.Reader] and returns
// an error if it did not pass.
//
// Note that when the transaction is an EIP-155 transaction, the chain-ID is
// not decoded although it could. The reason is that it is complicated to set
// it in the returned element as it "included" in the signature and we don't
// encode the signature.
func decodeLegacyTx(b *bytes.Reader) (parsedTx *types.LegacyTx, err error) {
decTx := []any{}
if err = rlp.Decode(b, &decTx); err != nil {
return nil, fmt.Errorf("could not rlp decode transaction: %w", err)
}
if len(decTx) != legacyTxNumField && len(decTx) != unprotectedTxNumField {
return nil, fmt.Errorf("unexpected number of field")
}
parsedTx = new(types.LegacyTx)
err = errors.Join(
TryCast(&parsedTx.Nonce, decTx[0], "nonce"),
TryCast(&parsedTx.GasPrice, decTx[1], "gas-price"),
TryCast(&parsedTx.Gas, decTx[2], "gas"),
TryCast(&parsedTx.To, decTx[3], "to"),
TryCast(&parsedTx.Value, decTx[4], "value"),
TryCast(&parsedTx.Data, decTx[5], "data"),
)
return
}
// TryCast will attempt to set t with the underlying value of `from` will return
// an error if the type does not match. The explainer string is used to generate
// the error if any.
func TryCast[T any](into *T, from any, explainer string) error {
if into == nil || from == nil {
return fmt.Errorf("from or into is/are nil")
}
// The rlp encoding is not "type-aware", if the underlying field is an
// access-list, it will decode into []interface{} (and we recursively parse
// it) otherwise, it always decodes to `[]byte`
if list, ok := (from).([]interface{}); ok {
var (
length = len(list)
err error
)
switch any(*into).(type) {
case types.AccessList:
accessList := make(types.AccessList, length)
for i := range accessList {
err = errors.Join(
err,
TryCast(&accessList[i], list[i], fmt.Sprintf("%v[%v]", explainer, i)),
)
}
*into = (any(accessList)).(T)
return err
case types.AccessTuple:
tuple := types.AccessTuple{}
err = errors.Join(
TryCast(&tuple.Address, list[0], fmt.Sprintf("%v.%v", explainer, "address")),
TryCast(&tuple.StorageKeys, list[1], fmt.Sprintf("%v.%v", explainer, "storage-key")),
)
*into = (any(tuple)).(T)
return err
case []common.Hash:
hashes := make([]common.Hash, length)
for i := range hashes {
TryCast(&hashes[i], list[i], fmt.Sprintf("%v[%v]", explainer, i))
}
*into = (any(hashes)).(T)
return err
default:
return fmt.Errorf("unsupported type %T for %v", *into, explainer)
}
}
var (
intoAny = any(*into)
fromBytes = from.([]byte)
)
switch intoAny.(type) {
case *common.Address:
// Parse the bytes as an UTF8 string (= direct casting in go).
// Then, the string as a hex string encoded address.
address := common.BytesToAddress(fromBytes)
*into = any(&address).(T)
case common.Address:
// Parse the bytes as an UTF8 string (= direct casting in go).
// Then, the string as an hexstring encoded address.
address := common.BytesToAddress(fromBytes)
*into = any(address).(T)
case common.Hash:
// Parse the bytes as an UTF8 string (= direct casting in go).
// Then, the string as a hexstring encoded address.
hash := common.BytesToHash(fromBytes)
*into = any(hash).(T)
case *big.Int:
var parsedBigInt big.Int
parsedBigInt.SetBytes(fromBytes)
*into = any(&parsedBigInt).(T)
case uint64:
// The encoding of uint64 can use less than 8 bytes. For this
// reason we go through a big integer.
var parsedBigInt big.Int
parsedBigInt.SetBytes(fromBytes)
*into = any(parsedBigInt.Uint64()).(T)
case []byte:
*into = any(fromBytes).(T)
default:
// Unsupported type - accumulate the error
return fmt.Errorf("could not decode %v (value %s, type %T) as type %T", explainer, from, from, *into)
}
return nil
}