Alert Source Discuss
🚧 Stagnant Standards Track: Interface

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_accountseth_signeth_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 未实现 挖矿功能将单独定义。

有关省略功能的具体原因,请参见“理由”部分。

测试用例

待定。

实现

版权

通过 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.