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

graphql: implement withdrawals (EIP-4895) #27072

Merged
merged 3 commits into from Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
58 changes: 58 additions & 0 deletions graphql/graphql.go
Expand Up @@ -183,6 +183,31 @@ func (at *AccessTuple) StorageKeys(ctx context.Context) []common.Hash {
return at.storageKeys
}

// Withdrawal represents a withdrawal of value from the beacon chain
// by a validator. For details see EIP-4895.
type Withdrawal struct {
index uint64
validator uint64
address common.Address
amount uint64
}

func (w *Withdrawal) Index(ctx context.Context) hexutil.Uint64 {
return hexutil.Uint64(w.index)
}

func (w *Withdrawal) Validator(ctx context.Context) hexutil.Uint64 {
return hexutil.Uint64(w.validator)
}

func (w *Withdrawal) Address(ctx context.Context) common.Address {
return w.address
}

func (w *Withdrawal) Amount(ctx context.Context) hexutil.Uint64 {
return hexutil.Uint64(w.amount)
}

// Transaction represents an Ethereum transaction.
// backend and hash are mandatory; all others will be fetched when required.
type Transaction struct {
Expand Down Expand Up @@ -966,6 +991,39 @@ func (b *Block) OmmerAt(ctx context.Context, args struct{ Index Long }) (*Block,
}, nil
}

func (b *Block) WithdrawalsRoot(ctx context.Context) (*common.Hash, error) {
header, err := b.resolveHeader(ctx)
if err != nil {
return nil, err
}
// Pre-shanghai blocks
if header.WithdrawalsHash == nil {
return nil, nil
}
return header.WithdrawalsHash, nil
}

func (b *Block) Withdrawals(ctx context.Context) (*[]*Withdrawal, error) {
block, err := b.resolve(ctx)
if err != nil || block == nil {
return nil, err
}
// Pre-shanghai blocks
if block.Header().WithdrawalsHash == nil {
return nil, nil
}
ret := make([]*Withdrawal, 0, len(block.Withdrawals()))
for _, w := range block.Withdrawals() {
ret = append(ret, &Withdrawal{
index: w.Index,
validator: w.Validator,
address: w.Address,
amount: w.Amount,
})
}
return &ret, nil
}

// BlockFilterCriteria encapsulates criteria passed to a `logs` accessor inside
// a block.
type BlockFilterCriteria struct {
Expand Down
81 changes: 76 additions & 5 deletions graphql/graphql_test.go
Expand Up @@ -28,6 +28,8 @@ import (
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/beacon"
"github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
Expand Down Expand Up @@ -67,7 +69,7 @@ func TestGraphQLBlockSerialization(t *testing.T) {
GasLimit: 11500000,
Difficulty: big.NewInt(1048576),
}
newGQLService(t, stack, genesis, 10, func(i int, gen *core.BlockGen) {})
newGQLService(t, stack, false, genesis, 10, func(i int, gen *core.BlockGen) {})
// start node
if err := stack.Start(); err != nil {
t.Fatalf("could not start node: %v", err)
Expand Down Expand Up @@ -191,7 +193,7 @@ func TestGraphQLBlockSerializationEIP2718(t *testing.T) {
BaseFee: big.NewInt(params.InitialBaseFee),
}
signer := types.LatestSigner(genesis.Config)
newGQLService(t, stack, genesis, 1, func(i int, gen *core.BlockGen) {
newGQLService(t, stack, false, genesis, 1, func(i int, gen *core.BlockGen) {
gen.SetCoinbase(common.Address{1})
tx, _ := types.SignNewTx(key, signer, &types.LegacyTx{
Nonce: uint64(0),
Expand Down Expand Up @@ -292,7 +294,7 @@ func TestGraphQLConcurrentResolvers(t *testing.T) {
defer stack.Close()

var tx *types.Transaction
handler, chain := newGQLService(t, stack, genesis, 1, func(i int, gen *core.BlockGen) {
handler, chain := newGQLService(t, stack, false, 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)})
Expand Down Expand Up @@ -360,6 +362,66 @@ func TestGraphQLConcurrentResolvers(t *testing.T) {
}
}

func TestWithdrawals(t *testing.T) {
var (
key, _ = crypto.GenerateKey()
addr = crypto.PubkeyToAddress(key.PublicKey)

genesis = &core.Genesis{
Config: params.AllEthashProtocolChanges,
GasLimit: 11500000,
Difficulty: common.Big1,
Alloc: core.GenesisAlloc{
addr: {Balance: big.NewInt(params.Ether)},
},
}
signer = types.LatestSigner(genesis.Config)
stack = createNode(t)
)
defer stack.Close()

handler, _ := newGQLService(t, stack, true, genesis, 1, func(i int, gen *core.BlockGen) {
tx, _ := types.SignNewTx(key, signer, &types.LegacyTx{To: &common.Address{}, Gas: 100000, GasPrice: big.NewInt(params.InitialBaseFee)})
gen.AddTx(tx)
gen.AddWithdrawal(&types.Withdrawal{
Validator: 5,
Address: common.Address{},
Amount: 10,
})
})
// start node
if err := stack.Start(); err != nil {
t.Fatalf("could not start node: %v", err)
}

for i, tt := range []struct {
body string
want string
}{
// Genesis block has no withdrawals.
{
body: "{block(number: 0) { withdrawalsRoot withdrawals { index } } }",
want: `{"block":{"withdrawalsRoot":null,"withdrawals":null}}`,
},
{
body: "{block(number: 1) { withdrawalsRoot withdrawals { validator amount } } }",
want: `{"block":{"withdrawalsRoot":"0x8418fc1a48818928f6692f148e9b10e99a88edc093b095cb8ca97950284b553d","withdrawals":[{"validator":"0x5","amount":"0xa"}]}}`,
},
} {
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.\nhave:\n%s\nwant:\n%s", i, have, tt.want)
}
}
}

func createNode(t *testing.T) *node.Node {
stack, err := node.New(&node.Config{
HTTPHost: "127.0.0.1",
Expand All @@ -374,7 +436,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, []*types.Block) {
func newGQLService(t *testing.T, stack *node.Node, shanghai bool, gspec *core.Genesis, genBlocks int, genfunc func(i int, gen *core.BlockGen)) (*handler, []*types.Block) {
ethConf := &ethconfig.Config{
Genesis: gspec,
Ethash: ethash.Config{
Expand All @@ -392,9 +454,18 @@ func newGQLService(t *testing.T, stack *node.Node, gspec *core.Genesis, genBlock
if err != nil {
t.Fatalf("could not create eth backend: %v", err)
}
var engine consensus.Engine = ethash.NewFaker()
if shanghai {
engine = beacon.NewFaker()
chainCfg := gspec.Config
chainCfg.TerminalTotalDifficultyPassed = true
chainCfg.TerminalTotalDifficulty = common.Big0
shanghaiTime := uint64(0)
chainCfg.ShanghaiTime = &shanghaiTime
}
// Create some blocks and import them
chain, _ := core.GenerateChain(params.AllEthashProtocolChanges, ethBackend.BlockChain().Genesis(),
ethash.NewFaker(), ethBackend.ChainDb(), genBlocks, genfunc)
engine, ethBackend.ChainDb(), genBlocks, genfunc)
_, err = ethBackend.BlockChain().InsertChain(chain)
if err != nil {
t.Fatalf("could not create import blocks: %v", err)
Expand Down
18 changes: 18 additions & 0 deletions graphql/schema.go
Expand Up @@ -77,6 +77,18 @@ const schema string = `
storageKeys : [Bytes32!]!
}
# EIP-4895
type Withdrawal {
# Index is a monotonically increasing identifier issued by consensus layer.
index: Long!
# Validator is index of the validator associated with withdrawal.
validator: Long!
# Recipient address of the withdrawn amount.
address: Address!
# Amount is the withdrawal value in Gwei.
amount: Long!
}
# Transaction is an Ethereum transaction.
type Transaction {
# Hash is the hash of this transaction.
Expand Down Expand Up @@ -248,6 +260,12 @@ const schema string = `
rawHeader: Bytes!
# Raw is the RLP encoding of the block.
raw: Bytes!
# WithdrawalsRoot is the withdrawals trie root in this block.
# If withdrawals are unavailable for this block, this field will be null.
withdrawalsRoot: Bytes32
# Withdrawals is a list of withdrawals associated with this block. If
# withdrawals are unavailable for this block, this field will be null.
withdrawals: [Withdrawal!]
}
# CallData represents the data associated with a local contract call.
Expand Down