efb9422c01
This will re-execute tipsets to forcibly re-compute and store events when they're missing. This is effectively lazy backfilling of events. NOTE: This _won't_ backfill the index itself, it'll just give us the events. fixes #11335
835 lines
26 KiB
Go
835 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/actors/builtin"
|
|
"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
|
|
}
|
|
|
|
// 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 decode as a native call first.
|
|
ethTx := ethtypes.EthTx{
|
|
To: &to,
|
|
From: from,
|
|
Input: encodeFilecoinParamsAsABI(msg.Method, codec, msg.Params),
|
|
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{},
|
|
}
|
|
|
|
// Then we try to see if it's "special". If we fail, we ignore the error and keep treating
|
|
// it as a native message. Unfortunately, the user is free to send garbage that may not
|
|
// properly decode.
|
|
if msg.Method == builtintypes.MethodsEVM.InvokeContract {
|
|
// try to decode it as a contract invocation first.
|
|
if inp, err := decodePayload(msg.Params, codec); err == nil {
|
|
ethTx.Input = []byte(inp)
|
|
}
|
|
} else if msg.To == builtin.EthereumAddressManagerActorAddr && msg.Method == builtintypes.MethodsEAM.CreateExternal {
|
|
// Then, try to decode it as a contract deployment from an EOA.
|
|
if inp, err := decodePayload(msg.Params, codec); err == nil {
|
|
ethTx.Input = []byte(inp)
|
|
ethTx.To = nil
|
|
}
|
|
}
|
|
|
|
return ethTx, 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, ca ChainAPI, 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 := ca.Chain.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 := ca.Chain.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
|
|
}
|
|
|
|
var events []types.Event
|
|
if rct := lookup.Receipt; rct.EventsRoot != nil {
|
|
events, err = ca.ChainGetEvents(ctx, *rct.EventsRoot)
|
|
if err != nil {
|
|
// Fore-recompute, we must have enabled the Event APIs after computing this
|
|
// tipset.
|
|
if _, _, err := sa.StateManager.RecomputeTipSetState(ctx, ts); err != nil {
|
|
|
|
return api.EthTxReceipt{}, xerrors.Errorf("failed get events: %w", err)
|
|
}
|
|
// Try again
|
|
events, err = ca.ChainGetEvents(ctx, *rct.EventsRoot)
|
|
if err != nil {
|
|
return api.EthTxReceipt{}, xerrors.Errorf("failed get events: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|