9b4df6a4d0
We need to always use the state-tree from the tipset _after_ the message executed. If we use any other state-tree, we might not find the address we're trying to resolve. This change also has some implication for pending messages: there's no guarantee we'll be able to generate a 0x-style address for a pending native message. So, instead of trying, I've removed support for pending native messages from the Eth API. Messages from EthAccounts will still work, and native messages will still show up in blocks/traces, they just won't show up as "pending". Which should affect exactly nobody. I'm also taking this opportunity to cleanup some edge-cases: 1. Pass contexts where appropriate. 2. Remove all state access from `ethTxHashFromSignedMessage`. Part of #11355
751 lines
23 KiB
Go
751 lines
23 KiB
Go
package full
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/ipfs/go-cid"
|
|
"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"
|
|
)
|
|
|
|
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 = ethTxFromNativeMessage(smsg.VMMessage(), st)
|
|
tx.Hash, err = ethtypes.EthHashFromCid(smsg.Cid())
|
|
if err != nil {
|
|
return tx, err
|
|
}
|
|
} else { // BLS Filecoin message
|
|
tx = ethTxFromNativeMessage(smsg.VMMessage(), st)
|
|
tx.Hash, err = ethtypes.EthHashFromCid(smsg.Message.Cid())
|
|
if err != nil {
|
|
return tx, err
|
|
}
|
|
}
|
|
|
|
return tx, nil
|
|
}
|
|
|
|
// ethTxFromNativeMessage does NOT populate:
|
|
// - BlockHash
|
|
// - BlockNumber
|
|
// - TransactionIndex
|
|
// - Hash
|
|
func ethTxFromNativeMessage(msg *types.Message, st *state.StateTree) ethtypes.EthTx {
|
|
// We don't care if we error here, conversion is best effort for non-eth transactions
|
|
from, _ := lookupEthAddress(msg.From, st)
|
|
to, _ := lookupEthAddress(msg.To, st)
|
|
return ethtypes.EthTx{
|
|
To: &to,
|
|
From: from,
|
|
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{},
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|