이더리움 go-etheruem Nonce 관련 Issue submit

hop6·2022년 6월 30일
0

이전 게시글에서 GetTransactionCount(), 즉 Nonce값을 가져오는 메서드를 분석했었다.
간단히 다시 한번 보자면

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가 rpc.PendingBlockNumber면,
s.b.GetPoolNonce(ctx,address)

아니라면
s.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)

s.b.GetPoolNonce(ctx, address)

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라는 구조체의 nonces라는 map에서 address에 대한 nonce값이 존재하면 그 값을, 존재하지 않으면 fallback(stateDB)에서 nonce 값을 리턴해준다.

s.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
위 메서드는 state.StateDB를 db로부터 생성해주게 된다.

때문에, txNoncer의 map에서 nonce 값을 가져오는 것은 물론, 존재하지 않을 경우에도 이미 state.StateDB가 존재하여 따로 생성해주는 과정이 없기 때문에 훨씬 빠를 것이다.

그렇다면 최대한 txNoncer로 부터 값을 가져와 리턴해주는 것이 성능으로 좋지 않을까? 란 의문부터 시작한다.
txNoncer의 map에는 transaction pool에 들어가있는 nonce 값까지 갖고 있다.
실제 state에 '0x0001'의 nonce 값이 5이고, transaction pool에 '0x0001' 의 트랜잭션이 두 개가 들어가 있으면,

{..., from: '0x0001', nonce: 6 }
{..., from: '0x0001', nonce: 7 }

txNoncer map에는 map['0x0001] == 8 이 들어가있는 것이다.
transaction pool 에는 pending과 queue, 두 개가 transaction을 보관하고 있다.
pending에는 실행이 가능한 transaction이 모여있고. queue에는 현재 실행 불가능한 transaction이 모여 있다. (예를 들어, nonce가 7인 '0x0001' address로, nonce 20으로 설정하여 transaction을 보내면 노드 transaction pool의 queue에 들어가게 된다.)
이를 이용하여 getTransactionCount()를 optimize 해보자.
만약 transaction pool의 pending, queue에 transaction이 하나도 없다면, txNoncer에 캐싱된 nonce값과 실제 state의 nonce값은 동일할 것이다. 그렇다면 굳이 db로 부터 state를 새로 생성하여 nonce를 가져올 필요 없이, txNoncer로 부터 가져오는 게 어떨까?

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
	}
    // *** Added
    // Ask txNoncer if not exist that transaction from address in transaction pool.
    pending, queue := s.b.TxPoolContentFrom(address)
    if len(pending) == 0 && len(queue) == 0 {
        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()
}

TxPoolContentFrom() 메서드는 transaction pool의 pending, queue에 있는 transaction들을 리턴해주는 메서드다. 이를 이용하여 두 개가 비어있는지 확인하고, 비어있다면 txNoncer에서 nonce를 가져와 리턴해주는 로직을 추가 작성하였다.
그런데 TxPoolContentFrom() 메서드는 transaction을 가져오는 과정에서 transaction nonce 값에 따라 sorting을 해주는 작업을 해 가볍지만은 않다고 할 수 있다. 따라서 2번 대안도 작성해보자면,

// get address stats in TxPool
func (pool *TxPool) StatsOf(address common.Address) (int, int) {
    pool.mu.Lock()
    defer pool.mu.Unlock()
    return pool.statsOf(address)
}
func (pool *TxPool) statsOf(address common.Address) (int, int) {
    return len(pool.pending[address]), len(pool.queue[address])
}

// go/ethereum/eth/api_backend.go
func (b *EthAPIBackend) StatsOf(ctx context.Context, address common.Address) (pending int, queued int) {
    return b.eth.TxPool().StatsOf(address)
}

단순히 transaction의 개수만 가져오는 메서드를 만들고,

pending, queue := s.b.StatsOf(address)
if pending == 0 && queue == 0 {
    nonce, err := s.b.GetPoolNonce(ctx, address)
    if err != nil {
	    return nil, err
    }
    return (*hexutil.Uint64)(&nonce), nil
}

위와 같이 수정해주면 될 듯 싶다. 물론 2번 대안 같은 경우에는 API가 추가되는 것이기 때문에 과한 면이 있다.

We are extremely cautious to not increase the API surface of package common any further.

위 글은 go-ethereum contributor중 한 명이, private method를 public method로 변경한 PR을 거절하며 쓴 글이다.
아무튼 두가지 대안을 작성하여 Issue를 올린다.


-
-
-

https://github.com/ethereum/go-ethereum/issues/25207
txNoncer에 캐싱 되어있는 값을 사용하는 것은 오용이라고 한다.
실제 블록의 데이터를 가져오는 과정에서 다른 모듈인 txNoncer에 의존해서는 안된다고 한다.
확실히, 사용자가 pending 옵션으로 getTransacionCount()를 요청하지 않는 이상, 실제 state에 대한 요청일 것이다.
일반 서버의 경우, db의 데이터와 캐싱데이터가 같다는 전제하에 캐싱된 데이터를 사용하는 것에 큰 문제가 없겠지만, 블록체인 특성상 한 노드에 존재하는 동일한 데이터라 할지라도 실제 블록에서 데이터를 가져오는지, 캐싱되어 있는 데이터를 가져오는지 의미를 따로 두고 있음을 볼 수 있는 재밌는 경험이었다.

0개의 댓글