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] archive-mode for eth_getBalance & eth_call #4529

Open
wants to merge 2 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
96 changes: 74 additions & 22 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 @@ -202,6 +204,7 @@ type (
gs *gasstation.GasStation
broadcastHandler BroadcastOutbound
cfg Config
archiveSupported bool
registry *protocol.Registry
chainListener apitypes.Listener
electionCommittee committee.Committee
Expand Down Expand Up @@ -245,13 +248,20 @@ func WithAPIStats(stats *nodestats.APILocalStats) Option {
}
}

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

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")
Copy link
Collaborator

Choose a reason for hiding this comment

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

comment

Copy link
Member Author

Choose a reason for hiding this comment

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

removed L261 comment, the name clears explains what the error is

)

// newcoreService creates a api server that contains major blockchain components
Expand Down Expand Up @@ -305,6 +315,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 @@ -324,17 +338,22 @@ func (core *coreService) Account(addr address.Address) (*iotextypes.AccountMeta,
if err != nil {
return nil, nil, status.Error(codes.Internal, err.Error())
}
return core.acccount(ctx, tipHeight, state, pendingNonce, addr)
}

func (core *coreService) acccount(ctx context.Context, height uint64, state *state.Account, pendingNonce uint64, addr address.Address) (*iotextypes.AccountMeta, *iotextypes.BlockIdentifier, error) {
if core.indexer == nil {
return nil, nil, status.Error(codes.NotFound, blockindex.ErrActionIndexNA.Error())
}
span := tracer.SpanFromContext(ctx)
span.AddEvent("indexer.GetActionCount")
numActions, err := core.indexer.GetActionCountByAddress(hash.BytesToHash160(addr.Bytes()))
if err != nil {
return nil, nil, status.Error(codes.NotFound, err.Error())
}
// TODO: deprecate nonce field in account meta
accountMeta := &iotextypes.AccountMeta{
Address: addrStr,
Address: addr.String(),
Balance: state.Balance.String(),
PendingNonce: pendingNonce,
NumActions: numActions,
Expand All @@ -349,15 +368,15 @@ func (core *coreService) Account(addr address.Address) (*iotextypes.AccountMeta,
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 @@ -520,7 +539,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 @@ -530,12 +563,12 @@ 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())
}
Expand Down Expand Up @@ -1680,7 +1713,7 @@ func (core *coreService) isGasLimitEnough(
) (bool, *action.Receipt, []byte, error) {
ctx, span := tracer.NewSpan(ctx, "Server.isGasLimitEnough")
defer span.End()
ret, receipt, err := core.simulateExecution(ctx, caller, elp, opts...)
ret, receipt, err := core.simulateExecution(ctx, core.bc.TipHeight(), false, caller, elp, opts...)
if err != nil {
return false, nil, nil, err
}
Expand Down Expand Up @@ -1825,10 +1858,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 @@ -1852,7 +1886,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 @@ -1880,7 +1914,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 @@ -1939,20 +1973,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,
envestcc marked this conversation as resolved.
Show resolved Hide resolved
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)
Copy link
Member Author

@dustinxie dustinxie Dec 27, 2024

Choose a reason for hiding this comment

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

these lines would be duplicated multiple times if use ws instead of boolean as input

}
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 @@ -1963,10 +2019,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 @@ -1024,9 +1025,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 @@ -1039,6 +1042,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
94 changes: 94 additions & 0 deletions api/coreservice_with_height.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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"
accountutil "github.com/iotexproject/iotex-core/v2/action/protocol/account/util"
"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"
"github.com/iotexproject/iotex-core/v2/state"
)

type (
// CoreServiceReaderWithHeight is an interface for state reader at certain height
CoreServiceReaderWithHeight interface {
Copy link
Collaborator

Choose a reason for hiding this comment

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

could you explain why we need to create such an interface?

Copy link
Member Author

@dustinxie dustinxie Dec 26, 2024

Choose a reason for hiding this comment

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

this interface is created to represent APIs needed for archive-mode query (there will be a total 4-6 of them, 2 enabled in this PR, and more to enable in upcoming PR), so we can keep using existing API without having to add the height parameter to it, that is, we can avoid changes like below

Account(address.Address, height uint64) (*iotextypes.AccountMeta, *iotextypes.BlockIdentifier, error)

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 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Member Author

@dustinxie dustinxie Dec 26, 2024

Choose a reason for hiding this comment

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

it is already supported by factory/statedb, the actual archive support at DB level will be enabled in #4517, #4518

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)
}
state, pendingNonce, err := core.stateAndNonce(addr)
if err != nil {
return nil, nil, err
}
return core.cs.acccount(ctx, core.height, state, pendingNonce, addr)
}

func (core *coreServiceReaderWithHeight) stateAndNonce(addr address.Address) (*state.Account, uint64, error) {
var (
g = core.cs.bc.Genesis()
ctx = genesis.WithGenesisContext(context.Background(), g)
)
ws, err := core.cs.sf.WorkingSetAtHeight(ctx, core.height)
if err != nil {
return nil, 0, status.Error(codes.Internal, err.Error())
}
state, err := accountutil.AccountState(ctx, ws, addr)
if err != nil {
return nil, 0, status.Error(codes.NotFound, err.Error())
}
var pendingNonce uint64
if g.IsSumatra(core.height) {
pendingNonce = state.PendingNonceConsideringFreshAccount()
} else {
pendingNonce = state.PendingNonce()
}
return state, pendingNonce, nil
}

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)
}
Loading
Loading