Skip to content

Commit

Permalink
Add Cancun GraphQL fields (#5923)
Browse files Browse the repository at this point in the history
Add the fields for Blobs into the GraphQL service.

Signed-off-by: Danno Ferrin <[email protected]>
  • Loading branch information
shemnon authored Sep 28, 2023
1 parent 8980835 commit 4f49ec9
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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()));
}

Expand Down Expand Up @@ -293,4 +294,12 @@ Optional<List<WithdrawalAdapter>> getWithdrawals(final DataFetchingEnvironment e
.getWithdrawals()
.map(wl -> wl.stream().map(WithdrawalAdapter::new).toList()));
}

public Optional<Long> getBlobGasUsed() {
return header.getBlobGasUsed();
}

public Optional<Long> getExcessBlobGas() {
return header.getExcessBlobGas().map(BlobGas::toLong);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import org.apache.tuweni.bytes.Bytes;

@SuppressWarnings("unused") // reflected by GraphQL
class CallResult {
public class CallResult {
private final Long status;
private final Long gasUsed;
private final Bytes data;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -119,17 +120,16 @@ public Wei getGasPrice() {
return transactionWithMetadata.getTransaction().getGasPrice().orElse(Wei.ZERO);
}

public Optional<Wei> getMaxPriorityFeePerGas() {
return transactionWithMetadata.getTransaction().getMaxPriorityFeePerGas();
}

public Optional<Wei> getMaxFeePerGas() {
return transactionWithMetadata.getTransaction().getMaxFeePerGas();
}

public Optional<Wei> getEffectiveGasPrice(final DataFetchingEnvironment environment) {
return getReceipt(environment)
.map(rwm -> rwm.getTransaction().getEffectiveGasPrice(rwm.getBaseFee()));
public Optional<Wei> getMaxPriorityFeePerGas() {
return transactionWithMetadata.getTransaction().getMaxPriorityFeePerGas();
}

public Optional<Wei> getMaxFeePerBlobGas() {
return transactionWithMetadata.getTransaction().getMaxFeePerBlobGas();
}

public Optional<Wei> getEffectiveTip(final DataFetchingEnvironment environment) {
Expand Down Expand Up @@ -170,6 +170,19 @@ public Optional<Long> getCumulativeGasUsed(final DataFetchingEnvironment environ
return getReceipt(environment).map(rpt -> rpt.getReceipt().getCumulativeGasUsed());
}

public Optional<Wei> getEffectiveGasPrice(final DataFetchingEnvironment environment) {
return getReceipt(environment)
.map(rwm -> rwm.getTransaction().getEffectiveGasPrice(rwm.getBaseFee()));
}

public Optional<Long> getBlobGasUsed(final DataFetchingEnvironment environment) {
return getReceipt(environment).flatMap(TransactionReceiptWithMetadata::getBlobGasUsed);
}

public Optional<Wei> getBlobGasPrice(final DataFetchingEnvironment environment) {
return getReceipt(environment).flatMap(TransactionReceiptWithMetadata::getBlobGasPrice);
}

public Optional<AccountAdapter> getCreatedContract(final DataFetchingEnvironment environment) {
final boolean contractCreated = transactionWithMetadata.getTransaction().isContractCreation();
if (contractCreated) {
Expand Down Expand Up @@ -245,4 +258,8 @@ public Optional<Bytes> getRawReceipt(final DataFetchingEnvironment environment)
return rlpOutput.encoded();
});
}

public List<VersionedHash> getBlobVersionedHashes() {
return transactionWithMetadata.getTransaction().getVersionedHashes().orElse(List.of());
}
}
50 changes: 38 additions & 12 deletions ethereum/api/src/main/resources/schema.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

"""
Expand All @@ -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!]!]
}
Expand Down Expand Up @@ -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!]!]
}
Expand Down Expand Up @@ -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.
"""
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"""
Expand All @@ -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."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,23 @@
*/
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;
import org.mockito.Mockito;

public abstract class AbstractDataFetcherTest {

DataFetcher<Optional<NormalBlockAdapter>> fetcher;
GraphQLDataFetchers fetchers;

@Mock protected Set<Capability> supportedCapabilities;

Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Optional<NormalBlockAdapter>> 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);
Expand All @@ -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);
Expand All @@ -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");
Expand All @@ -77,9 +88,29 @@ public void ibftMiner() throws Exception {
final Optional<NormalBlockAdapter> 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<NormalBlockAdapter> maybeBlock = fetcher.get(environment);
assertThat(maybeBlock).isPresent();
assertThat(maybeBlock.get().getBlobGasUsed()).contains(blobGasUsed);
assertThat(maybeBlock.get().getExcessBlobGas()).contains(excessBlobGas);
}
}
Loading

0 comments on commit 4f49ec9

Please sign in to comment.