以太坊源码分析-交易

机理

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

  • 用户输入转账的地址和转入的地址和转出的金额
  • 系统通过转出的地址的私钥对转账信息进行签名(用于证明这 笔交易确实有本人进行)
  • 系统对交易信息进行验证
  • 把这笔交易入到本地的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里面实现的

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 foundks.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 homesteadif chainID != nil {return types.SignTx(tx, types.NewEIP155Signer(chainID), unlockedKey.PrivateKey)}return types.SignTx(tx, types.HomesteadSigner{}, unlockedKey.PrivateKey)
}

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

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)
}
func (s EIP155Signer) Hash(tx *Transaction) common.Hash {return rlpHash([]interface{}{tx.data.AccountNonce,tx.data.Price,tx.data.GasLimit,tx.data.Recipient,tx.data.Amount,tx.data.Payload,s.chainId, uint(0), uint(0),})
}

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

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
}

到这里一个交易才算真正的完整(里面的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限量

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

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
}

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

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 statereplace, err := pool.add(tx, local)if err != nil {return err}// If we added a new transaction, run promotion checks and returnif !replace {state, err := pool.currentState()if err != nil {return err}from, _ := types.Sender(pool.signer, tx) // already validatedpool.promoteExecutables(state, []common.Address{from})}return nil
}

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

func (pool *TxPool) add(tx *types.Transaction, local bool) (bool, error) {// If the transaction is already known, discard ithash := 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 itif 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 transactionsif uint64(len(pool.all)) >= pool.config.GlobalSlots+pool.config.GlobalQueue {// If the new transaction is underpriced, don't accept itif 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 itdrop := 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 directlyfrom, _ := types.Sender(pool.signer, tx) // already validatedif list := pool.pending[from]; list != nil && list.Overlaps(tx) {// Nonce already pending, check if required price bump is metinserted, old := list.Add(tx, pool.config.PriceBump)if !inserted {pendingDiscardCounter.Inc(1)return false, ErrReplaceUnderpriced}// New transaction is better, replace old oneif old != nil {delete(pool.all, old.Hash())pool.priced.Removed()pendingReplaceCounter.Inc(1)}pool.all[tx.Hash()] = txpool.priced.Put(tx)pool.journalTx(from, 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 queuereplace, err := pool.enqueueTx(hash, tx)if err != nil {return false, err}// Mark local addresses and journal local transactionsif local {pool.locals.add(from)}pool.journalTx(from, tx)log.Trace("Pooled new future transaction", "hash", hash, "from", from, "to", tx.To())return replace, nil
}

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

  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的最后一步了,代码也挺长的,慢慢分析:

func (pool *TxPool) promoteExecutables(state *state.StateDB, accounts []common.Address) {gaslimit := pool.gasLimit()// Gather all the accounts potentially needing updatesif 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 transactionsfor _, 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 themfor _, 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 limitif !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 allowancespending := 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 firstspammers := prque.New()for addr, list := range pool.pending {// Only evict transactions from high rollersif !pool.locals.contains(addr) && uint64(list.Len()) > pool.config.AccountSlots {spammers.Push(addr, float32(list.Len()))}}// Gradually drop transactions from offendersoffenders := []common.Address{}for pending > pool.config.GlobalSlots && !spammers.Empty() {// Retrieve the next offender if not local addressoffender, _ := spammers.Pop()offenders = append(offenders, offender.(common.Address))// Equalize balances until all the same or below thresholdif len(offenders) > 1 {// Calculate the equalization threshold for all current offendersthreshold := pool.pending[offender.(common.Address)].Len()// Iteratively reduce all offenders until below limit or threshold reachedfor 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 toohash := tx.Hash()delete(pool.all, hash)pool.priced.Removed()// Update the account nonce to the dropped transactionif 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 allowanceif 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 toohash := tx.Hash()delete(pool.all, hash)pool.priced.Removed()// Update the account nonce to the dropped transactionif 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 onesqueued := uint64(0)for _, list := range pool.queue {queued += uint64(list.Len())}if queued > pool.config.GlobalQueue {// Sort all accounts with queued transactions by heartbeataddresses := make(addresssByHeartbeat, 0, len(pool.queue))for addr := range pool.queue {if !pool.locals.contains(addr) { // don't drop localsaddresses = append(addresses, addressByHeartbeat{addr, pool.beats[addr]})}}sort.Sort(addresses)// Drop transactions until the total is below the limit or only locals remainfor 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 overflowif size := uint64(list.Len()); size <= drop {for _, tx := range list.Flatten() {pool.removeTx(tx.Hash())}drop -= sizequeuedRateLimitCounter.Inc(int64(size))continue}// Otherwise drop only last few transactionstxs := list.Flatten()for i := len(txs) - 1; i >= 0 && drop > 0; i-- {pool.removeTx(txs[i].Hash())drop--queuedRateLimitCounter.Inc(1)}}}
}

这个方法就是把之前在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/


以太坊源码分析-交易相关推荐

  1. 3 v4 中心节点固定_死磕以太坊源码分析之p2p节点发现

    死磕以太坊源码分析之p2p节点发现 在阅读节点发现源码之前必须要理解kadmilia算法,可以参考:KAD算法详解. 节点发现概述 节点发现,使本地节点得知其他节点的信息,进而加入到p2p网络中. 以 ...

  2. php区块链以太坊,兄弟连区块链教程以太坊源码分析CMD深入分析(一)

    兄弟连区块链教程以太坊源码分析CMD深入分析. cmd包分析 cmd下面总共有13个子包,除了util包之外,每个子包都有一个主函数,每个主函数的init方法中都定义了该主函数支持的命令,如 geth ...

  3. kademlia java_死磕以太坊源码分析之Kademlia算法

    死磕以太坊源码分析之Kademlia算法 KAD 算法概述 Kademlia是一种点对点分布式哈希表(DHT),它在容易出错的环境中也具有可证明的一致性和性能.使用一种基于异或指标的拓扑结构来路由查询 ...

  4. go-ethereum-code-analysis 以太坊源码分析

    分析go-ethereum的过程,我希望从依赖比较少的底层技术组件开始,慢慢深入到核心逻辑. 目录 go-ethereum代码阅读环境搭建 以太坊黄皮书 符号索引 rlp源码解析 trie源码分析 e ...

  5. 以太坊源码分析(2)——以太坊APP对象

    前言 从这一节开始,我将开始以太坊代码全覆盖讲解,讲解的流程是: 以太坊程序入口 基本框架 以太坊协议 发送一笔交易后发生了什么 启动挖矿 以太坊共识 p2p 网络 阅读本系列文章,将默认读者具备一定 ...

  6. 【以太坊源码】交易(一)

    交易是区块链中最基本也是最核心的一个概念,在以太坊中,交易更是重中之重,因为以太坊是一个智能合约平台,以太坊上的应用都是通过智能合约与区块链进行交互,而智能合约的执行是由交易触发的,没有交易,智能合约 ...

  7. 以太坊源码分析之随心笔记

    链客,专为开发者而生,有问必答! 此文章来自区块链技术社区,未经允许拒绝转载. 以太坊索引 table.go 定期随机选取一些节点找他们要他们的节点,放到本地,也就是一个随机找节点的table 里头的 ...

  8. 以太坊源码分析:fetcher模块和区块传播

    前言 这篇文章从区块传播策略入手,介绍新区块是如何传播到远端节点,以及新区块加入到远端节点本地链的过程,同时会介绍fetcher模块,fetcher的功能是处理Peer通知的区块信息.在介绍过程中,还 ...

  9. 46.以太坊源码分析(46)p2p-peer.go源码分析

    nat是网络地址转换的意思. 这部分的源码比较独立而且单一,这里就暂时不分析了. 大家了解基本的功能就行了. nat下面有upnp和pmp两种网络协议. upnp的应用场景(pmp是和upnp类似的协 ...

最新文章

  1. 米勒罗宾素性测试(Miller–Rabin primality test)
  2. android MAT使用
  3. 给数组里面的对象添加一个新的数据
  4. boost::allocator_max_size的实例
  5. 信息系统管理工程师_关于备考信息系统项目管理师、系统集成项目管理工程师考试几点小建议...
  6. Win7system登录打开计算机,Windows7系统system文件丢失导致开机黑屏如何解决
  7. Spring mvc中@RequestMapping 6个基本用法小结
  8. java设置jdk环境变量
  9. mysql配置文件my.cnf的事例并附解释
  10. C++面试题-面向对象-多态性与虚函数
  11. 自定义checkbox大小(注:用CSS的ZOOM属性 )
  12. 懒人也能变美,AR试妆会让你剁手到停不下来吗?
  13. 【图像配准】基于matlab Harris+SIFT图像配准【含Matlab源码 1532期】
  14. 兼容百家的统一独立的执法记录仪可视指挥调度平台
  15. 七周成为数据分析师 第一周:数据分析思维
  16. Bandizip - Bandizip图标制作
  17. 【Windows】win10多桌面与多任务
  18. java中SSM环境搭建
  19. DPU芯片企业中科驭数加入龙蜥社区,构建异构算力生态
  20. 牛客_求将一个数组分割为两个差值最小的部分

热门文章

  1. 代码UITableView点击cell跳转
  2. JS刷新父窗口的几种方式
  3. iis 6.0上部署.net 2.0和4.0网站
  4. ORACLE数据库表空间备份方案
  5. MATLAB中的wavedec、wrcoef函数简析
  6. Linux内核——百度百科
  7. [云炬创业基础笔记]第一章创业环境测试7
  8. [云炬商业计划书阅读分享]无水洗车业 发展前景好
  9. 云炬Android开发报错处理教程 完美解决Android Studio maven { url ‘https://jitpack.io‘ } 无法下载问题
  10. 系列笔记 | 深度学习连载(6):卷积神经网络基础