当前位置:   article > 正文

以太坊(15)交易流程解析_以太坊 交易流程

以太坊 交易流程

以太坊源码分析-交易

机理

先说一点区块链转账的基本概念和流程

  • 用户输入转账的地址和转入的地址和转出的金额
  • 系统通过转出的地址的私钥对转账信息进行签名(用于证明这 笔交易确实有本人进行)
  • 系统对交易信息进行验证
  • 把这笔交易入到本地的txpool中(就是缓存交易池)
  • 把交易信息广播给其它节点

源码分析

正对于上面的流程对以太坊(golang)的源码进行必要的分析 面程序员对自己的区块链进行必要的改动

先来看Geth如何转账的:

eth.sendTransaction({"from":eth.accounts[0],to:eth.accounts[1],value:web3.toWei(10,'ether')})

geth的前端操作是用js来进行,调用了web3里面的sendTransaction,然后用json进行封装交易信息,最后调用golang里面的=>

func (s *PublicTransactionPoolAPI) SendTransaction(ctx  context.Context, args SendTxArgs) (common.Hash, error) 

至于怎么从js 转到golang的不细节将过程有点复杂,有空在专门开一个专题进行说明(JSON-RPC)

由此我们可以看出交易的入门在SendTransaction这个方面里面简单的说一下这个方面做了什么事情:

  1. 通过传入的from 来调取对应地址的钱包
  2. 通过交易信息生成 type包下面的Transaction结构体
  3. 对交易进行签名
  4. 最后提交交易

看下签名的方法(大多数要进行修改的地方在这里因为可以根据自己的交易需求来创建不同的交易信息):

	SignTx(account Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error)

此签名方法为一个接口,也就是说 以太坊支持多个不同钱包每个钱包有自己的签名方法 geth 官方的签名在keystore里面实现的

  1. func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
  2. // Look up the key to sign with and abort if it cannot be found
  3. ks.mu.RLock()
  4. defer ks.mu.RUnlock()
  5. unlockedKey, found := ks.unlocked[a.Address]
  6. if !found {
  7. return nil, ErrLocked
  8. }
  9. // Depending on the presence of the chain ID, sign with EIP155 or homestead
  10. if chainID != nil {
  11. return types.SignTx(tx, types.NewEIP155Signer(chainID), unlockedKey.PrivateKey)
  12. }
  13. return types.SignTx(tx, types.HomesteadSigner{}, unlockedKey.PrivateKey)
  14. }

需要转账的话先要给账户解锁,也就是你如果之前直接调用eth的send方法会出现XXXXX is locked的提示,需要用personal.unlockAccount(…)进行必要解锁,继续跟踪下去

  1. func SignTx(tx *Transaction, s Signer, prv *ecdsa.PrivateKey) (*Transaction, error) {
  2. h := s.Hash(tx)
  3. sig, err := crypto.Sign(h[:], prv)
  4. if err != nil {
  5. return nil, err
  6. }
  7. return s.WithSignature(tx, sig)
  8. }
  1. func (s EIP155Signer) Hash(tx *Transaction) common.Hash {
  2. return rlpHash([]interface{}{
  3. tx.data.AccountNonce,
  4. tx.data.Price,
  5. tx.data.GasLimit,
  6. tx.data.Recipient,
  7. tx.data.Amount,
  8. tx.data.Payload,
  9. s.chainId, uint(0), uint(0),
  10. })
  11. }

rlp是以太坊的编码规则,155多了一个chainId的字段(也就是把辣个networkid也放进来了),简单来的说就是把所有的信息转为一个byte数组用来签名用,以太坊的签名采用了椭圆曲线的签名。如何签名的就不展开来讲了。再然后就把签名 分成结构体里面的 R S V

  1. func (s EIP155Signer) WithSignature(tx *Transaction, sig []byte) (*Transaction, error) {
  2. if len(sig) != 65 {
  3. panic(fmt.Sprintf("wrong size for signature: got %d, want 65", len(sig)))
  4. }
  5. cpy := &Transaction{data: tx.data}
  6. cpy.data.R = new(big.Int).SetBytes(sig[:32])
  7. cpy.data.S = new(big.Int).SetBytes(sig[32:64])
  8. cpy.data.V = new(big.Int).SetBytes([]byte{sig[64]})
  9. if s.chainId.Sign() != 0 {
  10. cpy.data.V = big.NewInt(int64(sig[64] + 35))
  11. cpy.data.V.Add(cpy.data.V, s.chainIdMul)
  12. }
  13. return cpy, nil
  14. }

到这里一个交易才算真正的完整(里面的playload是智能合约的代码转为byte数组以后),总结一下一个交易的封装由以下信息. 这里真心要画重点了,目前到这里的时候,在生成的交易信息里面是不存from的,如果好奇的小伙伴直接打印出结果的话会有from的地址,但是那个是因为调用了String()方法,String()方法里面有从RSV里面恢复from 地址然后在赋值的过程,对了这里提醒下 fmt.println(object)的话 其实是调用 String()方法这一点和java一毛一样的。

  • to 转入地址
  • amount 金额
  • playload 只能合约的byte数组
  • nounce 交易独特的id
  • chainId networkid
  • gasprice gas价格
  • gaslimit gas限量

交易封装完成那自然要提交了,提交是最重要的一块。跟踪代码

  1. func submitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (common.Hash, error) {
  2. if err := b.SendTx(ctx, tx); err != nil {
  3. return common.Hash{}, err
  4. }
  5. if tx.To() == nil {
  6. signer := types.MakeSigner(b.ChainConfig(), b.CurrentBlock().Number())
  7. from, _ := types.Sender(signer, tx)
  8. addr := crypto.CreateAddress(from, tx.Nonce())
  9. log.Info("Submitted contract creation", "fullhash", tx.Hash().Hex(), "contract", addr.Hex())
  10. } else {
  11. log.Info("Submitted transaction", "fullhash", tx.Hash().Hex(), "recipient", tx.To())
  12. }
  13. return tx.Hash(), nil
  14. }

最后返回值是tx的hash值 也就是我们在提交交易后,界面上显示的那一串数组 交易的hash值,也就是去查看SendTx的方法继续跟踪下去会来到这里:

  1. func (pool *TxPool) addTx(tx *types.Transaction, local bool) error {
  2. pool.mu.Lock()
  3. defer pool.mu.Unlock()
  4. // Try to inject the transaction and update any state
  5. replace, err := pool.add(tx, local)
  6. if err != nil {
  7. return err
  8. }
  9. // If we added a new transaction, run promotion checks and return
  10. if !replace {
  11. state, err := pool.currentState()
  12. if err != nil {
  13. return err
  14. }
  15. from, _ := types.Sender(pool.signer, tx) // already validated
  16. pool.promoteExecutables(state, []common.Address{from})
  17. }
  18. return nil
  19. }

往txpool里面加入这笔交易,本来以为那个add方法仅仅是简单的数据结构的添加,但是点进去以后发现还是做了特别多的处理,也就是对于这笔交易的验证其实在add里面的验证的

  1. func (pool *TxPool) add(tx *types.Transaction, local bool) (bool, error) {
  2. // If the transaction is already known, discard it
  3. hash := tx.Hash()
  4. if pool.all[hash] != nil {
  5. log.Trace("Discarding already known transaction", "hash", hash)
  6. return false, fmt.Errorf("known transaction: %x", hash)
  7. }
  8. // If the transaction fails basic validation, discard it
  9. if err := pool.validateTx(tx, local); err != nil {
  10. log.Trace("Discarding invalid transaction", "hash", hash, "err", err)
  11. invalidTxCounter.Inc(1)
  12. return false, err
  13. }
  14. // If the transaction pool is full, discard underpriced transactions
  15. if uint64(len(pool.all)) >= pool.config.GlobalSlots+pool.config.GlobalQueue {
  16. // If the new transaction is underpriced, don't accept it
  17. if pool.priced.Underpriced(tx, pool.locals) {
  18. log.Trace("Discarding underpriced transaction", "hash", hash, "price", tx.GasPrice())
  19. underpricedTxCounter.Inc(1)
  20. return false, ErrUnderpriced
  21. }
  22. // New transaction is better than our worse ones, make room for it
  23. drop := pool.priced.Discard(len(pool.all)-int(pool.config.GlobalSlots+pool.config.GlobalQueue-1), pool.locals)
  24. for _, tx := range drop {
  25. log.Trace("Discarding freshly underpriced transaction", "hash", tx.Hash(), "price", tx.GasPrice())
  26. underpricedTxCounter.Inc(1)
  27. pool.removeTx(tx.Hash())
  28. }
  29. }
  30. // If the transaction is replacing an already pending one, do directly
  31. from, _ := types.Sender(pool.signer, tx) // already validated
  32. if list := pool.pending[from]; list != nil && list.Overlaps(tx) {
  33. // Nonce already pending, check if required price bump is met
  34. inserted, old := list.Add(tx, pool.config.PriceBump)
  35. if !inserted {
  36. pendingDiscardCounter.Inc(1)
  37. return false, ErrReplaceUnderpriced
  38. }
  39. // New transaction is better, replace old one
  40. if old != nil {
  41. delete(pool.all, old.Hash())
  42. pool.priced.Removed()
  43. pendingReplaceCounter.Inc(1)
  44. }
  45. pool.all[tx.Hash()] = tx
  46. pool.priced.Put(tx)
  47. pool.journalTx(from, tx)
  48. log.Trace("Pooled new executable transaction", "hash", hash, "from", from, "to", tx.To())
  49. return old != nil, nil
  50. }
  51. // New transaction isn't replacing a pending one, push into queue
  52. replace, err := pool.enqueueTx(hash, tx)
  53. if err != nil {
  54. return false, err
  55. }
  56. // Mark local addresses and journal local transactions
  57. if local {
  58. pool.locals.add(from)
  59. }
  60. pool.journalTx(from, tx)
  61. log.Trace("Pooled new future transaction", "hash", hash, "from", from, "to", tx.To())
  62. return replace, nil
  63. }

昂,这段代码挺多的,一点一点来分析就好:

  1. pool.all[hash] != nil 看下当前的缓存池里面有没有这笔交易了,如果通过正常方法来建立的话,那肯定是没有的
  2. pool.validateTx(tx, local), 对当前的交易进行验证
    • 交友大小有没有超过规定大小(32mb)
    • 交易金额不能小于0
    • gas量不能超过总量
    • 验证签名。签名说过from信息没有录入没,这里签名如果正确的话会从签名里面把pubkey取出来然后组成地址给from 赋值上去。
    • 验证给的gasprice 必须要大于最低的gas price
    • 验证是否可以取出对应的stat database
    • 当前交易的Noce 一定要比当前这个from的Noce大(当然了不然就不对了嘛)
    • 验证交易金额是不是小于当前账户的金额
  3. 查看当前的pool有没有满,如果满了的话会把金额最低的交易踢出去,当然了会把这笔交易放进去比较,这个就是为啥以太坊金额越低效率越慢
  4. 放入pending 或者 queuelist里面,pending list 是可以直接用来封装区块的,所以在封装的区块的时候会取pending里面的。pending里面的数列必须是有序的从小到大的,中间如果断掉会 先加入到queue list里面,一个交易走到这一步的时候都会进入queue list里面,因为直接进入pending的要求是 有相同的nonce,也就是说除非你是对原有的交易进行修改才会直接覆盖pending里面的某一项。
  5. add方法结束了,也就到了这里交易要么被扔掉(当前缓存池满了),要么就是进入pending要么就是进入queue队列

昂,接着就是addTx的最后一步了,代码也挺长的,慢慢分析:

  1. func (pool *TxPool) promoteExecutables(state *state.StateDB, accounts []common.Address) {
  2. gaslimit := pool.gasLimit()
  3. // Gather all the accounts potentially needing updates
  4. if accounts == nil {
  5. accounts = make([]common.Address, 0, len(pool.queue))
  6. for addr, _ := range pool.queue {
  7. accounts = append(accounts, addr)
  8. }
  9. }
  10. // Iterate over all accounts and promote any executable transactions
  11. for _, addr := range accounts {
  12. list := pool.queue[addr]
  13. if list == nil {
  14. continue // Just in case someone calls with a non existing account
  15. }
  16. // Drop all transactions that are deemed too old (low nonce)
  17. for _, tx := range list.Forward(state.GetNonce(addr)) {
  18. hash := tx.Hash()
  19. log.Trace("Removed old queued transaction", "hash", hash)
  20. delete(pool.all, hash)
  21. pool.priced.Removed()
  22. }
  23. // Drop all transactions that are too costly (low balance or out of gas)
  24. drops, _ := list.Filter(state.GetBalance(addr), gaslimit)
  25. for _, tx := range drops {
  26. hash := tx.Hash()
  27. log.Trace("Removed unpayable queued transaction", "hash", hash)
  28. delete(pool.all, hash)
  29. pool.priced.Removed()
  30. queuedNofundsCounter.Inc(1)
  31. }
  32. // Gather all executable transactions and promote them
  33. for _, tx := range list.Ready(pool.pendingState.GetNonce(addr)) {
  34. hash := tx.Hash()
  35. log.Trace("Promoting queued transaction", "hash", hash)
  36. pool.promoteTx(addr, hash, tx)
  37. }
  38. // Drop all transactions over the allowed limit
  39. if !pool.locals.contains(addr) {
  40. for _, tx := range list.Cap(int(pool.config.AccountQueue)) {
  41. hash := tx.Hash()
  42. delete(pool.all, hash)
  43. pool.priced.Removed()
  44. queuedRateLimitCounter.Inc(1)
  45. log.Trace("Removed cap-exceeding queued transaction", "hash", hash)
  46. }
  47. }
  48. // Delete the entire queue entry if it became empty.
  49. if list.Empty() {
  50. delete(pool.queue, addr)
  51. }
  52. }
  53. // If the pending limit is overflown, start equalizing allowances
  54. pending := uint64(0)
  55. for _, list := range pool.pending {
  56. pending += uint64(list.Len())
  57. }
  58. if pending > pool.config.GlobalSlots {
  59. pendingBeforeCap := pending
  60. // Assemble a spam order to penalize large transactors first
  61. spammers := prque.New()
  62. for addr, list := range pool.pending {
  63. // Only evict transactions from high rollers
  64. if !pool.locals.contains(addr) && uint64(list.Len()) > pool.config.AccountSlots {
  65. spammers.Push(addr, float32(list.Len()))
  66. }
  67. }
  68. // Gradually drop transactions from offenders
  69. offenders := []common.Address{}
  70. for pending > pool.config.GlobalSlots && !spammers.Empty() {
  71. // Retrieve the next offender if not local address
  72. offender, _ := spammers.Pop()
  73. offenders = append(offenders, offender.(common.Address))
  74. // Equalize balances until all the same or below threshold
  75. if len(offenders) > 1 {
  76. // Calculate the equalization threshold for all current offenders
  77. threshold := pool.pending[offender.(common.Address)].Len()
  78. // Iteratively reduce all offenders until below limit or threshold reached
  79. for pending > pool.config.GlobalSlots && pool.pending[offenders[len(offenders)-2]].Len() > threshold {
  80. for i := 0; i < len(offenders)-1; i++ {
  81. list := pool.pending[offenders[i]]
  82. for _, tx := range list.Cap(list.Len() - 1) {
  83. // Drop the transaction from the global pools too
  84. hash := tx.Hash()
  85. delete(pool.all, hash)
  86. pool.priced.Removed()
  87. // Update the account nonce to the dropped transaction
  88. if nonce := tx.Nonce(); pool.pendingState.GetNonce(offenders[i]) > nonce {
  89. pool.pendingState.SetNonce(offenders[i], nonce)
  90. }
  91. log.Trace("Removed fairness-exceeding pending transaction", "hash", hash)
  92. }
  93. pending--
  94. }
  95. }
  96. }
  97. }
  98. // If still above threshold, reduce to limit or min allowance
  99. if pending > pool.config.GlobalSlots && len(offenders) > 0 {
  100. for pending > pool.config.GlobalSlots && uint64(pool.pending[offenders[len(offenders)-1]].Len()) > pool.config.AccountSlots {
  101. for _, addr := range offenders {
  102. list := pool.pending[addr]
  103. for _, tx := range list.Cap(list.Len() - 1) {
  104. // Drop the transaction from the global pools too
  105. hash := tx.Hash()
  106. delete(pool.all, hash)
  107. pool.priced.Removed()
  108. // Update the account nonce to the dropped transaction
  109. if nonce := tx.Nonce(); pool.pendingState.GetNonce(addr) > nonce {
  110. pool.pendingState.SetNonce(addr, nonce)
  111. }
  112. log.Trace("Removed fairness-exceeding pending transaction", "hash", hash)
  113. }
  114. pending--
  115. }
  116. }
  117. }
  118. pendingRateLimitCounter.Inc(int64(pendingBeforeCap - pending))
  119. }
  120. // If we've queued more transactions than the hard limit, drop oldest ones
  121. queued := uint64(0)
  122. for _, list := range pool.queue {
  123. queued += uint64(list.Len())
  124. }
  125. if queued > pool.config.GlobalQueue {
  126. // Sort all accounts with queued transactions by heartbeat
  127. addresses := make(addresssByHeartbeat, 0, len(pool.queue))
  128. for addr := range pool.queue {
  129. if !pool.locals.contains(addr) { // don't drop locals
  130. addresses = append(addresses, addressByHeartbeat{addr, pool.beats[addr]})
  131. }
  132. }
  133. sort.Sort(addresses)
  134. // Drop transactions until the total is below the limit or only locals remain
  135. for drop := queued - pool.config.GlobalQueue; drop > 0 && len(addresses) > 0; {
  136. addr := addresses[len(addresses)-1]
  137. list := pool.queue[addr.address]
  138. addresses = addresses[:len(addresses)-1]
  139. // Drop all transactions if they are less than the overflow
  140. if size := uint64(list.Len()); size <= drop {
  141. for _, tx := range list.Flatten() {
  142. pool.removeTx(tx.Hash())
  143. }
  144. drop -= size
  145. queuedRateLimitCounter.Inc(int64(size))
  146. continue
  147. }
  148. // Otherwise drop only last few transactions
  149. txs := list.Flatten()
  150. for i := len(txs) - 1; i >= 0 && drop > 0; i-- {
  151. pool.removeTx(txs[i].Hash())
  152. drop--
  153. queuedRateLimitCounter.Inc(1)
  154. }
  155. }
  156. }
  157. }

这个方法就是把之前在queue 队列里面的交易往pending 里面移动。

  1. 获取gas限定,然后建立一个用于存address的数组,把队列里面的所有地址取出(这个队列由map实现key为地址,value 为一个存交易的list(至于list的数据结构还没来得及去研究)
  2. 如果一个账户的交易沉淀太多了没有被取出,会把一些比较沉底的交易暂时不管,先处理nonce比较高的交易,里面nonce 是用了一个最大堆的数据结构,
  3. 同理如果沉淀过多把那些交易金额比较低的和gas已经超标的给弄掉弄掉弄掉
  4. 剩下的操作就是如果,pending已经超过长度了,会把前面queue里面的东西和pending进行重新排列组合,主要是算法的代码,可以慢慢研究

好了到了这里然后交易就提交完毕啦,然后有人问咧,那么给其它节点传呢,这里不是只有local的麻。在promoteExecutables里面有个方法叫

pool.promoteTx(addr, hash, tx)

这个方法的最后一行

go pool.eventMux.Post(TxPreEvent{tx})

这里就是给其它节点传了。怎么传的话打算下次在重新整理一章以太坊的p2p模式还是挺复杂的

 

原文地址:

https://tianyun6655.github.io/2017/09/24/%E4%BB%A5%E5%A4%AA%E5%9D%8A%E6%BA%90%E7%A0%81%E4%BA%A4%E6%98%93/

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/正经夜光杯/article/detail/831875
推荐阅读
相关标签
  

闽ICP备14008679号