[Go/Ethereum] Ethereum JSON RPC Batch Call 직접 만들기 (Golang, Ethereum, RPC, net/http)
[2022-12-26] infura에서 batch call의 method 목록을 모두 요청 횟수로 카운트되게 수정되었습니다. 또한 batch call의 카운트까지 포함되어 method list + 1 로 카운트됩니다.
들어가며
Ethereum 클라이언트 앱을 개발하다 보면 INFURA를 많이 이용하게 됩니다. INFURA는 하루 10만 건의 요청을 무료로 제공해주며 유료로 사용할 땐 더 많은 요청 횟수를 제공받습니다. 개발할 때 요청 횟수도 신경 쓸 수밖에 없는데요.
JSON-RPC 표준에서 한 번의 요청으로 여러 method를 처리할 수가 있습니다. INFURA 또는 RPC Node는 단 한 번의 네트워크 I/O가 발생하게 되어 매우 유용한 기능입니다. 유료 요금제를 사용한다면 요금도 줄일 수도 있구요.
이번 글에서 go-ethereum(geth)의 client 라이브러리를 사용하지 않고 직접 구현해보겠습니다.
eth_getBlockByNumber
batch call을 테스트하기 위해 eth_getBlockByNumber method를 이용하겠습니다.
eth_getBlockByNumber는 Ethereum JSON RPC에서 blockNumber로 block 데이터를 조회하는 프로시저입니다.
먼저 Ethereum JSON RPC를 요청하기 위한 페이로드, 응답 구조체를 선언하겠습니다.
type Payload struct {
Jsonrpc string `json:"jsonrpc"`
Method string `json:"method"`
Params []interface{} `json:"params"`
ID int `json:"id"`
}
JSON RPC를 요청하기 위한 페이로드입니다.
표준에 맞춰 구조체를 선언해줍니다.
type TxRes struct {
BlockHash string `json:"blockHash"`
BlockNumber string `json:"blockNumber"`
From string `json:"from"`
To string `json:"to"`
Gas string `json:"gas"`
GasPrice string `json:"gasPrice"`
Hash string `json:"hash"`
Input string `json:"input"`
TransactionIndex string `json:"transactionIndex"`
Type string `json:"type"`
Value string `json:"value"`
V string `json:"v"`
R string `json:"r"`
S string `json:"s"`
}
eth_getBlockByNumber 호출 시 트랜잭션 리스트도 같이 받을 수 있습니다.
응답으로 받는 트랜잭션 구조체를 선언해줍니다.
type BlockRes struct {
Jsonrpc string `json:"jsonrpc"`
ID int `json:"id"`
Result struct {
BaseFeePerGas string `json:"baseFeePerGas"`
Difficulty string `json:"difficulty"`
ExtraData string `json:"extraData"`
GasLimit string `json:"gasLimit"`
GasUsed string `json:"gasUsed"`
Hash string `json:"hash"`
LogsBloom string `json:"logsBloom"`
Miner string `json:"miner"`
MixHash string `json:"mixHash"`
Nonce string `json:"nonce"`
Number string `json:"number"`
ParentHash string `json:"parentHash"`
ReceiptsRoot string `json:"receiptsRoot"`
Sha3Uncles string `json:"sha3Uncles"`
Size string `json:"size"`
StateRoot string `json:"stateRoot"`
Timestamp string `json:"timestamp"`
TotalDifficulty string `json:"totalDifficulty"`
Transactions []TxRes `json:"transactions"`
TransactionsRoot string `json:"transactionsRoot"`
Uncles []string `json:"uncles"`
} `json:"result"`
}
eth_getBlockByNumber JSON RPC 응답에 대한 구조체도 선언합니다.
func GetBlock(number uint64) (*BlockRes, error) {
data := Payload{
Jsonrpc: "2.0",
Method: "eth_getBlockByNumber",
Params: []interface{}{fmt.Sprintf("0x%x", number), true},
ID: 1,
}
payloadBytes, err := json.Marshal(data)
if err != nil {
return nil, err
}
body := bytes.NewReader(payloadBytes)
req, err := http.NewRequest("POST", "https://mainnet.infura.io/v3/<변경>", body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
bytes, _ := ioutil.ReadAll(res.Body)
result := &BlockRes{}
err = json.Unmarshal(bytes, result)
if err != nil {
return nil, err
}
return result, nil
}
다음으로 Go언어에서 기본으로 제공하는 net/http package를 활용해서 RPC Node로 요청을 보냅니다.
URL를 변경하여 원하는 블록체인 네트워크에서 테스트하시면 됩니다.
위 코드들을 하나의 패키지에 작성하고 _test 파일을 작성 후 이더리움 메인 넷에서 1111111 블록을 호출해보겠습니다.
import (
"testing"
)
func Test_getBlock(t *testing.T) {
result, err := GetBlock(1111111)
if err != nil {
t.Fatal(err)
}
t.Logf("%+v\n", result.Result)
}
올바른 데이터가 조회된 것을 볼 수 있습니다.
batch eth_getBlockByNumber
이제 한 번씩 조회했던 eth_getBlockByNumber method를 여러 개를 한 번에 요청하여 batch call을 구현해보겠습니다.
JSON-RPC 표준을 보면 아주 간단하게 batch call을 구현할 수 있습니다.
요청 시 리스트로 여러 RPC를 호출하는 모습입니다.
요청 리스트 순서와 다르게 응답 리스트가 달라질 수 있으므로 id 값을 통해서 요청에 대한 응답을 확인할 필요가 있습니다.
그럼 Go언어로 eth_getBlockByNumber를 batch call 하여 여러 블록을 한 번의 요청으로 조회해보겠습니다.
func GetBlocks(fromBlock, toBlock uint64) ([]BlockRes, error) {
payloads := make([]Payload, 0, toBlock-fromBlock+1)
for i := fromBlock; i <= toBlock; i++ {
p := Payload{
Jsonrpc: "2.0",
Method: "eth_getBlockByNumber",
Params: []interface{}{fmt.Sprintf("0x%x", i), true},
ID: int(i),
}
payloads = append(payloads, p)
}
payloadBytes, err := json.Marshal(payloads)
if err != nil {
return nil, err
}
body := bytes.NewReader(payloadBytes)
req, err := http.NewRequest("POST", "https://sepolia.infura.io/v3/<변경>", body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
bytes, _ := ioutil.ReadAll(res.Body)
result := []BlockRes{}
err = json.Unmarshal(bytes, &result)
if err != nil {
return nil, err
}
return result, nil
}
GetBlock 함수와 Payload를 만드는 부분만 다릅니다.
단지 Array 형태로 body를 만들어서 전송하면 batch call을 할 수 있습니다.
마찬가지로 테스트 코드를 작성 후 blockNumber만 확인해보겠습니다.
func Test_getBlocks(t *testing.T) {
result, err := GetBlocks(0, 10)
if err != nil {
t.Fatal(err)
}
for _, block := range result {
t.Log(block.Result.Number)
}
}
blockNumber가 정상적으로 출력이 되었습니다.
INFURA를 이용 중이시라면 dashboard를 통해 요청 횟수를 확인해서 0~10까지 block을 조회했을 때 단 한 번의 Request가 발생했다는 걸 확인할 수 있습니다.
method call은 10만 건을 넘었지만 실제 INFURA Request는 18번이 전부입니다.
마치며
batch call 방식은 동시에 여러 번의 RPC 호출이 필요한 특수한 상황에서 유용하게 쓸 수 있는 매우 유용한 기능 중 하나입니다.
주의할 점은 timeout deadline이 발생하지 않도록 요청 배열을 적절한 크기로 설정해야 되고 서로 연관 없는 독립적인 RPC를 호출할 수 있도록 구현해야 합니다.
또한 Ethereum RPC를 편리하게 사용할 수 있도록 제공되는 라이브러리를 사용하지 않고 직접 구현해보니 라이브러리에서 지정된 타입이 제한적이라는 것을 알게 되었습니다. 예를 들어 라이브러리를 통해 블록 조회 시 트랜잭션 리스트의 트랜잭션 구조에 to, value 등이 없어서 불필요하게 RPC call을 더 해야 될 수 있습니다. 그러므로 저는 매우 유용하게 batch call을 이용할 것 같습니다.
이상 마치도록 하며 다음엔 더 유익한 내용을 가져올 수 있도록 노력하겠습니다~
읽어주셔서 감사합니다.