반응형
Recent Posts
Recent Comments
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

Bbaktaeho

[Go/Ethereum] Ethereum JSON RPC Batch Call 직접 만들기 (Golang, Ethereum, RPC, net/http) 본문

프로그래밍 (Programming)/고랭 (Golang)

[Go/Ethereum] Ethereum JSON RPC Batch Call 직접 만들기 (Golang, Ethereum, RPC, net/http)

Bbaktaeho 2022. 9. 11. 14:52
반응형

[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)
}

 

test code 실행

올바른 데이터가 조회된 것을 볼 수 있습니다.

batch eth_getBlockByNumber

이제 한 번씩 조회했던 eth_getBlockByNumber method를 여러 개를 한 번에 요청하여 batch call을 구현해보겠습니다.

JSON-RPC 표준을 보면 아주 간단하게 batch call을 구현할 수 있습니다.

https://www.jsonrpc.org/specification#batch

요청 시 리스트로 여러 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)
	}
}

batch test code 실행

blockNumber가 정상적으로 출력이 되었습니다.

INFURA를 이용 중이시라면 dashboard를 통해 요청 횟수를 확인해서 0~10까지 block을 조회했을 때 단 한 번의 Request가 발생했다는 걸 확인할 수 있습니다.

여러 번의 batch call 수행 후 dashboard

method call은 10만 건을 넘었지만 실제 INFURA Request는 18번이 전부입니다.

마치며

batch call 방식은 동시에 여러 번의 RPC 호출이 필요한 특수한 상황에서 유용하게 쓸 수 있는 매우 유용한 기능 중 하나입니다. 

주의할 점은 timeout deadline이 발생하지 않도록 요청 배열을 적절한 크기로 설정해야 되고 서로 연관 없는 독립적인 RPC를 호출할 수 있도록 구현해야 합니다.
또한 Ethereum RPC를 편리하게 사용할 수 있도록 제공되는 라이브러리를 사용하지 않고 직접 구현해보니 라이브러리에서 지정된 타입이 제한적이라는 것을 알게 되었습니다. 예를 들어 라이브러리를 통해 블록 조회 시 트랜잭션 리스트의 트랜잭션 구조에 to, value 등이 없어서 불필요하게 RPC call을 더 해야 될 수 있습니다. 그러므로 저는 매우 유용하게 batch call을 이용할 것 같습니다.

이상 마치도록 하며 다음엔 더 유익한 내용을 가져올 수 있도록 노력하겠습니다~
읽어주셔서 감사합니다.

반응형