graphql: fix data races (#26965)
Fixes multiple data races caused by the fact that resolving fields are done concurrently by the graphql library. It also enforces caching at the stateobject level for account fields.
This commit is contained in:
parent
fb8a3aaf1e
commit
a236e03d00
@ -24,6 +24,7 @@ import (
|
|||||||
"math/big"
|
"math/big"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum"
|
"github.com/ethereum/go-ethereum"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
@ -80,12 +81,26 @@ type Account struct {
|
|||||||
r *Resolver
|
r *Resolver
|
||||||
address common.Address
|
address common.Address
|
||||||
blockNrOrHash rpc.BlockNumberOrHash
|
blockNrOrHash rpc.BlockNumberOrHash
|
||||||
|
state *state.StateDB
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// getState fetches the StateDB object for an account.
|
// getState fetches the StateDB object for an account.
|
||||||
func (a *Account) getState(ctx context.Context) (*state.StateDB, error) {
|
func (a *Account) getState(ctx context.Context) (*state.StateDB, error) {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
if a.state != nil {
|
||||||
|
return a.state, nil
|
||||||
|
}
|
||||||
state, _, err := a.r.backend.StateAndHeaderByNumberOrHash(ctx, a.blockNrOrHash)
|
state, _, err := a.r.backend.StateAndHeaderByNumberOrHash(ctx, a.blockNrOrHash)
|
||||||
return state, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
a.state = state
|
||||||
|
// Cache the state object. This is done so that concurrent resolvers
|
||||||
|
// don't have to fetch the object from DB individually.
|
||||||
|
a.state.GetOrNewStateObject(a.address)
|
||||||
|
return a.state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Account) Address(ctx context.Context) (common.Address, error) {
|
func (a *Account) Address(ctx context.Context) (common.Address, error) {
|
||||||
@ -184,32 +199,39 @@ func (at *AccessTuple) StorageKeys(ctx context.Context) []common.Hash {
|
|||||||
// Transaction represents an Ethereum transaction.
|
// Transaction represents an Ethereum transaction.
|
||||||
// backend and hash are mandatory; all others will be fetched when required.
|
// backend and hash are mandatory; all others will be fetched when required.
|
||||||
type Transaction struct {
|
type Transaction struct {
|
||||||
r *Resolver
|
r *Resolver
|
||||||
hash common.Hash
|
hash common.Hash // Must be present after initialization
|
||||||
|
mu sync.Mutex
|
||||||
|
// mu protects following resources
|
||||||
tx *types.Transaction
|
tx *types.Transaction
|
||||||
block *Block
|
block *Block
|
||||||
index uint64
|
index uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolve returns the internal transaction object, fetching it if needed.
|
// resolve returns the internal transaction object, fetching it if needed.
|
||||||
func (t *Transaction) resolve(ctx context.Context) (*types.Transaction, error) {
|
// It also returns the block the tx blongs to, unless it is a pending tx.
|
||||||
if t.tx == nil {
|
func (t *Transaction) resolve(ctx context.Context) (*types.Transaction, *Block, error) {
|
||||||
// Try to return an already finalized transaction
|
t.mu.Lock()
|
||||||
tx, blockHash, _, index, err := t.r.backend.GetTransaction(ctx, t.hash)
|
defer t.mu.Unlock()
|
||||||
if err == nil && tx != nil {
|
if t.tx != nil {
|
||||||
t.tx = tx
|
return t.tx, t.block, nil
|
||||||
blockNrOrHash := rpc.BlockNumberOrHashWithHash(blockHash, false)
|
|
||||||
t.block = &Block{
|
|
||||||
r: t.r,
|
|
||||||
numberOrHash: &blockNrOrHash,
|
|
||||||
}
|
|
||||||
t.index = index
|
|
||||||
return t.tx, nil
|
|
||||||
}
|
|
||||||
// No finalized transaction, try to retrieve it from the pool
|
|
||||||
t.tx = t.r.backend.GetPoolTransaction(t.hash)
|
|
||||||
}
|
}
|
||||||
return t.tx, nil
|
// Try to return an already finalized transaction
|
||||||
|
tx, blockHash, _, index, err := t.r.backend.GetTransaction(ctx, t.hash)
|
||||||
|
if err == nil && tx != nil {
|
||||||
|
t.tx = tx
|
||||||
|
blockNrOrHash := rpc.BlockNumberOrHashWithHash(blockHash, false)
|
||||||
|
t.block = &Block{
|
||||||
|
r: t.r,
|
||||||
|
numberOrHash: &blockNrOrHash,
|
||||||
|
hash: blockHash,
|
||||||
|
}
|
||||||
|
t.index = index
|
||||||
|
return t.tx, t.block, nil
|
||||||
|
}
|
||||||
|
// No finalized transaction, try to retrieve it from the pool
|
||||||
|
t.tx = t.r.backend.GetPoolTransaction(t.hash)
|
||||||
|
return t.tx, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) Hash(ctx context.Context) common.Hash {
|
func (t *Transaction) Hash(ctx context.Context) common.Hash {
|
||||||
@ -217,7 +239,7 @@ func (t *Transaction) Hash(ctx context.Context) common.Hash {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) InputData(ctx context.Context) (hexutil.Bytes, error) {
|
func (t *Transaction) InputData(ctx context.Context) (hexutil.Bytes, error) {
|
||||||
tx, err := t.resolve(ctx)
|
tx, _, err := t.resolve(ctx)
|
||||||
if err != nil || tx == nil {
|
if err != nil || tx == nil {
|
||||||
return hexutil.Bytes{}, err
|
return hexutil.Bytes{}, err
|
||||||
}
|
}
|
||||||
@ -225,7 +247,7 @@ func (t *Transaction) InputData(ctx context.Context) (hexutil.Bytes, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) Gas(ctx context.Context) (hexutil.Uint64, error) {
|
func (t *Transaction) Gas(ctx context.Context) (hexutil.Uint64, error) {
|
||||||
tx, err := t.resolve(ctx)
|
tx, _, err := t.resolve(ctx)
|
||||||
if err != nil || tx == nil {
|
if err != nil || tx == nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@ -233,7 +255,7 @@ func (t *Transaction) Gas(ctx context.Context) (hexutil.Uint64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) GasPrice(ctx context.Context) (hexutil.Big, error) {
|
func (t *Transaction) GasPrice(ctx context.Context) (hexutil.Big, error) {
|
||||||
tx, err := t.resolve(ctx)
|
tx, block, err := t.resolve(ctx)
|
||||||
if err != nil || tx == nil {
|
if err != nil || tx == nil {
|
||||||
return hexutil.Big{}, err
|
return hexutil.Big{}, err
|
||||||
}
|
}
|
||||||
@ -241,8 +263,8 @@ func (t *Transaction) GasPrice(ctx context.Context) (hexutil.Big, error) {
|
|||||||
case types.AccessListTxType:
|
case types.AccessListTxType:
|
||||||
return hexutil.Big(*tx.GasPrice()), nil
|
return hexutil.Big(*tx.GasPrice()), nil
|
||||||
case types.DynamicFeeTxType:
|
case types.DynamicFeeTxType:
|
||||||
if t.block != nil {
|
if block != nil {
|
||||||
if baseFee, _ := t.block.BaseFeePerGas(ctx); baseFee != nil {
|
if baseFee, _ := block.BaseFeePerGas(ctx); baseFee != nil {
|
||||||
// price = min(tip, gasFeeCap - baseFee) + baseFee
|
// price = min(tip, gasFeeCap - baseFee) + baseFee
|
||||||
return (hexutil.Big)(*math.BigMin(new(big.Int).Add(tx.GasTipCap(), baseFee.ToInt()), tx.GasFeeCap())), nil
|
return (hexutil.Big)(*math.BigMin(new(big.Int).Add(tx.GasTipCap(), baseFee.ToInt()), tx.GasFeeCap())), nil
|
||||||
}
|
}
|
||||||
@ -254,15 +276,15 @@ func (t *Transaction) GasPrice(ctx context.Context) (hexutil.Big, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) EffectiveGasPrice(ctx context.Context) (*hexutil.Big, error) {
|
func (t *Transaction) EffectiveGasPrice(ctx context.Context) (*hexutil.Big, error) {
|
||||||
tx, err := t.resolve(ctx)
|
tx, block, err := t.resolve(ctx)
|
||||||
if err != nil || tx == nil {
|
if err != nil || tx == nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Pending tx
|
// Pending tx
|
||||||
if t.block == nil {
|
if block == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
header, err := t.block.resolveHeader(ctx)
|
header, err := block.resolveHeader(ctx)
|
||||||
if err != nil || header == nil {
|
if err != nil || header == nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -273,7 +295,7 @@ func (t *Transaction) EffectiveGasPrice(ctx context.Context) (*hexutil.Big, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) MaxFeePerGas(ctx context.Context) (*hexutil.Big, error) {
|
func (t *Transaction) MaxFeePerGas(ctx context.Context) (*hexutil.Big, error) {
|
||||||
tx, err := t.resolve(ctx)
|
tx, _, err := t.resolve(ctx)
|
||||||
if err != nil || tx == nil {
|
if err != nil || tx == nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -288,7 +310,7 @@ func (t *Transaction) MaxFeePerGas(ctx context.Context) (*hexutil.Big, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) MaxPriorityFeePerGas(ctx context.Context) (*hexutil.Big, error) {
|
func (t *Transaction) MaxPriorityFeePerGas(ctx context.Context) (*hexutil.Big, error) {
|
||||||
tx, err := t.resolve(ctx)
|
tx, _, err := t.resolve(ctx)
|
||||||
if err != nil || tx == nil {
|
if err != nil || tx == nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -303,15 +325,15 @@ func (t *Transaction) MaxPriorityFeePerGas(ctx context.Context) (*hexutil.Big, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) EffectiveTip(ctx context.Context) (*hexutil.Big, error) {
|
func (t *Transaction) EffectiveTip(ctx context.Context) (*hexutil.Big, error) {
|
||||||
tx, err := t.resolve(ctx)
|
tx, block, err := t.resolve(ctx)
|
||||||
if err != nil || tx == nil {
|
if err != nil || tx == nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Pending tx
|
// Pending tx
|
||||||
if t.block == nil {
|
if block == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
header, err := t.block.resolveHeader(ctx)
|
header, err := block.resolveHeader(ctx)
|
||||||
if err != nil || header == nil {
|
if err != nil || header == nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -327,7 +349,7 @@ func (t *Transaction) EffectiveTip(ctx context.Context) (*hexutil.Big, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) Value(ctx context.Context) (hexutil.Big, error) {
|
func (t *Transaction) Value(ctx context.Context) (hexutil.Big, error) {
|
||||||
tx, err := t.resolve(ctx)
|
tx, _, err := t.resolve(ctx)
|
||||||
if err != nil || tx == nil {
|
if err != nil || tx == nil {
|
||||||
return hexutil.Big{}, err
|
return hexutil.Big{}, err
|
||||||
}
|
}
|
||||||
@ -338,7 +360,7 @@ func (t *Transaction) Value(ctx context.Context) (hexutil.Big, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) Nonce(ctx context.Context) (hexutil.Uint64, error) {
|
func (t *Transaction) Nonce(ctx context.Context) (hexutil.Uint64, error) {
|
||||||
tx, err := t.resolve(ctx)
|
tx, _, err := t.resolve(ctx)
|
||||||
if err != nil || tx == nil {
|
if err != nil || tx == nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@ -346,7 +368,7 @@ func (t *Transaction) Nonce(ctx context.Context) (hexutil.Uint64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) To(ctx context.Context, args BlockNumberArgs) (*Account, error) {
|
func (t *Transaction) To(ctx context.Context, args BlockNumberArgs) (*Account, error) {
|
||||||
tx, err := t.resolve(ctx)
|
tx, _, err := t.resolve(ctx)
|
||||||
if err != nil || tx == nil {
|
if err != nil || tx == nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -362,7 +384,7 @@ func (t *Transaction) To(ctx context.Context, args BlockNumberArgs) (*Account, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) From(ctx context.Context, args BlockNumberArgs) (*Account, error) {
|
func (t *Transaction) From(ctx context.Context, args BlockNumberArgs) (*Account, error) {
|
||||||
tx, err := t.resolve(ctx)
|
tx, _, err := t.resolve(ctx)
|
||||||
if err != nil || tx == nil {
|
if err != nil || tx == nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -376,17 +398,20 @@ func (t *Transaction) From(ctx context.Context, args BlockNumberArgs) (*Account,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) Block(ctx context.Context) (*Block, error) {
|
func (t *Transaction) Block(ctx context.Context) (*Block, error) {
|
||||||
if _, err := t.resolve(ctx); err != nil {
|
_, block, err := t.resolve(ctx)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return t.block, nil
|
return block, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) Index(ctx context.Context) (*int32, error) {
|
func (t *Transaction) Index(ctx context.Context) (*int32, error) {
|
||||||
if _, err := t.resolve(ctx); err != nil {
|
_, block, err := t.resolve(ctx)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if t.block == nil {
|
// Pending tx
|
||||||
|
if block == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
index := int32(t.index)
|
index := int32(t.index)
|
||||||
@ -395,13 +420,15 @@ func (t *Transaction) Index(ctx context.Context) (*int32, error) {
|
|||||||
|
|
||||||
// getReceipt returns the receipt associated with this transaction, if any.
|
// getReceipt returns the receipt associated with this transaction, if any.
|
||||||
func (t *Transaction) getReceipt(ctx context.Context) (*types.Receipt, error) {
|
func (t *Transaction) getReceipt(ctx context.Context) (*types.Receipt, error) {
|
||||||
if _, err := t.resolve(ctx); err != nil {
|
_, block, err := t.resolve(ctx)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if t.block == nil {
|
// Pending tx
|
||||||
|
if block == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
receipts, err := t.block.resolveReceipts(ctx)
|
receipts, err := block.resolveReceipts(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -451,28 +478,25 @@ func (t *Transaction) CreatedContract(ctx context.Context, args BlockNumberArgs)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) Logs(ctx context.Context) (*[]*Log, error) {
|
func (t *Transaction) Logs(ctx context.Context) (*[]*Log, error) {
|
||||||
if _, err := t.resolve(ctx); err != nil {
|
_, block, err := t.resolve(ctx)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if t.block == nil {
|
// Pending tx
|
||||||
|
if block == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if _, ok := t.block.numberOrHash.Hash(); !ok {
|
h, err := block.Hash(ctx)
|
||||||
header, err := t.r.backend.HeaderByNumberOrHash(ctx, *t.block.numberOrHash)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
hash := header.Hash()
|
|
||||||
t.block.numberOrHash.BlockHash = &hash
|
|
||||||
}
|
}
|
||||||
return t.getLogs(ctx)
|
return t.getLogs(ctx, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getLogs returns log objects for the given tx.
|
// getLogs returns log objects for the given tx.
|
||||||
// Assumes block hash is resolved.
|
// Assumes block hash is resolved.
|
||||||
func (t *Transaction) getLogs(ctx context.Context) (*[]*Log, error) {
|
func (t *Transaction) getLogs(ctx context.Context, hash common.Hash) (*[]*Log, error) {
|
||||||
var (
|
var (
|
||||||
hash, _ = t.block.numberOrHash.Hash()
|
|
||||||
filter = t.r.filterSystem.NewBlockFilter(hash, nil, nil)
|
filter = t.r.filterSystem.NewBlockFilter(hash, nil, nil)
|
||||||
logs, err = filter.Logs(ctx)
|
logs, err = filter.Logs(ctx)
|
||||||
)
|
)
|
||||||
@ -494,7 +518,7 @@ func (t *Transaction) getLogs(ctx context.Context) (*[]*Log, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) Type(ctx context.Context) (*int32, error) {
|
func (t *Transaction) Type(ctx context.Context) (*int32, error) {
|
||||||
tx, err := t.resolve(ctx)
|
tx, _, err := t.resolve(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -503,7 +527,7 @@ func (t *Transaction) Type(ctx context.Context) (*int32, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) AccessList(ctx context.Context) (*[]*AccessTuple, error) {
|
func (t *Transaction) AccessList(ctx context.Context) (*[]*AccessTuple, error) {
|
||||||
tx, err := t.resolve(ctx)
|
tx, _, err := t.resolve(ctx)
|
||||||
if err != nil || tx == nil {
|
if err != nil || tx == nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -519,7 +543,7 @@ func (t *Transaction) AccessList(ctx context.Context) (*[]*AccessTuple, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) R(ctx context.Context) (hexutil.Big, error) {
|
func (t *Transaction) R(ctx context.Context) (hexutil.Big, error) {
|
||||||
tx, err := t.resolve(ctx)
|
tx, _, err := t.resolve(ctx)
|
||||||
if err != nil || tx == nil {
|
if err != nil || tx == nil {
|
||||||
return hexutil.Big{}, err
|
return hexutil.Big{}, err
|
||||||
}
|
}
|
||||||
@ -528,7 +552,7 @@ func (t *Transaction) R(ctx context.Context) (hexutil.Big, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) S(ctx context.Context) (hexutil.Big, error) {
|
func (t *Transaction) S(ctx context.Context) (hexutil.Big, error) {
|
||||||
tx, err := t.resolve(ctx)
|
tx, _, err := t.resolve(ctx)
|
||||||
if err != nil || tx == nil {
|
if err != nil || tx == nil {
|
||||||
return hexutil.Big{}, err
|
return hexutil.Big{}, err
|
||||||
}
|
}
|
||||||
@ -537,7 +561,7 @@ func (t *Transaction) S(ctx context.Context) (hexutil.Big, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) V(ctx context.Context) (hexutil.Big, error) {
|
func (t *Transaction) V(ctx context.Context) (hexutil.Big, error) {
|
||||||
tx, err := t.resolve(ctx)
|
tx, _, err := t.resolve(ctx)
|
||||||
if err != nil || tx == nil {
|
if err != nil || tx == nil {
|
||||||
return hexutil.Big{}, err
|
return hexutil.Big{}, err
|
||||||
}
|
}
|
||||||
@ -546,7 +570,7 @@ func (t *Transaction) V(ctx context.Context) (hexutil.Big, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) Raw(ctx context.Context) (hexutil.Bytes, error) {
|
func (t *Transaction) Raw(ctx context.Context) (hexutil.Bytes, error) {
|
||||||
tx, err := t.resolve(ctx)
|
tx, _, err := t.resolve(ctx)
|
||||||
if err != nil || tx == nil {
|
if err != nil || tx == nil {
|
||||||
return hexutil.Bytes{}, err
|
return hexutil.Bytes{}, err
|
||||||
}
|
}
|
||||||
@ -568,16 +592,20 @@ type BlockType int
|
|||||||
// when required.
|
// when required.
|
||||||
type Block struct {
|
type Block struct {
|
||||||
r *Resolver
|
r *Resolver
|
||||||
numberOrHash *rpc.BlockNumberOrHash
|
numberOrHash *rpc.BlockNumberOrHash // Field resolvers assume numberOrHash is always present
|
||||||
hash common.Hash
|
mu sync.Mutex
|
||||||
header *types.Header
|
// mu protects following resources
|
||||||
block *types.Block
|
hash common.Hash // Must be resolved during initialization
|
||||||
receipts []*types.Receipt
|
header *types.Header
|
||||||
|
block *types.Block
|
||||||
|
receipts []*types.Receipt
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolve returns the internal Block object representing this block, fetching
|
// resolve returns the internal Block object representing this block, fetching
|
||||||
// it if necessary.
|
// it if necessary.
|
||||||
func (b *Block) resolve(ctx context.Context) (*types.Block, error) {
|
func (b *Block) resolve(ctx context.Context) (*types.Block, error) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
if b.block != nil {
|
if b.block != nil {
|
||||||
return b.block, nil
|
return b.block, nil
|
||||||
}
|
}
|
||||||
@ -587,10 +615,10 @@ func (b *Block) resolve(ctx context.Context) (*types.Block, error) {
|
|||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
b.block, err = b.r.backend.BlockByNumberOrHash(ctx, *b.numberOrHash)
|
b.block, err = b.r.backend.BlockByNumberOrHash(ctx, *b.numberOrHash)
|
||||||
if b.block != nil && b.header == nil {
|
if b.block != nil {
|
||||||
b.header = b.block.Header()
|
b.hash = b.block.Hash()
|
||||||
if hash, ok := b.numberOrHash.Hash(); ok {
|
if b.header == nil {
|
||||||
b.hash = hash
|
b.header = b.block.Header()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return b.block, err
|
return b.block, err
|
||||||
@ -600,39 +628,39 @@ func (b *Block) resolve(ctx context.Context) (*types.Block, error) {
|
|||||||
// if necessary. Call this function instead of `resolve` unless you need the
|
// if necessary. Call this function instead of `resolve` unless you need the
|
||||||
// additional data (transactions and uncles).
|
// additional data (transactions and uncles).
|
||||||
func (b *Block) resolveHeader(ctx context.Context) (*types.Header, error) {
|
func (b *Block) resolveHeader(ctx context.Context) (*types.Header, error) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
if b.header != nil {
|
||||||
|
return b.header, nil
|
||||||
|
}
|
||||||
if b.numberOrHash == nil && b.hash == (common.Hash{}) {
|
if b.numberOrHash == nil && b.hash == (common.Hash{}) {
|
||||||
return nil, errBlockInvariant
|
return nil, errBlockInvariant
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
if b.header == nil {
|
b.header, err = b.r.backend.HeaderByNumberOrHash(ctx, *b.numberOrHash)
|
||||||
if b.hash != (common.Hash{}) {
|
if err != nil {
|
||||||
b.header, err = b.r.backend.HeaderByHash(ctx, b.hash)
|
return nil, err
|
||||||
} else {
|
|
||||||
b.header, err = b.r.backend.HeaderByNumberOrHash(ctx, *b.numberOrHash)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return b.header, err
|
if b.hash == (common.Hash{}) {
|
||||||
|
b.hash = b.header.Hash()
|
||||||
|
}
|
||||||
|
return b.header, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveReceipts returns the list of receipts for this block, fetching them
|
// resolveReceipts returns the list of receipts for this block, fetching them
|
||||||
// if necessary.
|
// if necessary.
|
||||||
func (b *Block) resolveReceipts(ctx context.Context) ([]*types.Receipt, error) {
|
func (b *Block) resolveReceipts(ctx context.Context) ([]*types.Receipt, error) {
|
||||||
if b.receipts == nil {
|
b.mu.Lock()
|
||||||
hash := b.hash
|
defer b.mu.Unlock()
|
||||||
if hash == (common.Hash{}) {
|
if b.receipts != nil {
|
||||||
header, err := b.resolveHeader(ctx)
|
return b.receipts, nil
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
hash = header.Hash()
|
|
||||||
}
|
|
||||||
receipts, err := b.r.backend.GetReceipts(ctx, hash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
b.receipts = receipts
|
|
||||||
}
|
}
|
||||||
return b.receipts, nil
|
receipts, err := b.r.backend.GetReceipts(ctx, b.hash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b.receipts = receipts
|
||||||
|
return receipts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Block) Number(ctx context.Context) (Long, error) {
|
func (b *Block) Number(ctx context.Context) (Long, error) {
|
||||||
@ -645,13 +673,8 @@ func (b *Block) Number(ctx context.Context) (Long, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Block) Hash(ctx context.Context) (common.Hash, error) {
|
func (b *Block) Hash(ctx context.Context) (common.Hash, error) {
|
||||||
if b.hash == (common.Hash{}) {
|
b.mu.Lock()
|
||||||
header, err := b.resolveHeader(ctx)
|
defer b.mu.Unlock()
|
||||||
if err != nil {
|
|
||||||
return common.Hash{}, err
|
|
||||||
}
|
|
||||||
b.hash = header.Hash()
|
|
||||||
}
|
|
||||||
return b.hash, nil
|
return b.hash, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -705,11 +728,18 @@ func (b *Block) Parent(ctx context.Context) (*Block, error) {
|
|||||||
if b.header == nil || b.header.Number.Uint64() < 1 {
|
if b.header == nil || b.header.Number.Uint64() < 1 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
num := rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(b.header.Number.Uint64() - 1))
|
var (
|
||||||
|
num = rpc.BlockNumber(b.header.Number.Uint64() - 1)
|
||||||
|
hash = b.header.ParentHash
|
||||||
|
numOrHash = rpc.BlockNumberOrHash{
|
||||||
|
BlockNumber: &num,
|
||||||
|
BlockHash: &hash,
|
||||||
|
}
|
||||||
|
)
|
||||||
return &Block{
|
return &Block{
|
||||||
r: b.r,
|
r: b.r,
|
||||||
numberOrHash: &num,
|
numberOrHash: &numOrHash,
|
||||||
hash: b.header.ParentHash,
|
hash: hash,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -798,6 +828,7 @@ func (b *Block) Ommers(ctx context.Context) (*[]*Block, error) {
|
|||||||
r: b.r,
|
r: b.r,
|
||||||
numberOrHash: &blockNumberOrHash,
|
numberOrHash: &blockNumberOrHash,
|
||||||
header: uncle,
|
header: uncle,
|
||||||
|
hash: uncle.Hash(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return &ret, nil
|
return &ret, nil
|
||||||
@ -820,17 +851,13 @@ func (b *Block) LogsBloom(ctx context.Context) (hexutil.Bytes, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Block) TotalDifficulty(ctx context.Context) (hexutil.Big, error) {
|
func (b *Block) TotalDifficulty(ctx context.Context) (hexutil.Big, error) {
|
||||||
h := b.hash
|
hash, err := b.Hash(ctx)
|
||||||
if h == (common.Hash{}) {
|
if err != nil {
|
||||||
header, err := b.resolveHeader(ctx)
|
return hexutil.Big{}, err
|
||||||
if err != nil {
|
|
||||||
return hexutil.Big{}, err
|
|
||||||
}
|
|
||||||
h = header.Hash()
|
|
||||||
}
|
}
|
||||||
td := b.r.backend.GetTd(ctx, h)
|
td := b.r.backend.GetTd(ctx, hash)
|
||||||
if td == nil {
|
if td == nil {
|
||||||
return hexutil.Big{}, fmt.Errorf("total difficulty not found %x", b.hash)
|
return hexutil.Big{}, fmt.Errorf("total difficulty not found %x", hash)
|
||||||
}
|
}
|
||||||
return hexutil.Big(*td), nil
|
return hexutil.Big(*td), nil
|
||||||
}
|
}
|
||||||
@ -948,6 +975,7 @@ func (b *Block) OmmerAt(ctx context.Context, args struct{ Index int32 }) (*Block
|
|||||||
r: b.r,
|
r: b.r,
|
||||||
numberOrHash: &blockNumberOrHash,
|
numberOrHash: &blockNumberOrHash,
|
||||||
header: uncle,
|
header: uncle,
|
||||||
|
hash: uncle.Hash(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -997,15 +1025,11 @@ func (b *Block) Logs(ctx context.Context, args struct{ Filter BlockFilterCriteri
|
|||||||
if args.Filter.Topics != nil {
|
if args.Filter.Topics != nil {
|
||||||
topics = *args.Filter.Topics
|
topics = *args.Filter.Topics
|
||||||
}
|
}
|
||||||
hash := b.hash
|
|
||||||
if hash == (common.Hash{}) {
|
|
||||||
header, err := b.resolveHeader(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
hash = header.Hash()
|
|
||||||
}
|
|
||||||
// Construct the range filter
|
// Construct the range filter
|
||||||
|
hash, err := b.Hash(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
filter := b.r.filterSystem.NewBlockFilter(hash, addresses, topics)
|
filter := b.r.filterSystem.NewBlockFilter(hash, addresses, topics)
|
||||||
|
|
||||||
// Run the filter and return all the logs
|
// Run the filter and return all the logs
|
||||||
@ -1015,12 +1039,6 @@ func (b *Block) Logs(ctx context.Context, args struct{ Filter BlockFilterCriteri
|
|||||||
func (b *Block) Account(ctx context.Context, args struct {
|
func (b *Block) Account(ctx context.Context, args struct {
|
||||||
Address common.Address
|
Address common.Address
|
||||||
}) (*Account, error) {
|
}) (*Account, error) {
|
||||||
if b.numberOrHash == nil {
|
|
||||||
_, err := b.resolveHeader(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &Account{
|
return &Account{
|
||||||
r: b.r,
|
r: b.r,
|
||||||
address: args.Address,
|
address: args.Address,
|
||||||
@ -1063,12 +1081,6 @@ func (c *CallResult) Status() Long {
|
|||||||
func (b *Block) Call(ctx context.Context, args struct {
|
func (b *Block) Call(ctx context.Context, args struct {
|
||||||
Data ethapi.TransactionArgs
|
Data ethapi.TransactionArgs
|
||||||
}) (*CallResult, error) {
|
}) (*CallResult, error) {
|
||||||
if b.numberOrHash == nil {
|
|
||||||
_, err := b.resolve(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result, err := ethapi.DoCall(ctx, b.r.backend, args.Data, *b.numberOrHash, nil, b.r.backend.RPCEVMTimeout(), b.r.backend.RPCGasCap())
|
result, err := ethapi.DoCall(ctx, b.r.backend, args.Data, *b.numberOrHash, nil, b.r.backend.RPCEVMTimeout(), b.r.backend.RPCGasCap())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -1088,12 +1100,6 @@ func (b *Block) Call(ctx context.Context, args struct {
|
|||||||
func (b *Block) EstimateGas(ctx context.Context, args struct {
|
func (b *Block) EstimateGas(ctx context.Context, args struct {
|
||||||
Data ethapi.TransactionArgs
|
Data ethapi.TransactionArgs
|
||||||
}) (Long, error) {
|
}) (Long, error) {
|
||||||
if b.numberOrHash == nil {
|
|
||||||
_, err := b.resolveHeader(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gas, err := ethapi.DoEstimateGas(ctx, b.r.backend, args.Data, *b.numberOrHash, b.r.backend.RPCGasCap())
|
gas, err := ethapi.DoEstimateGas(ctx, b.r.backend, args.Data, *b.numberOrHash, b.r.backend.RPCGasCap())
|
||||||
return Long(gas), err
|
return Long(gas), err
|
||||||
}
|
}
|
||||||
@ -1173,29 +1179,21 @@ func (r *Resolver) Block(ctx context.Context, args struct {
|
|||||||
Number *Long
|
Number *Long
|
||||||
Hash *common.Hash
|
Hash *common.Hash
|
||||||
}) (*Block, error) {
|
}) (*Block, error) {
|
||||||
var block *Block
|
var numberOrHash rpc.BlockNumberOrHash
|
||||||
if args.Number != nil {
|
if args.Number != nil {
|
||||||
if *args.Number < 0 {
|
if *args.Number < 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
number := rpc.BlockNumber(*args.Number)
|
number := rpc.BlockNumber(*args.Number)
|
||||||
numberOrHash := rpc.BlockNumberOrHashWithNumber(number)
|
numberOrHash = rpc.BlockNumberOrHashWithNumber(number)
|
||||||
block = &Block{
|
|
||||||
r: r,
|
|
||||||
numberOrHash: &numberOrHash,
|
|
||||||
}
|
|
||||||
} else if args.Hash != nil {
|
} else if args.Hash != nil {
|
||||||
numberOrHash := rpc.BlockNumberOrHashWithHash(*args.Hash, false)
|
numberOrHash = rpc.BlockNumberOrHashWithHash(*args.Hash, false)
|
||||||
block = &Block{
|
|
||||||
r: r,
|
|
||||||
numberOrHash: &numberOrHash,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
numberOrHash := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
|
numberOrHash = rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
|
||||||
block = &Block{
|
}
|
||||||
r: r,
|
block := &Block{
|
||||||
numberOrHash: &numberOrHash,
|
r: r,
|
||||||
}
|
numberOrHash: &numberOrHash,
|
||||||
}
|
}
|
||||||
// Resolve the header, return nil if it doesn't exist.
|
// Resolve the header, return nil if it doesn't exist.
|
||||||
// Note we don't resolve block directly here since it will require an
|
// Note we don't resolve block directly here since it will require an
|
||||||
@ -1256,7 +1254,7 @@ func (r *Resolver) Transaction(ctx context.Context, args struct{ Hash common.Has
|
|||||||
hash: args.Hash,
|
hash: args.Hash,
|
||||||
}
|
}
|
||||||
// Resolve the transaction; if it doesn't exist, return nil.
|
// Resolve the transaction; if it doesn't exist, return nil.
|
||||||
t, err := tx.resolve(ctx)
|
t, _, err := tx.resolve(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if t == nil {
|
} else if t == nil {
|
||||||
|
@ -270,7 +270,7 @@ func TestGraphQLHTTPOnSamePort_GQLRequest_Unsuccessful(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGraphQLTransactionLogs(t *testing.T) {
|
func TestGraphQLConcurrentResolvers(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
key, _ = crypto.GenerateKey()
|
key, _ = crypto.GenerateKey()
|
||||||
addr = crypto.PubkeyToAddress(key.PublicKey)
|
addr = crypto.PubkeyToAddress(key.PublicKey)
|
||||||
@ -295,8 +295,9 @@ func TestGraphQLTransactionLogs(t *testing.T) {
|
|||||||
)
|
)
|
||||||
defer stack.Close()
|
defer stack.Close()
|
||||||
|
|
||||||
handler := newGQLService(t, stack, genesis, 1, func(i int, gen *core.BlockGen) {
|
var tx *types.Transaction
|
||||||
tx, _ := types.SignNewTx(key, signer, &types.LegacyTx{To: &dad, Gas: 100000, GasPrice: big.NewInt(params.InitialBaseFee)})
|
handler, chain := newGQLService(t, stack, genesis, 1, func(i int, gen *core.BlockGen) {
|
||||||
|
tx, _ = types.SignNewTx(key, signer, &types.LegacyTx{To: &dad, Gas: 100000, GasPrice: big.NewInt(params.InitialBaseFee)})
|
||||||
gen.AddTx(tx)
|
gen.AddTx(tx)
|
||||||
tx, _ = types.SignNewTx(key, signer, &types.LegacyTx{To: &dad, Nonce: 1, Gas: 100000, GasPrice: big.NewInt(params.InitialBaseFee)})
|
tx, _ = types.SignNewTx(key, signer, &types.LegacyTx{To: &dad, Nonce: 1, Gas: 100000, GasPrice: big.NewInt(params.InitialBaseFee)})
|
||||||
gen.AddTx(tx)
|
gen.AddTx(tx)
|
||||||
@ -307,18 +308,59 @@ func TestGraphQLTransactionLogs(t *testing.T) {
|
|||||||
if err := stack.Start(); err != nil {
|
if err := stack.Start(); err != nil {
|
||||||
t.Fatalf("could not start node: %v", err)
|
t.Fatalf("could not start node: %v", err)
|
||||||
}
|
}
|
||||||
query := `{block { transactions { logs { account { address } } } } }`
|
|
||||||
res := handler.Schema.Exec(context.Background(), query, "", map[string]interface{}{})
|
for i, tt := range []struct {
|
||||||
if res.Errors != nil {
|
body string
|
||||||
t.Fatalf("graphql query failed: %v", res.Errors)
|
want string
|
||||||
}
|
}{
|
||||||
have, err := json.Marshal(res.Data)
|
// Multiple txes race to get/set the block hash.
|
||||||
if err != nil {
|
{
|
||||||
t.Fatalf("failed to encode graphql response: %s", err)
|
body: "{block { transactions { logs { account { address } } } } }",
|
||||||
}
|
want: fmt.Sprintf(`{"block":{"transactions":[{"logs":[{"account":{"address":"%s"}},{"account":{"address":"%s"}}]},{"logs":[{"account":{"address":"%s"}},{"account":{"address":"%s"}}]},{"logs":[{"account":{"address":"%s"}},{"account":{"address":"%s"}}]}]}}`, dadStr, dadStr, dadStr, dadStr, dadStr, dadStr),
|
||||||
want := fmt.Sprintf(`{"block":{"transactions":[{"logs":[{"account":{"address":"%s"}},{"account":{"address":"%s"}}]},{"logs":[{"account":{"address":"%s"}},{"account":{"address":"%s"}}]},{"logs":[{"account":{"address":"%s"}},{"account":{"address":"%s"}}]}]}}`, dadStr, dadStr, dadStr, dadStr, dadStr, dadStr)
|
},
|
||||||
if string(have) != want {
|
// Multiple fields of a tx race to resolve it. Happens in this case
|
||||||
t.Errorf("response unmatch. expected %s, got %s", want, have)
|
// because resolving the tx body belonging to a log is delayed.
|
||||||
|
{
|
||||||
|
body: `{block { logs(filter: {}) { transaction { nonce value gasPrice }}}}`,
|
||||||
|
want: `{"block":{"logs":[{"transaction":{"nonce":"0x0","value":"0x0","gasPrice":"0x3b9aca00"}},{"transaction":{"nonce":"0x0","value":"0x0","gasPrice":"0x3b9aca00"}},{"transaction":{"nonce":"0x1","value":"0x0","gasPrice":"0x3b9aca00"}},{"transaction":{"nonce":"0x1","value":"0x0","gasPrice":"0x3b9aca00"}},{"transaction":{"nonce":"0x2","value":"0x0","gasPrice":"0x3b9aca00"}},{"transaction":{"nonce":"0x2","value":"0x0","gasPrice":"0x3b9aca00"}}]}}`,
|
||||||
|
},
|
||||||
|
// Multiple txes of a block race to set/retrieve receipts of a block.
|
||||||
|
{
|
||||||
|
body: "{block { transactions { status gasUsed } } }",
|
||||||
|
want: `{"block":{"transactions":[{"status":1,"gasUsed":21768},{"status":1,"gasUsed":21768},{"status":1,"gasUsed":21768}]}}`,
|
||||||
|
},
|
||||||
|
// Multiple fields of block race to resolve header and body.
|
||||||
|
{
|
||||||
|
body: "{ block { number hash gasLimit ommerCount transactionCount totalDifficulty } }",
|
||||||
|
want: fmt.Sprintf(`{"block":{"number":1,"hash":"%s","gasLimit":11500000,"ommerCount":0,"transactionCount":3,"totalDifficulty":"0x200000"}}`, chain[len(chain)-1].Hash()),
|
||||||
|
},
|
||||||
|
// Multiple fields of a block race to resolve the header and body.
|
||||||
|
{
|
||||||
|
body: fmt.Sprintf(`{ transaction(hash: "%s") { block { number hash gasLimit ommerCount transactionCount } } }`, tx.Hash()),
|
||||||
|
want: fmt.Sprintf(`{"transaction":{"block":{"number":1,"hash":"%s","gasLimit":11500000,"ommerCount":0,"transactionCount":3}}}`, chain[len(chain)-1].Hash()),
|
||||||
|
},
|
||||||
|
// Account fields race the resolve the state object.
|
||||||
|
{
|
||||||
|
body: fmt.Sprintf(`{ block { account(address: "%s") { balance transactionCount code } } }`, dadStr),
|
||||||
|
want: `{"block":{"account":{"balance":"0x0","transactionCount":"0x0","code":"0x60006000a060006000a060006000f3"}}}`,
|
||||||
|
},
|
||||||
|
// Test values for a non-existent account.
|
||||||
|
{
|
||||||
|
body: fmt.Sprintf(`{ block { account(address: "%s") { balance transactionCount code } } }`, "0x1111111111111111111111111111111111111111"),
|
||||||
|
want: `{"block":{"account":{"balance":"0x0","transactionCount":"0x0","code":"0x"}}}`,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
res := handler.Schema.Exec(context.Background(), tt.body, "", map[string]interface{}{})
|
||||||
|
if res.Errors != nil {
|
||||||
|
t.Fatalf("failed to execute query for testcase #%d: %v", i, res.Errors)
|
||||||
|
}
|
||||||
|
have, err := json.Marshal(res.Data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to encode graphql response for testcase #%d: %s", i, err)
|
||||||
|
}
|
||||||
|
if string(have) != tt.want {
|
||||||
|
t.Errorf("response unmatch for testcase #%d.\nExpected:\n%s\nGot:\n%s\n", i, tt.want, have)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,7 +378,7 @@ func createNode(t *testing.T) *node.Node {
|
|||||||
return stack
|
return stack
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGQLService(t *testing.T, stack *node.Node, gspec *core.Genesis, genBlocks int, genfunc func(i int, gen *core.BlockGen)) *handler {
|
func newGQLService(t *testing.T, stack *node.Node, gspec *core.Genesis, genBlocks int, genfunc func(i int, gen *core.BlockGen)) (*handler, []*types.Block) {
|
||||||
ethConf := ðconfig.Config{
|
ethConf := ðconfig.Config{
|
||||||
Genesis: gspec,
|
Genesis: gspec,
|
||||||
Ethash: ethash.Config{
|
Ethash: ethash.Config{
|
||||||
@ -367,5 +409,5 @@ func newGQLService(t *testing.T, stack *node.Node, gspec *core.Genesis, genBlock
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("could not create graphql service: %v", err)
|
t.Fatalf("could not create graphql service: %v", err)
|
||||||
}
|
}
|
||||||
return handler
|
return handler, chain
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user