본문 바로가기
블록체인

퀵노드 특정 케이스 크레딧 절감

by dbadoy 2024. 5. 4.

이 글을 작성하는 2024년 5월 4일에는 QuickNode RPC 별 크레딧 사용량이 개편되어 eth_getTransactionReceipt, eth_getBlockReceipts 동일하게 20 크레딧이 소요된다. 개편되기 전에 eth_getTransactionReceipt 은 2 크레딧, eth_getBlockReceipts 는 60 크레딧이 소요됐었는데, go-ethereum 같은 경우 LevelDB 의 특정 키에 블록의 모든 receipt 가 저장되어 있고 eth_getTransactionReceipt 는 블록의 모든 receipt 를 가져와서 해당 트랜잭션의 receipt 를 찾아 반환하는 방식으로 구현되어 있기 때문에 eth_getBlockReceipts 의 크레딧이 매우 크게 책정된 것은 합리적이지 않았다 (물론 receipt 의 특정 필드를 연산을 통해 채워줘야 하기 때문에 연산량이 eth_getTransactionReceipt 보다야 더 많긴 하겠지만 30배를 더 받을 정도의 연산량은 아님).

아래에서는 과거 기준에서 60 크레딧이 들던 eth_getBlockReceipts 의 결과값을 4 크레딧으로 가져올 수 있는 방법에 대해 적는다. 당시에도 혹시 탈이 있을까봐 테스트를 위한 몇 번의 호출외에는 사용하지 않았고, 이제는 의미도 없지만... 이런 방법도 있다 정도로만 가볍게 적어보겠다.

이더리움 RPC 네임스페이스 debug  에는 debug_dbGet 이라는 메서드가 존재한다. 이는 해당 노드의 데이터베이스로부터 key 값에 대한 value 를 조회할 수 있는 메서드로, 파라미터에 key 값을 넣어주면 된다. 
위에서 receipts 는 블록 단위로 저장된다고 했었는데, 이는 곧 해당 key 값을 알 수 있다면 debug_dbGet 을 통해 한 번에 가져올 수 있음을 의미하며 QuickNode 에서 debug_dbGet 메서드는 과거 기준 2 크레딧이 소요됐었다.

데이터베이스에 저장되는 key 값에는 정해진 prefix 들이 존재하는데 이 값들은 go-ethereum core/rawdb/schema.go 에 정의되어 있다.

blockReceiptsPrefix = []byte("r") // blockReceiptsPrefix + num (uint64 big endian) + hash -> block receipts

 

blockReceipts prefix 를 알았으니 원하는 블록의 receipts 가 저장된 key 를 구하는 메서드를 만든다.

var (
	blockReceiptsPrefix = []byte("r") // blockReceiptsPrefix + num (uint64 big endian) + hash -> block receipts
)

func encodeBlockNumber(number uint64) []byte {
	enc := make([]byte, 8)
	binary.BigEndian.PutUint64(enc, number)
	return enc
}

func blockReceiptsKey(number uint64, hash common.Hash) []byte {
	return append(append(blockReceiptsPrefix, encodeBlockNumber(number)...), hash.Bytes()...)
}

 

이제 debug_dbGet 메서드를 호출해보자. 10000000 번 블록의 receipt 들을 가져온다.

func main() {
    client, err := ethclient.Dial("ETHEREUM_ENDPOINT")
    if err != nil {
        panic(err)
    }
    
    blockNumber := 10000000
    
    block, err := client.BlockByNumber(context.Background(), big.NewInt(int64(blockNumber)))
    if err != nil {
        panic(err)
    }

    // 0x720000000000989680aa20f7bde5be60603f11a45fc4923aab7552be775403fc00c2e6b805e6297dbe
    rawKey := blockReceiptsKey(uint64(blockNumber), block.Hash())
    
    
    var result interface{}
    err = client.Client().Call(&result, "debug_dbGet", key)
    if err != nil {
        panic(err)
    }
    
    str := fmt.Sprintf("%v", result)
    str = strings.Split(str, "0x")[1]

    b := common.Hex2Bytes(str)
    
    storageReceipts := []*types.ReceiptForStorage{}
    if err := rlp.DecodeBytes(b, &storageReceipts); err != nil {
        panic(err)
    }
    
    receipts := make(types.Receipts, len(storageReceipts))
    for i, storageReceipt := range storageReceipts {
        receipts[i] = (*types.Receipt)(storageReceipt)
    }
    
    if err := receipts.DeriveFields(params.MainnetChainConfig, block.Hash(), block.NumberU64(), block.Time(), block.BaseFee(), block.BaseFee(), block.Transactions()); err != nil {
	    panic(err)
    }
    
    receipt1, err := receipts[1].MarshalJSON()
    if err != nil {
    	panic(err)
    }

    fmt.Println(string(receipt1))
}

 

블록 해시값과 receipts 의 특정 필드 후처리를 해주기 위해서는 Block 이 필요하기 때문에 eth_getBlockByNumber 호출 1회, debug_dbGet 호출 1회로, 블록의 모든 receipts 를 가져오는데 4 크레딧이 소요됐다. 60 크레딧이 들던 시기에 15배 더 저렴하게 값을 가져올 수 있는 것이다 (과거 기준).

사실 주로 사용하는 RPC 메서드는 정해져 있고, 한 번도 사용해본 적 없거나 처음보는 메서드들도 많이 존재한다. 온체인 데이터를 가져와야 하는 경우 존재하는 메서드를 한 번 둘러보고 더 나은 방법은 없는지 고민해보는 것도 의미가 있을 수 있다.