EIP-1767: 以太坊节点数据的 GraphQL 接口
Authors | Nick Johnson (@arachnid), Raúl Kripalani (@raulk), Kris Shinn (@kshinn) |
---|---|
Created | 2019-02-14 |
Discussion Link | https://ethereum-magicians.org/t/graphql-interface-to-ethereum-node-data/2710 |
摘要
本 EIP 指定了一个 GraphQL schema,用于访问存储在以太坊节点上的数据。它旨在为当前通过 JSON-RPC 接口暴露的只读信息提供一个完整的替代方案,同时改进可用性、一致性、效率和面向未来的能力。
动机
当前以太坊节点的 JSON-RPC 接口存在一些缺点。它在某些方面没有正式且完整地指定,这导致了诸如空字节字符串的表示(”” vs “0x” vs “0x0”)等问题上的不兼容,并且它必须对用户将请求的数据做出有根据的猜测,这通常会导致不必要的工作。
例如,totalDifficulty
字段在常见的以太坊节点实现中与区块头分开存储,并且许多调用者不需要此字段。但是,每次调用 eth_getBlock
仍然会检索此字段,需要单独的磁盘读取,因为 RPC 服务器无法知道用户是否需要此字段。
类似地,go-ethereum 中的交易收据在磁盘上存储为每个区块的单个二进制 blob。获取单个交易的收据需要获取和反序列化此 blob,然后找到相关的条目并返回它;这是通过 eth_getTransactionReceipt
API 调用完成的。API 使用者的一项常见任务是获取区块中的所有收据;因此,节点实现最终会重复获取和反序列化相同的数据,导致 O(n^2)
的工作量来获取区块中的所有交易收据,而不是 O(n)
。
其中一些问题可以通过更改现有的 JSON-RPC 接口来解决,但代价是接口会变得有些复杂。相反,我们建议采用一种标准的查询语言 GraphQL,它有助于更高效的 API 实现,同时也提高了灵活性。
先有技术
Nick Johnson 和 EthQL 独立开发了一个用于节点数据的 GraphQL schema。一旦各方意识到共同的努力,他们就努力使他们的 schema 保持一致。本 EIP 中提出的当前 schema 主要来源于 EthQL schema。
规范
节点 API
兼容的节点必须提供一个可通过 HTTP 访问的 GraphQL endpoint。默认情况下,这应该在端口 8547 上提供。GraphQL endpoint 的路径应该是 ‘/graphql’。
兼容的节点可以在根路径 (‘/’) 上提供一个 GraphiQL 交互式查询浏览器。
Schema
此服务的 GraphQL schema 定义如下:
# Bytes32 是一个 32 字节的二进制字符串,表示为带有 0x 前缀的十六进制。
scalar Bytes32
# Address 是一个 20 字节的以太坊地址,表示为带有 0x 前缀的十六进制。
scalar Address
# Bytes 是一个任意长度的二进制字符串,表示为带有 0x 前缀的十六进制。
# 空字节字符串表示为 '0x'。字节字符串必须具有偶数个十六进制半字节。
scalar Bytes
# BigInt 是一个大整数。输入接受 JSON 数字或字符串。
# 字符串可以是十进制或带有 0x 前缀的十六进制。所有输出值都是
# 带有 0x 前缀的十六进制。
scalar BigInt
# Long 是一个 64 位无符号整数。
scalar Long
schema {
query: Query
mutation: Mutation
}
# Account 是一个特定区块的以太坊账户。
type Account {
# Address 是拥有该账户的地址。
address: Address!
# Balance 是账户的余额,单位为 wei。
balance: BigInt!
# TransactionCount 是从此账户发送的交易数量,
# 或者在合约的情况下,是创建的合约数量。也称为 nonce。
transactionCount: Long!
# Code 包含此账户的智能合约代码,如果该帐户
# 是(非自毁的)合约。
code: Bytes!
# Storage 提供对合约账户存储的访问,按其 32 字节的插槽标识符进行索引。
storage(slot: Bytes32!): Bytes32!
}
# Log 是一个以太坊事件日志。
type Log {
# Index 是此日志在区块中的索引。
index: Int!
# Account 是生成此日志的账户——这始终是一个合约账户。
account(block: Long): Account!
# Topics 是此日志的 0-4 个索引主题的列表。
topics: [Bytes32!]!
# Data 是此日志的未索引数据。
data: Bytes!
# Transaction 是生成此日志条目的交易。
transaction: Transaction!
}
# Transaction 是一个以太坊交易。
type Transaction {
# Hash 是此交易的哈希。
hash: Bytes32!
# Nonce 是生成此交易的账户的 nonce。
nonce: Long!
# Index 是此交易在父区块中的索引。如果
# 交易尚未被挖掘,则此值将为 null。
index: Int
# From 是发送此交易的账户——这始终是
# 一个外部拥有的账户。
from(block: Long): Account!
# To 是交易发送到的账户。对于
# 创建合约的交易,此值为 null。
to(block: Long): Account
# Value 是与此交易一起发送的价值,单位为 wei。
value: BigInt!
# GasPrice 是为 gas 提供的价格,单位为 wei 每单位。
gasPrice: BigInt!
# Gas 是此交易可以消耗的最大 gas 量。
gas: Long!
# InputData 是提供给交易目标的数据。
inputData: Bytes!
# Block 是此交易被挖掘的区块。如果
# 交易尚未被挖掘,则此值为 null。
block: Block
# Status 是交易的返回状态。如果
# 交易成功,则此值为 1;如果交易失败(由于 revert 或
# 由于 gas 不足),则此值为 0。如果交易尚未被挖掘,则此
# 字段将为 null。
status: Long
# GasUsed 是处理此交易使用的 gas 量。
# 如果交易尚未被挖掘,则此字段将为 null。
gasUsed: Long
# CumulativeGasUsed 是区块中直到并包括
# 此交易为止使用的总 gas 量。如果交易尚未被挖掘,则此字段
# 将为 null。
cumulativeGasUsed: Long
# CreatedContract 是由创建合约的
# 交易创建的账户。如果交易不是创建合约的交易,
# 或者它尚未被挖掘,则此字段将为 null。
createdContract(block: Long): Account
# Logs 是此交易发出的日志条目的列表。如果
# 交易尚未被挖掘,则此字段将为 null。
logs: [Log!]
}
# BlockFilterCriteria 封装了应用于单个区块的过滤器的日志筛选条件。
input BlockFilterCriteria {
# Addresses 是感兴趣的地址列表。如果此列表
# 为空,则不会按地址筛选结果。
addresses: [Address!]
# Topics 列表将匹配项限制为特定的事件主题。每个事件都有一个
# 主题列表。Topics 匹配该列表的前缀。空元素数组匹配任何
# 主题。非空元素表示与任何
# 包含的主题匹配的替代方案。
#
# 例子:
# - [] 或 nil 匹配任何主题列表
# - [[A]] 匹配第一个位置的主题 A
# - [[], [B]] 匹配第一个位置的任何主题,第二个位置的主题 B
# - [[A], [B]] 匹配第一个位置的主题 A,第二个位置的主题 B
# - [[A, B]], [C, D]] 匹配第一个位置的主题 (A OR B),第二个位置的主题 (C OR D)
topics: [[Bytes32!]!]
}
# Block 是一个以太坊区块。
type Block {
# Number 是此区块的编号,从创世区块的 0 开始。
number: Long!
# Hash 是此区块的区块哈希。
hash: Bytes32!
# Parent 是此区块的父区块。
parent: Block
# Nonce 是区块 nonce,这是由矿工确定的 8 字节序列。
nonce: Bytes!
# TransactionsRoot 是此区块中交易 trie 的根的 keccak256 哈希。
transactionsRoot: Bytes32!
# TransactionCount 是此区块中的交易数量。如果
# 此区块的交易不可用,则此字段将为 null。
transactionCount: Int
# StateRoot 是处理此区块后状态 trie 的 keccak256 哈希。
stateRoot: Bytes32!
# ReceiptsRoot 是此区块中交易收据 trie 的 keccak256 哈希。
receiptsRoot: Bytes32!
# Miner 是挖掘此区块的账户。
miner(block: Long): Account!
# ExtraData 是由矿工提供的任意数据字段。
extraData: Bytes!
# GasLimit 是此区块中交易可用的最大 gas 量。
gasLimit: Long!
# GasUsed 是执行此区块中交易使用的 gas 量。
gasUsed: Long!
# Timestamp 是挖掘此区块的 unix 时间戳。
timestamp: BigInt!
# LogsBloom 是一个 bloom 过滤器,可用于检查区块是否可能
# 包含与过滤器匹配的日志条目。
logsBloom: Bytes!
# MixHash 是用作 PoW 过程输入的哈希。
mixHash: Bytes32!
# Difficulty 是衡量挖掘此区块的难度。
difficulty: BigInt!
# TotalDifficulty 是直到并包括
# 此区块的所有 difficulty 值的总和。
totalDifficulty: BigInt!
# OmmerCount 是与此
# 区块关联的的 ommer(又名 uncle)的数量。如果 ommer 不可用,则此字段将为 null。
ommerCount: Int
# Ommers 是与此区块关联的 ommer(又名 uncle)的列表。
# 如果 ommer 不可用,则此字段将为 null。取决于您的
# 节点,交易、transactionAt、transactionCount、ommers、
# ommerCount 和 ommerAt 字段可能在任何 ommer 区块上都不可用。
ommers: [Block]
# OmmerAt 返回指定索引处的 ommer(又名 uncle)。如果 ommer
# 不可用,或者索引超出范围,则此字段将为 null。
ommerAt(index: Int!): Block
# OmmerHash 是所有 ommer(又名 uncle)的 keccak256 哈希
# 与此区块关联。
ommerHash: Bytes32!
# Transactions 是与此区块关联的交易列表。如果
# 此区块的交易不可用,则此字段将为 null。
transactions: [Transaction!]
# TransactionAt 返回指定索引处的交易。如果
# 此区块的交易不可用,或者索引超出
# 范围,则此字段将为 null。
transactionAt(index: Int!): Transaction
# Logs 返回来自此区块的经过筛选的日志集。
logs(filter: BlockFilterCriteria!): [Log!]!
# Account 获取当前区块状态下的以太坊账户。
account(address: Address!): Account
# Call 在当前区块状态下执行本地调用操作。
call(data: CallData!): CallResult
# EstimateGas 估计在当前区块状态下成功
# 执行交易所需的 gas 量。
estimateGas(data: CallData!): Long!
}
# CallData 表示与本地合约调用关联的数据。
# 所有字段都是可选的。
input CallData {
# From 是进行调用的地址。
from: Address
# To 是将调用发送到的地址。
to: Address
# Gas 是与调用一起发送的 gas 量。
gas: Long
# GasPrice 是为每个单位 gas 提供的价格,单位为 wei。
gasPrice: BigInt
# Value 是与调用一起发送的价值,单位为 wei。
value: BigInt
# Data 是发送给被调方的数据。
data: Bytes
}
# CallResult 是本地调用操作的结果。
type CallResult {
# Data 是被调用合约的返回数据。
data: Bytes!
# GasUsed 是调用使用的 gas 量,在任何退款之后。
gasUsed: Long!
# Status 是调用的结果——1 表示成功,0 表示失败。
status: Long!
}
# FilterCriteria 封装了用于搜索日志条目的日志筛选条件。
input FilterCriteria {
# FromBlock 是开始搜索的区块,包括在内。如果未提供
# ,则默认为最新的区块。
fromBlock: Long
# ToBlock 是停止搜索的区块,包括在内。如果未提供
# ,则默认为最新的区块。
toBlock: Long
# Addresses 是感兴趣的地址列表。如果此列表
# 为空,则不会按地址筛选结果。
addresses: [Address!]
# Topics 列表将匹配项限制为特定的事件主题。每个事件都有一个
# 主题列表。Topics 匹配该列表的前缀。空元素数组匹配任何
# 主题。非空元素表示与任何
# 包含的主题匹配的替代。
#
# 例子:
# - [] 或 nil 匹配任何主题列表
# - [[A]] 匹配第一个位置的主题 A
# - [[], [B]] 匹配第一个位置的任何主题,第二个位置的主题 B
# - [[A], [B]] 匹配第一个位置的主题 A,第二个位置的主题 B
# - [[A, B]], [C, D]] 匹配第一个位置的主题 (A OR B),第二个位置的主题 (C OR D)
topics: [[Bytes32!]!]
}
# SyncState 包含客户端当前的同步状态。
type SyncState{
# StartingBlock 是开始同步的区块编号。
startingBlock: Long!
# CurrentBlock 是同步目前达到的点。
currentBlock: Long!
# HighestBlock 是最新的已知区块编号。
highestBlock: Long!
# PulledStates 是到目前为止提取的状态条目的数量,如果
# 未知或不相关,则为 null。
pulledStates: Long
# KnownStates 是节点目前知道的状态的数量,如果
# 未知或不相关,则为 null。
knownStates: Long
}
# Pending 表示当前pending 状态。
type Pending {
# TransactionCount 是 pending 状态下的交易数量。
transactionCount: Int!
# Transactions 是当前 pending 状态下的交易列表。
transactions: [Transaction!]
# Account 获取 pending 状态的以太坊账户。
account(address: Address!): Account
# Call 为 pending 状态执行本地调用操作。
call(data: CallData!): CallResult
# EstimateGas 估计成功执行 pending 状态的交易所需的 gas 量。
estimateGas(data: CallData!): Long!
}
type Query {
# Block 按编号或哈希获取以太坊区块。如果两者都未
# 提供,则返回最新的已知区块。
block(number: Long, hash: Bytes32): Block
# Blocks 返回两个数字之间的所有区块,包括在内。如果未提供
# to,则默认为最新的已知区块。
blocks(from: Long!, to: Long): [Block!]!
# Pending 返回当前的 pending 状态。
pending: Pending!
# Transaction 返回由其哈希指定的交易。
transaction(hash: Bytes32!): Transaction
# Logs 返回与提供的过滤器匹配的日志条目。
logs(filter: FilterCriteria!): [Log!]!
# GasPrice 返回节点估计的足以确保交易及时
# 被挖掘的 gas 价格。
gasPrice: BigInt!
# ProtocolVersion 返回当前的消息协议版本号。
protocolVersion: Int!
# Syncing 返回有关当前同步状态的信息。
syncing: SyncState
}
type Mutation {
# SendRawTransaction 将 RLP 编码的交易发送到网络。
sendRawTransaction(data: Bytes!): Bytes32!
}
节点可以提供此 schema 的超集,方法是添加新的字段或类型。实验性或特定于客户端的字段必须以 ‘client’ 为前缀(例如,’geth’ 或 ‘parity‘)。未加前缀的字段必须在扩展此 EIP 的新 EIP 中指定。
理由
以太坊节点已经逐渐放弃提供读写功能,例如交易和消息签名,以及其他服务,例如代码编译,而倾向于更“类似 unix”的方法,其中每个任务都由一个专用进程执行。因此,我们指定了一组核心类型和字段,这些类型和字段反映了这种趋势,而忽略了当前已弃用或计划弃用的功能:
eth_compile*
调用已弃用,因此此处未提供。eth_accounts
、eth_sign
和eth_sendTransaction
被许多人认为已弃用,因此此处未提供;调用者应改用本地帐户或单独的签名守护程序。
此外,出于简单起见,此初始标准中省略了当前 API 接口的两个区域,目的是在以后的 EIP 中定义它们:
- 过滤器将需要使用 GraphQL 订阅,并且需要仔细考虑围绕没有本地每调用者状态的节点的需求。
- 挖矿功能使用较少,并且从 GraphQL 的重新实现中受益较少,因此应在单独的 EIP 中指定。
向后兼容性
此 schema 实现了 JSON-RPC 节点接口提供的当前只读功能的大部分。现有的 RPC 调用可以映射到 GraphQL 查询,如下所示:
RPC | 状态 | 说明 |
---|---|---|
eth_blockNumber | 已实现 | { block { number } } |
eth_call | 已实现 | { call(data: { to: "0x...", data: "0x..." }) { data status gasUsed } } |
eth_estimateGas | 已实现 | { estimateGas(data: { to: "0x...", data: "0x..." }) } |
eth_gasPrice | 已实现 | { gasPrice } |
eth_getBalance | 已实现 | { account(address: "0x...") { balance } } |
eth_getBlockByHash | 已实现 | { block(hash: "0x...") { ... } } |
eth_getBlockByNumber | 已实现 | { block(number: 123) { ... } } |
eth_getBlockTransactionCountByHash | 已实现 | { block(hash: "0x...") { transactionCount } } |
eth_getBlockTransactionCountByNumber | 已实现 | { block(number: x) { transactionCounnt } } |
eth_getCode | 已实现 | { account(address: "0x...") { code } } |
eth_getLogs | 已实现 | { logs(filter: { ... }) { ... } } 或 { block(...) { logs(filter: { ... }) { ... } } } |
eth_getStorageAt | 已实现 | { account(address: "0x...") { storage(slot: "0x...") } } |
eth_getTransactionByBlockHashAndIndex | 已实现 | { block(hash: "0x...") { transactionAt(index: x) { ... } } } |
eth_getTransactionByBlockNumberAndIndex | 已实现 | { block(number: n) { transactionAt(index: x) { ... } } } |
eth_getTransactionByHash | 已实现 | { transaction(hash: "0x...") { ... } } |
eth_getTransactionCount | 已实现 | { account(address: "0x...") { transactionCount } } |
eth_getTransactionReceipt | 已实现 | { transaction(hash: "0x...") { ... } } |
eth_getUncleByBlockHashAndIndex | 已实现 | { block(hash: "0x...") { ommerAt(index: x) { ... } } } |
eth_getUncleByBlockNumberAndIndex | 已实现 | { block(number: n) { ommerAt(index: x) { ... } } } |
eth_getUncleCountByBlockHash | 已实现 | { block(hash: "0x...") { ommerCount } } |
eth_getUncleCountByBlockNumber | 已实现 | { block(number: x) { ommerCount } } |
eth_protocolVersion | 已实现 | { protocolVersion } |
eth_sendRawTransaction | 已实现 | mutation { sendRawTransaction(data: data) } |
eth_syncing | 已实现 | { syncing { ... } } |
eth_getCompilers | 未实现 | Compiler 功能在 JSON-RPC 中已弃用。 |
eth_compileLLL | 未实现 | Compiler 功能在 JSON-RPC 中已弃用。 |
eth_compileSolidity | 未实现 | Compiler 功能在 JSON-RPC 中已弃用。 |
eth_compileSerpent | 未实现 | Compiler 功能在 JSON-RPC 中已弃用。 |
eth_newFilter | 未实现 | 过滤器功能可能会在以后的 EIP 中指定。 |
eth_newBlockFilter | 未实现 | 过滤器功能可能会在以后的 EIP 中指定。 |
eth_newPendingTransactionFilter | 未实现 | 过滤器功能可能会在以后的 EIP 中指定。 |
eth_uninstallFilter | 未实现 | 过滤器功能可能会在以后的 EIP 中指定。 |
eth_getFilterChanges | 未实现 | 过滤器功能可能会在以后的 EIP 中指定。 |
eth_getFilterLogs | 未实现 | 过滤器功能可能会在以后的 EIP 中指定。 |
eth_accounts | 未实现 | 帐户功能不是核心节点 API 的一部分。 |
eth_sign | 未实现 | 帐户功能不是核心节点 API 的一部分。 |
eth_sendTransaction | 未实现 | 帐户功能不是核心节点 API 的一部分。 |
eth_coinbase | 未实现 | 挖矿功能将单独定义。 |
eth_getWork | 未实现 | 挖矿功能将单独定义。 |
eth_hashRate | 未实现 | 挖矿功能将单独定义。 |
eth_mining | 未实现 | 挖矿功能将单独定义。 |
eth_submitHashrate | 未实现 | 挖矿功能将单独定义。 |
eth_submitWork | 未实现 | 挖矿功能将单独定义。 |
有关省略功能的具体原因,请参见“理由”部分。
测试用例
待定。
实现
- 在 Go-ethereum 1.9.0 中实现并发布
- 在 Pantheon 1.1.1 中实现并发布
- 在 Trinity 中进行中
- 在 Parity 中进行中
版权
通过 CC0 放弃版权和相关权利。
Citation
Please cite this document as:
Nick Johnson (@arachnid), Raúl Kripalani (@raulk), Kris Shinn (@kshinn), "EIP-1767: 以太坊节点数据的 GraphQL 接口 [DRAFT]," Ethereum Improvement Proposals, no. 1767, February 2019. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1767.