이더리움 코어 get Nonce 과정 (txNoncer)

hop6·2022년 6월 26일
0

한동안 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 등록 과정

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
}

ethers.js 기준으로 위 과정을 한번 복기 해보자.
// 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
		}
	}
}

BlockNumberOrHash.UnmarshalJSON()
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
	}
}

위 switch 문은 뭘까? address만 입력했으면 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 속도 비교

0개의 댓글