节点查询
节点通过HTTP POST使用纯JSON-RPC 2.0协议。无需握手,无需加密,owner端面的凭据已自动创建。
每次调用由两个要素决定:
- 使用哪个接口。
/v2/owner是节点管理接口。/v2/foreign涵盖所有链读取操作及交易提交。两者的划分详见节点API。 - 双层信封结构。 响应包含一个JSON-RPC
result,其内部为Ok或Err。参见响应信封。
owner凭据位于~/.epic/main/.api_secret(其他网络对应floo或user),用户名为字面字符串epic。Foreign API调用无需凭据。参见身份验证。
两个客户端均支持NODE_URL和EPIC_NETWORK,因此运行本地网络中的本地链无需修改。在examples目录下运行:
NODE_URL=http://127.0.0.1:23413 EPIC_NETWORK=user python python/epic_node.py
完整客户端
- CLI
- Python
需要curl和jq。
#!/usr/bin/env bash
# Chain status and tip from a local Epic node, over JSON-RPC.
# Needs curl and jq. See https://devdocs.epiccash.com/examples/node-api
set -euo pipefail
NODE_URL="${NODE_URL:-http://127.0.0.1:3413}"
EPIC_NETWORK="${EPIC_NETWORK:-main}"
SECRET_FILE="${SECRET_FILE:-$HOME/.epic/$EPIC_NETWORK/.api_secret}"
command -v jq >/dev/null || { echo "this script needs jq" >&2; exit 1; }
# jq exits non-zero on the Err branch, so a pipeline fails loudly instead of
# printing an error object as if it were data.
epic_ok() {
jq -e 'if .error then error("jsonrpc: \(.error)")
elif .result.Err then error("epic: \(.result.Err)")
else (.result.Ok // .result) end'
}
# Owner surface: needs the secret when the node has one. Status, peers, chain admin.
owner_rpc() {
local -a cred=()
[ -r "$SECRET_FILE" ] && cred=(-u "epic:$(cat "$SECRET_FILE")")
curl -s "${cred[@]}" \
-H 'Content-Type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$1\",\"params\":${2:-[]}}" \
"$NODE_URL/v2/owner" | epic_ok
}
# Foreign surface: no credential. Every chain read lives here.
foreign_rpc() {
curl -s \
-H 'Content-Type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$1\",\"params\":${2:-[]}}" \
"$NODE_URL/v2/foreign" | epic_ok
}
status=$(owner_rpc get_status)
echo "height: $(jq -r '.tip.height' <<<"$status")"
echo "sync state: $(jq -r '.sync_status' <<<"$status")"
echo "peers: $(jq -r '.connections' <<<"$status")"
echo "mempool: $(foreign_rpc get_pool_size)"
echo "stempool: $(foreign_rpc get_stempool_size)"
echo "version: $(foreign_rpc get_version | jq -r '.node_version')"
需要requests。
"""Minimal Epic node JSON-RPC client.
Requires: requests. Run an `epic` node first.
See https://devdocs.epiccash.com/examples/node-api
Override the defaults for another network:
NODE_URL=http://127.0.0.1:23413 EPIC_NETWORK=user python epic_node.py
"""
from __future__ import annotations
import os
import time
from collections.abc import Iterator
from pathlib import Path
from typing import Any
import requests
NODE_URL = os.environ.get("NODE_URL", "http://127.0.0.1:3413")
NETWORK = os.environ.get("EPIC_NETWORK", "main")
# Only the owner surface needs this. Chain reads on /v2/foreign send no credential, so a
# missing file is not fatal.
_secret_file = Path.home() / ".epic" / NETWORK / ".api_secret"
AUTH = ("epic", _secret_file.read_text().strip()) if _secret_file.is_file() else None
def node_rpc(
method: str,
params: list[Any] | dict[str, Any] | None = None,
surface: str = "owner",
) -> Any:
"""Call a node JSON-RPC method.
`surface` is "owner" for status and peer management, "foreign" for chain reads
such as get_block and for push_transaction.
"""
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": method,
"params": params if params is not None else [],
}
response = requests.post(f"{NODE_URL}/v2/{surface}", json=payload, auth=AUTH, timeout=30)
response.raise_for_status()
body = response.json()
if "error" in body:
raise RuntimeError(f"JSON-RPC error: {body['error']}")
result = body["result"]
if "Err" in result:
raise RuntimeError(f"Node returned an error: {result['Err']}")
return result["Ok"]
def get_block(height: int | None = None, block_hash: str | None = None) -> Any:
"""Fetch a block by height or by hash. Foreign surface, positional params."""
return node_rpc("get_block", [height, block_hash, None], surface="foreign")
def watch_blocks(poll_seconds: int = 10) -> Iterator[Any]:
"""Yield each new block as the chain advances.
The node API has no subscription mechanism. get_tip is on the foreign surface, so
polling it needs no credential.
"""
last = node_rpc("get_tip", [], surface="foreign")["height"]
while True:
time.sleep(poll_seconds)
height = node_rpc("get_tip", [], surface="foreign")["height"]
# Catch up rather than skipping, so a slow consumer misses nothing.
while last < height:
last += 1
yield get_block(height=last)
if __name__ == "__main__":
status = node_rpc("get_status")
print(f"height: {status['tip']['height']}")
print(f"sync state: {status['sync_status']}")
print(f"peers: {status['connections']}")
tip = node_rpc("get_tip", [], surface="foreign")
print(f"tip hash: {tip['last_block_pushed']}")
链状态
get_status在owner端面返回区块高度、总难度、已连接对等节点及同步状态。
- CLI
- Python
curl -s -u "epic:$(cat ~/.epic/main/.api_secret)" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"get_status","params":[],"id":1}' \
http://127.0.0.1:3413/v2/owner | jq '.result.Ok'
在Windows PowerShell上:
$secret = (Get-Content "$HOME\.epic\main\.api_secret" -Raw).Trim()
$auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("epic:$secret"))
$body = '{"jsonrpc":"2.0","method":"get_status","params":[],"id":1}'
Invoke-RestMethod -Uri 'http://127.0.0.1:3413/v2/owner' -Method Post -Body $body `
-ContentType 'application/json' -Headers @{ Authorization = "Basic $auth" }
status = node_rpc("get_status")
print(status["tip"]["height"], status["sync_status"], status["connections"])
total_difficulty is an object, not a numberEpic运行多种工作量证明算法,因此难度按算法分别追踪。参见工作量证明。
{"total_difficulty": {"cuckaroo": 0, "cuckatoo": 1234, "randomx": 5678, "progpow": 9012}}
获取区块
get_block位于foreign端面,接受位置参数[height, hash, commit]。传入其中一个,其余置为null。
- CLI
- Python
curl -s -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"get_block","params":[1000000,null,null],"id":1}' \
http://127.0.0.1:3413/v2/foreign | jq '.result.Ok.header | {height, timestamp, proof}'
block = get_block(height=1_000_000)
header = block["header"]
print(f"{header['height']} via {header['proof']} at {header['timestamp']}")
print(f"{len(block['kernels'])} kernels, {len(block['outputs'])} outputs")
内核(kernel)根据方法不同以两种形式出现。get_block返回扁平化形式,而get_kernel和get_last_n_kernels返回带有外部标记features字段的原始类型。
监听新区块
节点API没有订阅机制,因此采用轮询方式。目标出块时间为60秒,每10到15秒轮询一次即可。在foreign端面轮询get_tip,无需凭据。
- CLI
- Python
#!/usr/bin/env bash
# Poll the tip and print each new block height. Needs curl and jq.
# The node also has a webhook mechanism if you would rather be pushed to;
# see [server.webhook_config] in epic-server.toml.
set -euo pipefail
NODE_URL="${NODE_URL:-http://127.0.0.1:3413}"
INTERVAL="${INTERVAL:-10}"
command -v jq >/dev/null || { echo "this script needs jq" >&2; exit 1; }
height() {
# tr -d '\r' because a Windows jq writes CRLF, which command substitution strips only the
# newline from. Without it the integer comparison below fails with "integer expression
# expected" and the loop never advances. Harmless on Linux and macOS.
curl -s -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"get_tip","params":[]}' \
"$NODE_URL/v2/foreign" | jq -e -r '.result.Ok.height' | tr -d '\r'
}
last=$(height)
echo "starting at height $last"
while true; do
sleep "$INTERVAL"
now=$(height)
while [ "$last" -lt "$now" ]; do
last=$((last + 1))
echo "new block: $last"
done
done
watch_blocks()在epic_node.py中,随着链顶端推进逐个产出新区块:
for block in watch_blocks():
header = block["header"]
print(f"block {header['height']} via {header['proof']}")
如需推送而非轮询,参见epic-server.toml中的[server.webhook_config],该选项可在交易、区块头和区块事件发生时发送POST请求。
内存池(mempool)与对等节点
内存池(mempool)大小和版本为foreign端面调用。对等节点列表为owner端面调用。
- CLI
- Python
rpc() { curl -s -H 'Content-Type: application/json' -d "$1" http://127.0.0.1:3413/v2/foreign; }
rpc '{"jsonrpc":"2.0","method":"get_pool_size","params":[],"id":1}' | jq '.result.Ok'
rpc '{"jsonrpc":"2.0","method":"get_stempool_size","params":[],"id":1}' | jq '.result.Ok'
curl -s -u "epic:$(cat ~/.epic/main/.api_secret)" -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"get_connected_peers","params":[],"id":1}' \
http://127.0.0.1:3413/v2/owner | jq '.result.Ok | length'
print("mempool:", node_rpc("get_pool_size", [], surface="foreign"))
print("stempool:", node_rpc("get_stempool_size", [], surface="foreign"))
print("peers:", len(node_rpc("get_connected_peers", [], surface="owner")))
其他所有方法
完整JSON-RPC方法列表,按接口分类
/v2/owner,定义于api/src/owner_rpc.rs:29:
get_status validate_chain compact_chain
get_peers get_connected_peers ban_peer
unban_peer get_onion_addresses
/v2/foreign,定义于api/src/foreign_rpc.rs:37:
get_header get_block get_blocks
get_tip get_version get_kernel
get_last_n_kernels get_outputs get_unspent_outputs
get_pmmr_indices get_pool_size get_stempool_size
get_unconfirmed_transactions push_transaction
get_block_template finalize_block_template submit_block
响应类型定义于api/src/types.rs。