Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[api] historical eth_getbalance & eth_call #4339

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 27 additions & 11 deletions action/protocol/account/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,22 +81,16 @@ func Recorded(sr protocol.StateReader, addr address.Address) (bool, error) {

// AccountState returns the confirmed account state on the chain
func AccountState(ctx context.Context, sr protocol.StateReader, addr address.Address) (*state.Account, error) {
a, _, err := AccountStateWithHeight(ctx, sr, addr)
return a, err
}

// AccountStateWithHeight returns the confirmed account state on the chain with what height the state is read from.
func AccountStateWithHeight(ctx context.Context, sr protocol.StateReader, addr address.Address) (*state.Account, uint64, error) {
pkHash := hash.BytesToHash160(addr.Bytes())
account := &state.Account{}
h, err := sr.State(account, protocol.LegacyKeyOption(pkHash))
_, err := sr.State(account, protocol.LegacyKeyOption(pkHash))
switch errors.Cause(err) {
case nil:
return account, h, nil
return account, nil
case state.ErrStateNotExist:
tip, err := sr.Height()
if err != nil {
return nil, 0, err
return nil, err
}
ctx = protocol.WithBlockCtx(ctx, protocol.BlockCtx{
BlockHeight: tip + 1,
Expand All @@ -107,8 +101,30 @@ func AccountStateWithHeight(ctx context.Context, sr protocol.StateReader, addr a
opts = append(opts, state.LegacyNonceAccountTypeOption())
}
account, err = state.NewAccount(opts...)
return account, h, err
return account, err
default:
return nil, errors.Wrapf(err, "error when loading state of %x", pkHash)
}
}

// AccountStateAtHeight returns the confirmed account state on the chain at a specific height
func AccountStateAtHeight(ctx context.Context, height uint64, sr protocol.HistroicalStateReader,
addr address.Address) (*state.Account, error) {
Comment on lines +111 to +112
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

format

pkHash := hash.BytesToHash160(addr.Bytes())
account := &state.Account{}
err := sr.StateAtHeight(height, account, protocol.LegacyKeyOption(pkHash))

switch errors.Cause(err) {
case nil:
return account, nil
case state.ErrStateNotExist:
var opts []state.AccountCreationOption
if protocol.MustGetFeatureCtx(ctx).CreateLegacyNonceAccount {
opts = append(opts, state.LegacyNonceAccountTypeOption())
}
account, err = state.NewAccount(opts...)
return account, err
default:
return nil, h, errors.Wrapf(err, "error when loading state of %x", pkHash)
return nil, errors.Wrapf(err, "error when loading state of %x", pkHash)
}
}
7 changes: 7 additions & 0 deletions action/protocol/managers.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ type (
ReadView(string) (interface{}, error)
}

// HistroicalStateReader defines an interface to read stateDB with historical state
HistroicalStateReader interface {
StateAtHeight(uint64, interface{}, ...StateOption) error
StatesAtHeight(uint64, ...StateOption) (state.Iterator, error)
ReadView(string) (interface{}, error)
}

// StateManager defines the stateDB interface atop IoTeX blockchain
StateManager interface {
StateReader
Expand Down
197 changes: 164 additions & 33 deletions api/coreservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/eth/tracers"
"github.com/ethereum/go-ethereum/rpc"

// Force-load the tracer engines to trigger registration
_ "github.com/ethereum/go-ethereum/eth/tracers/js"
Expand Down Expand Up @@ -76,9 +77,15 @@ const (
defaultTraceTimeout = 5 * time.Second
)

var (
errHistoricalNotSupported = errors.New("historical data is not supported")
)

type (
// CoreService provides api interface for user to interact with blockchain data
CoreService interface {
// BalanceOf returns the balance of an address
BalanceOf(addr address.Address, height rpc.BlockNumber) (*big.Int, error)
// Account returns the metadata of an account
Account(addr address.Address) (*iotextypes.AccountMeta, *iotextypes.BlockIdentifier, error)
// ChainMeta returns blockchain metadata
Expand All @@ -88,7 +95,7 @@ type (
// SendAction is the API to send an action to blockchain.
SendAction(ctx context.Context, in *iotextypes.Action) (string, error)
// ReadContract reads the state in a contract address specified by the slot
ReadContract(ctx context.Context, callerAddr address.Address, sc *action.Execution) (string, *iotextypes.Receipt, error)
ReadContract(ctx context.Context, height rpc.BlockNumber, callerAddr address.Address, sc *action.Execution) (string, *iotextypes.Receipt, error)
// ReadState reads state on blockchain
ReadState(protocolID string, height string, methodName []byte, arguments [][]byte) (*iotexapi.ReadStateResponse, error)
// SuggestGasPrice suggests gas price
Expand Down Expand Up @@ -202,6 +209,7 @@ type (
apiStats *nodestats.APILocalStats
sgdIndexer blockindex.SGDRegistry
getBlockTime evm.GetBlockTime
isArchiveSupport bool
}

// jobDesc provides a struct to get and store logs in core.LogsInRange
Expand Down Expand Up @@ -245,6 +253,13 @@ func WithSGDIndexer(sgdIndexer blockindex.SGDRegistry) Option {
}
}

// WithArchiveSupport is the option to enable archive support
func WithArchiveSupport(enabled bool) Option {
return func(svr *coreService) {
svr.isArchiveSupport = enabled
}
}

type intrinsicGasCalculator interface {
IntrinsicGas() (uint64, error)
}
Expand Down Expand Up @@ -306,6 +321,45 @@ func newCoreService(
return &core, nil
}

func (core *coreService) BalanceOf(addr address.Address, height rpc.BlockNumber) (*big.Int, error) {
ctx, span := tracer.NewSpan(context.Background(), "coreService.BalanceOf")
defer span.End()
addrStr := addr.String()
stateHeight, err := core.blocknumToStateHeight(height)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
if !core.isArchiveSupport && stateHeight != core.TipHeight() {
return nil, status.Error(codes.NotFound, errHistoricalNotSupported.Error())
}
if addrStr == address.RewardingPoolAddr || addrStr == address.StakingBucketPoolAddr {
// TODO: get protocol account at height
if height != rpc.BlockNumber(core.TipHeight()) {
return nil, status.Error(codes.NotFound, "system account state at height is not supported")
}
acc, _, err := core.getProtocolAccount(ctx, addrStr)
if err != nil {
return nil, err
}
balance, ok := new(big.Int).SetString(acc.Balance, 10)
if !ok {
return nil, status.Errorf(codes.Internal, "failed to parse balance %s", acc.Balance)
}
return balance, nil
}
span.AddEvent("accountutil.AccountState")
ctx = genesis.WithGenesisContext(ctx, core.bc.Genesis())
stateReader := protocol.StateReader(core.sf)
if core.isArchiveSupport {
stateReader = newStateReaderWithHeight(core.sf, stateHeight)
}
state, err := accountutil.AccountState(ctx, stateReader, addr)
if err != nil {
return nil, status.Error(codes.NotFound, err.Error())
}
return state.Balance, nil
}

// Account returns the metadata of an account
func (core *coreService) Account(addr address.Address) (*iotextypes.AccountMeta, *iotextypes.BlockIdentifier, error) {
ctx, span := tracer.NewSpan(context.Background(), "coreService.Account")
Expand All @@ -314,9 +368,9 @@ func (core *coreService) Account(addr address.Address) (*iotextypes.AccountMeta,
if addrStr == address.RewardingPoolAddr || addrStr == address.StakingBucketPoolAddr {
return core.getProtocolAccount(ctx, addrStr)
}
span.AddEvent("accountutil.AccountStateWithHeight")
span.AddEvent("accountutil.AccountState")
ctx = genesis.WithGenesisContext(ctx, core.bc.Genesis())
state, tipHeight, err := accountutil.AccountStateWithHeight(ctx, core.sf, addr)
state, err := accountutil.AccountState(ctx, core.sf, addr)
if err != nil {
return nil, nil, status.Error(codes.NotFound, err.Error())
}
Expand Down Expand Up @@ -350,6 +404,7 @@ func (core *coreService) Account(addr address.Address) (*iotextypes.AccountMeta,
accountMeta.ContractByteCode = code
}
span.AddEvent("bc.BlockHeaderByHeight")
tipHeight := core.bc.TipHeight()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

header, err := core.bc.BlockHeaderByHeight(tipHeight)
if err != nil {
return nil, nil, status.Error(codes.NotFound, err.Error())
Expand Down Expand Up @@ -527,48 +582,106 @@ func (core *coreService) validateChainID(chainID uint32) error {
return nil
}

// TODO: a polymorphism impl of coreservice might be more suitable for historical data retrieval
// ReadContract reads the state in a contract address specified by the slot
func (core *coreService) ReadContract(ctx context.Context, callerAddr address.Address, sc *action.Execution) (string, *iotextypes.Receipt, error) {
log.Logger("api").Debug("receive read smart contract request")
key := hash.Hash160b(append([]byte(sc.Contract()), sc.Data()...))
func (core *coreService) ReadContract(ctx context.Context, height rpc.BlockNumber, callerAddr address.Address, sc *action.Execution,
) (string, *iotextypes.Receipt, error) {
// TODO: either moving readcache into the upper layer or change the storage format
heightBytes, _ := height.MarshalText()
key := hash.Hash160b(bytes.Join([][]byte{heightBytes, []byte(sc.Contract()), sc.Data()}, nil))
if d, ok := core.readCache.Get(key); ok {
res := iotexapi.ReadContractResponse{}
if err := proto.Unmarshal(d, &res); err == nil {
return res.Data, res.Receipt, nil
}
}
ctx = genesis.WithGenesisContext(ctx, core.bc.Genesis())
state, err := accountutil.AccountState(ctx, core.sf, callerAddr)
if err != nil {
return "", nil, status.Error(codes.InvalidArgument, err.Error())
}
if ctx, err = core.bc.Context(ctx); err != nil {
return "", nil, err
}
ctx = protocol.WithFeatureCtx(protocol.WithBlockCtx(ctx, protocol.BlockCtx{
BlockHeight: core.bc.TipHeight(),
}))
var pendingNonce uint64
if protocol.MustGetFeatureCtx(ctx).RefactorFreshAccountConversion {
pendingNonce = state.PendingNonceConsideringFreshAccount()
} else {
pendingNonce = state.PendingNonce()
}
sc.SetNonce(pendingNonce)

var (
g = core.bc.Genesis()
blockGasLimit = g.BlockGasLimitByHeight(core.bc.TipHeight())
retval []byte
receipt = &action.Receipt{}
)
if sc.GasLimit() == 0 || blockGasLimit < sc.GasLimit() {
sc.SetGasLimit(blockGasLimit)
}
sc.SetGasPrice(big.NewInt(0)) // ReadContract() is read-only, use 0 to prevent insufficient gas
if !core.isArchiveSupport {
ctx, err := core.bc.Context(ctx)
if err != nil {
return "", nil, err
}
ctx = protocol.WithFeatureCtx(protocol.WithBlockCtx(ctx, protocol.BlockCtx{
BlockHeight: core.bc.TipHeight(),
}))
acct, err := accountutil.AccountState(ctx, core.sf, callerAddr)
if err != nil {
return "", nil, status.Error(codes.InvalidArgument, err.Error())
}
var pendingNonce uint64
if protocol.MustGetFeatureCtx(ctx).RefactorFreshAccountConversion {
pendingNonce = acct.PendingNonceConsideringFreshAccount()
} else {
pendingNonce = acct.PendingNonce()
}
sc.SetNonce(pendingNonce)
var (
g = core.bc.Genesis()
blockGasLimit = g.BlockGasLimitByHeight(core.bc.TipHeight())
)
if sc.GasLimit() == 0 || blockGasLimit < sc.GasLimit() {
sc.SetGasLimit(blockGasLimit)
}

retval, receipt, err := core.simulateExecution(ctx, callerAddr, sc, core.dao.GetBlockHash, core.getBlockTime)
if err != nil {
return "", nil, status.Error(codes.Internal, err.Error())
retval, receipt, err = core.simulateExecution(ctx, callerAddr, sc, core.dao.GetBlockHash, core.getBlockTime)
if err != nil {
return "", nil, status.Error(codes.Internal, err.Error())
}
} else {
stateHeight, err := core.blocknumToStateHeight(height)
if err != nil {
return "", nil, status.Error(codes.InvalidArgument, err.Error())
}
ctx, err := core.bc.ContextAtHeight(ctx, stateHeight)
if err != nil {
return "", nil, err
}
ctx = protocol.WithFeatureCtx(
protocol.WithBlockCtx(
ctx,
protocol.BlockCtx{
BlockHeight: stateHeight,
},
),
)

acct, err := accountutil.AccountStateAtHeight(ctx, stateHeight, core.sf, callerAddr)
if err != nil {
return "", nil, status.Error(codes.InvalidArgument, err.Error())
}

var pendingNonce uint64
if protocol.MustGetFeatureCtx(ctx).RefactorFreshAccountConversion {
pendingNonce = acct.PendingNonceConsideringFreshAccount()
} else {
pendingNonce = acct.PendingNonce()
}
sc.SetNonce(pendingNonce)

var (
g = core.bc.Genesis()
blockGasLimit = g.BlockGasLimitByHeight(stateHeight)
)
if sc.GasLimit() == 0 || blockGasLimit < sc.GasLimit() {
sc.SetGasLimit(blockGasLimit)
}

ctx = evm.WithHelperCtx(ctx, evm.HelperContext{
GetBlockHash: core.dao.GetBlockHash,
GetBlockTime: core.getBlockTime,
DepositGasFunc: rewarding.DepositGasWithSGD,
Sgd: core.sgdIndexer,
})
retval, receipt, err = core.sf.SimulateExecutionAtHeight(ctx, stateHeight, callerAddr, sc)
if err != nil {
return "", nil, status.Error(codes.Internal, err.Error())
}
}

// ReadContract() is read-only, if no error returned, we consider it a success
receipt.Status = uint64(iotextypes.ReceiptStatus_Success)
res := iotexapi.ReadContractResponse{
Expand Down Expand Up @@ -1740,6 +1853,7 @@ func (core *coreService) ReceiveBlock(blk *block.Block) error {
return core.chainListener.ReceiveBlock(blk)
}

// TODO: merge this func into EstimateGasForAction
func (core *coreService) SimulateExecution(ctx context.Context, addr address.Address, exec *action.Execution) ([]byte, *action.Receipt, error) {
ctx = genesis.WithGenesisContext(ctx, core.bc.Genesis())
state, err := accountutil.AccountState(ctx, core.sf, addr)
Expand Down Expand Up @@ -1775,6 +1889,7 @@ func (core *coreService) SyncingProgress() (uint64, uint64, uint64) {
return startingHeight, currentHeight, targetHeight
}

// TODO: support this by height
// TraceTransaction returns the trace result of transaction
func (core *coreService) TraceTransaction(ctx context.Context, actHash string, config *tracers.TraceConfig) ([]byte, *action.Receipt, any, error) {
actInfo, err := core.Action(util.Remove0xPrefix(actHash), false)
Expand All @@ -1797,6 +1912,7 @@ func (core *coreService) TraceTransaction(ctx context.Context, actHash string, c
return retval, receipt, tracer, err
}

// TODO: support this by height
// TraceCall returns the trace result of call
func (core *coreService) TraceCall(ctx context.Context,
callerAddr address.Address,
Expand Down Expand Up @@ -1926,3 +2042,18 @@ func filterReceipts(receipts []*action.Receipt, actHash hash.Hash256) *action.Re
}
return nil
}

func (core *coreService) blocknumToStateHeight(blockNum rpc.BlockNumber) (uint64, error) {
var stateHeight uint64
switch blockNum {
case rpc.SafeBlockNumber, rpc.FinalizedBlockNumber, rpc.LatestBlockNumber:
stateHeight = core.bc.TipHeight()
case rpc.EarliestBlockNumber:
stateHeight = 1
case rpc.PendingBlockNumber:
return 0, errors.New("pending block number is not supported")
default:
stateHeight = uint64(blockNum.Int64())
}
return stateHeight, nil
}
5 changes: 3 additions & 2 deletions api/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

"github.com/ethereum/go-ethereum/eth/tracers"
"github.com/ethereum/go-ethereum/eth/tracers/logger"
"github.com/ethereum/go-ethereum/rpc"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery"
grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
Expand Down Expand Up @@ -367,8 +368,8 @@ func (svr *gRPCHandler) ReadContract(ctx context.Context, in *iotexapi.ReadContr
return nil, status.Error(codes.InvalidArgument, err.Error())
}
sc.SetGasLimit(in.GetGasLimit())

data, receipt, err := svr.coreService.ReadContract(ctx, callerAddr, sc)
sc.SetGasPrice(big.NewInt(0)) // ReadContract() is read-only, use 0 to prevent insufficient gas
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be omitted? I mean, by default it should be 0?

data, receipt, err := svr.coreService.ReadContract(ctx, rpc.LatestBlockNumber, callerAddr, sc)
if err != nil {
return nil, err
}
Expand Down
Loading
Loading