zoukankan      html  css  js  c++  java
  • 以太坊源码分析-转账流程分析

    以太坊源码分析-以太坊启动
    前面我们分析以太坊的启动过程,在过程中已经创建了交易池(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(chainId*2)求和,其结果为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状态,等待处理。

  • 相关阅读:
    美国航天局的十大编码戒律(转)
    大型数据库应用解决方案总结
    IOCP模型
    SSH 连接慢的解决方案详解
    指针(详解)【转】
    有关推挽输出、开漏输出、复用开漏输出、复用推挽输出以及上拉输入、下拉输入、浮空输入、模拟输入区别【转】
    USB入门开发的八个问题&USB枚举『转』
    浅谈 STM32 硬件I2C的使用 (中断方式 无DMA 无最高优先级)(转)
    KEIL Code RO-data RW-data ZI-data 【转】
    262K Color
  • 原文地址:https://www.cnblogs.com/xiaocongcong888/p/9717659.html
Copyright © 2011-2022 走看看