a34cc5e4e9
Correctly handle "unresolvable" to/from addresses in top-level messages in the Ethereum API. Specifically: 1. Fail if we can't resolve the from address. As far as I can tell, this should be impossible (the message statically couldn't have been included in the block if the sender didn't exist). 2. If we can't resolve the "to" address to an ID, use "max uint64" as the ID (`0xff0000000000000000000000ffffffffffffffff`). This will only happen if the transaction was reverted. It'll be a little confusing, but the alternative is to (a) use an empty address (will look like a contract creation, which is definitely wrong) or (b) use a random/hashed address which will likely be more confusing as it won't be "obviously weird".
821 lines
26 KiB
Go
821 lines
26 KiB
Go
package full
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/ipfs/go-cid"
|
|
"github.com/multiformats/go-multicodec"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/filecoin-project/go-address"
|
|
"github.com/filecoin-project/go-state-types/abi"
|
|
"github.com/filecoin-project/go-state-types/big"
|
|
builtintypes "github.com/filecoin-project/go-state-types/builtin"
|
|
"github.com/filecoin-project/go-state-types/builtin/v10/eam"
|
|
"github.com/filecoin-project/go-state-types/crypto"
|
|
"github.com/filecoin-project/go-state-types/exitcode"
|
|
|
|
"github.com/filecoin-project/lotus/api"
|
|
"github.com/filecoin-project/lotus/build"
|
|
"github.com/filecoin-project/lotus/chain/actors"
|
|
"github.com/filecoin-project/lotus/chain/state"
|
|
"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"
|
|
)
|
|
|
|
// The address used in messages to actors that have since been deleted.
|
|
//
|
|
// 0xff0000000000000000000000ffffffffffffffff
|
|
var revertedEthAddress ethtypes.EthAddress
|
|
|
|
func init() {
|
|
revertedEthAddress[0] = 0xff
|
|
for i := 20 - 8; i < 20; i++ {
|
|
revertedEthAddress[i] = 0xff
|
|
}
|
|
}
|
|
|
|
func getTipsetByBlockNumber(ctx context.Context, chain *store.ChainStore, blkParam string, strict bool) (*types.TipSet, error) {
|
|
if blkParam == "earliest" {
|
|
return nil, fmt.Errorf("block param \"earliest\" is not supported")
|
|
}
|
|
|
|
head := chain.GetHeaviestTipSet()
|
|
switch blkParam {
|
|
case "pending":
|
|
return head, nil
|
|
case "latest":
|
|
parent, err := chain.GetTipSetFromKey(ctx, head.Parents())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot get parent tipset")
|
|
}
|
|
return parent, nil
|
|
default:
|
|
var num ethtypes.EthUint64
|
|
err := num.UnmarshalJSON([]byte(`"` + blkParam + `"`))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot parse block number: %v", err)
|
|
}
|
|
if abi.ChainEpoch(num) > head.Height()-1 {
|
|
return nil, fmt.Errorf("requested a future epoch (beyond 'latest')")
|
|
}
|
|
ts, err := chain.GetTipsetByHeight(ctx, abi.ChainEpoch(num), head, true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot get tipset at height: %v", num)
|
|
}
|
|
if strict && ts.Height() != abi.ChainEpoch(num) {
|
|
return nil, ErrNullRound
|
|
}
|
|
return ts, nil
|
|
}
|
|
}
|
|
|
|
func getTipsetByEthBlockNumberOrHash(ctx context.Context, chain *store.ChainStore, blkParam ethtypes.EthBlockNumberOrHash) (*types.TipSet, error) {
|
|
head := chain.GetHeaviestTipSet()
|
|
|
|
predefined := blkParam.PredefinedBlock
|
|
if predefined != nil {
|
|
if *predefined == "earliest" {
|
|
return nil, fmt.Errorf("block param \"earliest\" is not supported")
|
|
} else if *predefined == "pending" {
|
|
return head, nil
|
|
} else if *predefined == "latest" {
|
|
parent, err := chain.GetTipSetFromKey(ctx, head.Parents())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot get parent tipset")
|
|
}
|
|
return parent, nil
|
|
} else {
|
|
return nil, fmt.Errorf("unknown predefined block %s", *predefined)
|
|
}
|
|
}
|
|
|
|
if blkParam.BlockNumber != nil {
|
|
height := abi.ChainEpoch(*blkParam.BlockNumber)
|
|
if height > head.Height()-1 {
|
|
return nil, fmt.Errorf("requested a future epoch (beyond 'latest')")
|
|
}
|
|
ts, err := chain.GetTipsetByHeight(ctx, height, head, true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot get tipset at height: %v", height)
|
|
}
|
|
return ts, nil
|
|
}
|
|
|
|
if blkParam.BlockHash != nil {
|
|
ts, err := chain.GetTipSetByCid(ctx, blkParam.BlockHash.ToCid())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot get tipset by hash: %v", err)
|
|
}
|
|
|
|
// verify that the tipset is in the canonical chain
|
|
if blkParam.RequireCanonical {
|
|
// walk up the current chain (our head) until we reach ts.Height()
|
|
walkTs, err := chain.GetTipsetByHeight(ctx, ts.Height(), head, true)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot get tipset at height: %v", ts.Height())
|
|
}
|
|
|
|
// verify that it equals the expected tipset
|
|
if !walkTs.Equals(ts) {
|
|
return nil, fmt.Errorf("tipset is not canonical")
|
|
}
|
|
}
|
|
|
|
return ts, nil
|
|
}
|
|
|
|
return nil, errors.New("invalid block param")
|
|
}
|
|
|
|
func ethCallToFilecoinMessage(ctx context.Context, tx ethtypes.EthCall) (*types.Message, error) {
|
|
var from address.Address
|
|
if tx.From == nil || *tx.From == (ethtypes.EthAddress{}) {
|
|
// Send from the filecoin "system" address.
|
|
var err error
|
|
from, err = (ethtypes.EthAddress{}).ToFilecoinAddress()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to construct the ethereum system address: %w", err)
|
|
}
|
|
} else {
|
|
// The from address must be translatable to an f4 address.
|
|
var err error
|
|
from, err = tx.From.ToFilecoinAddress()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to translate sender address (%s): %w", tx.From.String(), err)
|
|
}
|
|
if p := from.Protocol(); p != address.Delegated {
|
|
return nil, fmt.Errorf("expected a class 4 address, got: %d: %w", p, err)
|
|
}
|
|
}
|
|
|
|
var params []byte
|
|
if len(tx.Data) > 0 {
|
|
initcode := abi.CborBytes(tx.Data)
|
|
params2, err := actors.SerializeParams(&initcode)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to serialize params: %w", err)
|
|
}
|
|
params = params2
|
|
}
|
|
|
|
var to address.Address
|
|
var method abi.MethodNum
|
|
if tx.To == nil {
|
|
// this is a contract creation
|
|
to = builtintypes.EthereumAddressManagerActorAddr
|
|
method = builtintypes.MethodsEAM.CreateExternal
|
|
} else {
|
|
addr, err := tx.To.ToFilecoinAddress()
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("cannot get Filecoin address: %w", err)
|
|
}
|
|
to = addr
|
|
method = builtintypes.MethodsEVM.InvokeContract
|
|
}
|
|
|
|
return &types.Message{
|
|
From: from,
|
|
To: to,
|
|
Value: big.Int(tx.Value),
|
|
Method: method,
|
|
Params: params,
|
|
GasLimit: build.BlockGasLimit,
|
|
GasFeeCap: big.Zero(),
|
|
GasPremium: big.Zero(),
|
|
}, nil
|
|
}
|
|
|
|
func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTxInfo bool, cs *store.ChainStore, sa StateAPI) (ethtypes.EthBlock, error) {
|
|
parentKeyCid, err := ts.Parents().Cid()
|
|
if err != nil {
|
|
return ethtypes.EthBlock{}, err
|
|
}
|
|
parentBlkHash, err := ethtypes.EthHashFromCid(parentKeyCid)
|
|
if err != nil {
|
|
return ethtypes.EthBlock{}, err
|
|
}
|
|
|
|
bn := ethtypes.EthUint64(ts.Height())
|
|
|
|
tsk := ts.Key()
|
|
blkCid, err := tsk.Cid()
|
|
if err != nil {
|
|
return ethtypes.EthBlock{}, err
|
|
}
|
|
blkHash, err := ethtypes.EthHashFromCid(blkCid)
|
|
if err != nil {
|
|
return ethtypes.EthBlock{}, err
|
|
}
|
|
|
|
stRoot, msgs, rcpts, err := executeTipset(ctx, ts, cs, sa)
|
|
if err != nil {
|
|
return ethtypes.EthBlock{}, xerrors.Errorf("failed to retrieve messages and receipts: %w", err)
|
|
}
|
|
|
|
st, err := sa.StateManager.StateTree(stRoot)
|
|
if err != nil {
|
|
return ethtypes.EthBlock{}, xerrors.Errorf("failed to load state-tree root %q: %w", stRoot, err)
|
|
}
|
|
|
|
block := ethtypes.NewEthBlock(len(msgs) > 0)
|
|
|
|
gasUsed := int64(0)
|
|
for i, msg := range msgs {
|
|
rcpt := rcpts[i]
|
|
ti := ethtypes.EthUint64(i)
|
|
gasUsed += rcpt.GasUsed
|
|
var smsg *types.SignedMessage
|
|
switch msg := msg.(type) {
|
|
case *types.SignedMessage:
|
|
smsg = msg
|
|
case *types.Message:
|
|
smsg = &types.SignedMessage{
|
|
Message: *msg,
|
|
Signature: crypto.Signature{
|
|
Type: crypto.SigTypeBLS,
|
|
},
|
|
}
|
|
default:
|
|
return ethtypes.EthBlock{}, xerrors.Errorf("failed to get signed msg %s: %w", msg.Cid(), err)
|
|
}
|
|
tx, err := newEthTxFromSignedMessage(smsg, st)
|
|
if err != nil {
|
|
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
|
|
|
|
if fullTxInfo {
|
|
block.Transactions = append(block.Transactions, tx)
|
|
} else {
|
|
block.Transactions = append(block.Transactions, tx.Hash.String())
|
|
}
|
|
}
|
|
|
|
block.Hash = blkHash
|
|
block.Number = bn
|
|
block.ParentHash = parentBlkHash
|
|
block.Timestamp = ethtypes.EthUint64(ts.Blocks()[0].Timestamp)
|
|
block.BaseFeePerGas = ethtypes.EthBigInt{Int: ts.Blocks()[0].ParentBaseFee.Int}
|
|
block.GasUsed = ethtypes.EthUint64(gasUsed)
|
|
return block, nil
|
|
}
|
|
|
|
func executeTipset(ctx context.Context, ts *types.TipSet, cs *store.ChainStore, sa StateAPI) (cid.Cid, []types.ChainMsg, []types.MessageReceipt, error) {
|
|
msgs, err := cs.MessagesForTipset(ctx, ts)
|
|
if err != nil {
|
|
return cid.Undef, nil, nil, xerrors.Errorf("error loading messages for tipset: %v: %w", ts, err)
|
|
}
|
|
|
|
stRoot, rcptRoot, err := sa.StateManager.TipSetState(ctx, ts)
|
|
if err != nil {
|
|
return cid.Undef, nil, nil, xerrors.Errorf("failed to compute state: %w", err)
|
|
}
|
|
|
|
rcpts, err := cs.ReadReceipts(ctx, rcptRoot)
|
|
if err != nil {
|
|
return cid.Undef, nil, nil, xerrors.Errorf("error loading receipts for tipset: %v: %w", ts, err)
|
|
}
|
|
|
|
if len(msgs) != len(rcpts) {
|
|
return cid.Undef, nil, nil, xerrors.Errorf("receipts and message array lengths didn't match for tipset: %v: %w", ts, err)
|
|
}
|
|
|
|
return stRoot, msgs, rcpts, nil
|
|
}
|
|
|
|
const errorFunctionSelector = "\x08\xc3\x79\xa0" // Error(string)
|
|
const panicFunctionSelector = "\x4e\x48\x7b\x71" // Panic(uint256)
|
|
// Eth ABI (solidity) panic codes.
|
|
var panicErrorCodes map[uint64]string = map[uint64]string{
|
|
0x00: "Panic()",
|
|
0x01: "Assert()",
|
|
0x11: "ArithmeticOverflow()",
|
|
0x12: "DivideByZero()",
|
|
0x21: "InvalidEnumVariant()",
|
|
0x22: "InvalidStorageArray()",
|
|
0x31: "PopEmptyArray()",
|
|
0x32: "ArrayIndexOutOfBounds()",
|
|
0x41: "OutOfMemory()",
|
|
0x51: "CalledUninitializedFunction()",
|
|
}
|
|
|
|
// Parse an ABI encoded revert reason. This reason should be encoded as if it were the parameters to
|
|
// an `Error(string)` function call.
|
|
//
|
|
// See https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require
|
|
func parseEthRevert(ret []byte) string {
|
|
if len(ret) == 0 {
|
|
return "none"
|
|
}
|
|
var cbytes abi.CborBytes
|
|
if err := cbytes.UnmarshalCBOR(bytes.NewReader(ret)); err != nil {
|
|
return "ERROR: revert reason is not cbor encoded bytes"
|
|
}
|
|
if len(cbytes) == 0 {
|
|
return "none"
|
|
}
|
|
// If it's not long enough to contain an ABI encoded response, return immediately.
|
|
if len(cbytes) < 4+32 {
|
|
return ethtypes.EthBytes(cbytes).String()
|
|
}
|
|
switch string(cbytes[:4]) {
|
|
case panicFunctionSelector:
|
|
cbytes := cbytes[4 : 4+32]
|
|
// Read the and check the code.
|
|
code, err := ethtypes.EthUint64FromBytes(cbytes)
|
|
if err != nil {
|
|
// If it's too big, just return the raw value.
|
|
codeInt := big.PositiveFromUnsignedBytes(cbytes)
|
|
return fmt.Sprintf("Panic(%s)", ethtypes.EthBigInt(codeInt).String())
|
|
}
|
|
if s, ok := panicErrorCodes[uint64(code)]; ok {
|
|
return s
|
|
}
|
|
return fmt.Sprintf("Panic(0x%x)", code)
|
|
case errorFunctionSelector:
|
|
cbytes := cbytes[4:]
|
|
cbytesLen := ethtypes.EthUint64(len(cbytes))
|
|
// Read the and check the offset.
|
|
offset, err := ethtypes.EthUint64FromBytes(cbytes[:32])
|
|
if err != nil {
|
|
break
|
|
}
|
|
if cbytesLen < offset {
|
|
break
|
|
}
|
|
|
|
// Read and check the length.
|
|
if cbytesLen-offset < 32 {
|
|
break
|
|
}
|
|
start := offset + 32
|
|
length, err := ethtypes.EthUint64FromBytes(cbytes[offset : offset+32])
|
|
if err != nil {
|
|
break
|
|
}
|
|
if cbytesLen-start < length {
|
|
break
|
|
}
|
|
// Slice the error message.
|
|
return fmt.Sprintf("Error(%s)", cbytes[start:start+length])
|
|
}
|
|
return ethtypes.EthBytes(cbytes).String()
|
|
}
|
|
|
|
// lookupEthAddress makes its best effort at finding the Ethereum address for a
|
|
// Filecoin address. It does the following:
|
|
//
|
|
// 1. If the supplied address is an f410 address, we return its payload as the EthAddress.
|
|
// 2. Otherwise (f0, f1, f2, f3), we look up the actor on the state tree. If it has a delegated address, we return it if it's f410 address.
|
|
// 3. Otherwise, we fall back to returning a masked ID Ethereum address. If the supplied address is an f0 address, we
|
|
// use that ID to form the masked ID address.
|
|
// 4. Otherwise, we fetch the actor's ID from the state tree and form the masked ID with it.
|
|
func lookupEthAddress(addr address.Address, st *state.StateTree) (ethtypes.EthAddress, error) {
|
|
// BLOCK A: We are trying to get an actual Ethereum address from an f410 address.
|
|
// Attempt to convert directly, if it's an f4 address.
|
|
ethAddr, err := ethtypes.EthAddressFromFilecoinAddress(addr)
|
|
if err == nil && !ethAddr.IsMaskedID() {
|
|
return ethAddr, nil
|
|
}
|
|
|
|
// Lookup on the target actor and try to get an f410 address.
|
|
if actor, err := st.GetActor(addr); err != nil {
|
|
return ethtypes.EthAddress{}, err
|
|
} else if actor.Address != nil {
|
|
if ethAddr, err := ethtypes.EthAddressFromFilecoinAddress(*actor.Address); err == nil && !ethAddr.IsMaskedID() {
|
|
return ethAddr, nil
|
|
}
|
|
}
|
|
|
|
// BLOCK B: We gave up on getting an actual Ethereum address and are falling back to a Masked ID address.
|
|
// Check if we already have an ID addr, and use it if possible.
|
|
if err == nil && ethAddr.IsMaskedID() {
|
|
return ethAddr, nil
|
|
}
|
|
|
|
// Otherwise, resolve the ID addr.
|
|
idAddr, err := st.LookupID(addr)
|
|
if err != nil {
|
|
return ethtypes.EthAddress{}, err
|
|
}
|
|
return ethtypes.EthAddressFromFilecoinAddress(idAddr)
|
|
}
|
|
|
|
func parseEthTopics(topics ethtypes.EthTopicSpec) (map[string][][]byte, error) {
|
|
keys := map[string][][]byte{}
|
|
for idx, vals := range topics {
|
|
if len(vals) == 0 {
|
|
continue
|
|
}
|
|
// Ethereum topics are emitted using `LOG{0..4}` opcodes resulting in topics1..4
|
|
key := fmt.Sprintf("t%d", idx+1)
|
|
for _, v := range vals {
|
|
v := v // copy the ethhash to avoid repeatedly referencing the same one.
|
|
keys[key] = append(keys[key], v[:])
|
|
}
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
func ethTxHashFromMessageCid(ctx context.Context, c cid.Cid, sa StateAPI) (ethtypes.EthHash, error) {
|
|
smsg, err := sa.Chain.GetSignedMessage(ctx, c)
|
|
if err == nil {
|
|
// This is an Eth Tx, Secp message, Or BLS message in the mpool
|
|
return ethTxHashFromSignedMessage(smsg)
|
|
}
|
|
|
|
_, err = sa.Chain.GetMessage(ctx, c)
|
|
if err == nil {
|
|
// This is a BLS message
|
|
return ethtypes.EthHashFromCid(c)
|
|
}
|
|
|
|
return ethtypes.EmptyEthHash, nil
|
|
}
|
|
|
|
func ethTxHashFromSignedMessage(smsg *types.SignedMessage) (ethtypes.EthHash, error) {
|
|
if smsg.Signature.Type == crypto.SigTypeDelegated {
|
|
tx, err := ethtypes.EthTxFromSignedEthMessage(smsg)
|
|
if err != nil {
|
|
return ethtypes.EthHash{}, xerrors.Errorf("failed to convert from signed message: %w", err)
|
|
}
|
|
|
|
return tx.TxHash()
|
|
} else if smsg.Signature.Type == crypto.SigTypeSecp256k1 {
|
|
return ethtypes.EthHashFromCid(smsg.Cid())
|
|
} else { // BLS message
|
|
return ethtypes.EthHashFromCid(smsg.Message.Cid())
|
|
}
|
|
}
|
|
|
|
func newEthTxFromSignedMessage(smsg *types.SignedMessage, st *state.StateTree) (ethtypes.EthTx, error) {
|
|
var tx ethtypes.EthTx
|
|
var err error
|
|
|
|
// This is an eth tx
|
|
if smsg.Signature.Type == crypto.SigTypeDelegated {
|
|
tx, err = ethtypes.EthTxFromSignedEthMessage(smsg)
|
|
if err != nil {
|
|
return ethtypes.EthTx{}, xerrors.Errorf("failed to convert from signed message: %w", err)
|
|
}
|
|
|
|
tx.Hash, err = tx.TxHash()
|
|
if err != nil {
|
|
return ethtypes.EthTx{}, xerrors.Errorf("failed to calculate hash for ethTx: %w", err)
|
|
}
|
|
} else if smsg.Signature.Type == crypto.SigTypeSecp256k1 { // Secp Filecoin Message
|
|
tx, err = ethTxFromNativeMessage(smsg.VMMessage(), st)
|
|
if err != nil {
|
|
return ethtypes.EthTx{}, err
|
|
}
|
|
tx.Hash, err = ethtypes.EthHashFromCid(smsg.Cid())
|
|
if err != nil {
|
|
return ethtypes.EthTx{}, err
|
|
}
|
|
} else { // BLS Filecoin message
|
|
tx, err = ethTxFromNativeMessage(smsg.VMMessage(), st)
|
|
if err != nil {
|
|
return ethtypes.EthTx{}, err
|
|
}
|
|
tx.Hash, err = ethtypes.EthHashFromCid(smsg.Message.Cid())
|
|
if err != nil {
|
|
return ethtypes.EthTx{}, err
|
|
}
|
|
}
|
|
|
|
return tx, nil
|
|
}
|
|
|
|
// Convert a native message to an eth transaction.
|
|
//
|
|
// - The state-tree must be from after the message was applied (ideally the following tipset).
|
|
// - In some cases, the "to" address may be `0xff0000000000000000000000ffffffffffffffff`. This
|
|
// means that the "to" address has not been assigned in the passed state-tree and can only
|
|
// happen if the transaction reverted.
|
|
//
|
|
// ethTxFromNativeMessage does NOT populate:
|
|
// - BlockHash
|
|
// - BlockNumber
|
|
// - TransactionIndex
|
|
// - Hash
|
|
func ethTxFromNativeMessage(msg *types.Message, st *state.StateTree) (ethtypes.EthTx, error) {
|
|
// Lookup the from address. This must succeed.
|
|
from, err := lookupEthAddress(msg.From, st)
|
|
if err != nil {
|
|
return ethtypes.EthTx{}, xerrors.Errorf("failed to lookup sender address %s when converting a native message to an eth txn: %w", msg.From, err)
|
|
}
|
|
// Lookup the to address. If the recipient doesn't exist, we replace the address with a
|
|
// known sentinel address.
|
|
to, err := lookupEthAddress(msg.To, st)
|
|
if err != nil {
|
|
if !errors.Is(err, types.ErrActorNotFound) {
|
|
return ethtypes.EthTx{}, xerrors.Errorf("failed to lookup receiver address %s when converting a native message to an eth txn: %w", msg.To, err)
|
|
}
|
|
to = revertedEthAddress
|
|
}
|
|
toPtr := &to
|
|
|
|
// Finally, convert the input parameters to "solidity ABI".
|
|
|
|
// For empty, we use "0" as the codec. Otherwise, we use CBOR for message
|
|
// parameters.
|
|
var codec uint64
|
|
if len(msg.Params) > 0 {
|
|
codec = uint64(multicodec.Cbor)
|
|
}
|
|
|
|
// We try to decode the input as an EVM method invocation and/or a contract creation. If
|
|
// that fails, we encode the "native" parameters as Solidity ABI.
|
|
var input []byte
|
|
switch msg.Method {
|
|
case builtintypes.MethodsEVM.InvokeContract, builtintypes.MethodsEAM.CreateExternal:
|
|
inp, err := decodePayload(msg.Params, codec)
|
|
if err == nil {
|
|
// If this is a valid "create external", unset the "to" address.
|
|
if msg.Method == builtintypes.MethodsEAM.CreateExternal {
|
|
toPtr = nil
|
|
}
|
|
input = []byte(inp)
|
|
break
|
|
}
|
|
// Yeah, we're going to ignore errors here because the user can send whatever they
|
|
// want and may send garbage.
|
|
fallthrough
|
|
default:
|
|
input = encodeFilecoinParamsAsABI(msg.Method, codec, msg.Params)
|
|
}
|
|
|
|
return ethtypes.EthTx{
|
|
To: toPtr,
|
|
From: from,
|
|
Input: input,
|
|
Nonce: ethtypes.EthUint64(msg.Nonce),
|
|
ChainID: ethtypes.EthUint64(build.Eip155ChainId),
|
|
Value: ethtypes.EthBigInt(msg.Value),
|
|
Type: ethtypes.Eip1559TxType,
|
|
Gas: ethtypes.EthUint64(msg.GasLimit),
|
|
MaxFeePerGas: ethtypes.EthBigInt(msg.GasFeeCap),
|
|
MaxPriorityFeePerGas: ethtypes.EthBigInt(msg.GasPremium),
|
|
AccessList: []ethtypes.EthHash{},
|
|
}, nil
|
|
}
|
|
|
|
func getSignedMessage(ctx context.Context, cs *store.ChainStore, msgCid cid.Cid) (*types.SignedMessage, error) {
|
|
smsg, err := cs.GetSignedMessage(ctx, msgCid)
|
|
if err != nil {
|
|
// We couldn't find the signed message, it might be a BLS message, so search for a regular message.
|
|
msg, err := cs.GetMessage(ctx, msgCid)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("failed to find msg %s: %w", msgCid, err)
|
|
}
|
|
smsg = &types.SignedMessage{
|
|
Message: *msg,
|
|
Signature: crypto.Signature{
|
|
Type: crypto.SigTypeBLS,
|
|
},
|
|
}
|
|
}
|
|
|
|
return smsg, nil
|
|
}
|
|
|
|
// newEthTxFromMessageLookup creates an ethereum transaction from filecoin message lookup. If a negative txIdx is passed
|
|
// into the function, it looks up the transaction index of the message in the tipset, otherwise it uses the txIdx passed into the
|
|
// function
|
|
func newEthTxFromMessageLookup(ctx context.Context, msgLookup *api.MsgLookup, txIdx int, cs *store.ChainStore, sa StateAPI) (ethtypes.EthTx, error) {
|
|
ts, err := cs.LoadTipSet(ctx, msgLookup.TipSet)
|
|
if err != nil {
|
|
return ethtypes.EthTx{}, err
|
|
}
|
|
|
|
// This tx is located in the parent tipset
|
|
parentTs, err := cs.LoadTipSet(ctx, ts.Parents())
|
|
if err != nil {
|
|
return ethtypes.EthTx{}, err
|
|
}
|
|
|
|
parentTsCid, err := parentTs.Key().Cid()
|
|
if err != nil {
|
|
return ethtypes.EthTx{}, err
|
|
}
|
|
|
|
// lookup the transactionIndex
|
|
if txIdx < 0 {
|
|
msgs, err := cs.MessagesForTipset(ctx, parentTs)
|
|
if err != nil {
|
|
return ethtypes.EthTx{}, err
|
|
}
|
|
for i, msg := range msgs {
|
|
if msg.Cid() == msgLookup.Message {
|
|
txIdx = i
|
|
break
|
|
}
|
|
}
|
|
if txIdx < 0 {
|
|
return ethtypes.EthTx{}, fmt.Errorf("cannot find the msg in the tipset")
|
|
}
|
|
}
|
|
|
|
blkHash, err := ethtypes.EthHashFromCid(parentTsCid)
|
|
if err != nil {
|
|
return ethtypes.EthTx{}, err
|
|
}
|
|
|
|
smsg, err := getSignedMessage(ctx, cs, msgLookup.Message)
|
|
if err != nil {
|
|
return ethtypes.EthTx{}, xerrors.Errorf("failed to get signed msg: %w", err)
|
|
}
|
|
|
|
st, err := sa.StateManager.StateTree(ts.ParentState())
|
|
if err != nil {
|
|
return ethtypes.EthTx{}, xerrors.Errorf("failed to load message state tree: %w", err)
|
|
}
|
|
|
|
tx, err := newEthTxFromSignedMessage(smsg, st)
|
|
if err != nil {
|
|
return ethtypes.EthTx{}, err
|
|
}
|
|
|
|
var (
|
|
bn = ethtypes.EthUint64(parentTs.Height())
|
|
ti = ethtypes.EthUint64(txIdx)
|
|
)
|
|
|
|
tx.BlockHash = &blkHash
|
|
tx.BlockNumber = &bn
|
|
tx.TransactionIndex = &ti
|
|
return tx, nil
|
|
}
|
|
|
|
func newEthTxReceipt(ctx context.Context, tx ethtypes.EthTx, lookup *api.MsgLookup, events []types.Event, cs *store.ChainStore, sa StateAPI) (api.EthTxReceipt, error) {
|
|
var (
|
|
transactionIndex ethtypes.EthUint64
|
|
blockHash ethtypes.EthHash
|
|
blockNumber ethtypes.EthUint64
|
|
)
|
|
|
|
if tx.TransactionIndex != nil {
|
|
transactionIndex = *tx.TransactionIndex
|
|
}
|
|
if tx.BlockHash != nil {
|
|
blockHash = *tx.BlockHash
|
|
}
|
|
if tx.BlockNumber != nil {
|
|
blockNumber = *tx.BlockNumber
|
|
}
|
|
|
|
receipt := api.EthTxReceipt{
|
|
TransactionHash: tx.Hash,
|
|
From: tx.From,
|
|
To: tx.To,
|
|
TransactionIndex: transactionIndex,
|
|
BlockHash: blockHash,
|
|
BlockNumber: blockNumber,
|
|
Type: ethtypes.EthUint64(2),
|
|
Logs: []ethtypes.EthLog{}, // empty log array is compulsory when no logs, or libraries like ethers.js break
|
|
LogsBloom: ethtypes.EmptyEthBloom[:],
|
|
}
|
|
|
|
if lookup.Receipt.ExitCode.IsSuccess() {
|
|
receipt.Status = 1
|
|
} else {
|
|
receipt.Status = 0
|
|
}
|
|
|
|
receipt.GasUsed = ethtypes.EthUint64(lookup.Receipt.GasUsed)
|
|
|
|
// TODO: handle CumulativeGasUsed
|
|
receipt.CumulativeGasUsed = ethtypes.EmptyEthInt
|
|
|
|
// TODO: avoid loading the tipset twice (once here, once when we convert the message to a txn)
|
|
ts, err := cs.GetTipSetFromKey(ctx, lookup.TipSet)
|
|
if err != nil {
|
|
return api.EthTxReceipt{}, xerrors.Errorf("failed to lookup tipset %s when constructing the eth txn receipt: %w", lookup.TipSet, err)
|
|
}
|
|
|
|
st, err := sa.StateManager.StateTree(ts.ParentState())
|
|
if err != nil {
|
|
return api.EthTxReceipt{}, xerrors.Errorf("failed to load the state %s when constructing the eth txn receipt: %w", ts.ParentState(), err)
|
|
}
|
|
|
|
// The tx is located in the parent tipset
|
|
parentTs, err := cs.LoadTipSet(ctx, ts.Parents())
|
|
if err != nil {
|
|
return api.EthTxReceipt{}, xerrors.Errorf("failed to lookup tipset %s when constructing the eth txn receipt: %w", ts.Parents(), err)
|
|
}
|
|
|
|
baseFee := parentTs.Blocks()[0].ParentBaseFee
|
|
gasOutputs := vm.ComputeGasOutputs(lookup.Receipt.GasUsed, int64(tx.Gas), baseFee, big.Int(tx.MaxFeePerGas), big.Int(tx.MaxPriorityFeePerGas), true)
|
|
totalSpent := big.Sum(gasOutputs.BaseFeeBurn, gasOutputs.MinerTip, gasOutputs.OverEstimationBurn)
|
|
|
|
effectiveGasPrice := big.Zero()
|
|
if lookup.Receipt.GasUsed > 0 {
|
|
effectiveGasPrice = big.Div(totalSpent, big.NewInt(lookup.Receipt.GasUsed))
|
|
}
|
|
receipt.EffectiveGasPrice = ethtypes.EthBigInt(effectiveGasPrice)
|
|
|
|
if receipt.To == nil && lookup.Receipt.ExitCode.IsSuccess() {
|
|
// Create and Create2 return the same things.
|
|
var ret eam.CreateExternalReturn
|
|
if err := ret.UnmarshalCBOR(bytes.NewReader(lookup.Receipt.Return)); err != nil {
|
|
return api.EthTxReceipt{}, xerrors.Errorf("failed to parse contract creation result: %w", err)
|
|
}
|
|
addr := ethtypes.EthAddress(ret.EthAddress)
|
|
receipt.ContractAddress = &addr
|
|
}
|
|
|
|
if len(events) > 0 {
|
|
receipt.Logs = make([]ethtypes.EthLog, 0, len(events))
|
|
for i, evt := range events {
|
|
l := ethtypes.EthLog{
|
|
Removed: false,
|
|
LogIndex: ethtypes.EthUint64(i),
|
|
TransactionHash: tx.Hash,
|
|
TransactionIndex: transactionIndex,
|
|
BlockHash: blockHash,
|
|
BlockNumber: blockNumber,
|
|
}
|
|
|
|
data, topics, ok := ethLogFromEvent(evt.Entries)
|
|
if !ok {
|
|
// not an eth event.
|
|
continue
|
|
}
|
|
for _, topic := range topics {
|
|
log.Debug("LogsBloom set for ", topic)
|
|
ethtypes.EthBloomSet(receipt.LogsBloom, topic[:])
|
|
}
|
|
l.Data = data
|
|
l.Topics = topics
|
|
|
|
addr, err := address.NewIDAddress(uint64(evt.Emitter))
|
|
if err != nil {
|
|
return api.EthTxReceipt{}, xerrors.Errorf("failed to create ID address: %w", err)
|
|
}
|
|
|
|
l.Address, err = lookupEthAddress(addr, st)
|
|
if err != nil {
|
|
return api.EthTxReceipt{}, xerrors.Errorf("failed to resolve Ethereum address: %w", err)
|
|
}
|
|
|
|
ethtypes.EthBloomSet(receipt.LogsBloom, l.Address[:])
|
|
receipt.Logs = append(receipt.Logs, l)
|
|
}
|
|
}
|
|
|
|
return receipt, nil
|
|
}
|
|
|
|
func encodeFilecoinParamsAsABI(method abi.MethodNum, codec uint64, params []byte) []byte {
|
|
buf := []byte{0x86, 0x8e, 0x10, 0xc4} // Native method selector.
|
|
return append(buf, encodeAsABIHelper(uint64(method), codec, params)...)
|
|
}
|
|
|
|
func encodeFilecoinReturnAsABI(exitCode exitcode.ExitCode, codec uint64, data []byte) []byte {
|
|
return encodeAsABIHelper(uint64(exitCode), codec, data)
|
|
}
|
|
|
|
// Format 2 numbers followed by an arbitrary byte array as solidity ABI. Both our native
|
|
// inputs/outputs follow the same pattern, so we can reuse this code.
|
|
func encodeAsABIHelper(param1 uint64, param2 uint64, data []byte) []byte {
|
|
const EVM_WORD_SIZE = 32
|
|
|
|
// The first two params are "static" numbers. Then, we record the offset of the "data" arg,
|
|
// then, at that offset, we record the length of the data.
|
|
//
|
|
// In practice, this means we have 4 256-bit words back to back where the third arg (the
|
|
// offset) is _always_ '32*3'.
|
|
staticArgs := []uint64{param1, param2, EVM_WORD_SIZE * 3, uint64(len(data))}
|
|
// We always pad out to the next EVM "word" (32 bytes).
|
|
totalWords := len(staticArgs) + (len(data) / EVM_WORD_SIZE)
|
|
if len(data)%EVM_WORD_SIZE != 0 {
|
|
totalWords++
|
|
}
|
|
len := totalWords * EVM_WORD_SIZE
|
|
buf := make([]byte, len)
|
|
offset := 0
|
|
// Below, we use copy instead of "appending" to preserve all the zero padding.
|
|
for _, arg := range staticArgs {
|
|
// Write each "arg" into the last 8 bytes of each 32 byte word.
|
|
offset += EVM_WORD_SIZE
|
|
start := offset - 8
|
|
binary.BigEndian.PutUint64(buf[start:offset], arg)
|
|
}
|
|
|
|
// Finally, we copy in the data.
|
|
copy(buf[offset:], data)
|
|
|
|
return buf
|
|
}
|