From 0279837f40d474dd7a4f9cef4916219937891373 Mon Sep 17 00:00:00 2001 From: Danno Ferrin Date: Thu, 21 Sep 2023 11:30:27 -0600 Subject: [PATCH] Add Cancun GraphQL fields Add the fields for Blobs into the GraphQL service. Signed-off-by: Danno Ferrin --- .../pojoadapter/BlockAdapterBase.java | 13 +- .../pojoadapter/TransactionAdapter.java | 31 +++- .../api/src/main/resources/schema.graphqls | 50 ++++-- .../api/graphql/AbstractDataFetcherTest.java | 14 +- .../api/graphql/BlockDataFetcherTest.java | 49 +++++- .../graphql/TransactionDataFetcherTest.java | 158 ++++++++++++++++++ 6 files changed, 279 insertions(+), 36 deletions(-) create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/TransactionDataFetcherTest.java diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/BlockAdapterBase.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/BlockAdapterBase.java index 4d10aa53a8c..4f976bc60a5 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/BlockAdapterBase.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/BlockAdapterBase.java @@ -15,6 +15,7 @@ package org.hyperledger.besu.ethereum.api.graphql.internal.pojoadapter; import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.BlobGas; import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.api.graphql.GraphQLContextType; @@ -86,7 +87,7 @@ public Bytes32 getReceiptsRoot() { return header.getReceiptsRoot(); } - public AdapterBase getMiner(final DataFetchingEnvironment environment) { + public AccountAdapter getMiner(final DataFetchingEnvironment environment) { final BlockchainQueries query = getBlockchainQueries(environment); long blockNumber = header.getNumber(); @@ -97,7 +98,7 @@ public AdapterBase getMiner(final DataFetchingEnvironment environment) { return query .getAndMapWorldState(blockNumber, ws -> Optional.ofNullable(ws.get(header.getCoinbase()))) - .map(account -> (AdapterBase) new AccountAdapter(account)) + .map(AccountAdapter::new) .orElseGet(() -> new EmptyAccountAdapter(header.getCoinbase())); } @@ -293,4 +294,12 @@ Optional> getWithdrawals(final DataFetchingEnvironment e .getWithdrawals() .map(wl -> wl.stream().map(WithdrawalAdapter::new).toList())); } + + public Optional getBlobGasUsed(final DataFetchingEnvironment environment) { + return header.getBlobGasUsed(); + } + + public Optional getExcessBlobGas(final DataFetchingEnvironment environment) { + return header.getExcessBlobGas().map(BlobGas::toLong); + } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/TransactionAdapter.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/TransactionAdapter.java index db849fdeb5e..6fd242205cd 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/TransactionAdapter.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/TransactionAdapter.java @@ -16,6 +16,7 @@ import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.VersionedHash; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.api.graphql.GraphQLContextType; import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; @@ -119,17 +120,16 @@ public Wei getGasPrice() { return transactionWithMetadata.getTransaction().getGasPrice().orElse(Wei.ZERO); } - public Optional getMaxPriorityFeePerGas() { - return transactionWithMetadata.getTransaction().getMaxPriorityFeePerGas(); - } - public Optional getMaxFeePerGas() { return transactionWithMetadata.getTransaction().getMaxFeePerGas(); } - public Optional getEffectiveGasPrice(final DataFetchingEnvironment environment) { - return getReceipt(environment) - .map(rwm -> rwm.getTransaction().getEffectiveGasPrice(rwm.getBaseFee())); + public Optional getMaxPriorityFeePerGas() { + return transactionWithMetadata.getTransaction().getMaxPriorityFeePerGas(); + } + + public Optional getMaxFeePerBlobGas() { + return transactionWithMetadata.getTransaction().getMaxFeePerBlobGas(); } public Optional getEffectiveTip(final DataFetchingEnvironment environment) { @@ -170,6 +170,19 @@ public Optional getCumulativeGasUsed(final DataFetchingEnvironment environ return getReceipt(environment).map(rpt -> rpt.getReceipt().getCumulativeGasUsed()); } + public Optional getEffectiveGasPrice(final DataFetchingEnvironment environment) { + return getReceipt(environment) + .map(rwm -> rwm.getTransaction().getEffectiveGasPrice(rwm.getBaseFee())); + } + + public Optional getBlobGasUsed(final DataFetchingEnvironment environment) { + return getReceipt(environment).flatMap(TransactionReceiptWithMetadata::getBlobGasUsed); + } + + public Optional getBlobGasPrice(final DataFetchingEnvironment environment) { + return getReceipt(environment).flatMap(TransactionReceiptWithMetadata::getBlobGasPrice); + } + public Optional getCreatedContract(final DataFetchingEnvironment environment) { final boolean contractCreated = transactionWithMetadata.getTransaction().isContractCreation(); if (contractCreated) { @@ -245,4 +258,8 @@ public Optional getRawReceipt(final DataFetchingEnvironment environment) return rlpOutput.encoded(); }); } + + public List getBlobVersionedHashes(final DataFetchingEnvironment environment) { + return transactionWithMetadata.getTransaction().getVersionedHashes().orElse(List.of()); + } } diff --git a/ethereum/api/src/main/resources/schema.graphqls b/ethereum/api/src/main/resources/schema.graphqls index 183bce31e9d..88598202ba4 100644 --- a/ethereum/api/src/main/resources/schema.graphqls +++ b/ethereum/api/src/main/resources/schema.graphqls @@ -189,7 +189,7 @@ type Block { raw: Bytes! """ - WithdrawalsRoot is the keccak256 hash of the root of the trie of withdrawals in this block. + WithdrawalsRoot is the withdrawals trie root in this block. If withdrawals are unavailable for this block, this field will be null. """ withdrawalsRoot: Bytes32 @@ -199,6 +199,14 @@ type Block { withdrawals are unavailable for this block, this field will be null. """ withdrawals: [Withdrawal!] + + """BlobGasUsed is the total amount of gas used by the transactions.""" + blobGasUsed: Long + + """ + ExcessBlobGas is a running total of blob gas consumed in excess of the target, prior to the block. + """ + excessBlobGas: Long } """ @@ -219,11 +227,11 @@ input BlockFilterCriteria { contained topics. Examples: - - [] or nil matches any topic list - - [[A]] matches topic A in first position - - [[], [B]] matches any topic in first position, B in second position - - [[A], [B]] matches topic A in first position, B in second position - - [[A, B]], [C, D]] matches topic (A OR B) in first position, (C OR D) in second position + - [] or nil matches any topic list + - [[A]] matches topic A in first position + - [[], [B]] matches any topic in first position, B in second position + - [[A], [B]] matches topic A in first position, B in second position + - [[A, B]], [C, D]] matches topic (A OR B) in first position, (C OR D) in second position """ topics: [[Bytes32!]!] } @@ -310,11 +318,11 @@ input FilterCriteria { contained topics. Examples: - - [] or nil matches any topic list - - [[A]] matches topic A in first position - - [[], [B]] matches any topic in first position, B in second position - - [[A], [B]] matches topic A in first position, B in second position - - [[A, B]], [C, D]] matches topic (A OR B) in first position, (C OR D) in second position + - [] or nil matches any topic list + - [[A]] matches topic A in first position + - [[], [B]] matches any topic in first position, B in second position + - [[A], [B]] matches topic A in first position, B in second position + - [[A, B]], [C, D]] matches topic (A OR B) in first position, (C OR D) in second position """ topics: [[Bytes32!]!] } @@ -472,6 +480,11 @@ type Transaction { """ maxPriorityFeePerGas: BigInt + """ + MaxFeePerBlobGas is the maximum blob gas fee cap per blob the sender is willing to pay for blob transaction, in wei. + """ + maxFeePerBlobGas: BigInt + """ EffectiveTip is the actual amount of reward going to miner after considering the max fee cap. """ @@ -520,6 +533,14 @@ type Transaction { """ effectiveGasPrice: BigInt + """BlobGasUsed is the amount of blob gas used by this transaction.""" + blobGasUsed: Long + + """ + blobGasPrice is the actual value per blob gas deducted from the senders account. + """ + blobGasPrice: BigInt + """ CreatedContract is the account that was created by a contract creation transaction. If the transaction was not a contract creation transaction, @@ -552,6 +573,11 @@ type Transaction { this is equivalent to TxType || ReceiptEncoding. """ rawReceipt: Bytes! + + """ + BlobVersionedHashes is a set of hash outputs from the blobs in the transaction. + """ + blobVersionedHashes: [Bytes32!] } """EIP-4895""" @@ -564,7 +590,7 @@ type Withdrawal { """Validator is index of the validator associated with withdrawal.""" validator: Long! - """Address recipient of the withdrawn amount.""" + """Recipient address of the withdrawn amount.""" address: Address! """Amount is the withdrawal value in Gwei.""" diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/AbstractDataFetcherTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/AbstractDataFetcherTest.java index 0c304fbc3fd..ebdd92d557a 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/AbstractDataFetcherTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/AbstractDataFetcherTest.java @@ -14,16 +14,15 @@ */ package org.hyperledger.besu.ethereum.api.graphql; -import org.hyperledger.besu.ethereum.api.graphql.internal.pojoadapter.NormalBlockAdapter; import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.TransactionReceipt; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; -import java.util.Optional; import java.util.Set; import graphql.GraphQLContext; -import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import org.junit.jupiter.api.BeforeEach; import org.mockito.Mock; @@ -31,7 +30,7 @@ public abstract class AbstractDataFetcherTest { - DataFetcher> fetcher; + GraphQLDataFetchers fetchers; @Mock protected Set supportedCapabilities; @@ -43,10 +42,13 @@ public abstract class AbstractDataFetcherTest { @Mock protected BlockHeader header; + @Mock protected Transaction transaction; + + @Mock protected TransactionReceipt transactionReceipt; + @BeforeEach public void before() { - final GraphQLDataFetchers fetchers = new GraphQLDataFetchers(supportedCapabilities); - fetcher = fetchers.getBlockDataFetcher(); + fetchers = new GraphQLDataFetchers(supportedCapabilities); Mockito.when(environment.getGraphQlContext()).thenReturn(graphQLContext); } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/BlockDataFetcherTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/BlockDataFetcherTest.java index 58689a18fdd..782ddb4c971 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/BlockDataFetcherTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/BlockDataFetcherTest.java @@ -19,25 +19,36 @@ import static org.mockito.Mockito.when; import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.BlobGas; import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.Wei; -import org.hyperledger.besu.ethereum.api.graphql.internal.pojoadapter.EmptyAccountAdapter; import org.hyperledger.besu.ethereum.api.graphql.internal.pojoadapter.NormalBlockAdapter; import org.hyperledger.besu.ethereum.api.query.BlockWithMetadata; import java.util.Optional; +import graphql.schema.DataFetcher; import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentMatchers; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -public class BlockDataFetcherTest extends AbstractDataFetcherTest { +class BlockDataFetcherTest extends AbstractDataFetcherTest { + + private DataFetcher> fetcher; + + @BeforeEach + @Override + public void before() { + super.before(); + fetcher = fetchers.getBlockDataFetcher(); + } @Test - public void bothNumberAndHashThrows() { + void bothNumberAndHashThrows() { final Hash fakedHash = Hash.hash(Bytes.of(1)); when(environment.getArgument("number")).thenReturn(1L); when(environment.getArgument("hash")).thenReturn(fakedHash); @@ -46,7 +57,7 @@ public void bothNumberAndHashThrows() { } @Test - public void onlyNumber() throws Exception { + void onlyNumber() throws Exception { when(environment.getArgument("number")).thenReturn(1L); when(environment.getArgument("hash")).thenReturn(null); @@ -56,11 +67,11 @@ public void onlyNumber() throws Exception { when(query.blockByNumber(ArgumentMatchers.anyLong())) .thenReturn(Optional.of(new BlockWithMetadata<>(null, null, null, null, 0))); - fetcher.get(environment); + assertThat(fetcher.get(environment)).isNotEmpty(); } @Test - public void ibftMiner() throws Exception { + void ibftMiner() throws Exception { // IBFT can mine blocks with a coinbase that is an empty account, hence not stored and returned // as null. The compromise is to report zeros and empty on query from a block. final Address testAddress = Address.fromHexString("0xdeadbeef"); @@ -77,9 +88,29 @@ public void ibftMiner() throws Exception { final Optional maybeBlock = fetcher.get(environment); assertThat(maybeBlock).isPresent(); assertThat(maybeBlock.get().getMiner(environment)).isNotNull(); - assertThat(((EmptyAccountAdapter) maybeBlock.get().getMiner(environment)).getBalance()) + assertThat(maybeBlock.get().getMiner(environment).getBalance()) .isGreaterThanOrEqualTo(Wei.ZERO); - assertThat(((EmptyAccountAdapter) maybeBlock.get().getMiner(environment)).getAddress()) - .isEqualTo(testAddress); + assertThat(maybeBlock.get().getMiner(environment).getAddress()).isEqualTo(testAddress); + } + + @Test + void blobData() throws Exception { + final long blobGasUsed = 0xb10b6a5; + final long excessBlobGas = 0xce556a5; + + when(environment.getGraphQlContext()).thenReturn(graphQLContext); + when(environment.getArgument("number")).thenReturn(1L); + when(environment.getArgument("hash")).thenReturn(null); + + when(graphQLContext.get(GraphQLContextType.BLOCKCHAIN_QUERIES)).thenReturn(query); + when(query.blockByNumber(ArgumentMatchers.anyLong())) + .thenReturn(Optional.of(new BlockWithMetadata<>(header, null, null, null, 0))); + when(header.getBlobGasUsed()).thenReturn(Optional.of(blobGasUsed)); + when(header.getExcessBlobGas()).thenReturn(Optional.of(BlobGas.of(excessBlobGas))); + + final Optional maybeBlock = fetcher.get(environment); + assertThat(maybeBlock).isPresent(); + assertThat(maybeBlock.get().getBlobGasUsed(environment)).contains(blobGasUsed); + assertThat(maybeBlock.get().getExcessBlobGas(environment)).contains(excessBlobGas); } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/TransactionDataFetcherTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/TransactionDataFetcherTest.java new file mode 100644 index 00000000000..ea487f9c31b --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/TransactionDataFetcherTest.java @@ -0,0 +1,158 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.api.graphql; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.VersionedHash; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.api.graphql.internal.pojoadapter.TransactionAdapter; +import org.hyperledger.besu.ethereum.api.query.TransactionReceiptWithMetadata; +import org.hyperledger.besu.ethereum.api.query.TransactionWithMetadata; + +import java.util.List; +import java.util.Optional; + +import graphql.schema.DataFetcher; +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class TransactionDataFetcherTest extends AbstractDataFetcherTest { + + private DataFetcher> fetcher; + final Hash fakedHash = Hash.hash(Bytes.fromBase64String("ThisIsAFakeHash")); + final VersionedHash fakeVersionedHash = + new VersionedHash(VersionedHash.SHA256_VERSION_ID, fakedHash); + final long blobGasUsed = 127 * 1024L; + final Wei blobGasPrice = Wei.of(128); + final Wei maxFeePerBlobGas = Wei.of(1280); + + @BeforeEach + @Override + public void before() { + super.before(); + fetcher = fetchers.getTransactionDataFetcher(); + } + + @Test + void emptyBlobs() throws Exception { + when(environment.getArgument("hash")).thenReturn(fakedHash); + + when(graphQLContext.get(GraphQLContextType.BLOCKCHAIN_QUERIES)).thenReturn(query); + + TransactionWithMetadata transactionWithMetadata = new TransactionWithMetadata(transaction); + when(query.transactionByHash(any())).thenReturn(Optional.of(transactionWithMetadata)); + when(transaction.getVersionedHashes()).thenReturn(Optional.empty()); + when(transaction.getMaxFeePerBlobGas()).thenReturn(Optional.empty()); + + TransactionReceiptWithMetadata transactionReceiptWithMetadata = + TransactionReceiptWithMetadata.create( + transactionReceipt, + transaction, + fakedHash, + 0, + 21000, + Optional.empty(), + fakedHash, + 1, + Optional.empty(), + Optional.empty()); + when(query.transactionReceiptByTransactionHash(any(), any())) + .thenReturn(Optional.of(transactionReceiptWithMetadata)); + + var transactionData = fetcher.get(environment); + assertThat(transactionData).isPresent(); + assertThat(transactionData.get().getBlobVersionedHashes(environment)).isEmpty(); + assertThat(transactionData.get().getBlobGasUsed(environment)).isEmpty(); + assertThat(transactionData.get().getBlobGasPrice(environment)).isEmpty(); + assertThat(transactionData.get().getMaxFeePerBlobGas()).isEmpty(); + } + + @Test + void hasZeroBlobs() throws Exception { + when(environment.getArgument("hash")).thenReturn(fakedHash); + + when(graphQLContext.get(GraphQLContextType.BLOCKCHAIN_QUERIES)).thenReturn(query); + + TransactionWithMetadata transactionWithMetadata = new TransactionWithMetadata(transaction); + when(query.transactionByHash(any())).thenReturn(Optional.of(transactionWithMetadata)); + when(transaction.getVersionedHashes()).thenReturn(Optional.of(List.of())); + when(transaction.getMaxFeePerBlobGas()).thenReturn(Optional.of(Wei.ZERO)); + + TransactionReceiptWithMetadata transactionReceiptWithMetadata = + TransactionReceiptWithMetadata.create( + transactionReceipt, + transaction, + fakedHash, + 0, + 21000, + Optional.empty(), + fakedHash, + 1, + Optional.of(0L), + Optional.of(Wei.ZERO)); + when(query.transactionReceiptByTransactionHash(any(), any())) + .thenReturn(Optional.of(transactionReceiptWithMetadata)); + + var transactionData = fetcher.get(environment); + assertThat(transactionData).isPresent(); + assertThat(transactionData.get().getBlobVersionedHashes(environment)).isEmpty(); + assertThat(transactionData.get().getBlobGasUsed(environment)).contains(0L); + assertThat(transactionData.get().getBlobGasPrice(environment)).contains(Wei.ZERO); + assertThat(transactionData.get().getMaxFeePerBlobGas()).contains(Wei.ZERO); + } + + @Test + void hasOneBlob() throws Exception { + when(environment.getArgument("hash")).thenReturn(fakedHash); + + when(graphQLContext.get(GraphQLContextType.BLOCKCHAIN_QUERIES)).thenReturn(query); + + TransactionWithMetadata transactionWithMetadata = new TransactionWithMetadata(transaction); + when(query.transactionByHash(any())).thenReturn(Optional.of(transactionWithMetadata)); + when(transaction.getVersionedHashes()).thenReturn(Optional.of(List.of(fakeVersionedHash))); + when(transaction.getMaxFeePerBlobGas()).thenReturn(Optional.of(maxFeePerBlobGas)); + + TransactionReceiptWithMetadata transactionReceiptWithMetadata = + TransactionReceiptWithMetadata.create( + transactionReceipt, + transaction, + fakedHash, + 0, + 21000, + Optional.empty(), + fakedHash, + 1, + Optional.of(blobGasUsed), + Optional.of(blobGasPrice)); + when(query.transactionReceiptByTransactionHash(any(), any())) + .thenReturn(Optional.of(transactionReceiptWithMetadata)); + + var transactionData = fetcher.get(environment); + assertThat(transactionData).isPresent(); + assertThat(transactionData.get().getBlobVersionedHashes(environment)) + .containsExactly(fakeVersionedHash); + assertThat(transactionData.get().getBlobGasUsed(environment)).contains(blobGasUsed); + assertThat(transactionData.get().getBlobGasPrice(environment)).contains(blobGasPrice); + assertThat(transactionData.get().getMaxFeePerBlobGas()).contains(maxFeePerBlobGas); + } +}