feat: ETH compatibility in Filecoin : Support Homestead and EIP-155 Ethereum transactions("legacy" transactions) in Filecoin (#11969)
* poc for eth legacy tx * print statements * finished * tests work * remove print statements * Remove all print statements * remove extraneous changes * cleaned up code and interface * run make jen * dont duplicate signature * go mod tidy and remove prints * clean up tests * test for conversion * changes as per review * more unit tests for legacy txns * Apply suggestions from code review Co-authored-by: Rod Vagg <rod@vagg.org> * address review comments from Rodd * changes as per zen's 2nd review * go mod tidy * feat: ETH compatibility in Filecoin : Support EIP-155 Ethereum transactions in Filecoin (#11970) * itests passing for 155 tx * first working version for EIP-155 transactions * green itest * add docs * tests * remove print stmt * remove print stmt * validate signature * changes as per zen's review * correct signature verification * gate tx by Network Version * handle arajsek review * fix imports order * fix lint * dont lock in mpool for network gating ETH messages * sender can be an ID address --------- Co-authored-by: Rod Vagg <rod@vagg.org>
This commit is contained in:
parent
423d8a798f
commit
c9c070727a
@ -6543,6 +6543,7 @@
|
||||
"gas": "0x5",
|
||||
"maxFeePerGas": "0x0",
|
||||
"maxPriorityFeePerGas": "0x0",
|
||||
"gasPrice": "0x0",
|
||||
"accessList": [
|
||||
"0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e"
|
||||
],
|
||||
@ -6598,6 +6599,10 @@
|
||||
"title": "number",
|
||||
"type": "number"
|
||||
},
|
||||
"gasPrice": {
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"hash": {
|
||||
"items": {
|
||||
"description": "Number is a number",
|
||||
@ -6738,6 +6743,7 @@
|
||||
"gas": "0x5",
|
||||
"maxFeePerGas": "0x0",
|
||||
"maxPriorityFeePerGas": "0x0",
|
||||
"gasPrice": "0x0",
|
||||
"accessList": [
|
||||
"0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e"
|
||||
],
|
||||
@ -6793,6 +6799,10 @@
|
||||
"title": "number",
|
||||
"type": "number"
|
||||
},
|
||||
"gasPrice": {
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"hash": {
|
||||
"items": {
|
||||
"description": "Number is a number",
|
||||
@ -6925,6 +6935,7 @@
|
||||
"gas": "0x5",
|
||||
"maxFeePerGas": "0x0",
|
||||
"maxPriorityFeePerGas": "0x0",
|
||||
"gasPrice": "0x0",
|
||||
"accessList": [
|
||||
"0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e"
|
||||
],
|
||||
@ -6980,6 +6991,10 @@
|
||||
"title": "number",
|
||||
"type": "number"
|
||||
},
|
||||
"gasPrice": {
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"hash": {
|
||||
"items": {
|
||||
"description": "Number is a number",
|
||||
@ -7129,6 +7144,7 @@
|
||||
"gas": "0x5",
|
||||
"maxFeePerGas": "0x0",
|
||||
"maxPriorityFeePerGas": "0x0",
|
||||
"gasPrice": "0x0",
|
||||
"accessList": [
|
||||
"0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e"
|
||||
],
|
||||
@ -7184,6 +7200,10 @@
|
||||
"title": "number",
|
||||
"type": "number"
|
||||
},
|
||||
"gasPrice": {
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"hash": {
|
||||
"items": {
|
||||
"description": "Number is a number",
|
||||
|
@ -3132,6 +3132,7 @@
|
||||
"gas": "0x5",
|
||||
"maxFeePerGas": "0x0",
|
||||
"maxPriorityFeePerGas": "0x0",
|
||||
"gasPrice": "0x0",
|
||||
"accessList": [
|
||||
"0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e"
|
||||
],
|
||||
@ -3187,6 +3188,10 @@
|
||||
"title": "number",
|
||||
"type": "number"
|
||||
},
|
||||
"gasPrice": {
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"hash": {
|
||||
"items": {
|
||||
"description": "Number is a number",
|
||||
@ -3336,6 +3341,7 @@
|
||||
"gas": "0x5",
|
||||
"maxFeePerGas": "0x0",
|
||||
"maxPriorityFeePerGas": "0x0",
|
||||
"gasPrice": "0x0",
|
||||
"accessList": [
|
||||
"0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e"
|
||||
],
|
||||
@ -3391,6 +3397,10 @@
|
||||
"title": "number",
|
||||
"type": "number"
|
||||
},
|
||||
"gasPrice": {
|
||||
"additionalProperties": false,
|
||||
"type": "object"
|
||||
},
|
||||
"hash": {
|
||||
"items": {
|
||||
"description": "Number is a number",
|
||||
|
@ -29,6 +29,7 @@ import (
|
||||
"github.com/filecoin-project/lotus/chain/stmgr"
|
||||
"github.com/filecoin-project/lotus/chain/store"
|
||||
"github.com/filecoin-project/lotus/chain/types"
|
||||
"github.com/filecoin-project/lotus/chain/types/ethtypes"
|
||||
"github.com/filecoin-project/lotus/chain/vm"
|
||||
"github.com/filecoin-project/lotus/lib/async"
|
||||
"github.com/filecoin-project/lotus/metrics"
|
||||
@ -131,6 +132,18 @@ func CommonBlkChecks(ctx context.Context, sm *stmgr.StateManager, cs *store.Chai
|
||||
}
|
||||
}
|
||||
|
||||
func IsValidEthTxForSending(nv network.Version, smsg *types.SignedMessage) bool {
|
||||
ethTx, err := ethtypes.EthTransactionFromSignedFilecoinMessage(smsg)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if nv < network.Version23 && ethTx.Type() != ethtypes.EIP1559TxType {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func IsValidForSending(nv network.Version, act *types.Actor) bool {
|
||||
// Before nv18 (Hygge), we only supported built-in account actors as senders.
|
||||
//
|
||||
@ -276,6 +289,10 @@ func checkBlockMessages(ctx context.Context, sm *stmgr.StateManager, cs *store.C
|
||||
return xerrors.Errorf("block had invalid signed message at index %d: %w", i, err)
|
||||
}
|
||||
|
||||
if m.Signature.Type == crypto.SigTypeDelegated && !IsValidEthTxForSending(nv, m) {
|
||||
return xerrors.Errorf("network version should be atleast NV23 for sending legacy ETH transactions; but current network version is %d", nv)
|
||||
}
|
||||
|
||||
if err := checkMsg(m); err != nil {
|
||||
return xerrors.Errorf("block had invalid secpk message at index %d: %w", i, err)
|
||||
}
|
||||
|
@ -18,34 +18,42 @@ import (
|
||||
// must be recognized by the registered verifier for the signature type.
|
||||
func AuthenticateMessage(msg *types.SignedMessage, signer address.Address) error {
|
||||
var digest []byte
|
||||
signatureType := msg.Signature.Type
|
||||
signatureCopy := msg.Signature
|
||||
|
||||
typ := msg.Signature.Type
|
||||
switch typ {
|
||||
switch signatureType {
|
||||
case crypto.SigTypeDelegated:
|
||||
txArgs, err := ethtypes.EthTxArgsFromUnsignedEthMessage(&msg.Message)
|
||||
signatureCopy.Data = make([]byte, len(msg.Signature.Data))
|
||||
copy(signatureCopy.Data, msg.Signature.Data)
|
||||
ethTx, err := ethtypes.EthTransactionFromSignedFilecoinMessage(msg)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to reconstruct eth transaction: %w", err)
|
||||
}
|
||||
roundTripMsg, err := txArgs.ToUnsignedMessage(msg.Message.From)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to reconstruct filecoin msg: %w", err)
|
||||
return xerrors.Errorf("failed to reconstruct Ethereum transaction: %w", err)
|
||||
}
|
||||
|
||||
if !msg.Message.Equals(roundTripMsg) {
|
||||
return xerrors.New("ethereum tx failed to roundtrip")
|
||||
filecoinMsg, err := ethTx.ToUnsignedFilecoinMessage(msg.Message.From)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to reconstruct Filecoin message: %w", err)
|
||||
}
|
||||
|
||||
rlpEncodedMsg, err := txArgs.ToRlpUnsignedMsg()
|
||||
if !msg.Message.Equals(filecoinMsg) {
|
||||
return xerrors.New("Ethereum transaction roundtrip mismatch")
|
||||
}
|
||||
|
||||
rlpEncodedMsg, err := ethTx.ToRlpUnsignedMsg()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to repack eth rlp message: %w", err)
|
||||
return xerrors.Errorf("failed to encode RLP message: %w", err)
|
||||
}
|
||||
digest = rlpEncodedMsg
|
||||
signatureCopy.Data, err = ethTx.ToVerifiableSignature(signatureCopy.Data)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to verify signature: %w", err)
|
||||
}
|
||||
default:
|
||||
digest = msg.Message.Cid().Bytes()
|
||||
}
|
||||
|
||||
if err := sigs.Verify(&msg.Signature, signer, digest); err != nil {
|
||||
return xerrors.Errorf("message %s has invalid signature (type %d): %w", msg.Cid(), typ, err)
|
||||
if err := sigs.Verify(&signatureCopy, signer, digest); err != nil {
|
||||
return xerrors.Errorf("invalid signature for message %s (type %d): %w", msg.Cid(), signatureType, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -887,6 +887,11 @@ func (mp *MessagePool) addTs(ctx context.Context, m *types.SignedMessage, curTs
|
||||
nv := mp.api.StateNetworkVersion(ctx, epoch)
|
||||
|
||||
// TODO: I'm not thrilled about depending on filcns here, but I prefer this to duplicating logic
|
||||
|
||||
if m.Signature.Type == crypto.SigTypeDelegated && !consensus.IsValidEthTxForSending(nv, m) {
|
||||
return false, xerrors.Errorf("network version should be atleast NV23 for sending legacy ETH transactions; but current network version is %d", nv)
|
||||
}
|
||||
|
||||
if !consensus.IsValidForSending(nv, senderAct) {
|
||||
return false, xerrors.Errorf("sender actor %s is not a valid top-level sender", m.Message.From)
|
||||
}
|
||||
|
@ -196,7 +196,7 @@ func (ms *MessageSigner) dstoreKey(addr address.Address) datastore.Key {
|
||||
|
||||
func SigningBytes(msg *types.Message, sigType address.Protocol) ([]byte, error) {
|
||||
if sigType == address.Delegated {
|
||||
txArgs, err := ethtypes.EthTxArgsFromUnsignedEthMessage(msg)
|
||||
txArgs, err := ethtypes.Eth1559TxArgsFromUnsignedFilecoinMessage(msg)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to reconstruct eth transaction: %w", err)
|
||||
}
|
||||
|
342
chain/types/ethtypes/eth_1559_transactions.go
Normal file
342
chain/types/ethtypes/eth_1559_transactions.go
Normal file
@ -0,0 +1,342 @@
|
||||
package ethtypes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/filecoin-project/go-address"
|
||||
"github.com/filecoin-project/go-state-types/big"
|
||||
typescrypto "github.com/filecoin-project/go-state-types/crypto"
|
||||
|
||||
"github.com/filecoin-project/lotus/build"
|
||||
"github.com/filecoin-project/lotus/chain/types"
|
||||
)
|
||||
|
||||
var _ EthTransaction = (*Eth1559TxArgs)(nil)
|
||||
|
||||
type Eth1559TxArgs struct {
|
||||
ChainID int `json:"chainId"`
|
||||
Nonce int `json:"nonce"`
|
||||
To *EthAddress `json:"to"`
|
||||
Value big.Int `json:"value"`
|
||||
MaxFeePerGas big.Int `json:"maxFeePerGas"`
|
||||
MaxPriorityFeePerGas big.Int `json:"maxPriorityFeePerGas"`
|
||||
GasLimit int `json:"gasLimit"`
|
||||
Input []byte `json:"input"`
|
||||
V big.Int `json:"v"`
|
||||
R big.Int `json:"r"`
|
||||
S big.Int `json:"s"`
|
||||
}
|
||||
|
||||
func (tx *Eth1559TxArgs) ToUnsignedFilecoinMessage(from address.Address) (*types.Message, error) {
|
||||
if tx.ChainID != build.Eip155ChainId {
|
||||
return nil, fmt.Errorf("invalid chain id: %d", tx.ChainID)
|
||||
}
|
||||
mi, err := getFilecoinMethodInfo(tx.To, tx.Input)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to get method info: %w", err)
|
||||
}
|
||||
|
||||
return &types.Message{
|
||||
Version: 0,
|
||||
To: mi.to,
|
||||
From: from,
|
||||
Nonce: uint64(tx.Nonce),
|
||||
Value: tx.Value,
|
||||
GasLimit: int64(tx.GasLimit),
|
||||
GasFeeCap: tx.MaxFeePerGas,
|
||||
GasPremium: tx.MaxPriorityFeePerGas,
|
||||
Method: mi.method,
|
||||
Params: mi.params,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tx *Eth1559TxArgs) ToRlpUnsignedMsg() ([]byte, error) {
|
||||
encoded, err := toRlpUnsignedMsg(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append([]byte{EIP1559TxType}, encoded...), nil
|
||||
}
|
||||
|
||||
func (tx *Eth1559TxArgs) TxHash() (EthHash, error) {
|
||||
rlp, err := tx.ToRlpSignedMsg()
|
||||
if err != nil {
|
||||
return EmptyEthHash, err
|
||||
}
|
||||
|
||||
return EthHashFromTxBytes(rlp), nil
|
||||
}
|
||||
|
||||
func (tx *Eth1559TxArgs) ToRlpSignedMsg() ([]byte, error) {
|
||||
encoded, err := toRlpSignedMsg(tx, tx.V, tx.R, tx.S)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append([]byte{EIP1559TxType}, encoded...), nil
|
||||
}
|
||||
|
||||
func (tx *Eth1559TxArgs) Signature() (*typescrypto.Signature, error) {
|
||||
r := tx.R.Int.Bytes()
|
||||
s := tx.S.Int.Bytes()
|
||||
v := tx.V.Int.Bytes()
|
||||
|
||||
sig := append([]byte{}, padLeadingZeros(r, 32)...)
|
||||
sig = append(sig, padLeadingZeros(s, 32)...)
|
||||
if len(v) == 0 {
|
||||
sig = append(sig, 0)
|
||||
} else {
|
||||
sig = append(sig, v[0])
|
||||
}
|
||||
|
||||
if len(sig) != 65 {
|
||||
return nil, xerrors.Errorf("signature is not 65 bytes")
|
||||
}
|
||||
return &typescrypto.Signature{
|
||||
Type: typescrypto.SigTypeDelegated, Data: sig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tx *Eth1559TxArgs) Sender() (address.Address, error) {
|
||||
return sender(tx)
|
||||
}
|
||||
|
||||
func (tx *Eth1559TxArgs) Type() int {
|
||||
return EIP1559TxType
|
||||
}
|
||||
|
||||
func (tx *Eth1559TxArgs) ToVerifiableSignature(sig []byte) ([]byte, error) {
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
func (tx *Eth1559TxArgs) ToEthTx(smsg *types.SignedMessage) (EthTx, error) {
|
||||
from, err := EthAddressFromFilecoinAddress(smsg.Message.From)
|
||||
if err != nil {
|
||||
return EthTx{}, xerrors.Errorf("sender was not an eth account")
|
||||
}
|
||||
hash, err := tx.TxHash()
|
||||
if err != nil {
|
||||
return EthTx{}, err
|
||||
}
|
||||
gasFeeCap := EthBigInt(tx.MaxFeePerGas)
|
||||
gasPremium := EthBigInt(tx.MaxPriorityFeePerGas)
|
||||
|
||||
ethTx := EthTx{
|
||||
ChainID: EthUint64(build.Eip155ChainId),
|
||||
Type: EIP1559TxType,
|
||||
Nonce: EthUint64(tx.Nonce),
|
||||
Hash: hash,
|
||||
To: tx.To,
|
||||
Value: EthBigInt(tx.Value),
|
||||
Input: tx.Input,
|
||||
Gas: EthUint64(tx.GasLimit),
|
||||
MaxFeePerGas: &gasFeeCap,
|
||||
MaxPriorityFeePerGas: &gasPremium,
|
||||
From: from,
|
||||
R: EthBigInt(tx.R),
|
||||
S: EthBigInt(tx.S),
|
||||
V: EthBigInt(tx.V),
|
||||
}
|
||||
|
||||
return ethTx, nil
|
||||
}
|
||||
|
||||
func (tx *Eth1559TxArgs) InitialiseSignature(sig typescrypto.Signature) error {
|
||||
if sig.Type != typescrypto.SigTypeDelegated {
|
||||
return xerrors.Errorf("RecoverSignature only supports Delegated signature")
|
||||
}
|
||||
|
||||
if len(sig.Data) != EthEIP1559TxSignatureLen {
|
||||
return xerrors.Errorf("signature should be 65 bytes long, but got %d bytes", len(sig.Data))
|
||||
}
|
||||
|
||||
r_, err := parseBigInt(sig.Data[0:32])
|
||||
if err != nil {
|
||||
return xerrors.Errorf("cannot parse r into EthBigInt")
|
||||
}
|
||||
|
||||
s_, err := parseBigInt(sig.Data[32:64])
|
||||
if err != nil {
|
||||
return xerrors.Errorf("cannot parse s into EthBigInt")
|
||||
}
|
||||
|
||||
v_, err := parseBigInt([]byte{sig.Data[64]})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("cannot parse v into EthBigInt")
|
||||
}
|
||||
|
||||
tx.R = r_
|
||||
tx.S = s_
|
||||
tx.V = v_
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tx *Eth1559TxArgs) packTxFields() ([]interface{}, error) {
|
||||
chainId, err := formatInt(tx.ChainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce, err := formatInt(tx.Nonce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maxPriorityFeePerGas, err := formatBigInt(tx.MaxPriorityFeePerGas)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maxFeePerGas, err := formatBigInt(tx.MaxFeePerGas)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gasLimit, err := formatInt(tx.GasLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value, err := formatBigInt(tx.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := []interface{}{
|
||||
chainId,
|
||||
nonce,
|
||||
maxPriorityFeePerGas,
|
||||
maxFeePerGas,
|
||||
gasLimit,
|
||||
formatEthAddr(tx.To),
|
||||
value,
|
||||
tx.Input,
|
||||
[]interface{}{}, // access list
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func parseEip1559Tx(data []byte) (*Eth1559TxArgs, error) {
|
||||
if data[0] != EIP1559TxType {
|
||||
return nil, xerrors.Errorf("not an EIP-1559 transaction: first byte is not %d", EIP1559TxType)
|
||||
}
|
||||
|
||||
d, err := DecodeRLP(data[1:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decoded, ok := d.([]interface{})
|
||||
if !ok {
|
||||
return nil, xerrors.Errorf("not an EIP-1559 transaction: decoded data is not a list")
|
||||
}
|
||||
|
||||
if len(decoded) != 12 {
|
||||
return nil, xerrors.Errorf("not an EIP-1559 transaction: should have 12 elements in the rlp list")
|
||||
}
|
||||
|
||||
chainId, err := parseInt(decoded[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce, err := parseInt(decoded[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maxPriorityFeePerGas, err := parseBigInt(decoded[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maxFeePerGas, err := parseBigInt(decoded[3])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gasLimit, err := parseInt(decoded[4])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
to, err := parseEthAddr(decoded[5])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value, err := parseBigInt(decoded[6])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
input, err := parseBytes(decoded[7])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessList, ok := decoded[8].([]interface{})
|
||||
if !ok || (ok && len(accessList) != 0) {
|
||||
return nil, xerrors.Errorf("access list should be an empty list")
|
||||
}
|
||||
|
||||
r, err := parseBigInt(decoded[10])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s, err := parseBigInt(decoded[11])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, err := parseBigInt(decoded[9])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// EIP-1559 and EIP-2930 transactions only support 0 or 1 for v
|
||||
// Legacy and EIP-155 transactions support other values
|
||||
// https://github.com/ethers-io/ethers.js/blob/56fabe987bb8c1e4891fdf1e5d3fe8a4c0471751/packages/transactions/src.ts/index.ts#L333
|
||||
if !v.Equals(big.NewInt(0)) && !v.Equals(big.NewInt(1)) {
|
||||
return nil, xerrors.Errorf("EIP-1559 transactions only support 0 or 1 for v")
|
||||
}
|
||||
|
||||
args := Eth1559TxArgs{
|
||||
ChainID: chainId,
|
||||
Nonce: nonce,
|
||||
To: to,
|
||||
MaxPriorityFeePerGas: maxPriorityFeePerGas,
|
||||
MaxFeePerGas: maxFeePerGas,
|
||||
GasLimit: gasLimit,
|
||||
Value: value,
|
||||
Input: input,
|
||||
V: v,
|
||||
R: r,
|
||||
S: s,
|
||||
}
|
||||
return &args, nil
|
||||
}
|
||||
|
||||
func Eth1559TxArgsFromUnsignedFilecoinMessage(msg *types.Message) (*Eth1559TxArgs, error) {
|
||||
if msg.Version != 0 {
|
||||
return nil, fmt.Errorf("unsupported msg version: %d", msg.Version)
|
||||
}
|
||||
|
||||
params, to, err := getEthParamsAndRecipient(msg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get eth params and recipient: %w", err)
|
||||
}
|
||||
|
||||
return &Eth1559TxArgs{
|
||||
ChainID: build.Eip155ChainId,
|
||||
Nonce: int(msg.Nonce),
|
||||
To: to,
|
||||
Value: msg.Value,
|
||||
Input: params,
|
||||
MaxFeePerGas: msg.GasFeeCap,
|
||||
MaxPriorityFeePerGas: msg.GasPremium,
|
||||
GasLimit: int(msg.GasLimit),
|
||||
}, nil
|
||||
}
|
241
chain/types/ethtypes/eth_1559_transactions_test.go
Normal file
241
chain/types/ethtypes/eth_1559_transactions_test.go
Normal file
File diff suppressed because one or more lines are too long
303
chain/types/ethtypes/eth_legacy_155_transactions.go
Normal file
303
chain/types/ethtypes/eth_legacy_155_transactions.go
Normal file
@ -0,0 +1,303 @@
|
||||
package ethtypes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/filecoin-project/go-address"
|
||||
"github.com/filecoin-project/go-state-types/big"
|
||||
typescrypto "github.com/filecoin-project/go-state-types/crypto"
|
||||
|
||||
"github.com/filecoin-project/lotus/build"
|
||||
"github.com/filecoin-project/lotus/chain/types"
|
||||
)
|
||||
|
||||
var _ EthTransaction = (*EthLegacy155TxArgs)(nil)
|
||||
|
||||
// EthLegacy155TxArgs is a legacy Ethereum transaction that uses the EIP-155 chain replay protection mechanism
|
||||
// by incorporating the chainId in the signature.
|
||||
// See how the `V` value in the signature is derived from the chainId at
|
||||
// https://github.com/ethereum/go-ethereum/blob/86a1f0c39494c8f5caddf6bd9fbddd4bdfa944fd/core/types/transaction_signing.go#L424
|
||||
// For EthLegacy155TxArgs, the digest that is used to create a signed transaction includes the `ChainID` but the serialised RLP transaction
|
||||
// does not include the `ChainID` as an explicit field. Instead, the `ChainID` is included in the V value of the signature as mentioned above.
|
||||
type EthLegacy155TxArgs struct {
|
||||
legacyTx *EthLegacyHomesteadTxArgs
|
||||
}
|
||||
|
||||
func NewEthLegacy155TxArgs(tx *EthLegacyHomesteadTxArgs) *EthLegacy155TxArgs {
|
||||
return &EthLegacy155TxArgs{legacyTx: tx}
|
||||
}
|
||||
|
||||
func (tx *EthLegacy155TxArgs) GetLegacyTx() *EthLegacyHomesteadTxArgs {
|
||||
return tx.legacyTx
|
||||
}
|
||||
|
||||
func (tx *EthLegacy155TxArgs) ToEthTx(smsg *types.SignedMessage) (EthTx, error) {
|
||||
from, err := EthAddressFromFilecoinAddress(smsg.Message.From)
|
||||
if err != nil {
|
||||
return EthTx{}, fmt.Errorf("sender was not an eth account")
|
||||
}
|
||||
hash, err := tx.TxHash()
|
||||
if err != nil {
|
||||
return EthTx{}, fmt.Errorf("failed to get tx hash: %w", err)
|
||||
}
|
||||
|
||||
gasPrice := EthBigInt(tx.legacyTx.GasPrice)
|
||||
ethTx := EthTx{
|
||||
ChainID: build.Eip155ChainId,
|
||||
Type: EthLegacyTxType,
|
||||
Nonce: EthUint64(tx.legacyTx.Nonce),
|
||||
Hash: hash,
|
||||
To: tx.legacyTx.To,
|
||||
Value: EthBigInt(tx.legacyTx.Value),
|
||||
Input: tx.legacyTx.Input,
|
||||
Gas: EthUint64(tx.legacyTx.GasLimit),
|
||||
GasPrice: &gasPrice,
|
||||
From: from,
|
||||
R: EthBigInt(tx.legacyTx.R),
|
||||
S: EthBigInt(tx.legacyTx.S),
|
||||
V: EthBigInt(tx.legacyTx.V),
|
||||
}
|
||||
|
||||
return ethTx, nil
|
||||
}
|
||||
|
||||
func (tx *EthLegacy155TxArgs) ToUnsignedFilecoinMessage(from address.Address) (*types.Message, error) {
|
||||
if err := validateEIP155ChainId(tx.legacyTx.V); err != nil {
|
||||
return nil, fmt.Errorf("failed to validate EIP155 chain id: %w", err)
|
||||
}
|
||||
return tx.legacyTx.ToUnsignedFilecoinMessage(from)
|
||||
}
|
||||
|
||||
func (tx *EthLegacy155TxArgs) ToRlpUnsignedMsg() ([]byte, error) {
|
||||
return toRlpUnsignedMsg(tx)
|
||||
}
|
||||
|
||||
func (tx *EthLegacy155TxArgs) TxHash() (EthHash, error) {
|
||||
encoded, err := tx.ToRawTxBytesSigned()
|
||||
if err != nil {
|
||||
return EthHash{}, fmt.Errorf("failed to encode rlp signed msg: %w", err)
|
||||
}
|
||||
|
||||
return EthHashFromTxBytes(encoded), nil
|
||||
}
|
||||
|
||||
func (tx *EthLegacy155TxArgs) ToRawTxBytesSigned() ([]byte, error) {
|
||||
packed1, err := tx.packTxFields()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
packed1 = packed1[:len(packed1)-3] // remove chainId, r and s as they are only used for signature verification
|
||||
|
||||
packed2, err := packSigFields(tx.legacyTx.V, tx.legacyTx.R, tx.legacyTx.S)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encoded, err := EncodeRLP(append(packed1, packed2...))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode rlp signed msg: %w", err)
|
||||
}
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
func (tx *EthLegacy155TxArgs) ToRlpSignedMsg() ([]byte, error) {
|
||||
return toRlpSignedMsg(tx, tx.legacyTx.V, tx.legacyTx.R, tx.legacyTx.S)
|
||||
}
|
||||
|
||||
func (tx *EthLegacy155TxArgs) Signature() (*typescrypto.Signature, error) {
|
||||
if err := validateEIP155ChainId(tx.legacyTx.V); err != nil {
|
||||
return nil, fmt.Errorf("failed to validate EIP155 chain id: %w", err)
|
||||
}
|
||||
r := tx.legacyTx.R.Int.Bytes()
|
||||
s := tx.legacyTx.S.Int.Bytes()
|
||||
v := tx.legacyTx.V.Int.Bytes()
|
||||
|
||||
sig := append([]byte{}, padLeadingZeros(r, 32)...)
|
||||
sig = append(sig, padLeadingZeros(s, 32)...)
|
||||
sig = append(sig, v...)
|
||||
|
||||
// pre-pend a one byte marker so nodes know that this is a legacy transaction
|
||||
sig = append([]byte{EthLegacy155TxSignaturePrefix}, sig...)
|
||||
|
||||
if len(sig) != EthLegacy155TxSignatureLen0 && len(sig) != EthLegacy155TxSignatureLen1 {
|
||||
return nil, fmt.Errorf("signature is not %d OR %d bytes; it is %d bytes", EthLegacy155TxSignatureLen0, EthLegacy155TxSignatureLen1,
|
||||
len(sig))
|
||||
}
|
||||
|
||||
return &typescrypto.Signature{
|
||||
Type: typescrypto.SigTypeDelegated, Data: sig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tx *EthLegacy155TxArgs) Sender() (address.Address, error) {
|
||||
if err := validateEIP155ChainId(tx.legacyTx.V); err != nil {
|
||||
return address.Address{}, fmt.Errorf("failed to validate EIP155 chain id: %w", err)
|
||||
}
|
||||
return sender(tx)
|
||||
}
|
||||
|
||||
func (tx *EthLegacy155TxArgs) Type() int {
|
||||
return EthLegacyTxType
|
||||
}
|
||||
|
||||
var big8 = big.NewInt(8)
|
||||
|
||||
func (tx *EthLegacy155TxArgs) ToVerifiableSignature(sig []byte) ([]byte, error) {
|
||||
if len(sig) != EthLegacy155TxSignatureLen0 && len(sig) != EthLegacy155TxSignatureLen1 {
|
||||
return nil, fmt.Errorf("signature should be %d or %d bytes long but got %d bytes",
|
||||
EthLegacy155TxSignatureLen0, EthLegacy155TxSignatureLen1, len(sig))
|
||||
}
|
||||
if sig[0] != EthLegacy155TxSignaturePrefix {
|
||||
return nil, fmt.Errorf("expected signature prefix 0x%x, but got 0x%x", EthLegacy155TxSignaturePrefix, sig[0])
|
||||
}
|
||||
|
||||
// Remove the prefix byte as it's only used for legacy transaction identification
|
||||
sig = sig[1:]
|
||||
|
||||
// Extract the 'v' value from the signature
|
||||
vValue := big.NewFromGo(big.NewInt(0).SetBytes(sig[64:]))
|
||||
|
||||
if err := validateEIP155ChainId(vValue); err != nil {
|
||||
return nil, fmt.Errorf("failed to validate EIP155 chain id: %w", err)
|
||||
}
|
||||
|
||||
// See https://github.com/ethereum/go-ethereum/blob/86a1f0c39494c8f5caddf6bd9fbddd4bdfa944fd/core/types/transaction_signing.go#L424
|
||||
chainIdMul := big.Mul(big.NewIntUnsigned(build.Eip155ChainId), big.NewInt(2))
|
||||
vValue = big.Sub(vValue, chainIdMul)
|
||||
vValue = big.Sub(vValue, big8)
|
||||
|
||||
// Adjust 'v' value for compatibility with new transactions: 27 -> 0, 28 -> 1
|
||||
if vValue.Equals(big.NewInt(27)) {
|
||||
sig[64] = 0
|
||||
} else if vValue.Equals(big.NewInt(28)) {
|
||||
sig[64] = 1
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid 'v' value: expected 27 or 28, got %d", vValue.Int64())
|
||||
}
|
||||
|
||||
return sig[0:65], nil
|
||||
}
|
||||
|
||||
func (tx *EthLegacy155TxArgs) InitialiseSignature(sig typescrypto.Signature) error {
|
||||
if sig.Type != typescrypto.SigTypeDelegated {
|
||||
return fmt.Errorf("RecoverSignature only supports Delegated signature")
|
||||
}
|
||||
|
||||
if len(sig.Data) != EthLegacy155TxSignatureLen0 && len(sig.Data) != EthLegacy155TxSignatureLen1 {
|
||||
return fmt.Errorf("signature should be %d or %d bytes long, but got %d bytes", EthLegacy155TxSignatureLen0,
|
||||
EthLegacy155TxSignatureLen1, len(sig.Data))
|
||||
}
|
||||
|
||||
if sig.Data[0] != EthLegacy155TxSignaturePrefix {
|
||||
return fmt.Errorf("expected signature prefix 0x01, but got 0x%x", sig.Data[0])
|
||||
}
|
||||
|
||||
// ignore the first byte of the signature as it's only used for legacy transaction identification
|
||||
r_, err := parseBigInt(sig.Data[1:33])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse r into EthBigInt: %w", err)
|
||||
}
|
||||
|
||||
s_, err := parseBigInt(sig.Data[33:65])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse s into EthBigInt: %w", err)
|
||||
}
|
||||
|
||||
v_, err := parseBigInt(sig.Data[65:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse v into EthBigInt: %w", err)
|
||||
}
|
||||
|
||||
if err := validateEIP155ChainId(v_); err != nil {
|
||||
return fmt.Errorf("failed to validate EIP155 chain id: %w", err)
|
||||
}
|
||||
|
||||
tx.legacyTx.R = r_
|
||||
tx.legacyTx.S = s_
|
||||
tx.legacyTx.V = v_
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tx *EthLegacy155TxArgs) packTxFields() ([]interface{}, error) {
|
||||
nonce, err := formatInt(tx.legacyTx.Nonce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// format gas price
|
||||
gasPrice, err := formatBigInt(tx.legacyTx.GasPrice)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gasLimit, err := formatInt(tx.legacyTx.GasLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value, err := formatBigInt(tx.legacyTx.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chainIdBigInt := big.NewIntUnsigned(build.Eip155ChainId)
|
||||
chainId, err := formatBigInt(chainIdBigInt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r, err := formatInt(0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s, err := formatInt(0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := []interface{}{
|
||||
nonce,
|
||||
gasPrice,
|
||||
gasLimit,
|
||||
formatEthAddr(tx.legacyTx.To),
|
||||
value,
|
||||
tx.legacyTx.Input,
|
||||
chainId,
|
||||
r, s,
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func validateEIP155ChainId(v big.Int) error {
|
||||
chainId := deriveEIP155ChainId(v)
|
||||
if !chainId.Equals(big.NewIntUnsigned(build.Eip155ChainId)) {
|
||||
return fmt.Errorf("invalid chain id, expected %d, got %s", build.Eip155ChainId, chainId.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deriveEIP155ChainId derives the chain id from the given v parameter
|
||||
func deriveEIP155ChainId(v big.Int) big.Int {
|
||||
if big.BitLen(v) <= 64 {
|
||||
vUint64 := v.Uint64()
|
||||
if vUint64 == 27 || vUint64 == 28 {
|
||||
return big.NewInt(0)
|
||||
}
|
||||
return big.NewIntUnsigned((vUint64 - 35) / 2)
|
||||
}
|
||||
|
||||
v = big.Sub(v, big.NewInt(35))
|
||||
return big.Div(v, big.NewInt(2))
|
||||
}
|
||||
|
||||
func calcEIP155TxSignatureLen(chain uint64, v int) int {
|
||||
chainId := big.NewIntUnsigned(chain)
|
||||
vVal := big.Add(big.Mul(chainId, big.NewInt(2)), big.NewInt(int64(v)))
|
||||
vLen := len(vVal.Int.Bytes())
|
||||
|
||||
// EthLegacyHomesteadTxSignatureLen includes the 1 byte legacy tx marker prefix and also 1 byte for the V value.
|
||||
// So we subtract 1 to not double count the length of the v value
|
||||
return EthLegacyHomesteadTxSignatureLen + vLen - 1
|
||||
}
|
188
chain/types/ethtypes/eth_legacy_155_transactions_test.go
Normal file
188
chain/types/ethtypes/eth_legacy_155_transactions_test.go
Normal file
@ -0,0 +1,188 @@
|
||||
package ethtypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/filecoin-project/go-state-types/big"
|
||||
builtintypes "github.com/filecoin-project/go-state-types/builtin"
|
||||
)
|
||||
|
||||
func TestEIP155Tx(t *testing.T) {
|
||||
txStr := "f86680843b9aca00835dc1ba94c2dca9a18d4a4057921d1bcb22da05e68e46b1d06480820297a0f91ee69c4603c4f21131467ee9e06ad4a96d0a29fa8064db61f3adaea0eb6e92a07181e306bb8f773d94cc3b75e9835de00c004e072e6630c0c46971d38706bb01"
|
||||
|
||||
bz := mustDecodeHex(txStr)
|
||||
|
||||
tx, err := parseLegacyTx(bz)
|
||||
require.NoError(t, err)
|
||||
|
||||
eth155Tx, ok := tx.(*EthLegacy155TxArgs)
|
||||
require.True(t, ok)
|
||||
|
||||
// Verify nonce
|
||||
require.EqualValues(t, 0, eth155Tx.legacyTx.Nonce)
|
||||
|
||||
// Verify recipient address
|
||||
expectedToAddr, err := ParseEthAddress("0xc2dca9a18d4a4057921d1bcb22da05e68e46b1d0")
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, expectedToAddr, *eth155Tx.legacyTx.To)
|
||||
|
||||
// Verify sender address
|
||||
expectedFromAddr, err := ParseEthAddress("0xA2BBB73aC59b256415e91A820b224dbAF2C268FA")
|
||||
require.NoError(t, err)
|
||||
sender, err := eth155Tx.Sender()
|
||||
require.NoError(t, err)
|
||||
expectedFromFilecoinAddr, err := expectedFromAddr.ToFilecoinAddress()
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, expectedFromFilecoinAddr, sender)
|
||||
|
||||
// Verify transaction value
|
||||
expectedValue, ok := big.NewInt(0).SetString("100", 10)
|
||||
require.True(t, ok)
|
||||
require.True(t, eth155Tx.legacyTx.Value.Cmp(expectedValue) == 0)
|
||||
|
||||
// Verify gas limit and gas price
|
||||
expectedGasPrice, ok := big.NewInt(0).SetString("1000000000", 10)
|
||||
require.True(t, ok)
|
||||
require.EqualValues(t, 6144442, eth155Tx.legacyTx.GasLimit)
|
||||
require.True(t, eth155Tx.legacyTx.GasPrice.Cmp(expectedGasPrice) == 0)
|
||||
|
||||
require.Empty(t, eth155Tx.legacyTx.Input)
|
||||
|
||||
// Verify signature values (v, r, s)
|
||||
expectedV, ok := big.NewInt(0).SetString("0297", 16)
|
||||
require.True(t, ok)
|
||||
require.True(t, eth155Tx.legacyTx.V.Cmp(expectedV) == 0)
|
||||
|
||||
expectedR, ok := big.NewInt(0).SetString("f91ee69c4603c4f21131467ee9e06ad4a96d0a29fa8064db61f3adaea0eb6e92", 16)
|
||||
require.True(t, ok)
|
||||
require.True(t, eth155Tx.legacyTx.R.Cmp(expectedR) == 0)
|
||||
|
||||
expectedS, ok := big.NewInt(0).SetString("7181e306bb8f773d94cc3b75e9835de00c004e072e6630c0c46971d38706bb01", 16)
|
||||
require.True(t, ok)
|
||||
require.True(t, eth155Tx.legacyTx.S.Cmp(expectedS) == 0)
|
||||
|
||||
// Convert to signed Filecoin message and verify fields
|
||||
smsg, err := ToSignedFilecoinMessage(eth155Tx)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.EqualValues(t, smsg.Message.From, sender)
|
||||
|
||||
expectedToFilecoinAddr, err := eth155Tx.legacyTx.To.ToFilecoinAddress()
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, smsg.Message.To, expectedToFilecoinAddr)
|
||||
require.EqualValues(t, smsg.Message.Value, eth155Tx.legacyTx.Value)
|
||||
require.EqualValues(t, smsg.Message.GasLimit, eth155Tx.legacyTx.GasLimit)
|
||||
require.EqualValues(t, smsg.Message.GasFeeCap, eth155Tx.legacyTx.GasPrice)
|
||||
require.EqualValues(t, smsg.Message.GasPremium, eth155Tx.legacyTx.GasPrice)
|
||||
require.EqualValues(t, smsg.Message.Nonce, eth155Tx.legacyTx.Nonce)
|
||||
require.Empty(t, smsg.Message.Params)
|
||||
require.EqualValues(t, smsg.Message.Method, builtintypes.MethodsEVM.InvokeContract)
|
||||
|
||||
// Convert signed Filecoin message back to Ethereum transaction and verify equality
|
||||
ethTx, err := EthTransactionFromSignedFilecoinMessage(smsg)
|
||||
require.NoError(t, err)
|
||||
convertedLegacyTx, ok := ethTx.(*EthLegacy155TxArgs)
|
||||
require.True(t, ok)
|
||||
eth155Tx.legacyTx.Input = nil
|
||||
require.EqualValues(t, convertedLegacyTx, eth155Tx)
|
||||
|
||||
// Verify EthTx fields
|
||||
ethTxVal, err := eth155Tx.ToEthTx(smsg)
|
||||
require.NoError(t, err)
|
||||
expectedHash, err := eth155Tx.TxHash()
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, ethTxVal.Hash, expectedHash)
|
||||
require.Nil(t, ethTxVal.MaxFeePerGas)
|
||||
require.Nil(t, ethTxVal.MaxPriorityFeePerGas)
|
||||
require.EqualValues(t, ethTxVal.Gas, eth155Tx.legacyTx.GasLimit)
|
||||
require.EqualValues(t, ethTxVal.Value, eth155Tx.legacyTx.Value)
|
||||
require.EqualValues(t, ethTxVal.Nonce, eth155Tx.legacyTx.Nonce)
|
||||
require.EqualValues(t, ethTxVal.To, eth155Tx.legacyTx.To)
|
||||
require.EqualValues(t, ethTxVal.From, expectedFromAddr)
|
||||
}
|
||||
|
||||
func TestDeriveEIP155ChainId(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
v big.Int
|
||||
expectedChainId big.Int
|
||||
}{
|
||||
{
|
||||
name: "V equals 27",
|
||||
v: big.NewInt(27),
|
||||
expectedChainId: big.NewInt(0),
|
||||
},
|
||||
{
|
||||
name: "V equals 28",
|
||||
v: big.NewInt(28),
|
||||
expectedChainId: big.NewInt(0),
|
||||
},
|
||||
{
|
||||
name: "V small chain ID",
|
||||
v: big.NewInt(37), // (37 - 35) / 2 = 1
|
||||
expectedChainId: big.NewInt(1),
|
||||
},
|
||||
{
|
||||
name: "V large chain ID",
|
||||
v: big.NewInt(1001), // (1001 - 35) / 2 = 483
|
||||
expectedChainId: big.NewInt(483),
|
||||
},
|
||||
{
|
||||
name: "V very large chain ID",
|
||||
v: big.NewInt(1 << 20), // (1048576 - 35) / 2 = 524770
|
||||
expectedChainId: big.NewInt(524270),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := deriveEIP155ChainId(tt.v)
|
||||
require.True(t, result.Equals(tt.expectedChainId), "Expected %s, got %s for V=%s", tt.expectedChainId.String(), result.String(), tt.v.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcEIP155TxSignatureLen(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
chainID uint64
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
name: "ChainID that fits in 1 byte",
|
||||
chainID: 0x01,
|
||||
expected: EthLegacyHomesteadTxSignatureLen + 1 - 1,
|
||||
},
|
||||
{
|
||||
name: "ChainID that fits in 2 bytes",
|
||||
chainID: 0x0100,
|
||||
expected: EthLegacyHomesteadTxSignatureLen + 2 - 1,
|
||||
},
|
||||
{
|
||||
name: "ChainID that fits in 3 bytes",
|
||||
chainID: 0x010000,
|
||||
expected: EthLegacyHomesteadTxSignatureLen + 3 - 1,
|
||||
},
|
||||
{
|
||||
name: "ChainID that fits in 4 bytes",
|
||||
chainID: 0x01000000,
|
||||
expected: EthLegacyHomesteadTxSignatureLen + 4 - 1,
|
||||
},
|
||||
{
|
||||
name: "ChainID that fits in 6 bytes",
|
||||
chainID: 0x010000000000,
|
||||
expected: EthLegacyHomesteadTxSignatureLen + 6 - 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := calcEIP155TxSignatureLen(tt.chainID, 1)
|
||||
if result != tt.expected {
|
||||
t.Errorf("calcEIP155TxSignatureLen(%d) = %d, want %d", tt.chainID, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
228
chain/types/ethtypes/eth_legacy_homestead_transactions.go
Normal file
228
chain/types/ethtypes/eth_legacy_homestead_transactions.go
Normal file
@ -0,0 +1,228 @@
|
||||
package ethtypes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/filecoin-project/go-address"
|
||||
"github.com/filecoin-project/go-state-types/big"
|
||||
typescrypto "github.com/filecoin-project/go-state-types/crypto"
|
||||
|
||||
"github.com/filecoin-project/lotus/chain/types"
|
||||
)
|
||||
|
||||
var _ EthTransaction = (*EthLegacyHomesteadTxArgs)(nil)
|
||||
|
||||
type EthLegacyHomesteadTxArgs struct {
|
||||
Nonce int `json:"nonce"`
|
||||
GasPrice big.Int `json:"gasPrice"`
|
||||
GasLimit int `json:"gasLimit"`
|
||||
To *EthAddress `json:"to"`
|
||||
Value big.Int `json:"value"`
|
||||
Input []byte `json:"input"`
|
||||
V big.Int `json:"v"`
|
||||
R big.Int `json:"r"`
|
||||
S big.Int `json:"s"`
|
||||
}
|
||||
|
||||
func (tx *EthLegacyHomesteadTxArgs) ToEthTx(smsg *types.SignedMessage) (EthTx, error) {
|
||||
from, err := EthAddressFromFilecoinAddress(smsg.Message.From)
|
||||
if err != nil {
|
||||
return EthTx{}, fmt.Errorf("sender was not an eth account")
|
||||
}
|
||||
hash, err := tx.TxHash()
|
||||
if err != nil {
|
||||
return EthTx{}, fmt.Errorf("failed to get tx hash: %w", err)
|
||||
}
|
||||
|
||||
gasPrice := EthBigInt(tx.GasPrice)
|
||||
ethTx := EthTx{
|
||||
ChainID: EthLegacyHomesteadTxChainID,
|
||||
Type: EthLegacyTxType,
|
||||
Nonce: EthUint64(tx.Nonce),
|
||||
Hash: hash,
|
||||
To: tx.To,
|
||||
Value: EthBigInt(tx.Value),
|
||||
Input: tx.Input,
|
||||
Gas: EthUint64(tx.GasLimit),
|
||||
GasPrice: &gasPrice,
|
||||
From: from,
|
||||
R: EthBigInt(tx.R),
|
||||
S: EthBigInt(tx.S),
|
||||
V: EthBigInt(tx.V),
|
||||
}
|
||||
|
||||
return ethTx, nil
|
||||
}
|
||||
|
||||
func (tx *EthLegacyHomesteadTxArgs) ToUnsignedFilecoinMessage(from address.Address) (*types.Message, error) {
|
||||
mi, err := getFilecoinMethodInfo(tx.To, tx.Input)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to get method info: %w", err)
|
||||
}
|
||||
|
||||
return &types.Message{
|
||||
Version: 0,
|
||||
To: mi.to,
|
||||
From: from,
|
||||
Nonce: uint64(tx.Nonce),
|
||||
Value: tx.Value,
|
||||
GasLimit: int64(tx.GasLimit),
|
||||
GasFeeCap: tx.GasPrice,
|
||||
GasPremium: tx.GasPrice,
|
||||
Method: mi.method,
|
||||
Params: mi.params,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tx *EthLegacyHomesteadTxArgs) ToVerifiableSignature(sig []byte) ([]byte, error) {
|
||||
if len(sig) != EthLegacyHomesteadTxSignatureLen {
|
||||
return nil, fmt.Errorf("signature should be %d bytes long (1 byte metadata, %d bytes sig data), but got %d bytes",
|
||||
EthLegacyHomesteadTxSignatureLen, EthLegacyHomesteadTxSignatureLen-1, len(sig))
|
||||
}
|
||||
if sig[0] != EthLegacyHomesteadTxSignaturePrefix {
|
||||
return nil, fmt.Errorf("expected signature prefix 0x%x, but got 0x%x", EthLegacyHomesteadTxSignaturePrefix, sig[0])
|
||||
}
|
||||
|
||||
// Remove the prefix byte as it's only used for legacy transaction identification
|
||||
sig = sig[1:]
|
||||
|
||||
// Extract the 'v' value from the signature, which is the last byte in Ethereum signatures
|
||||
vValue := big.NewFromGo(big.NewInt(0).SetBytes(sig[64:]))
|
||||
|
||||
// Adjust 'v' value for compatibility with new transactions: 27 -> 0, 28 -> 1
|
||||
if vValue.Equals(big.NewInt(27)) {
|
||||
sig[64] = 0
|
||||
} else if vValue.Equals(big.NewInt(28)) {
|
||||
sig[64] = 1
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid 'v' value: expected 27 or 28, got %d", vValue.Int64())
|
||||
}
|
||||
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
func (tx *EthLegacyHomesteadTxArgs) ToRlpUnsignedMsg() ([]byte, error) {
|
||||
return toRlpUnsignedMsg(tx)
|
||||
}
|
||||
|
||||
func (tx *EthLegacyHomesteadTxArgs) TxHash() (EthHash, error) {
|
||||
rlp, err := tx.ToRlpSignedMsg()
|
||||
if err != nil {
|
||||
return EthHash{}, err
|
||||
}
|
||||
return EthHashFromTxBytes(rlp), nil
|
||||
}
|
||||
|
||||
func (tx *EthLegacyHomesteadTxArgs) ToRlpSignedMsg() ([]byte, error) {
|
||||
return toRlpSignedMsg(tx, tx.V, tx.R, tx.S)
|
||||
}
|
||||
|
||||
func (tx *EthLegacyHomesteadTxArgs) Signature() (*typescrypto.Signature, error) {
|
||||
// throw an error if the v value is not 27 or 28
|
||||
if !tx.V.Equals(big.NewInt(27)) && !tx.V.Equals(big.NewInt(28)) {
|
||||
return nil, fmt.Errorf("legacy homestead transactions only support 27 or 28 for v")
|
||||
}
|
||||
r := tx.R.Int.Bytes()
|
||||
s := tx.S.Int.Bytes()
|
||||
v := tx.V.Int.Bytes()
|
||||
|
||||
sig := append([]byte{}, padLeadingZeros(r, 32)...)
|
||||
sig = append(sig, padLeadingZeros(s, 32)...)
|
||||
if len(v) == 0 {
|
||||
sig = append(sig, 0)
|
||||
} else {
|
||||
sig = append(sig, v[0])
|
||||
}
|
||||
// pre-pend a one byte marker so nodes know that this is a legacy transaction
|
||||
sig = append([]byte{EthLegacyHomesteadTxSignaturePrefix}, sig...)
|
||||
|
||||
if len(sig) != EthLegacyHomesteadTxSignatureLen {
|
||||
return nil, fmt.Errorf("signature is not %d bytes", EthLegacyHomesteadTxSignatureLen)
|
||||
}
|
||||
|
||||
return &typescrypto.Signature{
|
||||
Type: typescrypto.SigTypeDelegated, Data: sig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tx *EthLegacyHomesteadTxArgs) Sender() (address.Address, error) {
|
||||
return sender(tx)
|
||||
}
|
||||
|
||||
func (tx *EthLegacyHomesteadTxArgs) Type() int {
|
||||
return EthLegacyTxType
|
||||
}
|
||||
|
||||
func (tx *EthLegacyHomesteadTxArgs) InitialiseSignature(sig typescrypto.Signature) error {
|
||||
if sig.Type != typescrypto.SigTypeDelegated {
|
||||
return fmt.Errorf("RecoverSignature only supports Delegated signature")
|
||||
}
|
||||
|
||||
if len(sig.Data) != EthLegacyHomesteadTxSignatureLen {
|
||||
return fmt.Errorf("signature should be %d bytes long, but got %d bytes", EthLegacyHomesteadTxSignatureLen, len(sig.Data))
|
||||
}
|
||||
|
||||
if sig.Data[0] != EthLegacyHomesteadTxSignaturePrefix {
|
||||
return fmt.Errorf("expected signature prefix 0x01, but got 0x%x", sig.Data[0])
|
||||
}
|
||||
|
||||
// ignore the first byte of the signature as it's only used for legacy transaction identification
|
||||
r_, err := parseBigInt(sig.Data[1:33])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse r into EthBigInt: %w", err)
|
||||
}
|
||||
|
||||
s_, err := parseBigInt(sig.Data[33:65])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse s into EthBigInt: %w", err)
|
||||
}
|
||||
|
||||
v_, err := parseBigInt([]byte{sig.Data[65]})
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse v into EthBigInt: %w", err)
|
||||
}
|
||||
|
||||
if !v_.Equals(big.NewInt(27)) && !v_.Equals(big.NewInt(28)) {
|
||||
return fmt.Errorf("legacy homestead transactions only support 27 or 28 for v")
|
||||
}
|
||||
|
||||
tx.R = r_
|
||||
tx.S = s_
|
||||
tx.V = v_
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tx *EthLegacyHomesteadTxArgs) packTxFields() ([]interface{}, error) {
|
||||
nonce, err := formatInt(tx.Nonce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// format gas price
|
||||
gasPrice, err := formatBigInt(tx.GasPrice)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gasLimit, err := formatInt(tx.GasLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value, err := formatBigInt(tx.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := []interface{}{
|
||||
nonce,
|
||||
gasPrice,
|
||||
gasLimit,
|
||||
formatEthAddr(tx.To),
|
||||
value,
|
||||
tx.Input,
|
||||
}
|
||||
return res, nil
|
||||
}
|
299
chain/types/ethtypes/eth_legacy_homestead_transactions_test.go
Normal file
299
chain/types/ethtypes/eth_legacy_homestead_transactions_test.go
Normal file
@ -0,0 +1,299 @@
|
||||
package ethtypes
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/filecoin-project/go-state-types/big"
|
||||
builtintypes "github.com/filecoin-project/go-state-types/builtin"
|
||||
|
||||
"github.com/filecoin-project/lotus/lib/sigs"
|
||||
)
|
||||
|
||||
func TestEthLegacyHomesteadTxArgs(t *testing.T) {
|
||||
testcases := []struct {
|
||||
RawTx string
|
||||
ExpectedNonce uint64
|
||||
ExpectedTo string
|
||||
ExpectedInput string
|
||||
ExpectedGasPrice big.Int
|
||||
ExpectedGasLimit int
|
||||
ExpectErr bool
|
||||
}{
|
||||
{
|
||||
"0xf882800182540894095e7baea6a6c7c4c2dfeb977efac326af552d8780a3deadbeef0000000101010010101010101010101010101aaabbbbbbcccccccddddddddd1ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a01fffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804",
|
||||
0x0,
|
||||
"0x095e7baea6a6c7c4c2dfeb977efac326af552d87",
|
||||
"0xdeadbeef0000000101010010101010101010101010101aaabbbbbbcccccccddddddddd",
|
||||
big.NewInt(1),
|
||||
0x5408,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"0xf85f030182520794b94f5374fce5edbc8e2a8697c15331677e6ebf0b0a801ba098ff921201554726367d2be8c804a7ff89ccf285ebc57dff8ae4c44b9c19ac4aa07778cde41a8a37f6a087622b38bc201bd3e7df06dce067569d4def1b53dba98c",
|
||||
0x3,
|
||||
"0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b",
|
||||
"0x",
|
||||
big.NewInt(1),
|
||||
0x5207,
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testcases {
|
||||
// parse txargs
|
||||
tx, err := parseLegacyTx(mustDecodeHex(tc.RawTx))
|
||||
require.NoError(t, err)
|
||||
|
||||
msgRecovered, err := tx.ToRlpUnsignedMsg()
|
||||
require.NoError(t, err)
|
||||
|
||||
// verify signatures
|
||||
from, err := tx.Sender()
|
||||
require.NoError(t, err)
|
||||
|
||||
smsg, err := ToSignedFilecoinMessage(tx)
|
||||
require.NoError(t, err)
|
||||
|
||||
sig := smsg.Signature.Data[:]
|
||||
sig = sig[1:]
|
||||
vValue := big.NewInt(0).SetBytes(sig[64:])
|
||||
vValue_ := big.Sub(big.NewFromGo(vValue), big.NewInt(27))
|
||||
sig[64] = byte(vValue_.Uint64())
|
||||
smsg.Signature.Data = sig
|
||||
|
||||
err = sigs.Verify(&smsg.Signature, from, msgRecovered)
|
||||
require.NoError(t, err)
|
||||
|
||||
txArgs := tx.(*EthLegacyHomesteadTxArgs)
|
||||
// verify data
|
||||
require.EqualValues(t, tc.ExpectedNonce, txArgs.Nonce, i)
|
||||
|
||||
expectedTo, err := ParseEthAddress(tc.ExpectedTo)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, expectedTo, *txArgs.To, i)
|
||||
require.EqualValues(t, tc.ExpectedInput, "0x"+hex.EncodeToString(txArgs.Input))
|
||||
require.EqualValues(t, tc.ExpectedGasPrice, txArgs.GasPrice)
|
||||
require.EqualValues(t, tc.ExpectedGasLimit, txArgs.GasLimit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyHomesteadSignatures(t *testing.T) {
|
||||
testcases := []struct {
|
||||
RawTx string
|
||||
ExpectedR string
|
||||
ExpectedS string
|
||||
ExpectedV string
|
||||
ExpectErr bool
|
||||
ExpectErrMsg string
|
||||
ExpectVMismatch bool
|
||||
}{
|
||||
{
|
||||
"0xf882800182540894095e7baea6a6c7c4c2dfeb977efac326af552d8780a3deadbeef0000000101010010101010101010101010101aaabbbbbbcccccccddddddddd1ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a01fffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804",
|
||||
"0x48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353",
|
||||
"0x1fffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804",
|
||||
"0x1b",
|
||||
false,
|
||||
"",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"0xf85f030182520794b94f5374fce5edbc8e2a8697c15331677e6ebf0b0a801ba098ff921201554726367d2be8c804a7ff89ccf285ebc57dff8ae4c44b9c19ac4aa07778cde41a8a37f6a087622b38bc201bd3e7df06dce067569d4def1b53dba98c",
|
||||
"0x98ff921201554726367d2be8c804a7ff89ccf285ebc57dff8ae4c44b9c19ac4a",
|
||||
"0x7778cde41a8a37f6a087622b38bc201bd3e7df06dce067569d4def1b53dba98c",
|
||||
"0x1b",
|
||||
false,
|
||||
"",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"0xf882800182540894095e7baea6a6c7c4c2dfeb977efac326af552d8780a3deadbeef0000000101010010101010101010101010101aaabbbbbbcccccccddddddddd1ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a01fffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804",
|
||||
"0x48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353",
|
||||
"0x1fffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804",
|
||||
"0x1c",
|
||||
false,
|
||||
"",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"0xf882800182540894095e7baea6a6c7c4c2dfeb977efac326af552d8780a3deadbeef0000000101010010101010101010101010101aaabbbbbbcccccccddddddddd1ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a01fffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804",
|
||||
"0x48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353",
|
||||
"0x1fffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804",
|
||||
"0x1f",
|
||||
false,
|
||||
"",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"0x00",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
true,
|
||||
"not a legacy eth transaction",
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testcases {
|
||||
tx, err := parseLegacyTx(mustDecodeHex(tc.RawTx))
|
||||
if tc.ExpectErr {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.ExpectErrMsg)
|
||||
continue
|
||||
}
|
||||
require.Nil(t, err)
|
||||
|
||||
sig, err := tx.Signature()
|
||||
require.Nil(t, err)
|
||||
|
||||
require.NoError(t, tx.InitialiseSignature(*sig))
|
||||
|
||||
txArgs := tx.(*EthLegacyHomesteadTxArgs)
|
||||
|
||||
require.Equal(t, tc.ExpectedR, "0x"+txArgs.R.Text(16), i)
|
||||
require.Equal(t, tc.ExpectedS, "0x"+txArgs.S.Text(16), i)
|
||||
|
||||
if tc.ExpectVMismatch {
|
||||
require.NotEqual(t, tc.ExpectedV, "0x"+txArgs.V.Text(16), i)
|
||||
} else {
|
||||
require.Equal(t, tc.ExpectedV, "0x"+txArgs.V.Text(16), i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://etherscan.io/getRawTx?tx=0xc55e2b90168af6972193c1f86fa4d7d7b31a29c156665d15b9cd48618b5177ef
|
||||
// https://tools.deth.net/tx-decoder
|
||||
func TestEtherScanLegacyRLP(t *testing.T) {
|
||||
rlp := "0xf8718301efc58506fc23ac008305161594104994f45d9d697ca104e5704a7b77d7fec3537c890821878651a4d70000801ba051222d91a379452395d0abaff981af4cfcc242f25cfaf947dea8245a477731f9a03a997c910b4701cca5d933fb26064ee5af7fe3236ff0ef2b58aa50b25aff8ca5"
|
||||
bz := mustDecodeHex(rlp)
|
||||
|
||||
tx, err := parseLegacyTx(bz)
|
||||
require.NoError(t, err)
|
||||
|
||||
ethLegacyTx, ok := tx.(*EthLegacyHomesteadTxArgs)
|
||||
require.True(t, ok)
|
||||
|
||||
// Verify nonce
|
||||
require.EqualValues(t, 0x1efc5, ethLegacyTx.Nonce)
|
||||
|
||||
// Verify recipient address
|
||||
expectedToAddr, err := ParseEthAddress("0x104994f45d9d697ca104e5704a7b77d7fec3537c")
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, expectedToAddr, *ethLegacyTx.To)
|
||||
|
||||
// Verify sender address
|
||||
expectedFromAddr, err := ParseEthAddress("0x32Be343B94f860124dC4fEe278FDCBD38C102D88")
|
||||
require.NoError(t, err)
|
||||
sender, err := ethLegacyTx.Sender()
|
||||
require.NoError(t, err)
|
||||
expectedFromFilecoinAddr, err := expectedFromAddr.ToFilecoinAddress()
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, expectedFromFilecoinAddr, sender)
|
||||
|
||||
// Verify transaction value
|
||||
expectedValue, ok := big.NewInt(0).SetString("821878651a4d70000", 16)
|
||||
require.True(t, ok)
|
||||
require.True(t, ethLegacyTx.Value.Cmp(expectedValue) == 0)
|
||||
|
||||
// Verify gas limit and gas price
|
||||
expectedGasPrice, ok := big.NewInt(0).SetString("6fc23ac00", 16)
|
||||
require.True(t, ok)
|
||||
require.EqualValues(t, 0x51615, ethLegacyTx.GasLimit)
|
||||
require.True(t, ethLegacyTx.GasPrice.Cmp(expectedGasPrice) == 0)
|
||||
|
||||
require.Empty(t, ethLegacyTx.Input)
|
||||
|
||||
// Verify signature values (v, r, s)
|
||||
expectedV, ok := big.NewInt(0).SetString("1b", 16)
|
||||
require.True(t, ok)
|
||||
require.True(t, ethLegacyTx.V.Cmp(expectedV) == 0)
|
||||
|
||||
expectedR, ok := big.NewInt(0).SetString("51222d91a379452395d0abaff981af4cfcc242f25cfaf947dea8245a477731f9", 16)
|
||||
require.True(t, ok)
|
||||
require.True(t, ethLegacyTx.R.Cmp(expectedR) == 0)
|
||||
|
||||
expectedS, ok := big.NewInt(0).SetString("3a997c910b4701cca5d933fb26064ee5af7fe3236ff0ef2b58aa50b25aff8ca5", 16)
|
||||
require.True(t, ok)
|
||||
require.True(t, ethLegacyTx.S.Cmp(expectedS) == 0)
|
||||
|
||||
// Convert to signed Filecoin message and verify fields
|
||||
smsg, err := ToSignedFilecoinMessage(ethLegacyTx)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.EqualValues(t, smsg.Message.From, sender)
|
||||
|
||||
expectedToFilecoinAddr, err := ethLegacyTx.To.ToFilecoinAddress()
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, smsg.Message.To, expectedToFilecoinAddr)
|
||||
require.EqualValues(t, smsg.Message.Value, ethLegacyTx.Value)
|
||||
require.EqualValues(t, smsg.Message.GasLimit, ethLegacyTx.GasLimit)
|
||||
require.EqualValues(t, smsg.Message.GasFeeCap, ethLegacyTx.GasPrice)
|
||||
require.EqualValues(t, smsg.Message.GasPremium, ethLegacyTx.GasPrice)
|
||||
require.EqualValues(t, smsg.Message.Nonce, ethLegacyTx.Nonce)
|
||||
require.Empty(t, smsg.Message.Params)
|
||||
require.EqualValues(t, smsg.Message.Method, builtintypes.MethodsEVM.InvokeContract)
|
||||
|
||||
// Convert signed Filecoin message back to Ethereum transaction and verify equality
|
||||
ethTx, err := EthTransactionFromSignedFilecoinMessage(smsg)
|
||||
require.NoError(t, err)
|
||||
convertedLegacyTx, ok := ethTx.(*EthLegacyHomesteadTxArgs)
|
||||
require.True(t, ok)
|
||||
ethLegacyTx.Input = nil
|
||||
require.EqualValues(t, convertedLegacyTx, ethLegacyTx)
|
||||
|
||||
// Verify EthTx fields
|
||||
ethTxVal, err := ethLegacyTx.ToEthTx(smsg)
|
||||
require.NoError(t, err)
|
||||
expectedHash, err := ethLegacyTx.TxHash()
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, ethTxVal.Hash, expectedHash)
|
||||
require.Nil(t, ethTxVal.MaxFeePerGas)
|
||||
require.Nil(t, ethTxVal.MaxPriorityFeePerGas)
|
||||
require.EqualValues(t, ethTxVal.Gas, ethLegacyTx.GasLimit)
|
||||
require.EqualValues(t, ethTxVal.Value, ethLegacyTx.Value)
|
||||
require.EqualValues(t, ethTxVal.Nonce, ethLegacyTx.Nonce)
|
||||
require.EqualValues(t, ethTxVal.To, ethLegacyTx.To)
|
||||
require.EqualValues(t, ethTxVal.From, expectedFromAddr)
|
||||
}
|
||||
|
||||
func TestFailurePaths(t *testing.T) {
|
||||
// Test case for invalid RLP
|
||||
invalidRLP := "0x08718301efc58506fc23ac008305161594104994f45d9d697ca104e5704a7b77d7fec3537c890821878651a4d70000801ba051222d91a379452395d0abaff981af4cfcc242f25cfaf947dea8245a477731f9a03a997c910b4701cca5d933fb26064ee5af7fe3236ff0ef2b58aa50b25aff8ca5"
|
||||
decoded, err := hex.DecodeString(strings.TrimPrefix(invalidRLP, "0x"))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = parseLegacyTx(decoded)
|
||||
require.Error(t, err, "Expected error for invalid RLP")
|
||||
|
||||
// Test case for mangled signature
|
||||
mangledSignatureRLP := "0xf8718301efc58506fc23ac008305161594104994f45d9d697ca104e5704a7b77d7fec3537c890821878651a4d70000801ba051222d91a379452395d0abaff981af4cfcc242f25cfaf947dea8245a477731f9a03a997c910b4701cca5d933fb26064ee5af7fe3236ff0ef2b58aa50b25aff8ca5"
|
||||
decodedSig, err := hex.DecodeString(strings.TrimPrefix(mangledSignatureRLP, "0x"))
|
||||
require.NoError(t, err)
|
||||
|
||||
tx, err := parseLegacyTx(decodedSig)
|
||||
require.NoError(t, err)
|
||||
|
||||
ethLegacyTx, ok := tx.(*EthLegacyHomesteadTxArgs)
|
||||
require.True(t, ok)
|
||||
|
||||
// Mangle R value
|
||||
ethLegacyTx.R = big.Add(ethLegacyTx.R, big.NewInt(1))
|
||||
|
||||
expectedFromAddr, err := ParseEthAddress("0x32Be343B94f860124dC4fEe278FDCBD38C102D88")
|
||||
require.NoError(t, err)
|
||||
expectedFromFilecoinAddr, err := expectedFromAddr.ToFilecoinAddress()
|
||||
require.NoError(t, err)
|
||||
|
||||
senderAddr, err := ethLegacyTx.Sender()
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, senderAddr, expectedFromFilecoinAddr, "Expected sender address to not match after mangling R value")
|
||||
|
||||
// Mangle V value
|
||||
ethLegacyTx.V = big.NewInt(1)
|
||||
_, err = ethLegacyTx.Sender()
|
||||
require.Error(t, err, "Expected error when V value is not 27 or 28")
|
||||
}
|
@ -3,12 +3,12 @@ package ethtypes
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
mathbig "math/big"
|
||||
|
||||
cbg "github.com/whyrusleeping/cbor-gen"
|
||||
"golang.org/x/crypto/sha3"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/filecoin-project/go-address"
|
||||
gocrypto "github.com/filecoin-project/go-crypto"
|
||||
@ -21,8 +21,50 @@ import (
|
||||
"github.com/filecoin-project/lotus/chain/types"
|
||||
)
|
||||
|
||||
const Eip1559TxType = 2
|
||||
const (
|
||||
EthLegacyTxType = 0x00
|
||||
EIP1559TxType = 0x02
|
||||
)
|
||||
|
||||
const (
|
||||
EthEIP1559TxSignatureLen = 65
|
||||
EthLegacyHomesteadTxSignatureLen = 66
|
||||
EthLegacyHomesteadTxSignaturePrefix = 0x01
|
||||
EthLegacy155TxSignaturePrefix = 0x02
|
||||
EthLegacyHomesteadTxChainID = 0x00
|
||||
)
|
||||
|
||||
var (
|
||||
EthLegacy155TxSignatureLen0 int
|
||||
EthLegacy155TxSignatureLen1 int
|
||||
)
|
||||
|
||||
func init() {
|
||||
EthLegacy155TxSignatureLen0 = calcEIP155TxSignatureLen(build.Eip155ChainId, 35)
|
||||
EthLegacy155TxSignatureLen1 = calcEIP155TxSignatureLen(build.Eip155ChainId, 36)
|
||||
}
|
||||
|
||||
// EthTransaction defines the interface for Ethereum-like transactions.
|
||||
// It provides methods to convert transactions to various formats,
|
||||
// retrieve transaction details, and manipulate transaction signatures.
|
||||
type EthTransaction interface {
|
||||
Type() int
|
||||
Sender() (address.Address, error)
|
||||
Signature() (*typescrypto.Signature, error)
|
||||
InitialiseSignature(sig typescrypto.Signature) error
|
||||
ToUnsignedFilecoinMessage(from address.Address) (*types.Message, error)
|
||||
ToRlpUnsignedMsg() ([]byte, error)
|
||||
ToRlpSignedMsg() ([]byte, error)
|
||||
TxHash() (EthHash, error)
|
||||
ToVerifiableSignature(sig []byte) ([]byte, error)
|
||||
ToEthTx(*types.SignedMessage) (EthTx, error)
|
||||
}
|
||||
|
||||
// EthTx represents an Ethereum transaction structure, encapsulating fields that align with the standard Ethereum transaction components.
|
||||
// This structure can represent both EIP-1559 transactions and legacy Homestead transactions:
|
||||
// - In EIP-1559 transactions, the `GasPrice` field is set to nil/empty.
|
||||
// - In legacy Homestead transactions, the `GasPrice` field is populated to specify the fee per unit of gas, while the `MaxFeePerGas` and `MaxPriorityFeePerGas` fields are set to nil/empty.
|
||||
// Additionally, both the `ChainID` and the `Type` fields are set to 0 in legacy Homestead transactions to differentiate them from EIP-1559 transactions.
|
||||
type EthTx struct {
|
||||
ChainID EthUint64 `json:"chainId"`
|
||||
Nonce EthUint64 `json:"nonce"`
|
||||
@ -36,189 +78,129 @@ type EthTx struct {
|
||||
Type EthUint64 `json:"type"`
|
||||
Input EthBytes `json:"input"`
|
||||
Gas EthUint64 `json:"gas"`
|
||||
MaxFeePerGas EthBigInt `json:"maxFeePerGas"`
|
||||
MaxPriorityFeePerGas EthBigInt `json:"maxPriorityFeePerGas"`
|
||||
MaxFeePerGas *EthBigInt `json:"maxFeePerGas,omitempty"`
|
||||
MaxPriorityFeePerGas *EthBigInt `json:"maxPriorityFeePerGas,omitempty"`
|
||||
GasPrice *EthBigInt `json:"gasPrice,omitempty"`
|
||||
AccessList []EthHash `json:"accessList"`
|
||||
V EthBigInt `json:"v"`
|
||||
R EthBigInt `json:"r"`
|
||||
S EthBigInt `json:"s"`
|
||||
}
|
||||
|
||||
type EthTxArgs struct {
|
||||
ChainID int `json:"chainId"`
|
||||
Nonce int `json:"nonce"`
|
||||
To *EthAddress `json:"to"`
|
||||
Value big.Int `json:"value"`
|
||||
MaxFeePerGas big.Int `json:"maxFeePerGas"`
|
||||
MaxPriorityFeePerGas big.Int `json:"maxPriorityFeePerGas"`
|
||||
GasLimit int `json:"gasLimit"`
|
||||
Input []byte `json:"input"`
|
||||
V big.Int `json:"v"`
|
||||
R big.Int `json:"r"`
|
||||
S big.Int `json:"s"`
|
||||
func (tx *EthTx) GasFeeCap() (EthBigInt, error) {
|
||||
if tx.GasPrice == nil && tx.MaxFeePerGas == nil {
|
||||
return EthBigInt{}, fmt.Errorf("gas fee cap is not set")
|
||||
}
|
||||
if tx.MaxFeePerGas != nil {
|
||||
return *tx.MaxFeePerGas, nil
|
||||
}
|
||||
return *tx.GasPrice, nil
|
||||
}
|
||||
|
||||
// EthTxFromSignedEthMessage does NOT populate:
|
||||
// - BlockHash
|
||||
// - BlockNumber
|
||||
// - TransactionIndex
|
||||
// - Hash
|
||||
func EthTxFromSignedEthMessage(smsg *types.SignedMessage) (EthTx, error) {
|
||||
// The from address is always an f410f address, never an ID or other address.
|
||||
if !IsEthAddress(smsg.Message.From) {
|
||||
return EthTx{}, xerrors.Errorf("sender must be an eth account, was %s", smsg.Message.From)
|
||||
func (tx *EthTx) GasPremium() (EthBigInt, error) {
|
||||
if tx.GasPrice == nil && tx.MaxPriorityFeePerGas == nil {
|
||||
return EthBigInt{}, fmt.Errorf("gas premium is not set")
|
||||
}
|
||||
|
||||
// Probably redundant, but we might as well check.
|
||||
if tx.MaxPriorityFeePerGas != nil {
|
||||
return *tx.MaxPriorityFeePerGas, nil
|
||||
}
|
||||
|
||||
return *tx.GasPrice, nil
|
||||
}
|
||||
|
||||
func EthTransactionFromSignedFilecoinMessage(smsg *types.SignedMessage) (EthTransaction, error) {
|
||||
if smsg == nil {
|
||||
return nil, errors.New("signed message is nil")
|
||||
}
|
||||
|
||||
// Ensure the signature type is delegated.
|
||||
if smsg.Signature.Type != typescrypto.SigTypeDelegated {
|
||||
return EthTx{}, xerrors.Errorf("signature is not delegated type, is type: %d", smsg.Signature.Type)
|
||||
return nil, fmt.Errorf("signature is not delegated type, is type: %d", smsg.Signature.Type)
|
||||
}
|
||||
|
||||
txArgs, err := EthTxArgsFromUnsignedEthMessage(&smsg.Message)
|
||||
// Convert Filecoin address to Ethereum address.
|
||||
_, err := EthAddressFromFilecoinAddress(smsg.Message.From)
|
||||
if err != nil {
|
||||
return EthTx{}, xerrors.Errorf("failed to convert the unsigned message: %w", err)
|
||||
return nil, fmt.Errorf("sender was not an eth account")
|
||||
}
|
||||
|
||||
r, s, v, err := RecoverSignature(smsg.Signature)
|
||||
// Extract Ethereum parameters and recipient from the message.
|
||||
params, to, err := getEthParamsAndRecipient(&smsg.Message)
|
||||
if err != nil {
|
||||
return EthTx{}, xerrors.Errorf("failed to recover signature: %w", err)
|
||||
return nil, fmt.Errorf("failed to parse input params and recipient: %w", err)
|
||||
}
|
||||
|
||||
from, err := EthAddressFromFilecoinAddress(smsg.Message.From)
|
||||
if err != nil {
|
||||
// This should be impossible as we've already asserted that we have an EthAddress
|
||||
// sender...
|
||||
return EthTx{}, xerrors.Errorf("sender was not an eth account")
|
||||
// Check for supported message version.
|
||||
if smsg.Message.Version != 0 {
|
||||
return nil, fmt.Errorf("unsupported msg version: %d", smsg.Message.Version)
|
||||
}
|
||||
|
||||
return EthTx{
|
||||
Nonce: EthUint64(txArgs.Nonce),
|
||||
ChainID: EthUint64(txArgs.ChainID),
|
||||
To: txArgs.To,
|
||||
From: from,
|
||||
Value: EthBigInt(txArgs.Value),
|
||||
Type: Eip1559TxType,
|
||||
Gas: EthUint64(txArgs.GasLimit),
|
||||
MaxFeePerGas: EthBigInt(txArgs.MaxFeePerGas),
|
||||
MaxPriorityFeePerGas: EthBigInt(txArgs.MaxPriorityFeePerGas),
|
||||
AccessList: []EthHash{},
|
||||
V: v,
|
||||
R: r,
|
||||
S: s,
|
||||
Input: txArgs.Input,
|
||||
}, nil
|
||||
// Determine the type of transaction based on the signature length
|
||||
switch len(smsg.Signature.Data) {
|
||||
case EthEIP1559TxSignatureLen:
|
||||
tx := Eth1559TxArgs{
|
||||
ChainID: build.Eip155ChainId,
|
||||
Nonce: int(smsg.Message.Nonce),
|
||||
To: to,
|
||||
Value: smsg.Message.Value,
|
||||
Input: params,
|
||||
MaxFeePerGas: smsg.Message.GasFeeCap,
|
||||
MaxPriorityFeePerGas: smsg.Message.GasPremium,
|
||||
GasLimit: int(smsg.Message.GasLimit),
|
||||
}
|
||||
if err := tx.InitialiseSignature(smsg.Signature); err != nil {
|
||||
return nil, fmt.Errorf("failed to initialise signature: %w", err)
|
||||
}
|
||||
return &tx, nil
|
||||
|
||||
case EthLegacyHomesteadTxSignatureLen, EthLegacy155TxSignatureLen0, EthLegacy155TxSignatureLen1:
|
||||
legacyTx := &EthLegacyHomesteadTxArgs{
|
||||
Nonce: int(smsg.Message.Nonce),
|
||||
To: to,
|
||||
Value: smsg.Message.Value,
|
||||
Input: params,
|
||||
GasPrice: smsg.Message.GasFeeCap,
|
||||
GasLimit: int(smsg.Message.GasLimit),
|
||||
}
|
||||
// Process based on the first byte of the signature
|
||||
switch smsg.Signature.Data[0] {
|
||||
case EthLegacyHomesteadTxSignaturePrefix:
|
||||
if err := legacyTx.InitialiseSignature(smsg.Signature); err != nil {
|
||||
return nil, fmt.Errorf("failed to initialise signature: %w", err)
|
||||
}
|
||||
return legacyTx, nil
|
||||
case EthLegacy155TxSignaturePrefix:
|
||||
tx := &EthLegacy155TxArgs{
|
||||
legacyTx: legacyTx,
|
||||
}
|
||||
if err := tx.InitialiseSignature(smsg.Signature); err != nil {
|
||||
return nil, fmt.Errorf("failed to initialise signature: %w", err)
|
||||
}
|
||||
return tx, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported legacy transaction; first byte of signature is %d", smsg.Signature.Data[0])
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported signature length")
|
||||
}
|
||||
}
|
||||
|
||||
func EthTxArgsFromUnsignedEthMessage(msg *types.Message) (EthTxArgs, error) {
|
||||
var (
|
||||
to *EthAddress
|
||||
params []byte
|
||||
err error
|
||||
)
|
||||
|
||||
if msg.Version != 0 {
|
||||
return EthTxArgs{}, xerrors.Errorf("unsupported msg version: %d", msg.Version)
|
||||
}
|
||||
|
||||
if len(msg.Params) > 0 {
|
||||
paramsReader := bytes.NewReader(msg.Params)
|
||||
params, err = cbg.ReadByteArray(paramsReader, uint64(len(msg.Params)))
|
||||
if err != nil {
|
||||
return EthTxArgs{}, xerrors.Errorf("failed to read params byte array: %w", err)
|
||||
}
|
||||
if paramsReader.Len() != 0 {
|
||||
return EthTxArgs{}, xerrors.Errorf("extra data found in params")
|
||||
}
|
||||
if len(params) == 0 {
|
||||
return EthTxArgs{}, xerrors.Errorf("non-empty params encode empty byte array")
|
||||
}
|
||||
}
|
||||
|
||||
if msg.To == builtintypes.EthereumAddressManagerActorAddr {
|
||||
if msg.Method != builtintypes.MethodsEAM.CreateExternal {
|
||||
return EthTxArgs{}, fmt.Errorf("unsupported EAM method")
|
||||
}
|
||||
} else if msg.Method == builtintypes.MethodsEVM.InvokeContract {
|
||||
addr, err := EthAddressFromFilecoinAddress(msg.To)
|
||||
if err != nil {
|
||||
return EthTxArgs{}, err
|
||||
}
|
||||
to = &addr
|
||||
} else {
|
||||
return EthTxArgs{},
|
||||
xerrors.Errorf("invalid methodnum %d: only allowed method is InvokeContract(%d)",
|
||||
msg.Method, builtintypes.MethodsEVM.InvokeContract)
|
||||
}
|
||||
|
||||
return EthTxArgs{
|
||||
ChainID: build.Eip155ChainId,
|
||||
Nonce: int(msg.Nonce),
|
||||
To: to,
|
||||
Value: msg.Value,
|
||||
Input: params,
|
||||
MaxFeePerGas: msg.GasFeeCap,
|
||||
MaxPriorityFeePerGas: msg.GasPremium,
|
||||
GasLimit: int(msg.GasLimit),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tx *EthTxArgs) ToUnsignedMessage(from address.Address) (*types.Message, error) {
|
||||
if tx.ChainID != build.Eip155ChainId {
|
||||
return nil, xerrors.Errorf("unsupported chain id: %d", tx.ChainID)
|
||||
}
|
||||
|
||||
var err error
|
||||
var params []byte
|
||||
if len(tx.Input) > 0 {
|
||||
buf := new(bytes.Buffer)
|
||||
if err = cbg.WriteByteArray(buf, tx.Input); err != nil {
|
||||
return nil, xerrors.Errorf("failed to write input args: %w", err)
|
||||
}
|
||||
params = buf.Bytes()
|
||||
}
|
||||
|
||||
var to address.Address
|
||||
var method abi.MethodNum
|
||||
// nil indicates the EAM, only CreateExternal is allowed
|
||||
if tx.To == nil {
|
||||
method = builtintypes.MethodsEAM.CreateExternal
|
||||
to = builtintypes.EthereumAddressManagerActorAddr
|
||||
} else {
|
||||
method = builtintypes.MethodsEVM.InvokeContract
|
||||
to, err = tx.To.ToFilecoinAddress()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to convert To into filecoin addr: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &types.Message{
|
||||
Version: 0,
|
||||
To: to,
|
||||
From: from,
|
||||
Nonce: uint64(tx.Nonce),
|
||||
Value: tx.Value,
|
||||
GasLimit: int64(tx.GasLimit),
|
||||
GasFeeCap: tx.MaxFeePerGas,
|
||||
GasPremium: tx.MaxPriorityFeePerGas,
|
||||
Method: method,
|
||||
Params: params,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tx *EthTxArgs) ToSignedMessage() (*types.SignedMessage, error) {
|
||||
func ToSignedFilecoinMessage(tx EthTransaction) (*types.SignedMessage, error) {
|
||||
from, err := tx.Sender()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to calculate sender: %w", err)
|
||||
return nil, fmt.Errorf("failed to calculate sender: %w", err)
|
||||
}
|
||||
|
||||
unsignedMsg, err := tx.ToUnsignedMessage(from)
|
||||
unsignedMsg, err := tx.ToUnsignedFilecoinMessage(from)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to convert to unsigned msg: %w", err)
|
||||
return nil, fmt.Errorf("failed to convert to unsigned msg: %w", err)
|
||||
}
|
||||
|
||||
siggy, err := tx.Signature()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("failed to calculate signature: %w", err)
|
||||
return nil, fmt.Errorf("failed to calculate signature: %w", err)
|
||||
}
|
||||
|
||||
return &types.SignedMessage{
|
||||
@ -227,349 +209,91 @@ func (tx *EthTxArgs) ToSignedMessage() (*types.SignedMessage, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tx *EthTxArgs) HashedOriginalRlpMsg() ([]byte, error) {
|
||||
msg, err := tx.ToRlpUnsignedMsg()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hasher := sha3.NewLegacyKeccak256()
|
||||
hasher.Write(msg)
|
||||
hash := hasher.Sum(nil)
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
func (tx *EthTxArgs) ToRlpUnsignedMsg() ([]byte, error) {
|
||||
packed, err := tx.packTxFields()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encoded, err := EncodeRLP(packed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append([]byte{0x02}, encoded...), nil
|
||||
}
|
||||
|
||||
func (tx *EthTx) ToEthTxArgs() EthTxArgs {
|
||||
return EthTxArgs{
|
||||
ChainID: int(tx.ChainID),
|
||||
Nonce: int(tx.Nonce),
|
||||
To: tx.To,
|
||||
Value: big.Int(tx.Value),
|
||||
MaxFeePerGas: big.Int(tx.MaxFeePerGas),
|
||||
MaxPriorityFeePerGas: big.Int(tx.MaxPriorityFeePerGas),
|
||||
GasLimit: int(tx.Gas),
|
||||
Input: tx.Input,
|
||||
V: big.Int(tx.V),
|
||||
R: big.Int(tx.R),
|
||||
S: big.Int(tx.S),
|
||||
}
|
||||
}
|
||||
|
||||
func (tx *EthTx) TxHash() (EthHash, error) {
|
||||
ethTxArgs := tx.ToEthTxArgs()
|
||||
return (ðTxArgs).TxHash()
|
||||
}
|
||||
|
||||
func (tx *EthTxArgs) TxHash() (EthHash, error) {
|
||||
rlp, err := tx.ToRlpSignedMsg()
|
||||
if err != nil {
|
||||
return EmptyEthHash, err
|
||||
}
|
||||
|
||||
return EthHashFromTxBytes(rlp), nil
|
||||
}
|
||||
|
||||
func (tx *EthTxArgs) ToRlpSignedMsg() ([]byte, error) {
|
||||
packed1, err := tx.packTxFields()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
packed2, err := tx.packSigFields()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encoded, err := EncodeRLP(append(packed1, packed2...))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append([]byte{0x02}, encoded...), nil
|
||||
}
|
||||
|
||||
func (tx *EthTxArgs) packTxFields() ([]interface{}, error) {
|
||||
chainId, err := formatInt(tx.ChainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce, err := formatInt(tx.Nonce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maxPriorityFeePerGas, err := formatBigInt(tx.MaxPriorityFeePerGas)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maxFeePerGas, err := formatBigInt(tx.MaxFeePerGas)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gasLimit, err := formatInt(tx.GasLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value, err := formatBigInt(tx.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := []interface{}{
|
||||
chainId,
|
||||
nonce,
|
||||
maxPriorityFeePerGas,
|
||||
maxFeePerGas,
|
||||
gasLimit,
|
||||
formatEthAddr(tx.To),
|
||||
value,
|
||||
tx.Input,
|
||||
[]interface{}{}, // access list
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (tx *EthTxArgs) packSigFields() ([]interface{}, error) {
|
||||
r, err := formatBigInt(tx.R)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s, err := formatBigInt(tx.S)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, err := formatBigInt(tx.V)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := []interface{}{v, r, s}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (tx *EthTxArgs) Signature() (*typescrypto.Signature, error) {
|
||||
r := tx.R.Int.Bytes()
|
||||
s := tx.S.Int.Bytes()
|
||||
v := tx.V.Int.Bytes()
|
||||
|
||||
sig := append([]byte{}, padLeadingZeros(r, 32)...)
|
||||
sig = append(sig, padLeadingZeros(s, 32)...)
|
||||
if len(v) == 0 {
|
||||
sig = append(sig, 0)
|
||||
} else {
|
||||
sig = append(sig, v[0])
|
||||
}
|
||||
|
||||
if len(sig) != 65 {
|
||||
return nil, fmt.Errorf("signature is not 65 bytes")
|
||||
}
|
||||
return &typescrypto.Signature{
|
||||
Type: typescrypto.SigTypeDelegated, Data: sig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tx *EthTxArgs) Sender() (address.Address, error) {
|
||||
msg, err := tx.ToRlpUnsignedMsg()
|
||||
if err != nil {
|
||||
return address.Undef, err
|
||||
}
|
||||
|
||||
hasher := sha3.NewLegacyKeccak256()
|
||||
hasher.Write(msg)
|
||||
hash := hasher.Sum(nil)
|
||||
|
||||
sig, err := tx.Signature()
|
||||
if err != nil {
|
||||
return address.Undef, err
|
||||
}
|
||||
|
||||
pubk, err := gocrypto.EcRecover(hash, sig.Data)
|
||||
if err != nil {
|
||||
return address.Undef, err
|
||||
}
|
||||
|
||||
ethAddr, err := EthAddressFromPubKey(pubk)
|
||||
if err != nil {
|
||||
return address.Undef, err
|
||||
}
|
||||
|
||||
ea, err := CastEthAddress(ethAddr)
|
||||
if err != nil {
|
||||
return address.Undef, err
|
||||
}
|
||||
|
||||
return ea.ToFilecoinAddress()
|
||||
}
|
||||
|
||||
func RecoverSignature(sig typescrypto.Signature) (r, s, v EthBigInt, err error) {
|
||||
if sig.Type != typescrypto.SigTypeDelegated {
|
||||
return EthBigIntZero, EthBigIntZero, EthBigIntZero, fmt.Errorf("RecoverSignature only supports Delegated signature")
|
||||
}
|
||||
|
||||
if len(sig.Data) != 65 {
|
||||
return EthBigIntZero, EthBigIntZero, EthBigIntZero, fmt.Errorf("signature should be 65 bytes long, but got %d bytes", len(sig.Data))
|
||||
}
|
||||
|
||||
r_, err := parseBigInt(sig.Data[0:32])
|
||||
if err != nil {
|
||||
return EthBigIntZero, EthBigIntZero, EthBigIntZero, fmt.Errorf("cannot parse r into EthBigInt")
|
||||
}
|
||||
|
||||
s_, err := parseBigInt(sig.Data[32:64])
|
||||
if err != nil {
|
||||
return EthBigIntZero, EthBigIntZero, EthBigIntZero, fmt.Errorf("cannot parse s into EthBigInt")
|
||||
}
|
||||
|
||||
v_, err := parseBigInt([]byte{sig.Data[64]})
|
||||
if err != nil {
|
||||
return EthBigIntZero, EthBigIntZero, EthBigIntZero, fmt.Errorf("cannot parse v into EthBigInt")
|
||||
}
|
||||
|
||||
return EthBigInt(r_), EthBigInt(s_), EthBigInt(v_), nil
|
||||
}
|
||||
|
||||
func parseEip1559Tx(data []byte) (*EthTxArgs, error) {
|
||||
if data[0] != 2 {
|
||||
return nil, fmt.Errorf("not an EIP-1559 transaction: first byte is not 2")
|
||||
}
|
||||
|
||||
d, err := DecodeRLP(data[1:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decoded, ok := d.([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not an EIP-1559 transaction: decoded data is not a list")
|
||||
}
|
||||
|
||||
if len(decoded) != 12 {
|
||||
return nil, fmt.Errorf("not an EIP-1559 transaction: should have 12 elements in the rlp list")
|
||||
}
|
||||
|
||||
chainId, err := parseInt(decoded[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce, err := parseInt(decoded[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maxPriorityFeePerGas, err := parseBigInt(decoded[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maxFeePerGas, err := parseBigInt(decoded[3])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gasLimit, err := parseInt(decoded[4])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
to, err := parseEthAddr(decoded[5])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value, err := parseBigInt(decoded[6])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
input, err := parseBytes(decoded[7])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessList, ok := decoded[8].([]interface{})
|
||||
if !ok || (ok && len(accessList) != 0) {
|
||||
return nil, fmt.Errorf("access list should be an empty list")
|
||||
}
|
||||
|
||||
r, err := parseBigInt(decoded[10])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s, err := parseBigInt(decoded[11])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, err := parseBigInt(decoded[9])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// EIP-1559 and EIP-2930 transactions only support 0 or 1 for v
|
||||
// Legacy and EIP-155 transactions support other values
|
||||
// https://github.com/ethers-io/ethers.js/blob/56fabe987bb8c1e4891fdf1e5d3fe8a4c0471751/packages/transactions/src.ts/index.ts#L333
|
||||
if !v.Equals(big.NewInt(0)) && !v.Equals(big.NewInt(1)) {
|
||||
return nil, fmt.Errorf("EIP-1559 transactions only support 0 or 1 for v")
|
||||
}
|
||||
|
||||
args := EthTxArgs{
|
||||
ChainID: chainId,
|
||||
Nonce: nonce,
|
||||
To: to,
|
||||
MaxPriorityFeePerGas: maxPriorityFeePerGas,
|
||||
MaxFeePerGas: maxFeePerGas,
|
||||
GasLimit: gasLimit,
|
||||
Value: value,
|
||||
Input: input,
|
||||
V: v,
|
||||
R: r,
|
||||
S: s,
|
||||
}
|
||||
return &args, nil
|
||||
}
|
||||
|
||||
func ParseEthTxArgs(data []byte) (*EthTxArgs, error) {
|
||||
func ParseEthTransaction(data []byte) (EthTransaction, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("empty data")
|
||||
}
|
||||
|
||||
if data[0] > 0x7f {
|
||||
// legacy transaction
|
||||
return nil, fmt.Errorf("legacy transaction is not supported")
|
||||
}
|
||||
|
||||
if data[0] == 1 {
|
||||
switch data[0] {
|
||||
case 1:
|
||||
// EIP-2930
|
||||
return nil, fmt.Errorf("EIP-2930 transaction is not supported")
|
||||
}
|
||||
|
||||
if data[0] == Eip1559TxType {
|
||||
case EIP1559TxType:
|
||||
// EIP-1559
|
||||
return parseEip1559Tx(data)
|
||||
default:
|
||||
if data[0] > 0x7f {
|
||||
tx, err := parseLegacyTx(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse legacy transaction: %w", err)
|
||||
}
|
||||
return tx, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported transaction type")
|
||||
}
|
||||
|
||||
type methodInfo struct {
|
||||
to address.Address
|
||||
method abi.MethodNum
|
||||
params []byte
|
||||
}
|
||||
|
||||
func getFilecoinMethodInfo(recipient *EthAddress, input []byte) (*methodInfo, error) {
|
||||
var params []byte
|
||||
if len(input) > 0 {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := cbg.WriteByteArray(buf, input); err != nil {
|
||||
return nil, fmt.Errorf("failed to write input args: %w", err)
|
||||
}
|
||||
params = buf.Bytes()
|
||||
}
|
||||
|
||||
var to address.Address
|
||||
var method abi.MethodNum
|
||||
|
||||
if recipient == nil {
|
||||
// If recipient is nil, use Ethereum Address Manager Actor and CreateExternal method
|
||||
method = builtintypes.MethodsEAM.CreateExternal
|
||||
to = builtintypes.EthereumAddressManagerActorAddr
|
||||
} else {
|
||||
// Otherwise, use InvokeContract method and convert EthAddress to Filecoin address
|
||||
method = builtintypes.MethodsEVM.InvokeContract
|
||||
var err error
|
||||
to, err = recipient.ToFilecoinAddress()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert EthAddress to Filecoin address: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &methodInfo{
|
||||
to: to,
|
||||
method: method,
|
||||
params: params,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func packSigFields(v, r, s big.Int) ([]interface{}, error) {
|
||||
rr, err := formatBigInt(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ss, err := formatBigInt(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vv, err := formatBigInt(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := []interface{}{vv, rr, ss}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func padLeadingZeros(data []byte, length int) []byte {
|
||||
if len(data) >= length {
|
||||
return data
|
||||
@ -667,3 +391,204 @@ func parseEthAddr(v interface{}) (*EthAddress, error) {
|
||||
}
|
||||
return &addr, nil
|
||||
}
|
||||
|
||||
func getEthParamsAndRecipient(msg *types.Message) (params []byte, to *EthAddress, err error) {
|
||||
if len(msg.Params) > 0 {
|
||||
paramsReader := bytes.NewReader(msg.Params)
|
||||
var err error
|
||||
params, err = cbg.ReadByteArray(paramsReader, uint64(len(msg.Params)))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read params byte array: %w", err)
|
||||
}
|
||||
if paramsReader.Len() != 0 {
|
||||
return nil, nil, fmt.Errorf("extra data found in params")
|
||||
}
|
||||
if len(params) == 0 {
|
||||
return nil, nil, fmt.Errorf("non-empty params encode empty byte array")
|
||||
}
|
||||
}
|
||||
|
||||
if msg.To == builtintypes.EthereumAddressManagerActorAddr {
|
||||
if msg.Method != builtintypes.MethodsEAM.CreateExternal {
|
||||
return nil, nil, fmt.Errorf("unsupported EAM method")
|
||||
}
|
||||
} else if msg.Method == builtintypes.MethodsEVM.InvokeContract {
|
||||
addr, err := EthAddressFromFilecoinAddress(msg.To)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
to = &addr
|
||||
} else {
|
||||
return nil, nil,
|
||||
fmt.Errorf("invalid methodnum %d: only allowed method is InvokeContract(%d) or CreateExternal(%d)",
|
||||
msg.Method, builtintypes.MethodsEVM.InvokeContract, builtintypes.MethodsEAM.CreateExternal)
|
||||
}
|
||||
|
||||
return params, to, nil
|
||||
}
|
||||
|
||||
func parseLegacyTx(data []byte) (EthTransaction, error) {
|
||||
if data[0] <= 0x7f {
|
||||
return nil, fmt.Errorf("not a legacy eth transaction")
|
||||
}
|
||||
|
||||
d, err := DecodeRLP(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decoded, ok := d.([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not a Legacy transaction: decoded data is not a list")
|
||||
}
|
||||
|
||||
if len(decoded) != 9 {
|
||||
return nil, fmt.Errorf("not a Legacy transaction: should have 9 elements in the rlp list")
|
||||
}
|
||||
|
||||
nonce, err := parseInt(decoded[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gasPrice, err := parseBigInt(decoded[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gasLimit, err := parseInt(decoded[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
to, err := parseEthAddr(decoded[3])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
value, err := parseBigInt(decoded[4])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
input, ok := decoded[5].([]byte)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("input is not a byte slice")
|
||||
}
|
||||
|
||||
v, err := parseBigInt(decoded[6])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r, err := parseBigInt(decoded[7])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s, err := parseBigInt(decoded[8])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tx := &EthLegacyHomesteadTxArgs{
|
||||
Nonce: nonce,
|
||||
GasPrice: gasPrice,
|
||||
GasLimit: gasLimit,
|
||||
To: to,
|
||||
Value: value,
|
||||
Input: input,
|
||||
V: v,
|
||||
R: r,
|
||||
S: s,
|
||||
}
|
||||
|
||||
chainId := deriveEIP155ChainId(v)
|
||||
if chainId.Equals(big.NewInt(0)) {
|
||||
// This is a legacy Homestead transaction
|
||||
if !v.Equals(big.NewInt(27)) && !v.Equals(big.NewInt(28)) {
|
||||
return nil, fmt.Errorf("legacy homestead transactions only support 27 or 28 for v, got %d", v.Uint64())
|
||||
}
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
// This is a EIP-155 transaction -> ensure chainID protection
|
||||
if err := validateEIP155ChainId(v); err != nil {
|
||||
return nil, fmt.Errorf("failed to validate EIP155 chain id: %w", err)
|
||||
}
|
||||
|
||||
return &EthLegacy155TxArgs{
|
||||
legacyTx: tx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type RlpPackable interface {
|
||||
packTxFields() ([]interface{}, error)
|
||||
}
|
||||
|
||||
func toRlpUnsignedMsg(tx RlpPackable) ([]byte, error) {
|
||||
packedFields, err := tx.packTxFields()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encoded, err := EncodeRLP(packedFields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
func toRlpSignedMsg(tx RlpPackable, V, R, S big.Int) ([]byte, error) {
|
||||
packed1, err := tx.packTxFields()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
packed2, err := packSigFields(V, R, S)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encoded, err := EncodeRLP(append(packed1, packed2...))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode rlp signed msg: %w", err)
|
||||
}
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
func sender(tx EthTransaction) (address.Address, error) {
|
||||
msg, err := tx.ToRlpUnsignedMsg()
|
||||
if err != nil {
|
||||
return address.Undef, fmt.Errorf("failed to get rlp unsigned msg: %w", err)
|
||||
}
|
||||
|
||||
hasher := sha3.NewLegacyKeccak256()
|
||||
hasher.Write(msg)
|
||||
hash := hasher.Sum(nil)
|
||||
|
||||
sig, err := tx.Signature()
|
||||
if err != nil {
|
||||
return address.Undef, fmt.Errorf("failed to get signature: %w", err)
|
||||
}
|
||||
|
||||
sigData, err := tx.ToVerifiableSignature(sig.Data)
|
||||
if err != nil {
|
||||
return address.Undef, fmt.Errorf("failed to get verifiable signature: %w", err)
|
||||
}
|
||||
|
||||
pubk, err := gocrypto.EcRecover(hash, sigData)
|
||||
if err != nil {
|
||||
return address.Undef, fmt.Errorf("failed to recover pubkey: %w", err)
|
||||
}
|
||||
|
||||
ethAddr, err := EthAddressFromPubKey(pubk)
|
||||
if err != nil {
|
||||
return address.Undef, fmt.Errorf("failed to get eth address from pubkey: %w", err)
|
||||
}
|
||||
|
||||
ea, err := CastEthAddress(ethAddr)
|
||||
if err != nil {
|
||||
return address.Undef, fmt.Errorf("failed to cast eth address: %w", err)
|
||||
}
|
||||
|
||||
return ea.ToFilecoinAddress()
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -192,7 +192,7 @@ func TestDecodeError(t *testing.T) {
|
||||
|
||||
func TestDecode1(t *testing.T) {
|
||||
b := mustDecodeHex("0x02f8758401df5e7680832c8411832c8411830767f89452963ef50e27e06d72d59fcb4f3c2a687be3cfef880de0b6b3a764000080c080a094b11866f453ad85a980e0e8a2fc98cbaeb4409618c7734a7e12ae2f66fd405da042dbfb1b37af102023830ceeee0e703ffba0b8b3afeb8fe59f405eca9ed61072")
|
||||
decoded, err := ParseEthTxArgs(b)
|
||||
decoded, err := parseEip1559Tx(b)
|
||||
require.NoError(t, err)
|
||||
|
||||
sender, err := decoded.Sender()
|
||||
|
@ -88,16 +88,16 @@ var computeEthHashCmd = &cli.Command{
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case *types.SignedMessage:
|
||||
tx, err := ethtypes.EthTxFromSignedEthMessage(msg)
|
||||
tx, err := ethtypes.EthTransactionFromSignedFilecoinMessage(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert from signed message: %w", err)
|
||||
}
|
||||
|
||||
tx.Hash, err = tx.TxHash()
|
||||
hash, err := tx.TxHash()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to call TxHash: %w", err)
|
||||
}
|
||||
fmt.Println(tx.Hash)
|
||||
fmt.Println(hash)
|
||||
default:
|
||||
return fmt.Errorf("not a signed message")
|
||||
}
|
||||
|
@ -637,17 +637,17 @@ var backfillTxHashCmd = &cli.Command{
|
||||
continue
|
||||
}
|
||||
|
||||
tx, err := ethtypes.EthTxFromSignedEthMessage(smsg)
|
||||
tx, err := ethtypes.EthTransactionFromSignedFilecoinMessage(smsg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert from signed message: %w at epoch: %d", err, epoch)
|
||||
}
|
||||
|
||||
tx.Hash, err = tx.TxHash()
|
||||
hash, err := tx.TxHash()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to calculate hash for ethTx: %w at epoch: %d", err, epoch)
|
||||
}
|
||||
|
||||
res, err := insertStmt.Exec(tx.Hash.String(), smsg.Cid().String())
|
||||
res, err := insertStmt.Exec(hash.String(), smsg.Cid().String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error inserting tx mapping to db: %s at epoch: %d", err, epoch)
|
||||
}
|
||||
@ -658,7 +658,7 @@ var backfillTxHashCmd = &cli.Command{
|
||||
}
|
||||
|
||||
if rowsAffected > 0 {
|
||||
log.Debugf("Inserted txhash %s, cid: %s at epoch: %d", tx.Hash.String(), smsg.Cid().String(), epoch)
|
||||
log.Debugf("Inserted txhash %s, cid: %s at epoch: %d", hash.String(), smsg.Cid().String(), epoch)
|
||||
}
|
||||
|
||||
totalRowsAffected += rowsAffected
|
||||
|
@ -2728,6 +2728,7 @@ Response:
|
||||
"gas": "0x5",
|
||||
"maxFeePerGas": "0x0",
|
||||
"maxPriorityFeePerGas": "0x0",
|
||||
"gasPrice": "0x0",
|
||||
"accessList": [
|
||||
"0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e"
|
||||
],
|
||||
@ -2767,6 +2768,7 @@ Response:
|
||||
"gas": "0x5",
|
||||
"maxFeePerGas": "0x0",
|
||||
"maxPriorityFeePerGas": "0x0",
|
||||
"gasPrice": "0x0",
|
||||
"accessList": [
|
||||
"0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e"
|
||||
],
|
||||
@ -2805,6 +2807,7 @@ Response:
|
||||
"gas": "0x5",
|
||||
"maxFeePerGas": "0x0",
|
||||
"maxPriorityFeePerGas": "0x0",
|
||||
"gasPrice": "0x0",
|
||||
"accessList": [
|
||||
"0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e"
|
||||
],
|
||||
@ -2844,6 +2847,7 @@ Response:
|
||||
"gas": "0x5",
|
||||
"maxFeePerGas": "0x0",
|
||||
"maxPriorityFeePerGas": "0x0",
|
||||
"gasPrice": "0x0",
|
||||
"accessList": [
|
||||
"0x37690cfec6c1bf4c3b9288c7a5d783e98731e90b0a4c177c2a374c7a9427355e"
|
||||
],
|
||||
|
@ -74,7 +74,7 @@ func TestEthAccountAbstraction(t *testing.T) {
|
||||
msgFromPlaceholder, err = client.GasEstimateMessageGas(ctx, msgFromPlaceholder, nil, types.EmptyTSK)
|
||||
require.NoError(t, err)
|
||||
|
||||
txArgs, err := ethtypes.EthTxArgsFromUnsignedEthMessage(msgFromPlaceholder)
|
||||
txArgs, err := ethtypes.Eth1559TxArgsFromUnsignedFilecoinMessage(msgFromPlaceholder)
|
||||
require.NoError(t, err)
|
||||
|
||||
digest, err := txArgs.ToRlpUnsignedMsg()
|
||||
@ -111,7 +111,7 @@ func TestEthAccountAbstraction(t *testing.T) {
|
||||
msgFromPlaceholder, err = client.GasEstimateMessageGas(ctx, msgFromPlaceholder, nil, types.EmptyTSK)
|
||||
require.NoError(t, err)
|
||||
|
||||
txArgs, err = ethtypes.EthTxArgsFromUnsignedEthMessage(msgFromPlaceholder)
|
||||
txArgs, err = ethtypes.Eth1559TxArgsFromUnsignedFilecoinMessage(msgFromPlaceholder)
|
||||
require.NoError(t, err)
|
||||
|
||||
digest, err = txArgs.ToRlpUnsignedMsg()
|
||||
@ -185,7 +185,7 @@ func TestEthAccountAbstractionFailure(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
msgFromPlaceholder.Value = abi.TokenAmount(types.MustParseFIL("1000"))
|
||||
txArgs, err := ethtypes.EthTxArgsFromUnsignedEthMessage(msgFromPlaceholder)
|
||||
txArgs, err := ethtypes.Eth1559TxArgsFromUnsignedFilecoinMessage(msgFromPlaceholder)
|
||||
require.NoError(t, err)
|
||||
|
||||
digest, err := txArgs.ToRlpUnsignedMsg()
|
||||
@ -224,7 +224,7 @@ func TestEthAccountAbstractionFailure(t *testing.T) {
|
||||
msgFromPlaceholder, err = client.GasEstimateMessageGas(ctx, msgFromPlaceholder, nil, types.EmptyTSK)
|
||||
require.NoError(t, err)
|
||||
|
||||
txArgs, err = ethtypes.EthTxArgsFromUnsignedEthMessage(msgFromPlaceholder)
|
||||
txArgs, err = ethtypes.Eth1559TxArgsFromUnsignedFilecoinMessage(msgFromPlaceholder)
|
||||
require.NoError(t, err)
|
||||
|
||||
digest, err = txArgs.ToRlpUnsignedMsg()
|
||||
@ -285,7 +285,7 @@ func TestEthAccountAbstractionFailsFromEvmActor(t *testing.T) {
|
||||
maxPriorityFeePerGas, err := client.EthMaxPriorityFeePerGas(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
tx := ethtypes.EthTxArgs{
|
||||
tx := ethtypes.Eth1559TxArgs{
|
||||
ChainID: build.Eip155ChainId,
|
||||
Value: big.Zero(),
|
||||
Nonce: 0,
|
||||
@ -302,7 +302,7 @@ func TestEthAccountAbstractionFailsFromEvmActor(t *testing.T) {
|
||||
|
||||
client.EVM().SubmitTransaction(ctx, &tx)
|
||||
|
||||
smsg, err := tx.ToSignedMessage()
|
||||
smsg, err := ethtypes.ToSignedFilecoinMessage(&tx)
|
||||
require.NoError(t, err)
|
||||
|
||||
ml, err := client.StateWaitMsg(ctx, smsg.Cid(), 1, api.LookbackNoLimit, true)
|
||||
|
@ -463,7 +463,7 @@ func createRawSignedEthTx(ctx context.Context, t *testing.T, client *kit.TestFul
|
||||
maxPriorityFeePerGas, err := client.EthMaxPriorityFeePerGas(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
tx := ethtypes.EthTxArgs{
|
||||
tx := ethtypes.Eth1559TxArgs{
|
||||
ChainID: build.Eip155ChainId,
|
||||
Value: big.NewInt(100),
|
||||
Nonce: 0,
|
||||
|
@ -73,7 +73,7 @@ func TestDeployment(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// now deploy a contract from the placeholder, and validate it went well
|
||||
tx := ethtypes.EthTxArgs{
|
||||
tx := ethtypes.Eth1559TxArgs{
|
||||
ChainID: build.Eip155ChainId,
|
||||
Value: big.Zero(),
|
||||
Nonce: 0,
|
||||
|
@ -61,7 +61,7 @@ func TestTransactionHashLookup(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// now deploy a contract from the embryo, and validate it went well
|
||||
tx := ethtypes.EthTxArgs{
|
||||
tx := ethtypes.Eth1559TxArgs{
|
||||
ChainID: build.Eip155ChainId,
|
||||
Value: big.Zero(),
|
||||
Nonce: 0,
|
||||
@ -367,7 +367,7 @@ func TestEthGetMessageCidByTransactionHashEthTx(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// now deploy a contract from the embryo, and validate it went well
|
||||
tx := ethtypes.EthTxArgs{
|
||||
tx := ethtypes.Eth1559TxArgs{
|
||||
ChainID: build.Eip155ChainId,
|
||||
Value: big.Zero(),
|
||||
Nonce: 0,
|
||||
@ -385,7 +385,7 @@ func TestEthGetMessageCidByTransactionHashEthTx(t *testing.T) {
|
||||
sender, err := tx.Sender()
|
||||
require.NoError(t, err)
|
||||
|
||||
unsignedMessage, err := tx.ToUnsignedMessage(sender)
|
||||
unsignedMessage, err := tx.ToUnsignedFilecoinMessage(sender)
|
||||
require.NoError(t, err)
|
||||
|
||||
rawTxHash, err := tx.TxHash()
|
||||
|
400
itests/eth_legacy_transactions_test.go
Normal file
400
itests/eth_legacy_transactions_test.go
Normal file
@ -0,0 +1,400 @@
|
||||
package itests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/filecoin-project/go-state-types/abi"
|
||||
"github.com/filecoin-project/go-state-types/big"
|
||||
"github.com/filecoin-project/go-state-types/network"
|
||||
|
||||
"github.com/filecoin-project/lotus/build"
|
||||
"github.com/filecoin-project/lotus/chain/consensus/filcns"
|
||||
"github.com/filecoin-project/lotus/chain/stmgr"
|
||||
"github.com/filecoin-project/lotus/chain/types"
|
||||
"github.com/filecoin-project/lotus/chain/types/ethtypes"
|
||||
"github.com/filecoin-project/lotus/itests/kit"
|
||||
)
|
||||
|
||||
func TestLegacyValueTransferValidSignature(t *testing.T) {
|
||||
blockTime := 100 * time.Millisecond
|
||||
client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC())
|
||||
|
||||
ens.InterconnectAll().BeginMining(blockTime)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// create a new Ethereum account
|
||||
key, ethAddr, deployer := client.EVM().NewAccount()
|
||||
_, ethAddr2, _ := client.EVM().NewAccount()
|
||||
|
||||
kit.SendFunds(ctx, t, client, deployer, types.FromFil(1000))
|
||||
|
||||
gasParams, err := json.Marshal(ethtypes.EthEstimateGasParams{Tx: ethtypes.EthCall{
|
||||
From: ðAddr,
|
||||
To: ðAddr2,
|
||||
Value: ethtypes.EthBigInt(big.NewInt(100)),
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
|
||||
gaslimit, err := client.EthEstimateGas(ctx, gasParams)
|
||||
require.NoError(t, err)
|
||||
fmt.Println("gas limit is", gaslimit)
|
||||
|
||||
tx := ethtypes.EthLegacyHomesteadTxArgs{
|
||||
Value: big.NewInt(100),
|
||||
Nonce: 0,
|
||||
To: ðAddr2,
|
||||
GasPrice: types.NanoFil,
|
||||
GasLimit: int(gaslimit),
|
||||
V: big.Zero(),
|
||||
R: big.Zero(),
|
||||
S: big.Zero(),
|
||||
}
|
||||
|
||||
client.EVM().SignLegacyHomesteadTransaction(&tx, key.PrivateKey)
|
||||
// Mangle signature
|
||||
tx.V.Int.Xor(tx.V.Int, big.NewInt(1).Int)
|
||||
|
||||
signed, err := tx.ToRlpSignedMsg()
|
||||
require.NoError(t, err)
|
||||
// Submit transaction with bad signature
|
||||
_, err = client.EVM().EthSendRawTransaction(ctx, signed)
|
||||
require.Error(t, err)
|
||||
|
||||
// Submit transaction with valid signature
|
||||
client.EVM().SignLegacyHomesteadTransaction(&tx, key.PrivateKey)
|
||||
|
||||
hash := client.EVM().SubmitTransaction(ctx, &tx)
|
||||
|
||||
receipt, err := client.EVM().WaitTransaction(ctx, hash)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, receipt)
|
||||
require.EqualValues(t, ethAddr, receipt.From)
|
||||
require.EqualValues(t, ethAddr2, *receipt.To)
|
||||
require.EqualValues(t, hash, receipt.TransactionHash)
|
||||
|
||||
// Success.
|
||||
require.EqualValues(t, ethtypes.EthUint64(0x1), receipt.Status)
|
||||
|
||||
// Validate that we sent the expected transaction.
|
||||
ethTx, err := client.EthGetTransactionByHash(ctx, &hash)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, ethTx.MaxPriorityFeePerGas)
|
||||
require.Nil(t, ethTx.MaxFeePerGas)
|
||||
|
||||
require.EqualValues(t, ethAddr, ethTx.From)
|
||||
require.EqualValues(t, ethAddr2, *ethTx.To)
|
||||
require.EqualValues(t, tx.Nonce, ethTx.Nonce)
|
||||
require.EqualValues(t, hash, ethTx.Hash)
|
||||
require.EqualValues(t, tx.Value, ethTx.Value)
|
||||
require.EqualValues(t, 0, ethTx.Type)
|
||||
require.EqualValues(t, 0, ethTx.ChainID)
|
||||
require.EqualValues(t, ethtypes.EthBytes{}, ethTx.Input)
|
||||
require.EqualValues(t, tx.GasLimit, ethTx.Gas)
|
||||
require.EqualValues(t, tx.GasPrice, *ethTx.GasPrice)
|
||||
require.EqualValues(t, tx.R, ethTx.R)
|
||||
require.EqualValues(t, tx.S, ethTx.S)
|
||||
require.EqualValues(t, tx.V, ethTx.V)
|
||||
}
|
||||
|
||||
func TestLegacyEIP155ValueTransferValidSignatureFailsNV22(t *testing.T) {
|
||||
blockTime := 100 * time.Millisecond
|
||||
|
||||
nv23Height := 10
|
||||
// We will move to NV23 at epoch 10
|
||||
client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC(), kit.UpgradeSchedule(stmgr.Upgrade{
|
||||
Network: network.Version22,
|
||||
Height: -1,
|
||||
}, stmgr.Upgrade{
|
||||
Network: network.Version23,
|
||||
Height: abi.ChainEpoch(nv23Height),
|
||||
Migration: filcns.UpgradeActorsV13,
|
||||
}))
|
||||
|
||||
ens.InterconnectAll().BeginMining(blockTime)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// create a new Ethereum account
|
||||
key, ethAddr, deployer := client.EVM().NewAccount()
|
||||
_, ethAddr2, _ := client.EVM().NewAccount()
|
||||
|
||||
kit.SendFunds(ctx, t, client, deployer, types.FromFil(1000))
|
||||
|
||||
gasParams, err := json.Marshal(ethtypes.EthEstimateGasParams{Tx: ethtypes.EthCall{
|
||||
From: ðAddr,
|
||||
To: ðAddr2,
|
||||
Value: ethtypes.EthBigInt(big.NewInt(100)),
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
|
||||
gaslimit, err := client.EthEstimateGas(ctx, gasParams)
|
||||
require.NoError(t, err)
|
||||
|
||||
legacyTx := ðtypes.EthLegacyHomesteadTxArgs{
|
||||
Value: big.NewInt(100),
|
||||
Nonce: 0,
|
||||
To: ðAddr2,
|
||||
GasPrice: types.NanoFil,
|
||||
GasLimit: int(gaslimit),
|
||||
V: big.Zero(),
|
||||
R: big.Zero(),
|
||||
S: big.Zero(),
|
||||
}
|
||||
tx := ethtypes.NewEthLegacy155TxArgs(legacyTx)
|
||||
|
||||
// TX will fail as we're still at NV22
|
||||
client.EVM().SignLegacyEIP155Transaction(tx, key.PrivateKey, big.NewInt(build.Eip155ChainId))
|
||||
|
||||
signed, err := tx.ToRawTxBytesSigned()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.EVM().EthSendRawTransaction(ctx, signed)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "network version should be atleast NV23 for sending legacy ETH transactions")
|
||||
}
|
||||
|
||||
func TestLegacyEIP155ValueTransferValidSignature(t *testing.T) {
|
||||
blockTime := 100 * time.Millisecond
|
||||
client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC())
|
||||
|
||||
ens.InterconnectAll().BeginMining(blockTime)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// create a new Ethereum account
|
||||
key, ethAddr, deployer := client.EVM().NewAccount()
|
||||
_, ethAddr2, _ := client.EVM().NewAccount()
|
||||
|
||||
kit.SendFunds(ctx, t, client, deployer, types.FromFil(1000))
|
||||
|
||||
gasParams, err := json.Marshal(ethtypes.EthEstimateGasParams{Tx: ethtypes.EthCall{
|
||||
From: ðAddr,
|
||||
To: ðAddr2,
|
||||
Value: ethtypes.EthBigInt(big.NewInt(100)),
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
|
||||
gaslimit, err := client.EthEstimateGas(ctx, gasParams)
|
||||
require.NoError(t, err)
|
||||
|
||||
legacyTx := ðtypes.EthLegacyHomesteadTxArgs{
|
||||
Value: big.NewInt(100),
|
||||
Nonce: 0,
|
||||
To: ðAddr2,
|
||||
GasPrice: types.NanoFil,
|
||||
GasLimit: int(gaslimit),
|
||||
V: big.Zero(),
|
||||
R: big.Zero(),
|
||||
S: big.Zero(),
|
||||
}
|
||||
tx := ethtypes.NewEthLegacy155TxArgs(legacyTx)
|
||||
|
||||
client.EVM().SignLegacyEIP155Transaction(tx, key.PrivateKey, big.NewInt(build.Eip155ChainId))
|
||||
// Mangle signature
|
||||
innerTx := tx.GetLegacyTx()
|
||||
innerTx.V.Int.Xor(innerTx.V.Int, big.NewInt(1).Int)
|
||||
|
||||
signed, err := tx.ToRawTxBytesSigned()
|
||||
require.NoError(t, err)
|
||||
// Submit transaction with bad signature
|
||||
_, err = client.EVM().EthSendRawTransaction(ctx, signed)
|
||||
require.Error(t, err)
|
||||
|
||||
// Submit transaction with valid signature but incorrect chain ID
|
||||
client.EVM().SignLegacyEIP155Transaction(tx, key.PrivateKey, big.NewInt(build.Eip155ChainId))
|
||||
|
||||
signed, err = tx.ToRawTxBytesSigned()
|
||||
require.NoError(t, err)
|
||||
|
||||
hash, err := client.EVM().EthSendRawTransaction(ctx, signed)
|
||||
require.NoError(t, err)
|
||||
|
||||
receipt, err := client.EVM().WaitTransaction(ctx, hash)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, receipt)
|
||||
require.EqualValues(t, ethAddr, receipt.From)
|
||||
require.EqualValues(t, ethAddr2, *receipt.To)
|
||||
require.EqualValues(t, hash, receipt.TransactionHash)
|
||||
|
||||
// Success.
|
||||
require.EqualValues(t, ethtypes.EthUint64(0x1), receipt.Status)
|
||||
|
||||
// Validate that we sent the expected transaction.
|
||||
ethTx, err := client.EthGetTransactionByHash(ctx, &hash)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, ethTx.MaxPriorityFeePerGas)
|
||||
require.Nil(t, ethTx.MaxFeePerGas)
|
||||
|
||||
innerTx = tx.GetLegacyTx()
|
||||
require.EqualValues(t, ethAddr, ethTx.From)
|
||||
require.EqualValues(t, ethAddr2, *ethTx.To)
|
||||
require.EqualValues(t, innerTx.Nonce, ethTx.Nonce)
|
||||
require.EqualValues(t, hash, ethTx.Hash)
|
||||
require.EqualValues(t, innerTx.Value, ethTx.Value)
|
||||
require.EqualValues(t, 0, ethTx.Type)
|
||||
require.EqualValues(t, build.Eip155ChainId, ethTx.ChainID)
|
||||
require.EqualValues(t, ethtypes.EthBytes{}, ethTx.Input)
|
||||
require.EqualValues(t, innerTx.GasLimit, ethTx.Gas)
|
||||
require.EqualValues(t, innerTx.GasPrice, *ethTx.GasPrice)
|
||||
require.EqualValues(t, innerTx.R, ethTx.R)
|
||||
require.EqualValues(t, innerTx.S, ethTx.S)
|
||||
require.EqualValues(t, innerTx.V, ethTx.V)
|
||||
}
|
||||
|
||||
func TestLegacyContractInvocation(t *testing.T) {
|
||||
blockTime := 100 * time.Millisecond
|
||||
client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC())
|
||||
|
||||
ens.InterconnectAll().BeginMining(blockTime)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// create a new Ethereum account
|
||||
key, ethAddr, deployer := client.EVM().NewAccount()
|
||||
// send some funds to the f410 address
|
||||
kit.SendFunds(ctx, t, client, deployer, types.FromFil(10))
|
||||
|
||||
// DEPLOY CONTRACT
|
||||
tx, err := deployLegacyContractTx(ctx, t, client, ethAddr)
|
||||
require.NoError(t, err)
|
||||
|
||||
client.EVM().SignLegacyHomesteadTransaction(tx, key.PrivateKey)
|
||||
// Mangle signature
|
||||
tx.V.Int.Xor(tx.V.Int, big.NewInt(1).Int)
|
||||
|
||||
signed, err := tx.ToRlpSignedMsg()
|
||||
require.NoError(t, err)
|
||||
// Submit transaction with bad signature
|
||||
_, err = client.EVM().EthSendRawTransaction(ctx, signed)
|
||||
require.Error(t, err)
|
||||
|
||||
// Submit transaction with valid signature
|
||||
client.EVM().SignLegacyHomesteadTransaction(tx, key.PrivateKey)
|
||||
|
||||
hash := client.EVM().SubmitTransaction(ctx, tx)
|
||||
|
||||
receipt, err := client.EVM().WaitTransaction(ctx, hash)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, receipt)
|
||||
require.EqualValues(t, ethtypes.EthUint64(0x1), receipt.Status)
|
||||
|
||||
// Get contract address.
|
||||
contractAddr := client.EVM().ComputeContractAddress(ethAddr, 0)
|
||||
|
||||
// INVOKE CONTRACT
|
||||
|
||||
// Params
|
||||
// entry point for getBalance - f8b2cb4f
|
||||
// address - ff00000000000000000000000000000000000064
|
||||
params, err := hex.DecodeString("f8b2cb4f000000000000000000000000ff00000000000000000000000000000000000064")
|
||||
require.NoError(t, err)
|
||||
|
||||
gasParams, err := json.Marshal(ethtypes.EthEstimateGasParams{Tx: ethtypes.EthCall{
|
||||
From: ðAddr,
|
||||
To: &contractAddr,
|
||||
Data: params,
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
|
||||
gaslimit, err := client.EthEstimateGas(ctx, gasParams)
|
||||
require.NoError(t, err)
|
||||
|
||||
maxPriorityFeePerGas, err := client.EthMaxPriorityFeePerGas(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
invokeTx := ethtypes.EthLegacyHomesteadTxArgs{
|
||||
To: &contractAddr,
|
||||
Value: big.Zero(),
|
||||
Nonce: 1,
|
||||
GasPrice: big.Int(maxPriorityFeePerGas),
|
||||
GasLimit: int(gaslimit),
|
||||
Input: params,
|
||||
V: big.Zero(),
|
||||
R: big.Zero(),
|
||||
S: big.Zero(),
|
||||
}
|
||||
|
||||
client.EVM().SignLegacyHomesteadTransaction(&invokeTx, key.PrivateKey)
|
||||
// Mangle signature
|
||||
invokeTx.V.Int.Xor(invokeTx.V.Int, big.NewInt(1).Int)
|
||||
|
||||
signed, err = invokeTx.ToRlpSignedMsg()
|
||||
require.NoError(t, err)
|
||||
// Submit transaction with bad signature
|
||||
_, err = client.EVM().EthSendRawTransaction(ctx, signed)
|
||||
require.Error(t, err)
|
||||
|
||||
// Submit transaction with valid signature
|
||||
client.EVM().SignLegacyHomesteadTransaction(&invokeTx, key.PrivateKey)
|
||||
hash = client.EVM().SubmitTransaction(ctx, &invokeTx)
|
||||
|
||||
receipt, err = client.EVM().WaitTransaction(ctx, hash)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, receipt)
|
||||
|
||||
// Success.
|
||||
require.EqualValues(t, ethtypes.EthUint64(0x1), receipt.Status)
|
||||
|
||||
// Validate that we correctly computed the gas outputs.
|
||||
mCid, err := client.EthGetMessageCidByTransactionHash(ctx, &hash)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, mCid)
|
||||
|
||||
invokResult, err := client.StateReplay(ctx, types.EmptyTSK, *mCid)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, invokResult.GasCost.GasUsed, big.NewInt(int64(receipt.GasUsed)))
|
||||
effectiveGasPrice := big.Div(invokResult.GasCost.TotalCost, invokResult.GasCost.GasUsed)
|
||||
require.EqualValues(t, effectiveGasPrice, big.Int(receipt.EffectiveGasPrice))
|
||||
}
|
||||
|
||||
func deployLegacyContractTx(ctx context.Context, t *testing.T, client *kit.TestFullNode, ethAddr ethtypes.EthAddress) (*ethtypes.EthLegacyHomesteadTxArgs, error) {
|
||||
// install contract
|
||||
contractHex, err := os.ReadFile("./contracts/SimpleCoin.hex")
|
||||
require.NoError(t, err)
|
||||
|
||||
contract, err := hex.DecodeString(string(contractHex))
|
||||
require.NoError(t, err)
|
||||
|
||||
gasParams, err := json.Marshal(ethtypes.EthEstimateGasParams{Tx: ethtypes.EthCall{
|
||||
From: ðAddr,
|
||||
Data: contract,
|
||||
}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gaslimit, err := client.EthEstimateGas(ctx, gasParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maxPriorityFeePerGas, err := client.EthMaxPriorityFeePerGas(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// now deploy a contract from the embryo, and validate it went well
|
||||
return ðtypes.EthLegacyHomesteadTxArgs{
|
||||
Value: big.Zero(),
|
||||
Nonce: 0,
|
||||
GasPrice: big.Int(maxPriorityFeePerGas),
|
||||
GasLimit: int(gaslimit),
|
||||
Input: contract,
|
||||
V: big.Zero(),
|
||||
R: big.Zero(),
|
||||
S: big.Zero(),
|
||||
}, nil
|
||||
}
|
@ -64,7 +64,7 @@ func TestValueTransferValidSignature(t *testing.T) {
|
||||
maxPriorityFeePerGas, err := client.EthMaxPriorityFeePerGas(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
tx := ethtypes.EthTxArgs{
|
||||
tx := ethtypes.Eth1559TxArgs{
|
||||
ChainID: build.Eip155ChainId,
|
||||
Value: big.NewInt(100),
|
||||
Nonce: 0,
|
||||
@ -113,30 +113,13 @@ func TestValueTransferValidSignature(t *testing.T) {
|
||||
require.EqualValues(t, 2, ethTx.Type)
|
||||
require.EqualValues(t, ethtypes.EthBytes{}, ethTx.Input)
|
||||
require.EqualValues(t, tx.GasLimit, ethTx.Gas)
|
||||
require.EqualValues(t, tx.MaxFeePerGas, ethTx.MaxFeePerGas)
|
||||
require.EqualValues(t, tx.MaxPriorityFeePerGas, ethTx.MaxPriorityFeePerGas)
|
||||
require.EqualValues(t, tx.MaxFeePerGas, *ethTx.MaxFeePerGas)
|
||||
require.EqualValues(t, tx.MaxPriorityFeePerGas, *ethTx.MaxPriorityFeePerGas)
|
||||
require.EqualValues(t, tx.V, ethTx.V)
|
||||
require.EqualValues(t, tx.R, ethTx.R)
|
||||
require.EqualValues(t, tx.S, ethTx.S)
|
||||
}
|
||||
|
||||
func TestLegacyTransaction(t *testing.T) {
|
||||
blockTime := 100 * time.Millisecond
|
||||
client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC())
|
||||
|
||||
ens.InterconnectAll().BeginMining(blockTime)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// This is a legacy style transaction obtained from etherscan
|
||||
// Tx details: https://etherscan.io/getRawTx?tx=0x0763262208d89efeeb50c8bb05b50c537903fe9d7bdef3b223fd1f5f69f69b32
|
||||
txBytes, err := hex.DecodeString("f86f830131cf8504a817c800825208942cf1e5a8250ded8835694ebeb90cfa0237fcb9b1882ec4a5251d1100008026a0f5f8d2244d619e211eeb634acd1bea0762b7b4c97bba9f01287c82bfab73f911a015be7982898aa7cc6c6f27ff33e999e4119d6cd51330353474b98067ff56d930")
|
||||
require.NoError(t, err)
|
||||
_, err = client.EVM().EthSendRawTransaction(ctx, txBytes)
|
||||
require.ErrorContains(t, err, "legacy transaction is not supported")
|
||||
}
|
||||
|
||||
func TestContractDeploymentValidSignature(t *testing.T) {
|
||||
blockTime := 100 * time.Millisecond
|
||||
client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC())
|
||||
@ -255,7 +238,7 @@ func TestContractInvocation(t *testing.T) {
|
||||
maxPriorityFeePerGas, err := client.EthMaxPriorityFeePerGas(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
invokeTx := ethtypes.EthTxArgs{
|
||||
invokeTx := ethtypes.Eth1559TxArgs{
|
||||
ChainID: build.Eip155ChainId,
|
||||
To: &contractAddr,
|
||||
Value: big.Zero(),
|
||||
@ -363,7 +346,7 @@ func TestGetBlockByNumber(t *testing.T) {
|
||||
require.Equal(t, types.FromFil(10).Int, bal.Int)
|
||||
}
|
||||
|
||||
func deployContractTx(ctx context.Context, client *kit.TestFullNode, ethAddr ethtypes.EthAddress, contract []byte) (*ethtypes.EthTxArgs, error) {
|
||||
func deployContractTx(ctx context.Context, client *kit.TestFullNode, ethAddr ethtypes.EthAddress, contract []byte) (*ethtypes.Eth1559TxArgs, error) {
|
||||
gasParams, err := json.Marshal(ethtypes.EthEstimateGasParams{Tx: ethtypes.EthCall{
|
||||
From: ðAddr,
|
||||
Data: contract,
|
||||
@ -383,7 +366,7 @@ func deployContractTx(ctx context.Context, client *kit.TestFullNode, ethAddr eth
|
||||
}
|
||||
|
||||
// now deploy a contract from the embryo, and validate it went well
|
||||
return ðtypes.EthTxArgs{
|
||||
return ðtypes.Eth1559TxArgs{
|
||||
ChainID: build.Eip155ChainId,
|
||||
Value: big.Zero(),
|
||||
Nonce: 0,
|
||||
|
@ -678,7 +678,7 @@ func TestFEVMRecursiveActorCallEstimate(t *testing.T) {
|
||||
nonce, err := client.MpoolGetNonce(ctx, ethFilAddr)
|
||||
require.NoError(t, err)
|
||||
|
||||
tx := ðtypes.EthTxArgs{
|
||||
tx := ðtypes.Eth1559TxArgs{
|
||||
ChainID: build.Eip155ChainId,
|
||||
To: &contractAddr,
|
||||
Value: big.Zero(),
|
||||
@ -695,7 +695,7 @@ func TestFEVMRecursiveActorCallEstimate(t *testing.T) {
|
||||
client.EVM().SignTransaction(tx, key.PrivateKey)
|
||||
hash := client.EVM().SubmitTransaction(ctx, tx)
|
||||
|
||||
smsg, err := tx.ToSignedMessage()
|
||||
smsg, err := ethtypes.ToSignedFilecoinMessage(tx)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client.StateWaitMsg(ctx, smsg.Cid(), 0, 0, false)
|
||||
@ -834,7 +834,7 @@ func TestFEVMBareTransferTriggersSmartContractLogic(t *testing.T) {
|
||||
maxPriorityFeePerGas, err := client.EthMaxPriorityFeePerGas(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
tx := ethtypes.EthTxArgs{
|
||||
tx := ethtypes.Eth1559TxArgs{
|
||||
ChainID: build.Eip155ChainId,
|
||||
Value: big.NewInt(100),
|
||||
Nonce: 0,
|
||||
|
@ -44,6 +44,49 @@ func (f *TestFullNode) EVM() *EVM {
|
||||
return &EVM{f}
|
||||
}
|
||||
|
||||
// SignLegacyHomesteadTransaction signs a legacy Homstead Ethereum transaction in place with the supplied private key.
|
||||
func (e *EVM) SignLegacyEIP155Transaction(tx *ethtypes.EthLegacy155TxArgs, privKey []byte, chainID big.Int) {
|
||||
preimage, err := tx.ToRlpUnsignedMsg()
|
||||
require.NoError(e.t, err)
|
||||
|
||||
// sign the RLP payload
|
||||
signature, err := sigs.Sign(crypto.SigTypeDelegated, privKey, preimage)
|
||||
require.NoError(e.t, err)
|
||||
|
||||
signature.Data = append([]byte{ethtypes.EthLegacy155TxSignaturePrefix}, signature.Data...)
|
||||
|
||||
chainIdMul := big.Mul(chainID, big.NewInt(2))
|
||||
vVal := big.Add(chainIdMul, big.NewIntUnsigned(35))
|
||||
|
||||
switch signature.Data[len(signature.Data)-1] {
|
||||
case 0:
|
||||
vVal = big.Add(vVal, big.NewInt(0))
|
||||
case 1:
|
||||
vVal = big.Add(vVal, big.NewInt(1))
|
||||
}
|
||||
|
||||
signature.Data = append(signature.Data[:65], vVal.Int.Bytes()...)
|
||||
|
||||
err = tx.InitialiseSignature(*signature)
|
||||
require.NoError(e.t, err)
|
||||
}
|
||||
|
||||
// SignLegacyHomesteadTransaction signs a legacy Homstead Ethereum transaction in place with the supplied private key.
|
||||
func (e *EVM) SignLegacyHomesteadTransaction(tx *ethtypes.EthLegacyHomesteadTxArgs, privKey []byte) {
|
||||
preimage, err := tx.ToRlpUnsignedMsg()
|
||||
require.NoError(e.t, err)
|
||||
|
||||
// sign the RLP payload
|
||||
signature, err := sigs.Sign(crypto.SigTypeDelegated, privKey, preimage)
|
||||
require.NoError(e.t, err)
|
||||
|
||||
signature.Data = append([]byte{ethtypes.EthLegacyHomesteadTxSignaturePrefix}, signature.Data...)
|
||||
signature.Data[len(signature.Data)-1] += 27
|
||||
|
||||
err = tx.InitialiseSignature(*signature)
|
||||
require.NoError(e.t, err)
|
||||
}
|
||||
|
||||
func (e *EVM) DeployContractWithValue(ctx context.Context, sender address.Address, bytecode []byte, value big.Int) eam.CreateReturn {
|
||||
require := require.New(e.t)
|
||||
|
||||
@ -208,7 +251,7 @@ func (e *EVM) AssertAddressBalanceConsistent(ctx context.Context, addr address.A
|
||||
}
|
||||
|
||||
// SignTransaction signs an Ethereum transaction in place with the supplied private key.
|
||||
func (e *EVM) SignTransaction(tx *ethtypes.EthTxArgs, privKey []byte) {
|
||||
func (e *EVM) SignTransaction(tx *ethtypes.Eth1559TxArgs, privKey []byte) {
|
||||
preimage, err := tx.ToRlpUnsignedMsg()
|
||||
require.NoError(e.t, err)
|
||||
|
||||
@ -216,16 +259,12 @@ func (e *EVM) SignTransaction(tx *ethtypes.EthTxArgs, privKey []byte) {
|
||||
signature, err := sigs.Sign(crypto.SigTypeDelegated, privKey, preimage)
|
||||
require.NoError(e.t, err)
|
||||
|
||||
r, s, v, err := ethtypes.RecoverSignature(*signature)
|
||||
err = tx.InitialiseSignature(*signature)
|
||||
require.NoError(e.t, err)
|
||||
|
||||
tx.V = big.Int(v)
|
||||
tx.R = big.Int(r)
|
||||
tx.S = big.Int(s)
|
||||
}
|
||||
|
||||
// SubmitTransaction submits the transaction via the Eth endpoint.
|
||||
func (e *EVM) SubmitTransaction(ctx context.Context, tx *ethtypes.EthTxArgs) ethtypes.EthHash {
|
||||
func (e *EVM) SubmitTransaction(ctx context.Context, tx ethtypes.EthTransaction) ethtypes.EthHash {
|
||||
signed, err := tx.ToRlpSignedMsg()
|
||||
require.NoError(e.t, err)
|
||||
|
||||
|
@ -294,14 +294,16 @@ func (a *EthModule) EthGetTransactionByHashLimited(ctx context.Context, txHash *
|
||||
// This should be "fine" as anyone using an "Ethereum-centric" block
|
||||
// explorer shouldn't care about seeing pending messages from native
|
||||
// accounts.
|
||||
tx, err := ethtypes.EthTxFromSignedEthMessage(p)
|
||||
ethtx, err := ethtypes.EthTransactionFromSignedFilecoinMessage(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not convert Filecoin message into tx: %w", err)
|
||||
}
|
||||
tx.Hash, err = tx.TxHash()
|
||||
|
||||
tx, err := ethtx.ToEthTx(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not compute tx hash for eth txn: %w", err)
|
||||
return nil, fmt.Errorf("could not convert Eth transaction to EthTx: %w", err)
|
||||
}
|
||||
|
||||
return &tx, nil
|
||||
}
|
||||
}
|
||||
@ -817,12 +819,12 @@ func (a *EthModule) EthGasPrice(ctx context.Context) (ethtypes.EthBigInt, error)
|
||||
}
|
||||
|
||||
func (a *EthModule) EthSendRawTransaction(ctx context.Context, rawTx ethtypes.EthBytes) (ethtypes.EthHash, error) {
|
||||
txArgs, err := ethtypes.ParseEthTxArgs(rawTx)
|
||||
txArgs, err := ethtypes.ParseEthTransaction(rawTx)
|
||||
if err != nil {
|
||||
return ethtypes.EmptyEthHash, err
|
||||
}
|
||||
|
||||
smsg, err := txArgs.ToSignedMessage()
|
||||
smsg, err := ethtypes.ToSignedFilecoinMessage(txArgs)
|
||||
if err != nil {
|
||||
return ethtypes.EmptyEthHash, err
|
||||
}
|
||||
|
@ -250,7 +250,6 @@ func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTx
|
||||
return ethtypes.EthBlock{}, xerrors.Errorf("failed to convert msg to ethTx: %w", err)
|
||||
}
|
||||
|
||||
tx.ChainID = ethtypes.EthUint64(build.Eip155ChainId)
|
||||
tx.BlockHash = &blkHash
|
||||
tx.BlockNumber = &bn
|
||||
tx.TransactionIndex = &ti
|
||||
@ -449,7 +448,7 @@ func ethTxHashFromMessageCid(ctx context.Context, c cid.Cid, sa StateAPI) (ethty
|
||||
|
||||
func ethTxHashFromSignedMessage(smsg *types.SignedMessage) (ethtypes.EthHash, error) {
|
||||
if smsg.Signature.Type == crypto.SigTypeDelegated {
|
||||
tx, err := ethtypes.EthTxFromSignedEthMessage(smsg)
|
||||
tx, err := ethtypes.EthTransactionFromSignedFilecoinMessage(smsg)
|
||||
if err != nil {
|
||||
return ethtypes.EthHash{}, xerrors.Errorf("failed to convert from signed message: %w", err)
|
||||
}
|
||||
@ -468,14 +467,13 @@ func newEthTxFromSignedMessage(smsg *types.SignedMessage, st *state.StateTree) (
|
||||
|
||||
// This is an eth tx
|
||||
if smsg.Signature.Type == crypto.SigTypeDelegated {
|
||||
tx, err = ethtypes.EthTxFromSignedEthMessage(smsg)
|
||||
ethTx, err := ethtypes.EthTransactionFromSignedFilecoinMessage(smsg)
|
||||
if err != nil {
|
||||
return ethtypes.EthTx{}, xerrors.Errorf("failed to convert from signed message: %w", err)
|
||||
}
|
||||
|
||||
tx.Hash, err = tx.TxHash()
|
||||
tx, err = ethTx.ToEthTx(smsg)
|
||||
if err != nil {
|
||||
return ethtypes.EthTx{}, xerrors.Errorf("failed to calculate hash for ethTx: %w", err)
|
||||
return ethtypes.EthTx{}, xerrors.Errorf("failed to convert from signed message: %w", err)
|
||||
}
|
||||
} else if smsg.Signature.Type == crypto.SigTypeSecp256k1 { // Secp Filecoin Message
|
||||
tx, err = ethTxFromNativeMessage(smsg.VMMessage(), st)
|
||||
@ -535,6 +533,9 @@ func ethTxFromNativeMessage(msg *types.Message, st *state.StateTree) (ethtypes.E
|
||||
codec = uint64(multicodec.Cbor)
|
||||
}
|
||||
|
||||
maxFeePerGas := ethtypes.EthBigInt(msg.GasFeeCap)
|
||||
maxPriorityFeePerGas := ethtypes.EthBigInt(msg.GasPremium)
|
||||
|
||||
// We decode as a native call first.
|
||||
ethTx := ethtypes.EthTx{
|
||||
To: &to,
|
||||
@ -543,10 +544,10 @@ func ethTxFromNativeMessage(msg *types.Message, st *state.StateTree) (ethtypes.E
|
||||
Nonce: ethtypes.EthUint64(msg.Nonce),
|
||||
ChainID: ethtypes.EthUint64(build.Eip155ChainId),
|
||||
Value: ethtypes.EthBigInt(msg.Value),
|
||||
Type: ethtypes.Eip1559TxType,
|
||||
Type: ethtypes.EIP1559TxType,
|
||||
Gas: ethtypes.EthUint64(msg.GasLimit),
|
||||
MaxFeePerGas: ethtypes.EthBigInt(msg.GasFeeCap),
|
||||
MaxPriorityFeePerGas: ethtypes.EthBigInt(msg.GasPremium),
|
||||
MaxFeePerGas: &maxFeePerGas,
|
||||
MaxPriorityFeePerGas: &maxPriorityFeePerGas,
|
||||
AccessList: []ethtypes.EthHash{},
|
||||
}
|
||||
|
||||
@ -653,6 +654,7 @@ func newEthTxFromMessageLookup(ctx context.Context, msgLookup *api.MsgLookup, tx
|
||||
tx.BlockHash = &blkHash
|
||||
tx.BlockNumber = &bn
|
||||
tx.TransactionIndex = &ti
|
||||
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
@ -714,7 +716,18 @@ func newEthTxReceipt(ctx context.Context, tx ethtypes.EthTx, lookup *api.MsgLook
|
||||
}
|
||||
|
||||
baseFee := parentTs.Blocks()[0].ParentBaseFee
|
||||
gasOutputs := vm.ComputeGasOutputs(lookup.Receipt.GasUsed, int64(tx.Gas), baseFee, big.Int(tx.MaxFeePerGas), big.Int(tx.MaxPriorityFeePerGas), true)
|
||||
|
||||
gasFeeCap, err := tx.GasFeeCap()
|
||||
if err != nil {
|
||||
return api.EthTxReceipt{}, xerrors.Errorf("failed to get gas fee cap: %w", err)
|
||||
}
|
||||
gasPremium, err := tx.GasPremium()
|
||||
if err != nil {
|
||||
return api.EthTxReceipt{}, xerrors.Errorf("failed to get gas premium: %w", err)
|
||||
}
|
||||
|
||||
gasOutputs := vm.ComputeGasOutputs(lookup.Receipt.GasUsed, int64(tx.Gas), baseFee, big.Int(gasFeeCap),
|
||||
big.Int(gasPremium), true)
|
||||
totalSpent := big.Sum(gasOutputs.BaseFeeBurn, gasOutputs.MinerTip, gasOutputs.OverEstimationBurn)
|
||||
|
||||
effectiveGasPrice := big.Zero()
|
||||
|
@ -85,11 +85,12 @@ func (m *EthTxHashManager) ProcessSignedMessage(ctx context.Context, msg *types.
|
||||
return
|
||||
}
|
||||
|
||||
ethTx, err := ethtypes.EthTxFromSignedEthMessage(msg)
|
||||
ethTx, err := ethtypes.EthTransactionFromSignedFilecoinMessage(msg)
|
||||
if err != nil {
|
||||
log.Errorf("error converting filecoin message to eth tx: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
txHash, err := ethTx.TxHash()
|
||||
if err != nil {
|
||||
log.Errorf("error hashing transaction: %s", err)
|
||||
|
Loading…
Reference in New Issue
Block a user