1594 lines
47 KiB
Go
1594 lines
47 KiB
Go
package full
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/ipfs/go-cid"
|
|
cbg "github.com/whyrusleeping/cbor-gen"
|
|
"go.uber.org/fx"
|
|
"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/builtin/v10/evm"
|
|
|
|
"github.com/filecoin-project/lotus/api"
|
|
"github.com/filecoin-project/lotus/build"
|
|
"github.com/filecoin-project/lotus/chain/actors"
|
|
builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin"
|
|
"github.com/filecoin-project/lotus/chain/events/filter"
|
|
"github.com/filecoin-project/lotus/chain/messagepool"
|
|
"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/node/modules/dtypes"
|
|
)
|
|
|
|
type EthModuleAPI interface {
|
|
EthBlockNumber(ctx context.Context) (api.EthUint64, error)
|
|
EthAccounts(ctx context.Context) ([]api.EthAddress, error)
|
|
EthGetBlockTransactionCountByNumber(ctx context.Context, blkNum api.EthUint64) (api.EthUint64, error)
|
|
EthGetBlockTransactionCountByHash(ctx context.Context, blkHash api.EthHash) (api.EthUint64, error)
|
|
EthGetBlockByHash(ctx context.Context, blkHash api.EthHash, fullTxInfo bool) (api.EthBlock, error)
|
|
EthGetBlockByNumber(ctx context.Context, blkNum string, fullTxInfo bool) (api.EthBlock, error)
|
|
EthGetTransactionByHash(ctx context.Context, txHash *api.EthHash) (*api.EthTx, error)
|
|
EthGetTransactionCount(ctx context.Context, sender api.EthAddress, blkOpt string) (api.EthUint64, error)
|
|
EthGetTransactionReceipt(ctx context.Context, txHash api.EthHash) (*api.EthTxReceipt, error)
|
|
EthGetTransactionByBlockHashAndIndex(ctx context.Context, blkHash api.EthHash, txIndex api.EthUint64) (api.EthTx, error)
|
|
EthGetTransactionByBlockNumberAndIndex(ctx context.Context, blkNum api.EthUint64, txIndex api.EthUint64) (api.EthTx, error)
|
|
EthGetCode(ctx context.Context, address api.EthAddress, blkOpt string) (api.EthBytes, error)
|
|
EthGetStorageAt(ctx context.Context, address api.EthAddress, position api.EthBytes, blkParam string) (api.EthBytes, error)
|
|
EthGetBalance(ctx context.Context, address api.EthAddress, blkParam string) (api.EthBigInt, error)
|
|
EthFeeHistory(ctx context.Context, blkCount api.EthUint64, newestBlk string, rewardPercentiles []float64) (api.EthFeeHistory, error)
|
|
EthChainId(ctx context.Context) (api.EthUint64, error)
|
|
NetVersion(ctx context.Context) (string, error)
|
|
NetListening(ctx context.Context) (bool, error)
|
|
EthProtocolVersion(ctx context.Context) (api.EthUint64, error)
|
|
EthGasPrice(ctx context.Context) (api.EthBigInt, error)
|
|
EthEstimateGas(ctx context.Context, tx api.EthCall) (api.EthUint64, error)
|
|
EthCall(ctx context.Context, tx api.EthCall, blkParam string) (api.EthBytes, error)
|
|
EthMaxPriorityFeePerGas(ctx context.Context) (api.EthBigInt, error)
|
|
EthSendRawTransaction(ctx context.Context, rawTx api.EthBytes) (api.EthHash, error)
|
|
}
|
|
|
|
type EthEventAPI interface {
|
|
EthGetLogs(ctx context.Context, filter *api.EthFilterSpec) (*api.EthFilterResult, error)
|
|
EthGetFilterChanges(ctx context.Context, id api.EthFilterID) (*api.EthFilterResult, error)
|
|
EthGetFilterLogs(ctx context.Context, id api.EthFilterID) (*api.EthFilterResult, error)
|
|
EthNewFilter(ctx context.Context, filter *api.EthFilterSpec) (api.EthFilterID, error)
|
|
EthNewBlockFilter(ctx context.Context) (api.EthFilterID, error)
|
|
EthNewPendingTransactionFilter(ctx context.Context) (api.EthFilterID, error)
|
|
EthUninstallFilter(ctx context.Context, id api.EthFilterID) (bool, error)
|
|
EthSubscribe(ctx context.Context, eventType string, params *api.EthSubscriptionParams) (<-chan api.EthSubscriptionResponse, error)
|
|
EthUnsubscribe(ctx context.Context, id api.EthSubscriptionID) (bool, error)
|
|
}
|
|
|
|
var (
|
|
_ EthModuleAPI = *new(api.FullNode)
|
|
_ EthEventAPI = *new(api.FullNode)
|
|
)
|
|
|
|
// EthModule provides a default implementation of EthModuleAPI.
|
|
// It can be swapped out with another implementation through Dependency
|
|
// Injection (for example with a thin RPC client).
|
|
type EthModule struct {
|
|
fx.In
|
|
|
|
Chain *store.ChainStore
|
|
Mpool *messagepool.MessagePool
|
|
StateManager *stmgr.StateManager
|
|
|
|
ChainAPI
|
|
MpoolAPI
|
|
StateAPI
|
|
}
|
|
|
|
var _ EthModuleAPI = (*EthModule)(nil)
|
|
|
|
type EthEvent struct {
|
|
Chain *store.ChainStore
|
|
EventFilterManager *filter.EventFilterManager
|
|
TipSetFilterManager *filter.TipSetFilterManager
|
|
MemPoolFilterManager *filter.MemPoolFilterManager
|
|
FilterStore filter.FilterStore
|
|
SubManager *EthSubscriptionManager
|
|
MaxFilterHeightRange abi.ChainEpoch
|
|
}
|
|
|
|
var _ EthEventAPI = (*EthEvent)(nil)
|
|
|
|
type EthAPI struct {
|
|
fx.In
|
|
|
|
Chain *store.ChainStore
|
|
|
|
EthModuleAPI
|
|
EthEventAPI
|
|
}
|
|
|
|
func (a *EthModule) StateNetworkName(ctx context.Context) (dtypes.NetworkName, error) {
|
|
return stmgr.GetNetworkName(ctx, a.StateManager, a.Chain.GetHeaviestTipSet().ParentState())
|
|
}
|
|
|
|
func (a *EthModule) EthBlockNumber(context.Context) (api.EthUint64, error) {
|
|
height := a.Chain.GetHeaviestTipSet().Height()
|
|
return api.EthUint64(height), nil
|
|
}
|
|
|
|
func (a *EthModule) EthAccounts(context.Context) ([]api.EthAddress, error) {
|
|
// The lotus node is not expected to hold manage accounts, so we'll always return an empty array
|
|
return []api.EthAddress{}, nil
|
|
}
|
|
|
|
func (a *EthModule) countTipsetMsgs(ctx context.Context, ts *types.TipSet) (int, error) {
|
|
blkMsgs, err := a.Chain.BlockMsgsForTipset(ctx, ts)
|
|
if err != nil {
|
|
return 0, xerrors.Errorf("error loading messages for tipset: %v: %w", ts, err)
|
|
}
|
|
|
|
count := 0
|
|
for _, blkMsg := range blkMsgs {
|
|
// TODO: may need to run canonical ordering and deduplication here
|
|
count += len(blkMsg.BlsMessages) + len(blkMsg.SecpkMessages)
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
func (a *EthModule) EthGetBlockTransactionCountByNumber(ctx context.Context, blkNum api.EthUint64) (api.EthUint64, error) {
|
|
ts, err := a.Chain.GetTipsetByHeight(ctx, abi.ChainEpoch(blkNum), nil, false)
|
|
if err != nil {
|
|
return api.EthUint64(0), xerrors.Errorf("error loading tipset %s: %w", ts, err)
|
|
}
|
|
|
|
count, err := a.countTipsetMsgs(ctx, ts)
|
|
return api.EthUint64(count), err
|
|
}
|
|
|
|
func (a *EthModule) EthGetBlockTransactionCountByHash(ctx context.Context, blkHash api.EthHash) (api.EthUint64, error) {
|
|
ts, err := a.Chain.GetTipSetByCid(ctx, blkHash.ToCid())
|
|
if err != nil {
|
|
return api.EthUint64(0), xerrors.Errorf("error loading tipset %s: %w", ts, err)
|
|
}
|
|
count, err := a.countTipsetMsgs(ctx, ts)
|
|
return api.EthUint64(count), err
|
|
}
|
|
|
|
func (a *EthModule) EthGetBlockByHash(ctx context.Context, blkHash api.EthHash, fullTxInfo bool) (api.EthBlock, error) {
|
|
ts, err := a.Chain.GetTipSetByCid(ctx, blkHash.ToCid())
|
|
if err != nil {
|
|
return api.EthBlock{}, xerrors.Errorf("error loading tipset %s: %w", ts, err)
|
|
}
|
|
return newEthBlockFromFilecoinTipSet(ctx, ts, fullTxInfo, a.Chain, a.ChainAPI, a.StateAPI)
|
|
}
|
|
|
|
func (a *EthModule) EthGetBlockByNumber(ctx context.Context, blkNum string, fullTxInfo bool) (api.EthBlock, error) {
|
|
typ, num, err := api.ParseBlkNumOption(blkNum)
|
|
if err != nil {
|
|
return api.EthBlock{}, fmt.Errorf("cannot parse block number: %v", err)
|
|
}
|
|
|
|
switch typ {
|
|
case api.BlkNumLatest:
|
|
num = api.EthUint64(a.Chain.GetHeaviestTipSet().Height()) - 1
|
|
case api.BlkNumPending:
|
|
num = api.EthUint64(a.Chain.GetHeaviestTipSet().Height())
|
|
}
|
|
|
|
ts, err := a.Chain.GetTipsetByHeight(ctx, abi.ChainEpoch(num), nil, false)
|
|
if err != nil {
|
|
return api.EthBlock{}, xerrors.Errorf("error loading tipset %s: %w", ts, err)
|
|
}
|
|
return newEthBlockFromFilecoinTipSet(ctx, ts, fullTxInfo, a.Chain, a.ChainAPI, a.StateAPI)
|
|
}
|
|
|
|
func (a *EthModule) EthGetTransactionByHash(ctx context.Context, txHash *api.EthHash) (*api.EthTx, error) {
|
|
// Ethereum's behavior is to return null when the txHash is invalid, so we use nil to check if txHash is valid
|
|
if txHash == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
cid := txHash.ToCid()
|
|
|
|
msgLookup, err := a.StateAPI.StateSearchMsg(ctx, types.EmptyTSK, cid, api.LookbackNoLimit, true)
|
|
if err != nil {
|
|
return nil, nil
|
|
}
|
|
|
|
tx, err := newEthTxFromFilecoinMessageLookup(ctx, msgLookup, a.Chain, a.ChainAPI, a.StateAPI)
|
|
if err != nil {
|
|
return nil, nil
|
|
}
|
|
return &tx, nil
|
|
}
|
|
|
|
func (a *EthModule) EthGetTransactionCount(ctx context.Context, sender api.EthAddress, blkParam string) (api.EthUint64, error) {
|
|
addr, err := sender.ToFilecoinAddress()
|
|
if err != nil {
|
|
return api.EthUint64(0), nil
|
|
}
|
|
nonce, err := a.Mpool.GetNonce(ctx, addr, types.EmptyTSK)
|
|
if err != nil {
|
|
return api.EthUint64(0), nil
|
|
}
|
|
return api.EthUint64(nonce), nil
|
|
}
|
|
|
|
func (a *EthModule) EthGetTransactionReceipt(ctx context.Context, txHash api.EthHash) (*api.EthTxReceipt, error) {
|
|
cid := txHash.ToCid()
|
|
|
|
msgLookup, err := a.StateAPI.StateSearchMsg(ctx, types.EmptyTSK, cid, api.LookbackNoLimit, true)
|
|
if err != nil {
|
|
return nil, nil
|
|
}
|
|
|
|
tx, err := newEthTxFromFilecoinMessageLookup(ctx, msgLookup, a.Chain, a.ChainAPI, a.StateAPI)
|
|
if err != nil {
|
|
return nil, nil
|
|
}
|
|
|
|
replay, err := a.StateAPI.StateReplay(ctx, types.EmptyTSK, cid)
|
|
if err != nil {
|
|
return nil, nil
|
|
}
|
|
|
|
var events []types.Event
|
|
if rct := replay.MsgRct; rct != nil && rct.EventsRoot != nil {
|
|
events, err = a.ChainAPI.ChainGetEvents(ctx, *rct.EventsRoot)
|
|
if err != nil {
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
receipt, err := newEthTxReceipt(ctx, tx, msgLookup, replay, events, a.StateAPI)
|
|
if err != nil {
|
|
return nil, nil
|
|
}
|
|
|
|
return &receipt, nil
|
|
}
|
|
|
|
func (a *EthModule) EthGetTransactionByBlockHashAndIndex(ctx context.Context, blkHash api.EthHash, txIndex api.EthUint64) (api.EthTx, error) {
|
|
return api.EthTx{}, nil
|
|
}
|
|
|
|
func (a *EthModule) EthGetTransactionByBlockNumberAndIndex(ctx context.Context, blkNum api.EthUint64, txIndex api.EthUint64) (api.EthTx, error) {
|
|
return api.EthTx{}, nil
|
|
}
|
|
|
|
// EthGetCode returns string value of the compiled bytecode
|
|
func (a *EthModule) EthGetCode(ctx context.Context, ethAddr api.EthAddress, blkOpt string) (api.EthBytes, error) {
|
|
to, err := ethAddr.ToFilecoinAddress()
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("cannot get Filecoin address: %w", err)
|
|
}
|
|
|
|
// use the system actor as the caller
|
|
from, err := address.NewIDAddress(0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to construct system sender address: %w", err)
|
|
}
|
|
msg := &types.Message{
|
|
From: from,
|
|
To: to,
|
|
Value: big.Zero(),
|
|
Method: builtintypes.MethodsEVM.GetBytecode,
|
|
Params: nil,
|
|
GasLimit: build.BlockGasLimit,
|
|
GasFeeCap: big.Zero(),
|
|
GasPremium: big.Zero(),
|
|
}
|
|
|
|
ts := a.Chain.GetHeaviestTipSet()
|
|
|
|
// Try calling until we find a height with no migration.
|
|
var res *api.InvocResult
|
|
for {
|
|
res, err = a.StateManager.Call(ctx, msg, ts)
|
|
if err != stmgr.ErrExpensiveFork {
|
|
break
|
|
}
|
|
ts, err = a.Chain.GetTipSetFromKey(ctx, ts.Parents())
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("getting parent tipset: %w", err)
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
// if the call resulted in error, this is not an EVM smart contract;
|
|
// return no bytecode.
|
|
return nil, nil
|
|
}
|
|
|
|
if res.MsgRct == nil {
|
|
return nil, fmt.Errorf("no message receipt")
|
|
}
|
|
|
|
if res.MsgRct.ExitCode.IsError() {
|
|
return nil, xerrors.Errorf("message execution failed: exit %s, reason: %s", res.MsgRct.ExitCode, res.Error)
|
|
}
|
|
|
|
var bytecodeCid cbg.CborCid
|
|
if err := bytecodeCid.UnmarshalCBOR(bytes.NewReader(res.MsgRct.Return)); err != nil {
|
|
return nil, fmt.Errorf("failed to decode EVM bytecode CID: %w", err)
|
|
}
|
|
|
|
blk, err := a.Chain.StateBlockstore().Get(ctx, cid.Cid(bytecodeCid))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get EVM bytecode: %w", err)
|
|
}
|
|
|
|
return blk.RawData(), nil
|
|
}
|
|
|
|
func (a *EthModule) EthGetStorageAt(ctx context.Context, ethAddr api.EthAddress, position api.EthBytes, blkParam string) (api.EthBytes, error) {
|
|
l := len(position)
|
|
if l > 32 {
|
|
return nil, fmt.Errorf("supplied storage key is too long")
|
|
}
|
|
|
|
// pad with zero bytes if smaller than 32 bytes
|
|
position = append(make([]byte, 32-l, 32-l), position...)
|
|
|
|
to, err := ethAddr.ToFilecoinAddress()
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("cannot get Filecoin address: %w", err)
|
|
}
|
|
|
|
// use the system actor as the caller
|
|
from, err := address.NewIDAddress(0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to construct system sender address: %w", err)
|
|
}
|
|
|
|
// TODO super duper hack (raulk). The EVM runtime actor uses the U256 parameter type in
|
|
// GetStorageAtParams, which serializes as a hex-encoded string. It should serialize
|
|
// as bytes. We didn't get to fix in time for Iron, so for now we just pass
|
|
// through the hex-encoded value passed through the Eth JSON-RPC API, by remarshalling it.
|
|
// We don't fix this at origin (builtin-actors) because we are not updating the bundle
|
|
// for Iron.
|
|
tmp, err := position.MarshalJSON()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
params, err := actors.SerializeParams(&evm.GetStorageAtParams{
|
|
StorageKey: tmp[1 : len(tmp)-1], // TODO strip the JSON-encoding quotes -- yuck
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to serialize parameters: %w", err)
|
|
}
|
|
|
|
msg := &types.Message{
|
|
From: from,
|
|
To: to,
|
|
Value: big.Zero(),
|
|
Method: builtintypes.MethodsEVM.GetStorageAt,
|
|
Params: params,
|
|
GasLimit: build.BlockGasLimit,
|
|
GasFeeCap: big.Zero(),
|
|
GasPremium: big.Zero(),
|
|
}
|
|
|
|
ts := a.Chain.GetHeaviestTipSet()
|
|
|
|
// Try calling until we find a height with no migration.
|
|
var res *api.InvocResult
|
|
for {
|
|
res, err = a.StateManager.Call(ctx, msg, ts)
|
|
if err != stmgr.ErrExpensiveFork {
|
|
break
|
|
}
|
|
ts, err = a.Chain.GetTipSetFromKey(ctx, ts.Parents())
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("getting parent tipset: %w", err)
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("Call failed: %w", err)
|
|
}
|
|
|
|
if res.MsgRct == nil {
|
|
return nil, fmt.Errorf("no message receipt")
|
|
}
|
|
|
|
return res.MsgRct.Return, nil
|
|
}
|
|
|
|
func (a *EthModule) EthGetBalance(ctx context.Context, address api.EthAddress, blkParam string) (api.EthBigInt, error) {
|
|
filAddr, err := address.ToFilecoinAddress()
|
|
if err != nil {
|
|
return api.EthBigInt{}, err
|
|
}
|
|
|
|
actor, err := a.StateGetActor(ctx, filAddr, types.EmptyTSK)
|
|
if xerrors.Is(err, types.ErrActorNotFound) {
|
|
return api.EthBigIntZero, nil
|
|
} else if err != nil {
|
|
return api.EthBigInt{}, err
|
|
}
|
|
|
|
return api.EthBigInt{Int: actor.Balance.Int}, nil
|
|
}
|
|
|
|
func (a *EthModule) EthChainId(ctx context.Context) (api.EthUint64, error) {
|
|
return api.EthUint64(build.Eip155ChainId), nil
|
|
}
|
|
|
|
func (a *EthModule) EthFeeHistory(ctx context.Context, blkCount api.EthUint64, newestBlkNum string, rewardPercentiles []float64) (api.EthFeeHistory, error) {
|
|
if blkCount > 1024 {
|
|
return api.EthFeeHistory{}, fmt.Errorf("block count should be smaller than 1024")
|
|
}
|
|
|
|
newestBlkHeight := uint64(a.Chain.GetHeaviestTipSet().Height())
|
|
|
|
// TODO https://github.com/filecoin-project/ref-fvm/issues/1016
|
|
var blkNum api.EthUint64
|
|
err := blkNum.UnmarshalJSON([]byte(`"` + newestBlkNum + `"`))
|
|
if err == nil && uint64(blkNum) < newestBlkHeight {
|
|
newestBlkHeight = uint64(blkNum)
|
|
}
|
|
|
|
// Deal with the case that the chain is shorter than the number of
|
|
// requested blocks.
|
|
oldestBlkHeight := uint64(1)
|
|
if uint64(blkCount) <= newestBlkHeight {
|
|
oldestBlkHeight = newestBlkHeight - uint64(blkCount) + 1
|
|
}
|
|
|
|
ts, err := a.Chain.GetTipsetByHeight(ctx, abi.ChainEpoch(newestBlkHeight), nil, false)
|
|
if err != nil {
|
|
return api.EthFeeHistory{}, fmt.Errorf("cannot load find block height: %v", newestBlkHeight)
|
|
}
|
|
|
|
// FIXME: baseFeePerGas should include the next block after the newest of the returned range, because this
|
|
// can be inferred from the newest block. we use the newest block's baseFeePerGas for now but need to fix it
|
|
// In other words, due to deferred execution, we might not be returning the most useful value here for the client.
|
|
baseFeeArray := []api.EthBigInt{api.EthBigInt(ts.Blocks()[0].ParentBaseFee)}
|
|
gasUsedRatioArray := []float64{}
|
|
|
|
for ts.Height() >= abi.ChainEpoch(oldestBlkHeight) {
|
|
// Unfortunately we need to rebuild the full message view so we can
|
|
// totalize gas used in the tipset.
|
|
block, err := newEthBlockFromFilecoinTipSet(ctx, ts, false, a.Chain, a.ChainAPI, a.StateAPI)
|
|
if err != nil {
|
|
return api.EthFeeHistory{}, fmt.Errorf("cannot create eth block: %v", err)
|
|
}
|
|
|
|
// both arrays should be reversed at the end
|
|
baseFeeArray = append(baseFeeArray, api.EthBigInt(ts.Blocks()[0].ParentBaseFee))
|
|
gasUsedRatioArray = append(gasUsedRatioArray, float64(block.GasUsed)/float64(build.BlockGasLimit))
|
|
|
|
parentTsKey := ts.Parents()
|
|
ts, err = a.Chain.LoadTipSet(ctx, parentTsKey)
|
|
if err != nil {
|
|
return api.EthFeeHistory{}, fmt.Errorf("cannot load tipset key: %v", parentTsKey)
|
|
}
|
|
}
|
|
|
|
// Reverse the arrays; we collected them newest to oldest; the client expects oldest to newest.
|
|
|
|
for i, j := 0, len(baseFeeArray)-1; i < j; i, j = i+1, j-1 {
|
|
baseFeeArray[i], baseFeeArray[j] = baseFeeArray[j], baseFeeArray[i]
|
|
}
|
|
for i, j := 0, len(gasUsedRatioArray)-1; i < j; i, j = i+1, j-1 {
|
|
gasUsedRatioArray[i], gasUsedRatioArray[j] = gasUsedRatioArray[j], gasUsedRatioArray[i]
|
|
}
|
|
|
|
return api.EthFeeHistory{
|
|
OldestBlock: oldestBlkHeight,
|
|
BaseFeePerGas: baseFeeArray,
|
|
GasUsedRatio: gasUsedRatioArray,
|
|
}, nil
|
|
}
|
|
|
|
func (a *EthModule) NetVersion(ctx context.Context) (string, error) {
|
|
// Note that networkId is not encoded in hex
|
|
nv, err := a.StateNetworkVersion(ctx, types.EmptyTSK)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strconv.FormatUint(uint64(nv), 10), nil
|
|
}
|
|
|
|
func (a *EthModule) NetListening(ctx context.Context) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
func (a *EthModule) EthProtocolVersion(ctx context.Context) (api.EthUint64, error) {
|
|
height := a.Chain.GetHeaviestTipSet().Height()
|
|
return api.EthUint64(a.StateManager.GetNetworkVersion(ctx, height)), nil
|
|
}
|
|
|
|
func (a *EthModule) EthMaxPriorityFeePerGas(ctx context.Context) (api.EthBigInt, error) {
|
|
gasPremium, err := a.GasAPI.GasEstimateGasPremium(ctx, 0, builtinactors.SystemActorAddr, 10000, types.EmptyTSK)
|
|
if err != nil {
|
|
return api.EthBigInt(big.Zero()), err
|
|
}
|
|
return api.EthBigInt(gasPremium), nil
|
|
}
|
|
|
|
func (a *EthModule) EthGasPrice(ctx context.Context) (api.EthBigInt, error) {
|
|
// According to Geth's implementation, eth_gasPrice should return base + tip
|
|
// Ref: https://github.com/ethereum/pm/issues/328#issuecomment-853234014
|
|
|
|
ts := a.Chain.GetHeaviestTipSet()
|
|
baseFee := ts.Blocks()[0].ParentBaseFee
|
|
|
|
premium, err := a.EthMaxPriorityFeePerGas(ctx)
|
|
if err != nil {
|
|
return api.EthBigInt(big.Zero()), nil
|
|
}
|
|
|
|
gasPrice := big.Add(baseFee, big.Int(premium))
|
|
return api.EthBigInt(gasPrice), nil
|
|
}
|
|
|
|
func (a *EthModule) EthSendRawTransaction(ctx context.Context, rawTx api.EthBytes) (api.EthHash, error) {
|
|
txArgs, err := api.ParseEthTxArgs(rawTx)
|
|
if err != nil {
|
|
return api.EmptyEthHash, err
|
|
}
|
|
|
|
smsg, err := txArgs.ToSignedMessage()
|
|
if err != nil {
|
|
return api.EmptyEthHash, err
|
|
}
|
|
|
|
_, err = a.StateAPI.StateGetActor(ctx, smsg.Message.To, types.EmptyTSK)
|
|
if err != nil {
|
|
// if actor does not exist on chain yet, set the method to 0 because
|
|
// embryos only implement method 0
|
|
smsg.Message.Method = builtinactors.MethodSend
|
|
}
|
|
|
|
cid, err := a.MpoolAPI.MpoolPush(ctx, smsg)
|
|
if err != nil {
|
|
return api.EmptyEthHash, err
|
|
}
|
|
return api.NewEthHashFromCid(cid)
|
|
}
|
|
|
|
func (a *EthModule) ethCallToFilecoinMessage(ctx context.Context, tx api.EthCall) (*types.Message, error) {
|
|
var err error
|
|
var from address.Address
|
|
if tx.From == nil {
|
|
// Send from the filecoin "system" address.
|
|
from, err = (api.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.
|
|
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
|
|
var to address.Address
|
|
var method abi.MethodNum
|
|
if tx.To == nil {
|
|
// this is a contract creation
|
|
to = builtintypes.EthereumAddressManagerActorAddr
|
|
|
|
nonce, err := a.Mpool.GetNonce(ctx, from, types.EmptyTSK)
|
|
if err != nil {
|
|
nonce = 0 // assume a zero nonce on error (e.g. sender doesn't exist).
|
|
}
|
|
|
|
params2, err := actors.SerializeParams(&eam.CreateParams{
|
|
Initcode: tx.Data,
|
|
Nonce: nonce,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to serialize Create params: %w", err)
|
|
}
|
|
params = params2
|
|
method = builtintypes.MethodsEAM.Create
|
|
} else {
|
|
addr, err := tx.To.ToFilecoinAddress()
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("cannot get Filecoin address: %w", err)
|
|
}
|
|
to = addr
|
|
|
|
if len(tx.Data) > 0 {
|
|
var buf bytes.Buffer
|
|
if err := cbg.WriteByteArray(&buf, tx.Data); err != nil {
|
|
return nil, fmt.Errorf("failed to encode tx input into a cbor byte-string")
|
|
}
|
|
params = buf.Bytes()
|
|
method = builtintypes.MethodsEVM.InvokeContract
|
|
} else {
|
|
method = builtintypes.MethodSend
|
|
}
|
|
}
|
|
|
|
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 (a *EthModule) applyMessage(ctx context.Context, msg *types.Message) (res *api.InvocResult, err error) {
|
|
ts := a.Chain.GetHeaviestTipSet()
|
|
|
|
// Try calling until we find a height with no migration.
|
|
for {
|
|
res, err = a.StateManager.CallWithGas(ctx, msg, []types.ChainMsg{}, ts)
|
|
if err != stmgr.ErrExpensiveFork {
|
|
break
|
|
}
|
|
ts, err = a.Chain.GetTipSetFromKey(ctx, ts.Parents())
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("getting parent tipset: %w", err)
|
|
}
|
|
}
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("CallWithGas failed: %w", err)
|
|
}
|
|
if res.MsgRct.ExitCode.IsError() {
|
|
return nil, xerrors.Errorf("message execution failed: exit %s, reason: %s", res.MsgRct.ExitCode, res.Error)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (a *EthModule) EthEstimateGas(ctx context.Context, tx api.EthCall) (api.EthUint64, error) {
|
|
msg, err := a.ethCallToFilecoinMessage(ctx, tx)
|
|
if err != nil {
|
|
return api.EthUint64(0), err
|
|
}
|
|
|
|
// Set the gas limit to the zero sentinel value, which makes
|
|
// gas estimation actually run.
|
|
msg.GasLimit = 0
|
|
|
|
msg, err = a.GasAPI.GasEstimateMessageGas(ctx, msg, nil, types.EmptyTSK)
|
|
if err != nil {
|
|
return api.EthUint64(0), err
|
|
}
|
|
|
|
return api.EthUint64(msg.GasLimit), nil
|
|
}
|
|
|
|
func (a *EthModule) EthCall(ctx context.Context, tx api.EthCall, blkParam string) (api.EthBytes, error) {
|
|
msg, err := a.ethCallToFilecoinMessage(ctx, tx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
invokeResult, err := a.applyMessage(ctx, msg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(invokeResult.MsgRct.Return) > 0 {
|
|
return cbg.ReadByteArray(bytes.NewReader(invokeResult.MsgRct.Return), uint64(len(invokeResult.MsgRct.Return)))
|
|
}
|
|
return api.EthBytes{}, nil
|
|
}
|
|
|
|
func (e *EthEvent) EthGetLogs(ctx context.Context, filterSpec *api.EthFilterSpec) (*api.EthFilterResult, error) {
|
|
if e.EventFilterManager == nil {
|
|
return nil, api.ErrNotSupported
|
|
}
|
|
|
|
// Create a temporary filter
|
|
f, err := e.installEthFilterSpec(ctx, filterSpec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ces := f.TakeCollectedEvents(ctx)
|
|
|
|
_ = e.uninstallFilter(ctx, f)
|
|
|
|
return ethFilterResultFromEvents(ces)
|
|
}
|
|
|
|
func (e *EthEvent) EthGetFilterChanges(ctx context.Context, id api.EthFilterID) (*api.EthFilterResult, error) {
|
|
if e.FilterStore == nil {
|
|
return nil, api.ErrNotSupported
|
|
}
|
|
|
|
f, err := e.FilterStore.Get(ctx, types.FilterID(id))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch fc := f.(type) {
|
|
case filterEventCollector:
|
|
return ethFilterResultFromEvents(fc.TakeCollectedEvents(ctx))
|
|
case filterTipSetCollector:
|
|
return ethFilterResultFromTipSets(fc.TakeCollectedTipSets(ctx))
|
|
case filterMessageCollector:
|
|
return ethFilterResultFromMessages(fc.TakeCollectedMessages(ctx))
|
|
}
|
|
|
|
return nil, xerrors.Errorf("unknown filter type")
|
|
}
|
|
|
|
func (e *EthEvent) EthGetFilterLogs(ctx context.Context, id api.EthFilterID) (*api.EthFilterResult, error) {
|
|
if e.FilterStore == nil {
|
|
return nil, api.ErrNotSupported
|
|
}
|
|
|
|
f, err := e.FilterStore.Get(ctx, types.FilterID(id))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch fc := f.(type) {
|
|
case filterEventCollector:
|
|
return ethFilterResultFromEvents(fc.TakeCollectedEvents(ctx))
|
|
}
|
|
|
|
return nil, xerrors.Errorf("wrong filter type")
|
|
}
|
|
|
|
func (e *EthEvent) installEthFilterSpec(ctx context.Context, filterSpec *api.EthFilterSpec) (*filter.EventFilter, error) {
|
|
var (
|
|
minHeight abi.ChainEpoch
|
|
maxHeight abi.ChainEpoch
|
|
tipsetCid cid.Cid
|
|
addresses []address.Address
|
|
keys = map[string][][]byte{}
|
|
)
|
|
|
|
if filterSpec.BlockHash != nil {
|
|
if filterSpec.FromBlock != nil || filterSpec.ToBlock != nil {
|
|
return nil, xerrors.Errorf("must not specify block hash and from/to block")
|
|
}
|
|
|
|
// TODO: derive a tipset hash from eth hash - might need to push this down into the EventFilterManager
|
|
} else {
|
|
if filterSpec.FromBlock == nil || *filterSpec.FromBlock == "latest" {
|
|
ts := e.Chain.GetHeaviestTipSet()
|
|
minHeight = ts.Height()
|
|
} else if *filterSpec.FromBlock == "earliest" {
|
|
minHeight = 0
|
|
} else if *filterSpec.FromBlock == "pending" {
|
|
return nil, api.ErrNotSupported
|
|
} else {
|
|
epoch, err := api.EthUint64FromHex(*filterSpec.FromBlock)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("invalid epoch")
|
|
}
|
|
minHeight = abi.ChainEpoch(epoch)
|
|
}
|
|
|
|
if filterSpec.ToBlock == nil || *filterSpec.ToBlock == "latest" {
|
|
// here latest means the latest at the time
|
|
maxHeight = -1
|
|
} else if *filterSpec.ToBlock == "earliest" {
|
|
maxHeight = 0
|
|
} else if *filterSpec.ToBlock == "pending" {
|
|
return nil, api.ErrNotSupported
|
|
} else {
|
|
epoch, err := api.EthUint64FromHex(*filterSpec.ToBlock)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("invalid epoch")
|
|
}
|
|
maxHeight = abi.ChainEpoch(epoch)
|
|
}
|
|
|
|
// Validate height ranges are within limits set by node operator
|
|
if minHeight == -1 && maxHeight > 0 {
|
|
// Here the client is looking for events between the head and some future height
|
|
ts := e.Chain.GetHeaviestTipSet()
|
|
if maxHeight-ts.Height() > e.MaxFilterHeightRange {
|
|
return nil, xerrors.Errorf("invalid epoch range")
|
|
}
|
|
} else if minHeight >= 0 && maxHeight == -1 {
|
|
// Here the client is looking for events between some time in the past and the current head
|
|
ts := e.Chain.GetHeaviestTipSet()
|
|
if ts.Height()-minHeight > e.MaxFilterHeightRange {
|
|
return nil, xerrors.Errorf("invalid epoch range")
|
|
}
|
|
|
|
} else if minHeight >= 0 && maxHeight >= 0 {
|
|
if minHeight > maxHeight || maxHeight-minHeight > e.MaxFilterHeightRange {
|
|
return nil, xerrors.Errorf("invalid epoch range")
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Convert all addresses to filecoin f4 addresses
|
|
for _, ea := range filterSpec.Address {
|
|
a, err := ea.ToFilecoinAddress()
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("invalid address %x", ea)
|
|
}
|
|
addresses = append(addresses, a)
|
|
}
|
|
|
|
for idx, vals := range filterSpec.Topics {
|
|
// Ethereum topics are emitted using `LOG{0..4}` opcodes resulting in topics1..4
|
|
key := fmt.Sprintf("topic%d", idx+1)
|
|
keyvals := make([][]byte, len(vals))
|
|
for i, v := range vals {
|
|
keyvals[i] = v[:]
|
|
}
|
|
keys[key] = keyvals
|
|
}
|
|
|
|
return e.EventFilterManager.Install(ctx, minHeight, maxHeight, tipsetCid, addresses, keys)
|
|
}
|
|
|
|
func (e *EthEvent) EthNewFilter(ctx context.Context, filterSpec *api.EthFilterSpec) (api.EthFilterID, error) {
|
|
if e.FilterStore == nil || e.EventFilterManager == nil {
|
|
return api.EthFilterID{}, api.ErrNotSupported
|
|
}
|
|
|
|
f, err := e.installEthFilterSpec(ctx, filterSpec)
|
|
if err != nil {
|
|
return api.EthFilterID{}, err
|
|
}
|
|
|
|
if err := e.FilterStore.Add(ctx, f); err != nil {
|
|
// Could not record in store, attempt to delete filter to clean up
|
|
err2 := e.TipSetFilterManager.Remove(ctx, f.ID())
|
|
if err2 != nil {
|
|
return api.EthFilterID{}, xerrors.Errorf("encountered error %v while removing new filter due to %v", err2, err)
|
|
}
|
|
|
|
return api.EthFilterID{}, err
|
|
}
|
|
|
|
return api.EthFilterID(f.ID()), nil
|
|
}
|
|
|
|
func (e *EthEvent) EthNewBlockFilter(ctx context.Context) (api.EthFilterID, error) {
|
|
if e.FilterStore == nil || e.TipSetFilterManager == nil {
|
|
return api.EthFilterID{}, api.ErrNotSupported
|
|
}
|
|
|
|
f, err := e.TipSetFilterManager.Install(ctx)
|
|
if err != nil {
|
|
return api.EthFilterID{}, err
|
|
}
|
|
|
|
if err := e.FilterStore.Add(ctx, f); err != nil {
|
|
// Could not record in store, attempt to delete filter to clean up
|
|
err2 := e.TipSetFilterManager.Remove(ctx, f.ID())
|
|
if err2 != nil {
|
|
return api.EthFilterID{}, xerrors.Errorf("encountered error %v while removing new filter due to %v", err2, err)
|
|
}
|
|
|
|
return api.EthFilterID{}, err
|
|
}
|
|
|
|
return api.EthFilterID(f.ID()), nil
|
|
}
|
|
|
|
func (e *EthEvent) EthNewPendingTransactionFilter(ctx context.Context) (api.EthFilterID, error) {
|
|
if e.FilterStore == nil || e.MemPoolFilterManager == nil {
|
|
return api.EthFilterID{}, api.ErrNotSupported
|
|
}
|
|
|
|
f, err := e.MemPoolFilterManager.Install(ctx)
|
|
if err != nil {
|
|
return api.EthFilterID{}, err
|
|
}
|
|
|
|
if err := e.FilterStore.Add(ctx, f); err != nil {
|
|
// Could not record in store, attempt to delete filter to clean up
|
|
err2 := e.MemPoolFilterManager.Remove(ctx, f.ID())
|
|
if err2 != nil {
|
|
return api.EthFilterID{}, xerrors.Errorf("encountered error %v while removing new filter due to %v", err2, err)
|
|
}
|
|
|
|
return api.EthFilterID{}, err
|
|
}
|
|
|
|
return api.EthFilterID(f.ID()), nil
|
|
}
|
|
|
|
func (e *EthEvent) EthUninstallFilter(ctx context.Context, id api.EthFilterID) (bool, error) {
|
|
if e.FilterStore == nil {
|
|
return false, api.ErrNotSupported
|
|
}
|
|
|
|
f, err := e.FilterStore.Get(ctx, types.FilterID(id))
|
|
if err != nil {
|
|
if errors.Is(err, filter.ErrFilterNotFound) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
if err := e.uninstallFilter(ctx, f); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (e *EthEvent) uninstallFilter(ctx context.Context, f filter.Filter) error {
|
|
switch f.(type) {
|
|
case *filter.EventFilter:
|
|
err := e.EventFilterManager.Remove(ctx, f.ID())
|
|
if err != nil && !errors.Is(err, filter.ErrFilterNotFound) {
|
|
return err
|
|
}
|
|
case *filter.TipSetFilter:
|
|
err := e.TipSetFilterManager.Remove(ctx, f.ID())
|
|
if err != nil && !errors.Is(err, filter.ErrFilterNotFound) {
|
|
return err
|
|
}
|
|
case *filter.MemPoolFilter:
|
|
err := e.MemPoolFilterManager.Remove(ctx, f.ID())
|
|
if err != nil && !errors.Is(err, filter.ErrFilterNotFound) {
|
|
return err
|
|
}
|
|
default:
|
|
return xerrors.Errorf("unknown filter type")
|
|
}
|
|
|
|
return e.FilterStore.Remove(ctx, f.ID())
|
|
}
|
|
|
|
const (
|
|
EthSubscribeEventTypeHeads = "newHeads"
|
|
EthSubscribeEventTypeLogs = "logs"
|
|
)
|
|
|
|
func (e *EthEvent) EthSubscribe(ctx context.Context, eventType string, params *api.EthSubscriptionParams) (<-chan api.EthSubscriptionResponse, error) {
|
|
if e.SubManager == nil {
|
|
return nil, api.ErrNotSupported
|
|
}
|
|
// Note that go-jsonrpc will set the method field of the response to "xrpc.ch.val" but the ethereum api expects the name of the
|
|
// method to be "eth_subscription". This probably doesn't matter in practice.
|
|
|
|
sub, err := e.SubManager.StartSubscription(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch eventType {
|
|
case EthSubscribeEventTypeHeads:
|
|
f, err := e.TipSetFilterManager.Install(ctx)
|
|
if err != nil {
|
|
// clean up any previous filters added and stop the sub
|
|
_, _ = e.EthUnsubscribe(ctx, sub.id)
|
|
return nil, err
|
|
}
|
|
sub.addFilter(ctx, f)
|
|
|
|
case EthSubscribeEventTypeLogs:
|
|
keys := map[string][][]byte{}
|
|
if params != nil {
|
|
for idx, vals := range params.Topics {
|
|
// Ethereum topics are emitted using `LOG{0..4}` opcodes resulting in topics1..4
|
|
key := fmt.Sprintf("topic%d", idx+1)
|
|
keyvals := make([][]byte, len(vals))
|
|
for i, v := range vals {
|
|
keyvals[i] = v[:]
|
|
}
|
|
keys[key] = keyvals
|
|
}
|
|
}
|
|
|
|
f, err := e.EventFilterManager.Install(ctx, -1, -1, cid.Undef, []address.Address{}, keys)
|
|
if err != nil {
|
|
// clean up any previous filters added and stop the sub
|
|
_, _ = e.EthUnsubscribe(ctx, sub.id)
|
|
return nil, err
|
|
}
|
|
sub.addFilter(ctx, f)
|
|
default:
|
|
return nil, xerrors.Errorf("unsupported event type: %s", eventType)
|
|
}
|
|
|
|
return sub.out, nil
|
|
}
|
|
|
|
func (e *EthEvent) EthUnsubscribe(ctx context.Context, id api.EthSubscriptionID) (bool, error) {
|
|
if e.SubManager == nil {
|
|
return false, api.ErrNotSupported
|
|
}
|
|
|
|
filters, err := e.SubManager.StopSubscription(ctx, id)
|
|
if err != nil {
|
|
return false, nil
|
|
}
|
|
|
|
for _, f := range filters {
|
|
if err := e.uninstallFilter(ctx, f); err != nil {
|
|
// this will leave the filter a zombie, collecting events up to the maximum allowed
|
|
log.Warnf("failed to remove filter when unsubscribing: %v", err)
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// GC runs a garbage collection loop, deleting filters that have not been used within the ttl window
|
|
func (e *EthEvent) GC(ctx context.Context, ttl time.Duration) {
|
|
if e.FilterStore == nil {
|
|
return
|
|
}
|
|
|
|
tt := time.NewTicker(time.Minute * 30)
|
|
defer tt.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-tt.C:
|
|
fs := e.FilterStore.NotTakenSince(time.Now().Add(-ttl))
|
|
for _, f := range fs {
|
|
if err := e.uninstallFilter(ctx, f); err != nil {
|
|
log.Warnf("Failed to remove actor event filter during garbage collection: %v", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type filterEventCollector interface {
|
|
TakeCollectedEvents(context.Context) []*filter.CollectedEvent
|
|
}
|
|
|
|
type filterMessageCollector interface {
|
|
TakeCollectedMessages(context.Context) []cid.Cid
|
|
}
|
|
|
|
type filterTipSetCollector interface {
|
|
TakeCollectedTipSets(context.Context) []types.TipSetKey
|
|
}
|
|
|
|
func ethFilterResultFromEvents(evs []*filter.CollectedEvent) (*api.EthFilterResult, error) {
|
|
res := &api.EthFilterResult{}
|
|
for _, ev := range evs {
|
|
log := api.EthLog{
|
|
Removed: ev.Reverted,
|
|
LogIndex: api.EthUint64(ev.EventIdx),
|
|
TransactionIndex: api.EthUint64(ev.MsgIdx),
|
|
BlockNumber: api.EthUint64(ev.Height),
|
|
}
|
|
|
|
var err error
|
|
|
|
for _, entry := range ev.Entries {
|
|
value := api.EthBytes(leftpad32(decodeLogBytes(entry.Value)))
|
|
if entry.Key == api.EthTopic1 || entry.Key == api.EthTopic2 || entry.Key == api.EthTopic3 || entry.Key == api.EthTopic4 {
|
|
log.Topics = append(log.Topics, value)
|
|
} else {
|
|
log.Data = value
|
|
}
|
|
}
|
|
|
|
log.Address, err = api.EthAddressFromFilecoinAddress(ev.EmitterAddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.TransactionHash, err = api.NewEthHashFromCid(ev.MsgCid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c, err := ev.TipSetKey.Cid()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
log.BlockHash, err = api.NewEthHashFromCid(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res.Results = append(res.Results, log)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func ethFilterResultFromTipSets(tsks []types.TipSetKey) (*api.EthFilterResult, error) {
|
|
res := &api.EthFilterResult{}
|
|
|
|
for _, tsk := range tsks {
|
|
c, err := tsk.Cid()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hash, err := api.NewEthHashFromCid(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res.Results = append(res.Results, hash)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func ethFilterResultFromMessages(cs []cid.Cid) (*api.EthFilterResult, error) {
|
|
res := &api.EthFilterResult{}
|
|
|
|
for _, c := range cs {
|
|
hash, err := api.NewEthHashFromCid(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res.Results = append(res.Results, hash)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
type EthSubscriptionManager struct {
|
|
Chain *store.ChainStore
|
|
StateAPI StateAPI
|
|
ChainAPI ChainAPI
|
|
mu sync.Mutex
|
|
subs map[api.EthSubscriptionID]*ethSubscription
|
|
}
|
|
|
|
func (e *EthSubscriptionManager) StartSubscription(ctx context.Context) (*ethSubscription, error) { // nolint
|
|
rawid, err := uuid.NewRandom()
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("new uuid: %w", err)
|
|
}
|
|
id := api.EthSubscriptionID{}
|
|
copy(id[:], rawid[:]) // uuid is 16 bytes
|
|
|
|
ctx, quit := context.WithCancel(ctx)
|
|
|
|
sub := ðSubscription{
|
|
Chain: e.Chain,
|
|
StateAPI: e.StateAPI,
|
|
ChainAPI: e.ChainAPI,
|
|
id: id,
|
|
in: make(chan interface{}, 200),
|
|
out: make(chan api.EthSubscriptionResponse, 20),
|
|
quit: quit,
|
|
}
|
|
|
|
e.mu.Lock()
|
|
if e.subs == nil {
|
|
e.subs = make(map[api.EthSubscriptionID]*ethSubscription)
|
|
}
|
|
e.subs[sub.id] = sub
|
|
e.mu.Unlock()
|
|
|
|
go sub.start(ctx)
|
|
|
|
return sub, nil
|
|
}
|
|
|
|
func (e *EthSubscriptionManager) StopSubscription(ctx context.Context, id api.EthSubscriptionID) ([]filter.Filter, error) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
sub, ok := e.subs[id]
|
|
if !ok {
|
|
return nil, xerrors.Errorf("subscription not found")
|
|
}
|
|
sub.stop()
|
|
delete(e.subs, id)
|
|
|
|
return sub.filters, nil
|
|
}
|
|
|
|
type ethSubscription struct {
|
|
Chain *store.ChainStore
|
|
StateAPI StateAPI
|
|
ChainAPI ChainAPI
|
|
id api.EthSubscriptionID
|
|
in chan interface{}
|
|
out chan api.EthSubscriptionResponse
|
|
|
|
mu sync.Mutex
|
|
filters []filter.Filter
|
|
quit func()
|
|
}
|
|
|
|
func (e *ethSubscription) addFilter(ctx context.Context, f filter.Filter) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
f.SetSubChannel(e.in)
|
|
e.filters = append(e.filters, f)
|
|
}
|
|
|
|
func (e *ethSubscription) start(ctx context.Context) {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case v := <-e.in:
|
|
resp := api.EthSubscriptionResponse{
|
|
SubscriptionID: e.id,
|
|
}
|
|
|
|
var err error
|
|
switch vt := v.(type) {
|
|
case *filter.CollectedEvent:
|
|
resp.Result, err = ethFilterResultFromEvents([]*filter.CollectedEvent{vt})
|
|
case *types.TipSet:
|
|
eb, err := newEthBlockFromFilecoinTipSet(ctx, vt, true, e.Chain, e.ChainAPI, e.StateAPI)
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
resp.Result = eb
|
|
default:
|
|
log.Warnf("unexpected subscription value type: %T", vt)
|
|
}
|
|
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
select {
|
|
case e.out <- resp:
|
|
default:
|
|
// Skip if client is not reading responses
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *ethSubscription) stop() {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
if e.quit != nil {
|
|
e.quit()
|
|
close(e.out)
|
|
e.quit = nil
|
|
}
|
|
}
|
|
|
|
func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTxInfo bool, cs *store.ChainStore, ca ChainAPI, sa StateAPI) (api.EthBlock, error) {
|
|
parent, err := cs.LoadTipSet(ctx, ts.Parents())
|
|
if err != nil {
|
|
return api.EthBlock{}, err
|
|
}
|
|
parentKeyCid, err := parent.Key().Cid()
|
|
if err != nil {
|
|
return api.EthBlock{}, err
|
|
}
|
|
parentBlkHash, err := api.NewEthHashFromCid(parentKeyCid)
|
|
if err != nil {
|
|
return api.EthBlock{}, err
|
|
}
|
|
|
|
blkCid, err := ts.Key().Cid()
|
|
if err != nil {
|
|
return api.EthBlock{}, err
|
|
}
|
|
blkHash, err := api.NewEthHashFromCid(blkCid)
|
|
if err != nil {
|
|
return api.EthBlock{}, err
|
|
}
|
|
|
|
blkMsgs, err := cs.BlockMsgsForTipset(ctx, ts)
|
|
if err != nil {
|
|
return api.EthBlock{}, xerrors.Errorf("error loading messages for tipset: %v: %w", ts, err)
|
|
}
|
|
|
|
block := api.NewEthBlock()
|
|
|
|
// this seems to be a very expensive way to get gasUsed of the block. may need to find an efficient way to do it
|
|
gasUsed := int64(0)
|
|
for _, blkMsg := range blkMsgs {
|
|
for _, msg := range append(blkMsg.BlsMessages, blkMsg.SecpkMessages...) {
|
|
msgLookup, err := sa.StateSearchMsg(ctx, types.EmptyTSK, msg.Cid(), api.LookbackNoLimit, true)
|
|
if err != nil || msgLookup == nil {
|
|
return api.EthBlock{}, nil
|
|
}
|
|
gasUsed += msgLookup.Receipt.GasUsed
|
|
|
|
if fullTxInfo {
|
|
tx, err := newEthTxFromFilecoinMessageLookup(ctx, msgLookup, cs, ca, sa)
|
|
if err != nil {
|
|
return api.EthBlock{}, nil
|
|
}
|
|
block.Transactions = append(block.Transactions, tx)
|
|
} else {
|
|
hash, err := api.NewEthHashFromCid(msg.Cid())
|
|
if err != nil {
|
|
return api.EthBlock{}, err
|
|
}
|
|
block.Transactions = append(block.Transactions, hash.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
block.Hash = blkHash
|
|
block.Number = api.EthUint64(ts.Height())
|
|
block.ParentHash = parentBlkHash
|
|
block.Timestamp = api.EthUint64(ts.Blocks()[0].Timestamp)
|
|
block.BaseFeePerGas = api.EthBigInt{Int: ts.Blocks()[0].ParentBaseFee.Int}
|
|
block.GasUsed = api.EthUint64(gasUsed)
|
|
return block, nil
|
|
}
|
|
|
|
// 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 predictable 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(ctx context.Context, addr address.Address, sa StateAPI) (api.EthAddress, error) {
|
|
// Attempt to convert directly.
|
|
if ethAddr, ok, err := api.TryEthAddressFromFilecoinAddress(addr, false); err != nil {
|
|
return api.EthAddress{}, err
|
|
} else if ok {
|
|
return ethAddr, nil
|
|
}
|
|
|
|
// Lookup on the target actor.
|
|
actor, err := sa.StateGetActor(ctx, addr, types.EmptyTSK)
|
|
if err != nil {
|
|
return api.EthAddress{}, err
|
|
}
|
|
if actor.Address != nil {
|
|
if ethAddr, ok, err := api.TryEthAddressFromFilecoinAddress(*actor.Address, false); err != nil {
|
|
return api.EthAddress{}, err
|
|
} else if ok {
|
|
return ethAddr, nil
|
|
}
|
|
}
|
|
|
|
// Check if we already have an ID addr, and use it if possible.
|
|
if ethAddr, ok, err := api.TryEthAddressFromFilecoinAddress(addr, true); err != nil {
|
|
return api.EthAddress{}, err
|
|
} else if ok {
|
|
return ethAddr, nil
|
|
}
|
|
|
|
// Otherwise, resolve the ID addr.
|
|
idAddr, err := sa.StateLookupID(ctx, addr, types.EmptyTSK)
|
|
if err != nil {
|
|
return api.EthAddress{}, err
|
|
}
|
|
return api.EthAddressFromFilecoinAddress(idAddr)
|
|
}
|
|
|
|
func newEthTxFromFilecoinMessageLookup(ctx context.Context, msgLookup *api.MsgLookup, cs *store.ChainStore, ca ChainAPI, sa StateAPI) (api.EthTx, error) {
|
|
if msgLookup == nil {
|
|
return api.EthTx{}, fmt.Errorf("msg does not exist")
|
|
}
|
|
cid := msgLookup.Message
|
|
txHash, err := api.NewEthHashFromCid(cid)
|
|
if err != nil {
|
|
return api.EthTx{}, err
|
|
}
|
|
|
|
ts, err := cs.LoadTipSet(ctx, msgLookup.TipSet)
|
|
if err != nil {
|
|
return api.EthTx{}, err
|
|
}
|
|
|
|
// This tx is located in the parent tipset
|
|
parentTs, err := cs.LoadTipSet(ctx, ts.Parents())
|
|
if err != nil {
|
|
return api.EthTx{}, err
|
|
}
|
|
|
|
parentTsCid, err := parentTs.Key().Cid()
|
|
if err != nil {
|
|
return api.EthTx{}, err
|
|
}
|
|
|
|
// lookup the transactionIndex
|
|
txIdx := -1
|
|
msgs, err := cs.MessagesForTipset(ctx, parentTs)
|
|
if err != nil {
|
|
return api.EthTx{}, err
|
|
}
|
|
for i, msg := range msgs {
|
|
if msg.Cid() == msgLookup.Message {
|
|
txIdx = i
|
|
}
|
|
}
|
|
if txIdx == -1 {
|
|
return api.EthTx{}, fmt.Errorf("cannot find the msg in the tipset")
|
|
}
|
|
|
|
blkHash, err := api.NewEthHashFromCid(parentTsCid)
|
|
if err != nil {
|
|
return api.EthTx{}, err
|
|
}
|
|
|
|
msg, err := ca.ChainGetMessage(ctx, msgLookup.Message)
|
|
if err != nil {
|
|
return api.EthTx{}, err
|
|
}
|
|
|
|
fromEthAddr, err := lookupEthAddress(ctx, msg.From, sa)
|
|
if err != nil {
|
|
return api.EthTx{}, err
|
|
}
|
|
|
|
toEthAddr, err := lookupEthAddress(ctx, msg.To, sa)
|
|
if err != nil {
|
|
return api.EthTx{}, err
|
|
}
|
|
|
|
toAddr := &toEthAddr
|
|
input := msg.Params
|
|
// Check to see if we need to decode as contract deployment.
|
|
// We don't need to resolve the to address, because there's only one form (an ID).
|
|
if msg.To == builtintypes.EthereumAddressManagerActorAddr {
|
|
switch msg.Method {
|
|
case builtintypes.MethodsEAM.Create:
|
|
toAddr = nil
|
|
var params eam.CreateParams
|
|
err = params.UnmarshalCBOR(bytes.NewReader(msg.Params))
|
|
input = params.Initcode
|
|
case builtintypes.MethodsEAM.Create2:
|
|
toAddr = nil
|
|
var params eam.Create2Params
|
|
err = params.UnmarshalCBOR(bytes.NewReader(msg.Params))
|
|
input = params.Initcode
|
|
}
|
|
if err != nil {
|
|
return api.EthTx{}, err
|
|
}
|
|
}
|
|
// Otherwise, try to decode as a cbor byte array.
|
|
// TODO: Actually check if this is an ethereum call. This code will work for demo purposes, but is not correct.
|
|
if toAddr != nil {
|
|
if decodedParams, err := cbg.ReadByteArray(bytes.NewReader(msg.Params), uint64(len(msg.Params))); err == nil {
|
|
input = decodedParams
|
|
}
|
|
}
|
|
|
|
tx := api.EthTx{
|
|
ChainID: api.EthUint64(build.Eip155ChainId),
|
|
Hash: txHash,
|
|
BlockHash: blkHash,
|
|
BlockNumber: api.EthUint64(parentTs.Height()),
|
|
From: fromEthAddr,
|
|
To: toAddr,
|
|
Value: api.EthBigInt(msg.Value),
|
|
Type: api.EthUint64(2),
|
|
TransactionIndex: api.EthUint64(txIdx),
|
|
Gas: api.EthUint64(msg.GasLimit),
|
|
MaxFeePerGas: api.EthBigInt(msg.GasFeeCap),
|
|
MaxPriorityFeePerGas: api.EthBigInt(msg.GasPremium),
|
|
V: api.EthBytes{},
|
|
R: api.EthBytes{},
|
|
S: api.EthBytes{},
|
|
Input: input,
|
|
}
|
|
return tx, nil
|
|
}
|
|
|
|
func newEthTxReceipt(ctx context.Context, tx api.EthTx, lookup *api.MsgLookup, replay *api.InvocResult, events []types.Event, sa StateAPI) (api.EthTxReceipt, error) {
|
|
receipt := api.EthTxReceipt{
|
|
TransactionHash: tx.Hash,
|
|
TransactionIndex: tx.TransactionIndex,
|
|
BlockHash: tx.BlockHash,
|
|
BlockNumber: tx.BlockNumber,
|
|
From: tx.From,
|
|
To: tx.To,
|
|
Type: api.EthUint64(2),
|
|
LogsBloom: []byte{0},
|
|
}
|
|
|
|
if receipt.To == nil && lookup.Receipt.ExitCode.IsSuccess() {
|
|
// Create and Create2 return the same things.
|
|
var ret eam.CreateReturn
|
|
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 := api.EthAddress(ret.EthAddress)
|
|
receipt.ContractAddress = &addr
|
|
}
|
|
|
|
if lookup.Receipt.ExitCode.IsSuccess() {
|
|
receipt.Status = 1
|
|
}
|
|
if lookup.Receipt.ExitCode.IsError() {
|
|
receipt.Status = 0
|
|
}
|
|
|
|
if len(events) > 0 {
|
|
// TODO return a dummy non-zero bloom to signal that there are logs
|
|
// need to figure out how worth it is to populate with a real bloom
|
|
// should be feasible here since we are iterating over the logs anyway
|
|
receipt.LogsBloom = make([]byte, 256)
|
|
receipt.LogsBloom[255] = 0x01
|
|
|
|
receipt.Logs = make([]api.EthLog, 0, len(events))
|
|
for i, evt := range events {
|
|
l := api.EthLog{
|
|
Removed: false,
|
|
LogIndex: api.EthUint64(i),
|
|
TransactionIndex: tx.TransactionIndex,
|
|
TransactionHash: tx.Hash,
|
|
BlockHash: tx.BlockHash,
|
|
BlockNumber: tx.BlockNumber,
|
|
}
|
|
|
|
for _, entry := range evt.Entries {
|
|
value := api.EthBytes(leftpad32(decodeLogBytes(entry.Value)))
|
|
if entry.Key == api.EthTopic1 || entry.Key == api.EthTopic2 || entry.Key == api.EthTopic3 || entry.Key == api.EthTopic4 {
|
|
l.Topics = append(l.Topics, value)
|
|
} else {
|
|
l.Data = value
|
|
}
|
|
}
|
|
|
|
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(ctx, addr, sa)
|
|
if err != nil {
|
|
return api.EthTxReceipt{}, xerrors.Errorf("failed to resolve Ethereum address: %w", err)
|
|
}
|
|
|
|
receipt.Logs = append(receipt.Logs, l)
|
|
}
|
|
}
|
|
|
|
receipt.GasUsed = api.EthUint64(lookup.Receipt.GasUsed)
|
|
|
|
// TODO: handle CumulativeGasUsed
|
|
receipt.CumulativeGasUsed = api.EmptyEthInt
|
|
|
|
effectiveGasPrice := big.Div(replay.GasCost.TotalCost, big.NewInt(lookup.Receipt.GasUsed))
|
|
receipt.EffectiveGasPrice = api.EthBigInt(effectiveGasPrice)
|
|
|
|
return receipt, nil
|
|
}
|
|
|
|
// decodeLogBytes decodes a CBOR-serialized array into its original form.
|
|
//
|
|
// This function swallows errors and returns the original array if it failed
|
|
// to decode.
|
|
func decodeLogBytes(orig []byte) []byte {
|
|
if orig == nil {
|
|
return orig
|
|
}
|
|
decoded, err := cbg.ReadByteArray(bytes.NewReader(orig), uint64(len(orig)))
|
|
if err != nil {
|
|
return orig
|
|
}
|
|
return decoded
|
|
}
|
|
|
|
// TODO we could also emit full EVM words from the EVM runtime, but not doing so
|
|
// makes the contract slightly cheaper (and saves storage bytes), at the expense
|
|
// of having to left pad in the API, which is a pretty acceptable tradeoff at
|
|
// face value. There may be other protocol implications to consider.
|
|
func leftpad32(orig []byte) []byte {
|
|
needed := 32 - len(orig)
|
|
if needed <= 0 {
|
|
return orig
|
|
}
|
|
ret := make([]byte, 32)
|
|
copy(ret[needed:], orig)
|
|
return ret
|
|
}
|