한동안 typescript로 ethers를 사용하다가 최근에는 ethclient를 사용하여 코드를 작성하다 보니, 트랜잭션에 필요한 account nonce를 얻어오는 getTransactionCount()를 썼다.
그 과정에서 갑자기 궁금증이 생긴 것이, 매번 db에서 가져오나? 그러면 성능이 괜찮을까? 어디 캐싱을 해놓았을 수도 있지 않을까? 싶어 코드를 분석해본다.
// go-ethereum/ethclient/ethclient.go
func (ec *Client) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) {
var result hexutil.Uint64
err := ec.c.CallContext(ctx, &result, "eth_getTransactionCount", account, toBlockNumArg(blockNumber))
return uint64(result), err
}
시작 지점은 찾기 쉽다.
geth client를 쓰는 경우에는 NonceAt(), web3.js나, ethers.js를 쓰는 경우에는 rpc 요청으로 getTransactionCount()를 호출하게 된다.
공통적으로 호출되는 메서드 getTransactionCount()를 보자.
func (s *PublicTransactionPoolAPI) GetTransactionCount(ctx context.Context, address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (*hexutil.Uint64, error) {
// Ask transaction pool for the nonce which includes pending transactions
if blockNr, ok := blockNrOrHash.Number(); ok && blockNr == rpc.PendingBlockNumber {
nonce, err := s.b.GetPoolNonce(ctx, address)
if err != nil {
return nil, err
}
return (*hexutil.Uint64)(&nonce), nil
}
// Resolve block number and use its state to ask for the nonce
state, _, err := s.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if state == nil || err != nil {
return nil, err
}
nonce := state.GetNonce(address)
return (*hexutil.Uint64)(&nonce), state.Error()
}
파라미터를 보면 address, blockNrOrHash 를 넘겨 받고 있다.
그러나, 실제로 GetTransactionCount()를 호출할 때는 아래와 같이 호출할 것이다.
// web3
web3.eth.getTransactionCount(address)
web3.eth.getTransactionCount(address, 39683)
// geth
// latest 블록 기준 nonce값
ethclient.NonceAt(ctx.context, account.address, nil)
// 39683 블록 기준 nonce값
ethclient.NonceAt(ctx.context, account.address, 39683)
즉, address 파라미터 뒤, 블록번호를 넣어주는 파라미터가 넘길 때는 비워두거나 숫자를 넣으면 어디선가 blockNrOrHash (rpc.BlockNumberOrHash) 타입으로 변환이 되어 넘어오고 있다.
해당 부분을 찾아보자.
// go-ethereum/rpc/http.go
// ServeHTTP serves JSON-RPC requests over HTTP.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Permit dumb empty requests for remote health-checks (AWS)
if r.Method == http.MethodGet && r.ContentLength == 0 && r.URL.RawQuery == "" {
w.WriteHeader(http.StatusOK)
return
}
if code, err := validateRequest(r); err != nil {
http.Error(w, err.Error(), code)
return
}
// Create request-scoped context.
connInfo := PeerInfo{Transport: "http", RemoteAddr: r.RemoteAddr}
connInfo.HTTP.Version = r.Proto
connInfo.HTTP.Host = r.Host
connInfo.HTTP.Origin = r.Header.Get("Origin")
connInfo.HTTP.UserAgent = r.Header.Get("User-Agent")
ctx := r.Context()
ctx = context.WithValue(ctx, peerInfoContextKey{}, connInfo)
// All checks passed, create a codec that reads directly from the request body
// until EOF, writes the response to w, and orders the server to process a
// single request.
w.Header().Set("content-type", contentType)
codec := newHTTPServerConn(r, w)
defer codec.close()
s.serveSingleRequest(ctx, codec)
}
http 요청이 들어오면 ServeHTTP으로 가장 먼저 들어온다. (ServeHTTP 메소드는 요청 데이터를 받아 응답을 해주는 것이 주요 역할)
마지막에 호출하는 serveSingleRequest()를 따라가보자.
// go-ethereum/rpc/http.go
func (s *Server) serveSingleRequest(ctx context.Context, codec ServerCodec) {
// Don't serve if server is stopped.
if atomic.LoadInt32(&s.run) == 0 {
return
}
h := newHandler(ctx, codec, s.idgen, &s.services)
h.allowSubscribe = false
defer h.close(io.EOF, nil)
reqs, batch, err := codec.readBatch()
if err != nil {
if err != io.EOF {
codec.writeJSON(ctx, errorMessage(&invalidMessageError{"parse error"}))
}
return
}
if batch {
h.handleBatch(reqs)
} else {
h.handleMsg(reqs[0])
}
}
위 코드에서 봐야할 부분은 h.handleMsg(reqs[0]) 메서드다.
// go-ethereum/rpc/handler.go
func (h *handler) handleMsg(msg *jsonrpcMessage) {
if ok := h.handleImmediate(msg); ok {
return
}
h.startCallProc(func(cp *callProc) {
answer := h.handleCallMsg(cp, msg)
h.addSubscriptions(cp.notifiers)
if answer != nil {
h.conn.writeJSON(cp.ctx, answer)
}
for _, n := range cp.notifiers {
n.activate()
}
})
}
...
func (h *handler) startCallProc(fn func(*callProc)) {
h.callWG.Add(1)
go func() {
ctx, cancel := context.WithCancel(h.rootCtx)
defer h.callWG.Done()
defer cancel()
fn(&callProc{ctx: ctx})
}()
}
...
// answer := h.handleCallMsg(cp, msg)
func (h *handler) handleCallMsg(ctx *callProc, msg *jsonrpcMessage) *jsonrpcMessage {
start := time.Now()
switch {
case msg.isNotification():
h.handleCall(ctx, msg)
h.log.Debug("Served "+msg.Method, "duration", time.Since(start))
return nil
case msg.isCall():
resp := h.handleCall(ctx, msg)
var ctx []interface{}
ctx = append(ctx, "reqid", idForLog{msg.ID}, "duration", time.Since(start))
if resp.Error != nil {
ctx = append(ctx, "err", resp.Error.Message)
if resp.Error.Data != nil {
ctx = append(ctx, "errdata", resp.Error.Data)
}
h.log.Warn("Served "+msg.Method, ctx...)
} else {
h.log.Debug("Served "+msg.Method, ctx...)
}
return resp
case msg.hasValidID():
return msg.errorResponse(&invalidRequestError{"invalid request"})
default:
return errorMessage(&invalidRequestError{"invalid request"})
}
}
위 switch 문에서 msg.isCall() 분기를 진행하게 된다.
// resp := h.handleCall(ctx, msg)
func (h *handler) handleCall(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage {
if msg.isSubscribe() {
return h.handleSubscribe(cp, msg)
}
var callb *callback
if msg.isUnsubscribe() {
callb = h.unsubscribeCb
} else {
callb = h.reg.callback(msg.Method)
}
if callb == nil {
return msg.errorResponse(&methodNotFoundError{method: msg.Method})
}
args, err := parsePositionalArguments(msg.Params, callb.argTypes)
if err != nil {
return msg.errorResponse(&invalidParamsError{err.Error()})
}
start := time.Now()
answer := h.runMethod(cp.ctx, msg, callb, args)
// Collect the statistics for RPC calls if metrics is enabled.
// We only care about pure rpc call. Filter out subscription.
if callb != h.unsubscribeCb {
rpcRequestGauge.Inc(1)
if answer.Error != nil {
failedRequestGauge.Inc(1)
} else {
successfulRequestGauge.Inc(1)
}
rpcServingTimer.UpdateSince(start)
updateServeTimeHistogram(msg.Method, answer.Error == nil, time.Since(start))
}
return answer
}
어디선가 많이 본 callback 타입이 등장한다. API 등록 과정에서 보았던 그 얘다.
type callback struct {
fn reflect.Value // the function
rcvr reflect.Value // receiver object of method, set if fn is method
argTypes []reflect.Type // input argument types
hasCtx bool // method's first argument is a context (not included in argTypes)
errPos int // err return idx, of -1 when method cannot return error
isSubscribe bool // true if this is a subscription callback
}
API 등록 과정을 통하여, GetTransactionCount() 파라미터의 타입, 함수 등이 callback에 저장되어 있을 것이다.
callb = h.reg.callback(msg.Method)
해당 메서드를 통하여 callback을 가져오고,
args, err := parsePositionalArguments(msg.Params, callb.argTypes)
parsePositionalArguments() 메서드를 통하여 address, blockNumber를 파싱한다.
// go-ethereum/rpc/json.go
func parsePositionalArguments(rawArgs json.RawMessage, types []reflect.Type) ([]reflect.Value, error) {
dec := json.NewDecoder(bytes.NewReader(rawArgs))
var args []reflect.Value
tok, err := dec.Token()
switch {
case err == io.EOF || tok == nil && err == nil:
// "params" is optional and may be empty. Also allow "params":null even though it's
// not in the spec because our own client used to send it.
case err != nil:
return nil, err
case tok == json.Delim('['):
// Read argument array.
if args, err = parseArgumentArray(dec, types); err != nil {
return nil, err
}
default:
return nil, errors.New("non-array args")
}
// Set any missing args to nil.
for i := len(args); i < len(types); i++ {
if types[i].Kind() != reflect.Ptr {
return nil, fmt.Errorf("missing value for required argument %d", i)
}
args = append(args, reflect.Zero(types[i]))
}
return args, nil
}
func parseArgumentArray(dec *json.Decoder, types []reflect.Type) ([]reflect.Value, error) {
args := make([]reflect.Value, 0, len(types))
for i := 0; dec.More(); i++ {
if i >= len(types) {
return args, fmt.Errorf("too many arguments, want at most %d", len(types))
}
argval := reflect.New(types[i])
if err := dec.Decode(argval.Interface()); err != nil {
return args, fmt.Errorf("invalid argument %d: %v", i, err)
}
if argval.IsNil() && types[i].Kind() != reflect.Ptr {
return args, fmt.Errorf("missing value for required argument %d", i)
}
args = append(args, argval.Elem())
}
// Read end of args array.
_, err := dec.Token()
return args, err
}
// request 1
provider.getTransactionCount("0x---")
// request 2
provider.getTransactionCount("0x---", 3712)
-> ServeHTTP()
-> ServeHTTP() -> serveSingleRequest()
-> ServeHTTP() -> serveSingleRequest() -> handleCallMsg()
-> ServeHTTP() -> serveSingleRequest() -> handleCallMsg() -> handleCall()
-> ServeHTTP() -> serveSingleRequest() -> handleCallMsg() -> handleCall() -> parsePositionalArguments()
-> ServeHTTP() -> serveSingleRequest() -> handleCallMsg() -> handleCall() -> parsePositionalArguments() -> parseArgumentArray()
callb = h.reg.callback(msg.Method)
callback.argTypes = []reflect.Type(
common.Address,
rpc.BlockNumberOrHash
)
// request 1
json.RawMessage
`["0x---", ""]`
// request 2
json.RawMessage
`["0x---", 3712]`
// parseArgumentArray
... dec.Decode(argval.Interface())
// 위 코드에서 각 타입 (common.Address, rpc.BlockNumberOrHash)의 UnmarshalJSON() 메서드를 호출한다.
// 이 내용은 후에 reflect 관련 글에서 다루도록 한다.
// common.Address
func (a *Address) UnmarshalJSON(input []byte) error {
return hexutil.UnmarshalFixedJSON(addressT, input, a[:])
}
// rpc.BlockNumberOrHash
type BlockNumberOrHash struct {
BlockNumber *BlockNumber `json:"blockNumber,omitempty"`
BlockHash *common.Hash `json:"blockHash,omitempty"`
RequireCanonical bool `json:"requireCanonical,omitempty"`
}
func (bnh *BlockNumberOrHash) UnmarshalJSON(data []byte) error {
type erased BlockNumberOrHash
e := erased{}
err := json.Unmarshal(data, &e)
if err == nil {
if e.BlockNumber != nil && e.BlockHash != nil {
return fmt.Errorf("cannot specify both BlockHash and BlockNumber, choose one or the other")
}
bnh.BlockNumber = e.BlockNumber
bnh.BlockHash = e.BlockHash
bnh.RequireCanonical = e.RequireCanonical
return nil
}
var input string
err = json.Unmarshal(data, &input)
if err != nil {
return err
}
switch input {
case "earliest":
bn := EarliestBlockNumber
bnh.BlockNumber = &bn
return nil
case "latest":
bn := LatestBlockNumber
bnh.BlockNumber = &bn
return nil
case "pending":
bn := PendingBlockNumber
bnh.BlockNumber = &bn
return nil
case "finalized":
bn := FinalizedBlockNumber
bnh.BlockNumber = &bn
return nil
default:
if len(input) == 66 {
hash := common.Hash{}
err := hash.UnmarshalText([]byte(input))
if err != nil {
return err
}
bnh.BlockHash = &hash
return nil
} else {
blckNum, err := hexutil.DecodeUint64(input)
if err != nil {
return err
}
if blckNum > math.MaxInt64 {
return fmt.Errorf("blocknumber too high")
}
bn := BlockNumber(blckNum)
bnh.BlockNumber = &bn
return nil
}
}
}
err := json.Unmarshal(data, &e)
if err == nil {
if e.BlockNumber != nil && e.Blockhash != nil {
return fmt.Errorf("cannot specify both BlockHash and BlockNumber, choose one or the other")
}
bnh.BlockNumber = e.BlockNumber
bnh.BlockHash = e.BlockHash
bnh.RequireCanonical = e.RequireCanonical
return nil
}
// -> getTransactionCount() 요청을 할 때, (address, blockNumber || blockHash) 를 입력해주면 위 if 문에서 blockNumberOrHash에 값을 넣어준다.
// -> address만 입력해주었다면 위 구문은 무시하고 지나가겠다.
var input string
err = json.Unmarshal(data, &input)
if err != nil {
return err
}
switch input {
case "earliest":
bn := EarliestBlockNumber
bnh.BlockNumber = &bn
return nil
case "latest":
bn := LatestBlockNumber
bnh.BlockNumber = &bn
return nil
case "pending":
bn := PendingBlockNumber
bnh.BlockNumber = &bn
return nil
case "finalized":
bn := FinalizedBlockNumber
bnh.BlockNumber = &bn
return nil
default:
if len(input) == 66 {
hash := common.Hash{}
err := hash.UnmarshalText([]byte(input))
if err != nil {
return err
}
bnh.BlockHash = &hash
return nil
} else {
blckNum, err := hexutil.DecodeUint64(input)
if err != nil {
return err
}
if blckNum > math.MaxInt64 {
return fmt.Errorf("blocknumber too high")
}
bn := BlockNumber(blckNum)
bnh.BlockNumber = &bn
return nil
}
}
// go-ethereum/ethclient/ethclient.go
func (ec *Client) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) {
var result hexutil.Uint64
err := ec.c.CallContext(ctx, &result, "eth_getTransactionCount", account, toBlockNumArg(blockNumber))
return uint64(result), err
}
func toBlockNumArg(number *big.Int) string {
if number == nil {
return "latest"
}
pending := big.NewInt(-1)
if number.Cmp(pending) == 0 {
return "pending"
}
return hexutil.EncodeBig(number)
}
위 코드를 보면 blockNumber를 toBlockNumArg() 메서드를 거쳐서 넘겨주는 것을 알 수 있다. nil이면 "latest", -1 이면 "pending", 그 외 숫자면 hexutil.EncodeBig() 메서드 결과값을 리턴해준다.
즉, address만 입력하여 요청하면 (address, "latest") 가 최종적으로 request 된다.
web3.js 같은 경우도, (address) 만 요청 할 경우, 내부적으로 "latest"를 붙여 rpc call을 해준다.
참고
다시 돌아와서, switch문에서 "lastest" 분기점을 보자.
const (
FinalizedBlockNumber = BlockNumber(-3)
PendingBlockNumber = BlockNumber(-2)
LatestBlockNumber = BlockNumber(-1)
EarliestBlockNumber = BlockNumber(0)
)
.
.
.
case "latest":
bn := LatestBlockNumber
bnh.BlockNumber = &bn
return nil
.
.
.
최종적으로 address만 입력된 request의 경우,
common.Address : "0x---"
rpc.BlockNumberOrHash: {
BlockNumber : -1
BlockHash : nil
RequireCanonical : false
}
이렇게 되겠다.
돌고 돌아 드디어 본 함수를 살펴볼 수 있게되었다.
func (s *PublicTransactionPoolAPI) GetTransactionCount(ctx context.Context, address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (*hexutil.Uint64, error) {
// Ask transaction pool for the nonce which includes pending transactions
if blockNr, ok := blockNrOrHash.Number(); ok && blockNr == rpc.PendingBlockNumber {
nonce, err := s.b.GetPoolNonce(ctx, address)
if err != nil {
return nil, err
}
return (*hexutil.Uint64)(&nonce), nil
}
// Resolve block number and use its state to ask for the nonce
state, _, err := s.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if state == nil || err != nil {
return nil, err
}
nonce := state.GetNonce(address)
return (*hexutil.Uint64)(&nonce), state.Error()
}
첫번째로 blockNrOrHash에 대한 Number() 메서드가 true 이며, PendingBlockNumber (-2)인지 확인해서 둘다 true 경우 s.b.GetPoolNonce()메서드를, 아닐 경우 s.b.StateAndHeaderByNumberOrHash() 메서드를 호출한다.
먼저, blockNumber를 따로 입력하지 않은 경우 호출되는 s.b.StateAndHeaderByNumberOrHash() 를 살펴보자.
func (b *EthAPIBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*state.StateDB, *types.Header, error) {
if blockNr, ok := blockNrOrHash.Number(); ok {
return b.StateAndHeaderByNumber(ctx, blockNr)
}
if hash, ok := blockNrOrHash.Hash(); ok {
header, err := b.HeaderByHash(ctx, hash)
if err != nil {
return nil, nil, err
}
if header == nil {
return nil, nil, errors.New("header for hash not found")
}
if blockNrOrHash.RequireCanonical && b.eth.blockchain.GetCanonicalHash(header.Number.Uint64()) != hash {
return nil, nil, errors.New("hash is not currently canonical")
}
stateDb, err := b.eth.BlockChain().StateAt(header.Root)
return stateDb, header, err
}
return nil, nil, errors.New("invalid arguments; neither block nor hash specified")
}
blockNumber 해당 시점의 state에서 GetNonce(Addres) 를 해준다.
이번에는 blockNumber를 입력했거나, PendingBlockNumber request인 경우 호출되는 s.b.GetPoolNonce() 를 본다.
// go-ethereum/eth/api_backend.go
func (b *EthAPIBackend) GetPoolNonce(ctx context.Context, addr common.Address) (uint64, error) {
return b.eth.txPool.Nonce(addr), nil
}
// go-etheruem/core/tx_pool.go
func (pool *TxPool) Nonce(addr common.Address) uint64 {
pool.mu.RLock()
defer pool.mu.RUnlock()
return pool.pendingNonces.get(addr)
}
// pool.pendingNonces *txNoncer
// go-ethereum/core/tx_noncer.go
// txNoncer is a tiny virtual state database to manage the executable nonces of
// accounts in the pool, falling back to reading from a real state database if
// an account is unknown.
type txNoncer struct {
fallback *state.StateDB
nonces map[common.Address]uint64
lock sync.Mutex
}
func (txn *txNoncer) get(addr common.Address) uint64 {
// We use mutex for get operation is the underlying
// state will mutate db even for read access.
txn.lock.Lock()
defer txn.lock.Unlock()
if _, ok := txn.nonces[addr]; !ok {
txn.nonces[addr] = txn.fallback.GetNonce(addr)
}
return txn.nonces[addr]
}
해당 메서드의 경우, txNoncer에 nonce값이 존재하는지 확인한 뒤, 존재하면 그 nonce를 리턴, 존재하지 않으면 StateDB에서 GetNonce를 수행한다.
txNoncer는, pool에 존재하는 계정의 nonce값을 리턴하는 방식으로 구조체를 보면 알 수 있듯이 map형태로 되어 있기 때문에 state에서 nonce를 가져오는 것보다 빠를 것으로 예상이 된다.
그러나, pool에 pending되어 있는 tx들이 전부 다 성공하리란 보장이 없는데 pool의 nonce값을 가져다 쓰면 nonce값이 꼬일 수도 있지 않을까? 상황에 맞게 적절히 사용하면 될 것 같다.
------- TODO
(address, "latest")
(address, "pending")
(address, nil)
위 세가지 reuqest 속도 비교