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:
Sina Mahmoodi 2023-03-28 09:08:10 +02:00 committed by GitHub
parent fb8a3aaf1e
commit a236e03d00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 212 additions and 172 deletions

@ -24,6 +24,7 @@ import (
"math/big"
"sort"
"strconv"
"sync"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
@ -80,12 +81,26 @@ type Account struct {
r *Resolver
address common.Address
blockNrOrHash rpc.BlockNumberOrHash
state *state.StateDB
mu sync.Mutex
}
// getState fetches the StateDB object for an account.
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)
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) {
@ -184,32 +199,39 @@ func (at *AccessTuple) StorageKeys(ctx context.Context) []common.Hash {
// Transaction represents an Ethereum transaction.
// backend and hash are mandatory; all others will be fetched when required.
type Transaction struct {
r *Resolver
hash common.Hash
r *Resolver
hash common.Hash // Must be present after initialization
mu sync.Mutex
// mu protects following resources
tx *types.Transaction
block *Block
index uint64
}
// resolve returns the internal transaction object, fetching it if needed.
func (t *Transaction) resolve(ctx context.Context) (*types.Transaction, error) {
if 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,
}
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)
// It also returns the block the tx blongs to, unless it is a pending tx.
func (t *Transaction) resolve(ctx context.Context) (*types.Transaction, *Block, error) {
t.mu.Lock()
defer t.mu.Unlock()
if t.tx != nil {
return t.tx, t.block, nil
}
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 {
@ -217,7 +239,7 @@ func (t *Transaction) Hash(ctx context.Context) common.Hash {
}
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 {
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) {
tx, err := t.resolve(ctx)
tx, _, err := t.resolve(ctx)
if err != nil || tx == nil {
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) {
tx, err := t.resolve(ctx)
tx, block, err := t.resolve(ctx)
if err != nil || tx == nil {
return hexutil.Big{}, err
}
@ -241,8 +263,8 @@ func (t *Transaction) GasPrice(ctx context.Context) (hexutil.Big, error) {
case types.AccessListTxType:
return hexutil.Big(*tx.GasPrice()), nil
case types.DynamicFeeTxType:
if t.block != nil {
if baseFee, _ := t.block.BaseFeePerGas(ctx); baseFee != nil {
if block != nil {
if baseFee, _ := block.BaseFeePerGas(ctx); baseFee != nil {
// price = min(tip, gasFeeCap - baseFee) + baseFee
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) {
tx, err := t.resolve(ctx)
tx, block, err := t.resolve(ctx)
if err != nil || tx == nil {
return nil, err
}
// Pending tx
if t.block == nil {
if block == nil {
return nil, nil
}
header, err := t.block.resolveHeader(ctx)
header, err := block.resolveHeader(ctx)
if err != nil || header == nil {
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) {
tx, err := t.resolve(ctx)
tx, _, err := t.resolve(ctx)
if err != nil || tx == nil {
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) {
tx, err := t.resolve(ctx)
tx, _, err := t.resolve(ctx)
if err != nil || tx == nil {
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) {
tx, err := t.resolve(ctx)
tx, block, err := t.resolve(ctx)
if err != nil || tx == nil {
return nil, err
}
// Pending tx
if t.block == nil {
if block == nil {
return nil, nil
}
header, err := t.block.resolveHeader(ctx)
header, err := block.resolveHeader(ctx)
if err != nil || header == nil {
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) {
tx, err := t.resolve(ctx)
tx, _, err := t.resolve(ctx)
if err != nil || tx == nil {
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) {
tx, err := t.resolve(ctx)
tx, _, err := t.resolve(ctx)
if err != nil || tx == nil {
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) {
tx, err := t.resolve(ctx)
tx, _, err := t.resolve(ctx)
if err != nil || tx == nil {
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) {
tx, err := t.resolve(ctx)
tx, _, err := t.resolve(ctx)
if err != nil || tx == nil {
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) {
if _, err := t.resolve(ctx); err != nil {
_, block, err := t.resolve(ctx)
if err != nil {
return nil, err
}
return t.block, nil
return block, nil
}
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
}
if t.block == nil {
// Pending tx
if block == nil {
return nil, nil
}
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.
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
}
if t.block == nil {
// Pending tx
if block == nil {
return nil, nil
}
receipts, err := t.block.resolveReceipts(ctx)
receipts, err := block.resolveReceipts(ctx)
if err != nil {
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) {
if _, err := t.resolve(ctx); err != nil {
_, block, err := t.resolve(ctx)
if err != nil {
return nil, err
}
if t.block == nil {
// Pending tx
if block == nil {
return nil, nil
}
if _, ok := t.block.numberOrHash.Hash(); !ok {
header, err := t.r.backend.HeaderByNumberOrHash(ctx, *t.block.numberOrHash)
if err != nil {
return nil, err
}
hash := header.Hash()
t.block.numberOrHash.BlockHash = &hash
h, err := block.Hash(ctx)
if err != nil {
return nil, err
}
return t.getLogs(ctx)
return t.getLogs(ctx, h)
}
// getLogs returns log objects for the given tx.
// 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 (
hash, _ = t.block.numberOrHash.Hash()
filter = t.r.filterSystem.NewBlockFilter(hash, nil, nil)
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) {
tx, err := t.resolve(ctx)
tx, _, err := t.resolve(ctx)
if err != nil {
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) {
tx, err := t.resolve(ctx)
tx, _, err := t.resolve(ctx)
if err != nil || tx == nil {
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) {
tx, err := t.resolve(ctx)
tx, _, err := t.resolve(ctx)
if err != nil || tx == nil {
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) {
tx, err := t.resolve(ctx)
tx, _, err := t.resolve(ctx)
if err != nil || tx == nil {
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) {
tx, err := t.resolve(ctx)
tx, _, err := t.resolve(ctx)
if err != nil || tx == nil {
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) {
tx, err := t.resolve(ctx)
tx, _, err := t.resolve(ctx)
if err != nil || tx == nil {
return hexutil.Bytes{}, err
}
@ -568,16 +592,20 @@ type BlockType int
// when required.
type Block struct {
r *Resolver
numberOrHash *rpc.BlockNumberOrHash
hash common.Hash
header *types.Header
block *types.Block
receipts []*types.Receipt
numberOrHash *rpc.BlockNumberOrHash // Field resolvers assume numberOrHash is always present
mu sync.Mutex
// mu protects following resources
hash common.Hash // Must be resolved during initialization
header *types.Header
block *types.Block
receipts []*types.Receipt
}
// resolve returns the internal Block object representing this block, fetching
// it if necessary.
func (b *Block) resolve(ctx context.Context) (*types.Block, error) {
b.mu.Lock()
defer b.mu.Unlock()
if b.block != nil {
return b.block, nil
}
@ -587,10 +615,10 @@ func (b *Block) resolve(ctx context.Context) (*types.Block, error) {
}
var err error
b.block, err = b.r.backend.BlockByNumberOrHash(ctx, *b.numberOrHash)
if b.block != nil && b.header == nil {
b.header = b.block.Header()
if hash, ok := b.numberOrHash.Hash(); ok {
b.hash = hash
if b.block != nil {
b.hash = b.block.Hash()
if b.header == nil {
b.header = b.block.Header()
}
}
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
// additional data (transactions and uncles).
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{}) {
return nil, errBlockInvariant
}
var err error
if b.header == nil {
if b.hash != (common.Hash{}) {
b.header, err = b.r.backend.HeaderByHash(ctx, b.hash)
} else {
b.header, err = b.r.backend.HeaderByNumberOrHash(ctx, *b.numberOrHash)
}
b.header, err = b.r.backend.HeaderByNumberOrHash(ctx, *b.numberOrHash)
if err != nil {
return nil, err
}
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
// if necessary.
func (b *Block) resolveReceipts(ctx context.Context) ([]*types.Receipt, error) {
if b.receipts == nil {
hash := b.hash
if hash == (common.Hash{}) {
header, err := b.resolveHeader(ctx)
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
b.mu.Lock()
defer b.mu.Unlock()
if b.receipts != nil {
return b.receipts, nil
}
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) {
@ -645,13 +673,8 @@ func (b *Block) Number(ctx context.Context) (Long, error) {
}
func (b *Block) Hash(ctx context.Context) (common.Hash, error) {
if b.hash == (common.Hash{}) {
header, err := b.resolveHeader(ctx)
if err != nil {
return common.Hash{}, err
}
b.hash = header.Hash()
}
b.mu.Lock()
defer b.mu.Unlock()
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 {
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{
r: b.r,
numberOrHash: &num,
hash: b.header.ParentHash,
numberOrHash: &numOrHash,
hash: hash,
}, nil
}
@ -798,6 +828,7 @@ func (b *Block) Ommers(ctx context.Context) (*[]*Block, error) {
r: b.r,
numberOrHash: &blockNumberOrHash,
header: uncle,
hash: uncle.Hash(),
})
}
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) {
h := b.hash
if h == (common.Hash{}) {
header, err := b.resolveHeader(ctx)
if err != nil {
return hexutil.Big{}, err
}
h = header.Hash()
hash, err := b.Hash(ctx)
if err != nil {
return hexutil.Big{}, err
}
td := b.r.backend.GetTd(ctx, h)
td := b.r.backend.GetTd(ctx, hash)
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
}
@ -948,6 +975,7 @@ func (b *Block) OmmerAt(ctx context.Context, args struct{ Index int32 }) (*Block
r: b.r,
numberOrHash: &blockNumberOrHash,
header: uncle,
hash: uncle.Hash(),
}, nil
}
@ -997,15 +1025,11 @@ func (b *Block) Logs(ctx context.Context, args struct{ Filter BlockFilterCriteri
if args.Filter.Topics != nil {
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
hash, err := b.Hash(ctx)
if err != nil {
return nil, err
}
filter := b.r.filterSystem.NewBlockFilter(hash, addresses, topics)
// 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 {
Address common.Address
}) (*Account, error) {
if b.numberOrHash == nil {
_, err := b.resolveHeader(ctx)
if err != nil {
return nil, err
}
}
return &Account{
r: b.r,
address: args.Address,
@ -1063,12 +1081,6 @@ func (c *CallResult) Status() Long {
func (b *Block) Call(ctx context.Context, args struct {
Data ethapi.TransactionArgs
}) (*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())
if err != nil {
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 {
Data ethapi.TransactionArgs
}) (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())
return Long(gas), err
}
@ -1173,29 +1179,21 @@ func (r *Resolver) Block(ctx context.Context, args struct {
Number *Long
Hash *common.Hash
}) (*Block, error) {
var block *Block
var numberOrHash rpc.BlockNumberOrHash
if args.Number != nil {
if *args.Number < 0 {
return nil, nil
}
number := rpc.BlockNumber(*args.Number)
numberOrHash := rpc.BlockNumberOrHashWithNumber(number)
block = &Block{
r: r,
numberOrHash: &numberOrHash,
}
numberOrHash = rpc.BlockNumberOrHashWithNumber(number)
} else if args.Hash != nil {
numberOrHash := rpc.BlockNumberOrHashWithHash(*args.Hash, false)
block = &Block{
r: r,
numberOrHash: &numberOrHash,
}
numberOrHash = rpc.BlockNumberOrHashWithHash(*args.Hash, false)
} else {
numberOrHash := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
block = &Block{
r: r,
numberOrHash: &numberOrHash,
}
numberOrHash = rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
}
block := &Block{
r: r,
numberOrHash: &numberOrHash,
}
// Resolve the header, return nil if it doesn't exist.
// 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,
}
// Resolve the transaction; if it doesn't exist, return nil.
t, err := tx.resolve(ctx)
t, _, err := tx.resolve(ctx)
if err != nil {
return nil, err
} else if t == nil {

@ -270,7 +270,7 @@ func TestGraphQLHTTPOnSamePort_GQLRequest_Unsuccessful(t *testing.T) {
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
}
func TestGraphQLTransactionLogs(t *testing.T) {
func TestGraphQLConcurrentResolvers(t *testing.T) {
var (
key, _ = crypto.GenerateKey()
addr = crypto.PubkeyToAddress(key.PublicKey)
@ -295,8 +295,9 @@ func TestGraphQLTransactionLogs(t *testing.T) {
)
defer stack.Close()
handler := 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)})
var tx *types.Transaction
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)
tx, _ = types.SignNewTx(key, signer, &types.LegacyTx{To: &dad, Nonce: 1, Gas: 100000, GasPrice: big.NewInt(params.InitialBaseFee)})
gen.AddTx(tx)
@ -307,18 +308,59 @@ func TestGraphQLTransactionLogs(t *testing.T) {
if err := stack.Start(); err != nil {
t.Fatalf("could not start node: %v", err)
}
query := `{block { transactions { logs { account { address } } } } }`
res := handler.Schema.Exec(context.Background(), query, "", map[string]interface{}{})
if res.Errors != nil {
t.Fatalf("graphql query failed: %v", res.Errors)
}
have, err := json.Marshal(res.Data)
if err != nil {
t.Fatalf("failed to encode graphql response: %s", err)
}
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 {
t.Errorf("response unmatch. expected %s, got %s", want, have)
for i, tt := range []struct {
body string
want string
}{
// Multiple txes race to get/set the block hash.
{
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),
},
// Multiple fields of a tx race to resolve it. Happens in this case
// 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
}
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 := &ethconfig.Config{
Genesis: gspec,
Ethash: ethash.Config{
@ -367,5 +409,5 @@ func newGQLService(t *testing.T, stack *node.Node, gspec *core.Genesis, genBlock
if err != nil {
t.Fatalf("could not create graphql service: %v", err)
}
return handler
return handler, chain
}