Skip to content

Commit

Permalink
[api] archive-mode for eth_getBalance & eth_call
Browse files Browse the repository at this point in the history
  • Loading branch information
dustinxie committed Dec 19, 2024
1 parent 89dafcd commit bd7ca1f
Show file tree
Hide file tree
Showing 14 changed files with 318 additions and 114 deletions.
124 changes: 98 additions & 26 deletions api/coreservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import (
"github.com/iotexproject/iotex-core/v2/pkg/log"
"github.com/iotexproject/iotex-core/v2/pkg/tracer"
"github.com/iotexproject/iotex-core/v2/pkg/unit"
"github.com/iotexproject/iotex-core/v2/pkg/util/byteutil"
"github.com/iotexproject/iotex-core/v2/pkg/version"
"github.com/iotexproject/iotex-core/v2/server/itx/nodestats"
"github.com/iotexproject/iotex-core/v2/state"
Expand All @@ -81,6 +82,7 @@ const (
type (
// CoreService provides api interface for user to interact with blockchain data
CoreService interface {
WithHeight(uint64) CoreServiceReaderWithHeight
// Account returns the metadata of an account
Account(addr address.Address) (*iotextypes.AccountMeta, *iotextypes.BlockIdentifier, error)
// ChainMeta returns blockchain metadata
Expand Down Expand Up @@ -200,6 +202,7 @@ type (
gs *gasstation.GasStation
broadcastHandler BroadcastOutbound
cfg Config
archiveSupported bool
registry *protocol.Registry
chainListener apitypes.Listener
electionCommittee committee.Committee
Expand Down Expand Up @@ -243,13 +246,21 @@ func WithAPIStats(stats *nodestats.APILocalStats) Option {
}
}

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

type intrinsicGasCalculator interface {
IntrinsicGas() (uint64, error)
}

var (
// ErrNotFound indicates the record isn't found
ErrNotFound = errors.New("not found")
ErrNotFound = errors.New("not found")
ErrArchiveNotSupported = errors.New("archive-mode not supported")
)

// newcoreService creates a api server that contains major blockchain components
Expand Down Expand Up @@ -303,6 +314,10 @@ func newCoreService(
return &core, nil
}

func (core *coreService) WithHeight(height uint64) CoreServiceReaderWithHeight {
return newCoreServiceWithHeight(core, height)
}

// 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 @@ -311,16 +326,38 @@ 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")
ctx = genesis.WithGenesisContext(ctx, core.bc.Genesis())
state, tipHeight, err := accountutil.AccountStateWithHeight(ctx, core.sf, addr)
return core.acccount(ctx, core.bc.TipHeight(), false, core.sf, addr)
}

func (core *coreService) acccount(ctx context.Context, height uint64, archive bool, sr protocol.StateReader, addr address.Address) (*iotextypes.AccountMeta, *iotextypes.BlockIdentifier, error) {
span := tracer.SpanFromContext(ctx)
span.AddEvent("accountutil.AccountStateWithHeight")
state, height, err := accountutil.AccountStateWithHeight(ctx, sr, addr)
if err != nil {
return nil, nil, status.Error(codes.NotFound, err.Error())
}
span.AddEvent("ap.GetPendingNonce")
pendingNonce, err := core.ap.GetPendingNonce(addrStr)
if err != nil {
return nil, nil, status.Error(codes.Internal, err.Error())
var (
addrStr = addr.String()
pendingNonce uint64
)
if archive {
state, err := accountutil.AccountState(ctx, sr, addr)
if err != nil {
return nil, nil, status.Error(codes.NotFound, err.Error())
}
g := core.bc.Genesis()
if g.IsSumatra(height) {
pendingNonce = state.PendingNonceConsideringFreshAccount()
} else {
pendingNonce = state.PendingNonce()
}
} else {
pendingNonce, err = core.ap.GetPendingNonce(addrStr)
if err != nil {
return nil, nil, status.Error(codes.Internal, err.Error())
}
}
if core.indexer == nil {
return nil, nil, status.Error(codes.NotFound, blockindex.ErrActionIndexNA.Error())
Expand All @@ -340,22 +377,22 @@ func (core *coreService) Account(addr address.Address) (*iotextypes.AccountMeta,
}
if state.IsContract() {
var code protocol.SerializableBytes
_, err = core.sf.State(&code, protocol.NamespaceOption(evm.CodeKVNameSpace), protocol.KeyOption(state.CodeHash))
_, err = sr.State(&code, protocol.NamespaceOption(evm.CodeKVNameSpace), protocol.KeyOption(state.CodeHash))
if err != nil {
return nil, nil, status.Error(codes.NotFound, err.Error())
}
accountMeta.ContractByteCode = code
}
span.AddEvent("bc.BlockHeaderByHeight")
header, err := core.bc.BlockHeaderByHeight(tipHeight)
header, err := core.bc.BlockHeaderByHeight(height)
if err != nil {
return nil, nil, status.Error(codes.NotFound, err.Error())
}
hash := header.HashBlock()
span.AddEvent("coreService.Account.End")
return accountMeta, &iotextypes.BlockIdentifier{
Hash: hex.EncodeToString(hash[:]),
Height: tipHeight,
Height: height,
}, nil
}

Expand Down Expand Up @@ -518,7 +555,21 @@ func (core *coreService) ReadContract(ctx context.Context, callerAddr address.Ad
if !ok {
return "", nil, status.Error(codes.InvalidArgument, "expecting action.Execution")
}
key := hash.Hash160b(append([]byte(exec.Contract()), exec.Data()...))
var (
tipHeight = core.bc.TipHeight()
hdBytes = append(byteutil.Uint64ToBytesBigEndian(tipHeight), []byte(exec.Contract())...)
key = hash.Hash160b(append(hdBytes, exec.Data()...))
)
return core.readContract(ctx, key, tipHeight, false, callerAddr, elp)
}

func (core *coreService) readContract(
ctx context.Context,
key hash.Hash160,
height uint64,
archive bool,
callerAddr address.Address,
elp action.Envelope) (string, *iotextypes.Receipt, error) {
// TODO: either moving readcache into the upper layer or change the storage format
if d, ok := core.readCache.Get(key); ok {
res := iotexapi.ReadContractResponse{}
Expand All @@ -528,15 +579,17 @@ func (core *coreService) ReadContract(ctx context.Context, callerAddr address.Ad
}
var (
g = core.bc.Genesis()
blockGasLimit = g.BlockGasLimitByHeight(core.bc.TipHeight())
blockGasLimit = g.BlockGasLimitByHeight(height)
)
if elp.Gas() == 0 || blockGasLimit < elp.Gas() {
elp.SetGas(blockGasLimit)
}
retval, receipt, err := core.simulateExecution(ctx, callerAddr, elp)
retval, receipt, err := core.simulateExecution(ctx, height, archive, callerAddr, elp)
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{
Data: hex.EncodeToString(retval),
Receipt: receipt.ConvertToReceiptPb(),
Expand Down Expand Up @@ -1670,7 +1723,7 @@ func (core *coreService) isGasLimitEnough(
) (bool, *action.Receipt, error) {
ctx, span := tracer.NewSpan(ctx, "Server.isGasLimitEnough")
defer span.End()
_, receipt, err := core.simulateExecution(ctx, caller, elp, opts...)
_, receipt, err := core.simulateExecution(ctx, core.bc.TipHeight(), false, caller, elp, opts...)
if err != nil {
return false, nil, err
}
Expand Down Expand Up @@ -1815,10 +1868,11 @@ func (core *coreService) ReceiveBlock(blk *block.Block) error {
func (core *coreService) SimulateExecution(ctx context.Context, addr address.Address, elp action.Envelope) ([]byte, *action.Receipt, error) {
var (
g = core.bc.Genesis()
blockGasLimit = g.BlockGasLimitByHeight(core.bc.TipHeight())
tipHeight = core.bc.TipHeight()
blockGasLimit = g.BlockGasLimitByHeight(tipHeight)
)
elp.SetGas(blockGasLimit)
return core.simulateExecution(ctx, addr, elp)
return core.simulateExecution(ctx, tipHeight, false, addr, elp)
}

// SyncingProgress returns the syncing status of node
Expand All @@ -1842,7 +1896,7 @@ func (core *coreService) TraceTransaction(ctx context.Context, actHash string, c
}
addr, _ := address.FromString(address.ZeroAddress)
return core.traceTx(ctx, new(tracers.Context), config, func(ctx context.Context) ([]byte, *action.Receipt, error) {
return core.simulateExecution(ctx, addr, act.Envelope)
return core.simulateExecution(ctx, core.bc.TipHeight(), false, addr, act.Envelope)
})
}

Expand Down Expand Up @@ -1870,7 +1924,7 @@ func (core *coreService) TraceCall(ctx context.Context,
elp := (&action.EnvelopeBuilder{}).SetAction(action.NewExecution(contractAddress, amount, data)).
SetGasLimit(gasLimit).Build()
return core.traceTx(ctx, new(tracers.Context), config, func(ctx context.Context) ([]byte, *action.Receipt, error) {
return core.simulateExecution(ctx, callerAddr, elp)
return core.simulateExecution(ctx, core.bc.TipHeight(), false, callerAddr, elp)
})
}

Expand Down Expand Up @@ -1929,20 +1983,42 @@ func (core *coreService) traceTx(ctx context.Context, txctx *tracers.Context, co
return retval, receipt, tracer, err
}

func (core *coreService) simulateExecution(ctx context.Context, addr address.Address, elp action.Envelope, opts ...protocol.SimulateOption) ([]byte, *action.Receipt, error) {
ctx, err := core.bc.Context(ctx)
func (core *coreService) simulateExecution(
ctx context.Context,
height uint64,
archive bool,
addr address.Address,
elp action.Envelope,
opts ...protocol.SimulateOption) ([]byte, *action.Receipt, error) {
var (
err error
ws protocol.StateManager
)
if archive {
ctx, err = core.bc.ContextAtHeight(ctx, height)
if err != nil {
return nil, nil, status.Error(codes.Internal, err.Error())
}
ws, err = core.sf.WorkingSetAtHeight(ctx, height)
} else {
ctx, err = core.bc.Context(ctx)
if err != nil {
return nil, nil, status.Error(codes.Internal, err.Error())
}
ws, err = core.sf.WorkingSet(ctx)
}
if err != nil {
return nil, nil, status.Error(codes.Internal, err.Error())
}
state, err := accountutil.AccountState(ctx, core.sf, addr)
state, err := accountutil.AccountState(ctx, ws, addr)
if err != nil {
return nil, nil, status.Error(codes.InvalidArgument, err.Error())
}
var pendingNonce uint64
ctx = protocol.WithFeatureCtx(protocol.WithBlockCtx(ctx, protocol.BlockCtx{
BlockHeight: core.bc.TipHeight(),
BlockHeight: height,
}))
if protocol.MustGetFeatureCtx(ctx).RefactorFreshAccountConversion {
if protocol.MustGetFeatureCtx(ctx).UseZeroNonceForFreshAccount {
pendingNonce = state.PendingNonceConsideringFreshAccount()
} else {
pendingNonce = state.PendingNonce()
Expand All @@ -1953,10 +2029,6 @@ func (core *coreService) simulateExecution(ctx context.Context, addr address.Add
GetBlockTime: core.getBlockTime,
DepositGasFunc: rewarding.DepositGas,
})
ws, err := core.sf.WorkingSet(ctx)
if err != nil {
return nil, nil, status.Error(codes.Internal, err.Error())
}
return evm.SimulateExecution(ctx, ws, addr, elp, opts...)
}

Expand Down
8 changes: 6 additions & 2 deletions api/coreservice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,11 +488,12 @@ func TestEstimateExecutionGasConsumption(t *testing.T) {
p := NewPatches()
defer p.Reset()

p = p.ApplyFuncReturn(accountutil.AccountState, nil, errors.New(t.Name()))
p = p.ApplyFuncReturn(accountutil.AccountStateWithHeight, nil, uint64(0), errors.New(t.Name()))

bc.EXPECT().Genesis().Return(genesis.Genesis{}).Times(1)
bc.EXPECT().TipHeight().Return(uint64(1)).Times(1)
bc.EXPECT().TipHeight().Return(uint64(1)).Times(2)
bc.EXPECT().Context(gomock.Any()).Return(ctx, nil).Times(1)
sf.EXPECT().WorkingSet(gomock.Any()).Return(nil, nil).Times(1)
elp := (&action.EnvelopeBuilder{}).SetAction(&action.Execution{}).Build()
_, err := cs.EstimateExecutionGasConsumption(ctx, elp, &address.AddrV1{})
require.ErrorContains(err, t.Name())
Expand Down Expand Up @@ -1022,9 +1023,11 @@ func TestSimulateExecution(t *testing.T) {
var (
bc = mock_blockchain.NewMockBlockchain(ctrl)
dao = mock_blockdao.NewMockBlockDAO(ctrl)
sf = mock_factory.NewMockFactory(ctrl)
cs = &coreService{
bc: bc,
dao: dao,
sf: sf,
}
ctx = context.Background()
)
Expand All @@ -1037,6 +1040,7 @@ func TestSimulateExecution(t *testing.T) {
bc.EXPECT().Genesis().Return(genesis.Genesis{}).Times(1)
bc.EXPECT().TipHeight().Return(uint64(1)).Times(1)
bc.EXPECT().Context(gomock.Any()).Return(ctx, nil).Times(1)
sf.EXPECT().WorkingSet(gomock.Any()).Return(nil, nil).Times(1)
elp := (&action.EnvelopeBuilder{}).SetAction(&action.Execution{}).Build()
_, _, err := cs.SimulateExecution(ctx, &address.AddrV1{}, elp)
require.ErrorContains(err, t.Name())
Expand Down
71 changes: 71 additions & 0 deletions api/coreservice_with_height.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package api

import (
"context"

"github.com/iotexproject/go-pkgs/hash"
"github.com/iotexproject/iotex-address/address"
"github.com/iotexproject/iotex-proto/golang/iotextypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/iotexproject/iotex-core/v2/action"
"github.com/iotexproject/iotex-core/v2/blockchain/genesis"
"github.com/iotexproject/iotex-core/v2/pkg/log"
"github.com/iotexproject/iotex-core/v2/pkg/tracer"
"github.com/iotexproject/iotex-core/v2/pkg/util/byteutil"
)

type (
// CoreServiceReaderWithHeight is an interface for state reader at certain height
CoreServiceReaderWithHeight interface {
Account(address.Address) (*iotextypes.AccountMeta, *iotextypes.BlockIdentifier, error)
ReadContract(context.Context, address.Address, action.Envelope) (string, *iotextypes.Receipt, error)
}

coreServiceReaderWithHeight struct {
cs *coreService
height uint64
}
)

func newCoreServiceWithHeight(cs *coreService, height uint64) *coreServiceReaderWithHeight {
return &coreServiceReaderWithHeight{
cs: cs,
height: height,
}
}

func (core *coreServiceReaderWithHeight) Account(addr address.Address) (*iotextypes.AccountMeta, *iotextypes.BlockIdentifier, error) {
if !core.cs.archiveSupported {
return nil, nil, ErrArchiveNotSupported
}
ctx, span := tracer.NewSpan(context.Background(), "coreServiceReaderWithHeight.Account")
defer span.End()
addrStr := addr.String()
if addrStr == address.RewardingPoolAddr || addrStr == address.StakingBucketPoolAddr {
return core.cs.getProtocolAccount(ctx, addrStr)
}
ctx = genesis.WithGenesisContext(ctx, core.cs.bc.Genesis())
ws, err := core.cs.sf.WorkingSetAtHeight(ctx, core.height)
if err != nil {
return nil, nil, err
}
return core.cs.acccount(ctx, core.height, true, ws, addr)
}

func (core *coreServiceReaderWithHeight) ReadContract(ctx context.Context, callerAddr address.Address, elp action.Envelope) (string, *iotextypes.Receipt, error) {
if !core.cs.archiveSupported {
return "", nil, ErrArchiveNotSupported
}
log.Logger("api").Debug("receive read smart contract request")
exec, ok := elp.Action().(*action.Execution)
if !ok {
return "", nil, status.Error(codes.InvalidArgument, "expecting action.Execution")
}
var (
hdBytes = append(byteutil.Uint64ToBytesBigEndian(core.height), []byte(exec.Contract())...)
key = hash.Hash160b(append(hdBytes, exec.Data()...))
)
return core.cs.readContract(ctx, key, core.height, true, callerAddr, elp)
}
2 changes: 1 addition & 1 deletion api/grpcserver_integrity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1485,7 +1485,7 @@ func TestGrpcServer_ReadContractIntegrity(t *testing.T) {
res, err := grpcHandler.ReadContract(context.Background(), request)
require.NoError(err)
require.Equal(test.retValue, res.Data)
require.EqualValues(0, res.Receipt.Status)
require.EqualValues(1, res.Receipt.Status)
require.Equal(test.actionHash, hex.EncodeToString(res.Receipt.ActHash))
require.Equal(test.gasConsumed, res.Receipt.GasConsumed)
}
Expand Down
Loading

0 comments on commit bd7ca1f

Please sign in to comment.