以太坊源码分析-转账流程分析

30 10月 2018

以太坊源码分析-以太坊启动
前面我们分析以太坊的启动过程,在过程中已经创建了交易池(tx_pool),现在我们猜测一下转账的大概步骤:

创建一笔交易,并发送
接收到交易信息,然后做一些验证
验证合法,将该交易放入交易池,等待打包到Block中
首先,我们从命令行行模拟一个交易,账户A向账户B转账3ether,在转账前,我们需要先对账户A解锁授权,解锁命令如下:

personal.unlockAccount(eth.accounts[0])
输入密码即可解锁该账户。接下来,我们从A账户像B账户转账3以太币,转账命令如下:

eth.sendTransaction({from:eth.accounts[0],to:eth.accounts[1],value:web3.toWei(3,'ether')})
sendTransaction接受一个json参数,其key分别对应的含义如下:

from:转出账户
to:转入账户
value:交易金额。以太坊的基本单位是维,1eth = pow(10,18)
sendTransaction经过RPC方式调用后,最终调用ethapi/api.go中的SendTransaction方法,该方法的实现逻辑如下:

func (s *PrivateAccountAPI) SendTransaction(ctx context.Context, args SendTxArgs, passwd string) (common.Hash, error) {
	// Look up the wallet containing the requested signer
	account := accounts.Account{Address: args.From}

	wallet, err := s.am.Find(account)
	if err != nil {
		return common.Hash{}, err
	}

	if args.Nonce == nil {
		// Hold the addresse's mutex around signing to prevent concurrent assignment of
		// the same nonce to multiple accounts.
		s.nonceLock.LockAddr(args.From)
		defer s.nonceLock.UnlockAddr(args.From)
	}

	// Set some sanity defaults and terminate on failure
	if err := args.setDefaults(ctx, s.b); err != nil {
		return common.Hash{}, err
	}
	// Assemble the transaction and sign with the wallet
	tx := args.toTransaction()

	var chainID *big.Int
	if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) {
		chainID = config.ChainId
	}
	signed, err := wallet.SignTxWithPassphrase(account, passwd, tx, chainID)
	if err != nil {
		return common.Hash{}, err
	}
	return submitTransaction(ctx, s.b, signed)
}

首先,利用传入的参数from构造一个account变量,该变量代表转出方A,接着通过AccountManager获取该账户的wallet,wallet主要是对该交易进行签名,(关于AccountManager的创建,参考上一章以太坊源码分析-以太坊启动)
。接着调用setDefaults方法设置一些默认值,如果没有设置Gas,GasPrice,Nonce将会设置,这里提一下Nonce参数,该参数用户防双花攻击,对于每个账户,Nonce随着转账数的增加而增加。由于基本默认值都设置完成了,接下来就是利用这些值,创建一笔交易。生成一笔交易由toTransaction方法实现,该方法的实现如下:

func (args *SendTxArgs) toTransaction() *types.Transaction {
	if args.To == nil {
		return types.NewContractCreation(uint64(*args.Nonce), (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data)
	}
	return types.NewTransaction(uint64(*args.Nonce), *args.To, (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data)
}

实现很简单,仅仅是判断是否To参数。对于合约而言,它是没有To值的;而对于我们发起的这笔转账,我们是一笔真实的从A用户向B用户转账,此时的To代表的就是账户B的地址。NewTransaction最终调用newTransaction创建一笔交易信息的,如下

func newTransaction(nonce uint64, to *common.Address, amount, gasLimit, gasPrice *big.Int, data []byte) *Transaction {
	if len(data) > 0 {
		data = common.CopyBytes(data)
	}
	d := txdata{
		AccountNonce: nonce,
		Recipient:    to,
		Payload:      data,
		Amount:       new(big.Int),
		GasLimit:     new(big.Int),
		Price:        new(big.Int),
		V:            new(big.Int),
		R:            new(big.Int),
		S:            new(big.Int),
	}
	if amount != nil {
		d.Amount.Set(amount)
	}
	if gasLimit != nil {
		d.GasLimit.Set(gasLimit)
	}
	if gasPrice != nil {
		d.Price.Set(gasPrice)
	}

	return &Transaction{data: d}
}

很简单,就是填充一些参数。现在交易变量已经创建好了,我们回到创建交易的变量的地方,接着分析。接着获取区块链的配置,检查是否是EIP155的区块号(关于以太坊第四次硬分叉修复重放攻击,参考EIP155).接着我们就对该笔交易签名来确保该笔交易的真实有效性。我们找到实现SignTx的keystore.go,实现签名的逻辑如下:

func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
	// Look up the key to sign with and abort if it cannot be found
	ks.mu.RLock()
	defer ks.mu.RUnlock()

	unlockedKey, found := ks.unlocked[a.Address]
	if !found {
		return nil, ErrLocked
	}
	// Depending on the presence of the chain ID, sign with EIP155 or homestead
	if chainID != nil {
		return types.SignTx(tx, types.NewEIP155Signer(chainID), unlockedKey.PrivateKey)
	}
	return types.SignTx(tx, types.HomesteadSigner{}, unlockedKey.PrivateKey)
}

首先获取到所有已经解锁的账户,然后确认该当前账户是否解锁,如果没有解锁将异常退出。由于我们前面已经对A账户解锁,此时将能够在已解锁的账户中找到。接下来检查chainID,如果当前链的区块号在EIP155之前,由于我这里在初始化创世块时指定了chainID,因此此时将使用EIP155Signer签名。签名的代码如下:

func SignTx(tx *Transaction, s Signer, prv *ecdsa.PrivateKey) (*Transaction, error) {
	h := s.Hash(tx)
	sig, err := crypto.Sign(h[:], prv)
	if err != nil {
		return nil, err
	}
	return s.WithSignature(tx, sig)
}

首先获取该交易的RLP编码哈希值,然后使用私钥对该值进行ECDSA签名处理。接着调用WithSignature来对交易的R、S、V初始化。EIP155Signer和HomesteadSigner如下:

EIP155Signer如下

func (s EIP155Signer) WithSignature(tx *Transaction, sig []byte) (*Transaction, error) {
	if len(sig) != 65 {
		panic(fmt.Sprintf("wrong size for signature: got %d, want 65", len(sig)))
	}

	cpy := &Transaction{data: tx.data}
	cpy.data.R = new(big.Int).SetBytes(sig[:32])
	cpy.data.S = new(big.Int).SetBytes(sig[32:64])
	cpy.data.V = new(big.Int).SetBytes([]byte{sig[64]})
	if s.chainId.Sign() != 0 {
		cpy.data.V = big.NewInt(int64(sig[64] + 35))
		cpy.data.V.Add(cpy.data.V, s.chainIdMul)
	}
	return cpy, nil
}
HomesteadSigner如下

func (hs HomesteadSigner) WithSignature(tx *Transaction, sig []byte) (*Transaction, error) {
	if len(sig) != 65 {
		panic(fmt.Sprintf("wrong size for snature: got %d, want 65", len(sig)))
	}
	cpy := &Transaction{data: tx.data}
	cpy.data.R = new(big.Int).SetBytes(sig[:32])
	cpy.data.S = new(big.Int).SetBytes(sig[32:64])
	cpy.data.V = new(big.Int).SetBytes([]byte{sig[64] + 27})
	return cpy, nil
}

他们唯一的差别就是在V的处理上,对于EIP155Singer将签名的第64位转换成int然后加上35,在跟chainIdMul(chainId2)求和,其结果为V = int64(sig[64]) + 35 + chainId 2,对于我这里在初始化创世块是指定chainId=10,此时相当于V=int64(sig[64]) + 55.而对于HomesteadSigner的WithSignature计算很简单,仅仅是sig[64]+27。该值主要是预防重放攻击。整个签名就完成了,并重新包装生成一个带签名的交易变量。我们回到调用签名的地方,此时将签名后的交易提交出去,下面我们来看看submitTransaction方法的逻辑:

func submitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (common.Hash, error) {
	if err := b.SendTx(ctx, tx); err != nil {
		return common.Hash{}, err
	}
	if tx.To() == nil {
		signer := types.MakeSigner(b.ChainConfig(), b.CurrentBlock().Number())
		from, _ := types.Sender(signer, tx)
		addr := crypto.CreateAddress(from, tx.Nonce())
		log.Info("Submitted contract creation", "fullhash", tx.Hash().Hex(), "contract", addr.Hex())
	} else {
		log.Info("Submitted transaction", "fullhash", tx.Hash().Hex(), "recipient", tx.To())
	}
	return tx.Hash(), nil
}

该方法首先将该交易发送给backend处理,返回经过签名后交易的hash值。我们来看看发送给backend是如何处理该比交易的,该方法在api_backend.go中实现,该方法仅仅是转给tx_pool的AddLocal处理,在转给pool.addTx将该比交易放入到交易池等待处理,我们来看看其实现逻辑:

func (pool *TxPool) addTx(tx *types.Transaction, local bool) error {
	pool.mu.Lock()
	defer pool.mu.Unlock()

	// Try to inject the transaction and update any state
	replace, err := pool.add(tx, local)
	if err != nil {
		return err
	}
	// If we added a new transaction, run promotion checks and return
	if !replace {
		state, err := pool.currentState()
		if err != nil {
			return err
		}
		from, _ := types.Sender(pool.signer, tx) // already validated
		pool.promoteExecutables(state, []common.Address{from})
	}
	return nil
}

这里我们分两步来解释。第一步主要是调用add方法,将该交易放入交易池,add的实现如下:

func (pool *TxPool) add(tx *types.Transaction, local bool) (bool, error) {
	// If the transaction is already known, discard it
	hash := tx.Hash()
	if pool.all[hash] != nil {
		log.Trace("Discarding already known transaction", "hash", hash)
		return false, fmt.Errorf("known transaction: %x", hash)
	}
	// If the transaction fails basic validation, discard it
	if err := pool.validateTx(tx, local); err != nil {
		log.Trace("Discarding invalid transaction", "hash", hash, "err", err)
		invalidTxCounter.Inc(1)
		return false, err
	}
	// If the transaction pool is full, discard underpriced transactions
	if uint64(len(pool.all)) >= pool.config.GlobalSlots+pool.config.GlobalQueue {
		// If the new transaction is underpriced, don't accept it
		if pool.priced.Underpriced(tx, pool.locals) {
			log.Trace("Discarding underpriced transaction", "hash", hash, "price", tx.GasPrice())
			underpricedTxCounter.Inc(1)
			return false, ErrUnderpriced
		}
		// New transaction is better than our worse ones, make room for it
		drop := pool.priced.Discard(len(pool.all)-int(pool.config.GlobalSlots+pool.config.GlobalQueue-1), pool.locals)
		for _, tx := range drop {
			log.Trace("Discarding freshly underpriced transaction", "hash", tx.Hash(), "price", tx.GasPrice())
			underpricedTxCounter.Inc(1)
			pool.removeTx(tx.Hash())
		}
	}
	// If the transaction is replacing an already pending one, do directly
	from, _ := types.Sender(pool.signer, tx) // already validated
	if list := pool.pending[from]; list != nil && list.Overlaps(tx) {
		// Nonce already pending, check if required price bump is met
		inserted, old := list.Add(tx, pool.config.PriceBump)
		if !inserted {
			pendingDiscardCounter.Inc(1)
			return false, ErrReplaceUnderpriced
		}
		// New transaction is better, replace old one
		if old != nil {
			delete(pool.all, old.Hash())
			pool.priced.Removed()
			pendingReplaceCounter.Inc(1)
		}
		pool.all[tx.Hash()] = tx
		pool.priced.Put(tx)

		log.Trace("Pooled new executable transaction", "hash", hash, "from", from, "to", tx.To())
		return old != nil, nil
	}
	// New transaction isn't replacing a pending one, push into queue and potentially mark local
	replace, err := pool.enqueueTx(hash, tx)
	if err != nil {
		return false, err
	}
	if local {
		pool.locals.add(from)
	}
	log.Trace("Pooled new future transaction", "hash", hash, "from", from, "to", tx.To())
	return replace, nil
}

该方法首先检查交易池是否已经存在该笔交易了,接下来调用validateTx对交易的合法性进行验证。接下来交易池是否超过容量。如果超过容量,首先检查该交易的交易费用是否低于当前交易列表的最小值,如果低于则拒绝该比交易;如果比其它交易高,则从已有的交易中移除一笔交易费用最低的交易,为当前这笔交易留出空间。接着继续检查该比交易的Nonce值,确认该用户下的交易是否存在该比交易,如果已经存在该比交易,则删除之前的交易,并将该比交易放入交易池中,然后返回。如果该用户下的交易列表中不含有该比交易,则调用enqueueTx将该比交易放入交易池中。如果该比交易是本地发出,需要将发送者(转出方)保存在交易池的locals中。接下来我们来看看validateTx对该比交易做了哪些验证:

func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
	// Heuristic limit, reject transactions over 32KB to prevent DOS attacks
	if tx.Size() > 32*1024 {
		return ErrOversizedData
	}
	// Transactions can't be negative. This may never happen using RLP decoded
	// transactions but may occur if you create a transaction using the RPC.
	if tx.Value().Sign() < 0 {
		return ErrNegativeValue
	}
	// Ensure the transaction doesn't exceed the current block limit gas.
	if pool.gasLimit().Cmp(tx.Gas()) < 0 {
		return ErrGasLimit
	}
	// Make sure the transaction is signed properly
	from, err := types.Sender(pool.signer, tx)
	if err != nil {
		return ErrInvalidSender
	}
	// Drop non-local transactions under our own minimal accepted gas price
	local = local || pool.locals.contains(from) // account may be local even if the transaction arrived from the network
	if !local && pool.gasPrice.Cmp(tx.GasPrice()) > 0 {
		return ErrUnderpriced
	}
	// Ensure the transaction adheres to nonce ordering
	currentState, err := pool.currentState()
	if err != nil {
		return err
	}
	if currentState.GetNonce(from) > tx.Nonce() {
		return ErrNonceTooLow
	}
	// Transactor should have enough funds to cover the costs
	// cost == V + GP * GL
	if currentState.GetBalance(from).Cmp(tx.Cost()) < 0 {
		return ErrInsufficientFunds
	}
	intrGas := IntrinsicGas(tx.Data(), tx.To() == nil, pool.homestead)
	if tx.Gas().Cmp(intrGas) < 0 {
		return ErrIntrinsicGas
	}
	return nil
}

主要是对一下几点进行验证:

验证该比交易的大小,如果大小大于32KB则拒绝该笔交易,这样做主要是防止DDOS攻击
接着验证转账金额,如果金额小于0则拒绝该笔无效交易
该笔交易的gas不能大于消息池gas的限制
该笔交易已经进行了正确的签名
如果该笔交易不是来自本地(来自其它节点)并且该交易的GasPrice小于当前交易池的GasPrice,则拒绝该笔交易。可见交易池是可以拒绝低GasPrice交易的
当前用户的nonce如果大于该笔交易的nonce,则拒绝
验证当前转出用户A的余额是否充足,如果不足拒绝。cost == V + GP * GL
验证该笔交易的固有花费,如果小于交易池的Gas,则拒绝该笔交易。相关的计算参考state_transaction.IntrinsicGas函数
以上就是对该交易的合法性的完整验证。接着我们回到第二步,在上面经过见证后,如果合法则将该笔交易添加到交易池,如果该笔交易原来不存在,则replace=false,此时执行promoteExecutables方法,该方法主要是将可处理的交易待处理(pending)列表,其实现如下:

    func (pool *TxPool) promoteExecutables(state *state.StateDB, accounts []common.Address) {
	gaslimit := pool.gasLimit()

	// Gather all the accounts potentially needing updates
	if accounts == nil {
		accounts = make([]common.Address, 0, len(pool.queue))
		for addr, _ := range pool.queue {
			accounts = append(accounts, addr)
		}
	}
	// Iterate over all accounts and promote any executable transactions
	for _, addr := range accounts {
		list := pool.queue[addr]
		if list == nil {
			continue // Just in case someone calls with a non existing account
		}
		// Drop all transactions that are deemed too old (low nonce)
		for _, tx := range list.Forward(state.GetNonce(addr)) {
			hash := tx.Hash()
			log.Trace("Removed old queued transaction", "hash", hash)
			delete(pool.all, hash)
			pool.priced.Removed()
		}
		// Drop all transactions that are too costly (low balance or out of gas)
		drops, _ := list.Filter(state.GetBalance(addr), gaslimit)
		for _, tx := range drops {
			hash := tx.Hash()
			log.Trace("Removed unpayable queued transaction", "hash", hash)
			delete(pool.all, hash)
			pool.priced.Removed()
			queuedNofundsCounter.Inc(1)
		}
		// Gather all executable transactions and promote them
		for _, tx := range list.Ready(pool.pendingState.GetNonce(addr)) {
			hash := tx.Hash()
			log.Trace("Promoting queued transaction", "hash", hash)
			pool.promoteTx(addr, hash, tx)
		}
		// Drop all transactions over the allowed limit
		if !pool.locals.contains(addr) {
			for _, tx := range list.Cap(int(pool.config.AccountQueue)) {
				hash := tx.Hash()
				delete(pool.all, hash)
				pool.priced.Removed()
				queuedRateLimitCounter.Inc(1)
				log.Trace("Removed cap-exceeding queued transaction", "hash", hash)
			}
		}
		// Delete the entire queue entry if it became empty.
		if list.Empty() {
			delete(pool.queue, addr)
		}
	}
	// If the pending limit is overflown, start equalizing allowances
	pending := uint64(0)
	for _, list := range pool.pending {
		pending += uint64(list.Len())
	}
	if pending > pool.config.GlobalSlots {
		pendingBeforeCap := pending
		// Assemble a spam order to penalize large transactors first
		spammers := prque.New()
		for addr, list := range pool.pending {
			// Only evict transactions from high rollers
			if !pool.locals.contains(addr) && uint64(list.Len()) > pool.config.AccountSlots {
				spammers.Push(addr, float32(list.Len()))
			}
		}
		// Gradually drop transactions from offenders
		offenders := []common.Address{}
		for pending > pool.config.GlobalSlots && !spammers.Empty() {
			// Retrieve the next offender if not local address
			offender, _ := spammers.Pop()
			offenders = append(offenders, offender.(common.Address))

			// Equalize balances until all the same or below threshold
			if len(offenders) > 1 {
				// Calculate the equalization threshold for all current offenders
				threshold := pool.pending[offender.(common.Address)].Len()

				// Iteratively reduce all offenders until below limit or threshold reached
				for pending > pool.config.GlobalSlots && pool.pending[offenders[len(offenders)-2]].Len() > threshold {
					for i := 0; i < len(offenders)-1; i++ {
						list := pool.pending[offenders[i]]
						for _, tx := range list.Cap(list.Len() - 1) {
							// Drop the transaction from the global pools too
							hash := tx.Hash()
							delete(pool.all, hash)
							pool.priced.Removed()

							// Update the account nonce to the dropped transaction
							if nonce := tx.Nonce(); pool.pendingState.GetNonce(offenders[i]) > nonce {
								pool.pendingState.SetNonce(offenders[i], nonce)
							}
							log.Trace("Removed fairness-exceeding pending transaction", "hash", hash)
						}
						pending--
					}
				}
			}
		}
		// If still above threshold, reduce to limit or min allowance
		if pending > pool.config.GlobalSlots && len(offenders) > 0 {
			for pending > pool.config.GlobalSlots && uint64(pool.pending[offenders[len(offenders)-1]].Len()) > pool.config.AccountSlots {
				for _, addr := range offenders {
					list := pool.pending[addr]
					for _, tx := range list.Cap(list.Len() - 1) {
						// Drop the transaction from the global pools too
						hash := tx.Hash()
						delete(pool.all, hash)
						pool.priced.Removed()

						// Update the account nonce to the dropped transaction
						if nonce := tx.Nonce(); pool.pendingState.GetNonce(addr) > nonce {
							pool.pendingState.SetNonce(addr, nonce)
						}
						log.Trace("Removed fairness-exceeding pending transaction", "hash", hash)
					}
					pending--
				}
			}
		}
		pendingRateLimitCounter.Inc(int64(pendingBeforeCap - pending))
	}
	// If we've queued more transactions than the hard limit, drop oldest ones
	queued := uint64(0)
	for _, list := range pool.queue {
		queued += uint64(list.Len())
	}
	if queued > pool.config.GlobalQueue {
		// Sort all accounts with queued transactions by heartbeat
		addresses := make(addresssByHeartbeat, 0, len(pool.queue))
		for addr := range pool.queue {
			if !pool.locals.contains(addr) { // don't drop locals
				addresses = append(addresses, addressByHeartbeat{addr, pool.beats[addr]})
			}
		}
		sort.Sort(addresses)

		// Drop transactions until the total is below the limit or only locals remain
		for drop := queued - pool.config.GlobalQueue; drop > 0 && len(addresses) > 0; {
			addr := addresses[len(addresses)-1]
			list := pool.queue[addr.address]

			addresses = addresses[:len(addresses)-1]

			// Drop all transactions if they are less than the overflow
			if size := uint64(list.Len()); size <= drop {
				for _, tx := range list.Flatten() {
					pool.removeTx(tx.Hash())
				}
				drop -= size
				queuedRateLimitCounter.Inc(int64(size))
				continue
			}
			// Otherwise drop only last few transactions
			txs := list.Flatten()
			for i := len(txs) - 1; i >= 0 && drop > 0; i-- {
				pool.removeTx(txs[i].Hash())
				drop--
				queuedRateLimitCounter.Inc(1)
			}
		}
	}
}

首先迭代所有当前账户的交易,检查当前交易的nonce是否太低(说明该笔交易不合法),如果太低则删除,接着检查余额不足或者gas不足的交易并删除,接着调用promoteTx方法,将该比交易的状态更新为penging并且放在penging集合中,然后将当前消息池该用户的nonce值+1,接着广播TxPreEvent事件,告诉他们本地有一笔新的合法交易等待处理。最终将通过handler.txBroadcastLoop 广播给其它节点,然后在整个以太坊网络上传播并被其它节点接收,等待验证。
接着检查消息池的pending列表是否超过容量,如果超过将进行扩容操作。如果一个账户进行的状态超过限制,从交易池中删除最先添加的交易。到此,发送一笔交易就分析完了,此时交易池中的交易等待挖矿打包处理,后面我们将分析挖矿打包处理,并执行状态转换函数(执行转账)的逻辑。下面我们在命令行看看刚才这笔交易的状态:

>txpool.status
{
  pending: 1,
  queued: 0
}

可以看到有1笔交易处于penging状态,等待处理。

郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除。

区块志

相关文章
关注我们