以太坊源码分析-交易
以太坊源碼分析-交易
機理
先說一點區塊鏈轉賬的基本概念和流程
- 用戶輸入轉賬的地址和轉入的地址和轉出的金額
- 系統通過轉出的地址的私鑰對轉賬信息進行簽名(用于證明這 筆交易確實有本人進行)
- 系統對交易信息進行驗證
- 把這筆交易入到本地的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這個方面里面簡單的說一下這個方面做了什么事情:
看下簽名的方法(大多數要進行修改的地方在這里因為可以根據自己的交易需求來創建不同的交易信息):
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 }昂,這段代碼挺多的,一點一點來分析就好:
- 交友大小有沒有超過規定大小(32mb)
- 交易金額不能小于0
- gas量不能超過總量
- 驗證簽名。簽名說過from信息沒有錄入沒,這里簽名如果正確的話會從簽名里面把pubkey取出來然后組成地址給from 賦值上去。
- 驗證給的gasprice 必須要大于最低的gas price
- 驗證是否可以取出對應的stat database
- 當前交易的Noce 一定要比當前這個from的Noce大(當然了不然就不對了嘛)
- 驗證交易金額是不是小于當前賬戶的金額
昂,接著就是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 里面移動。
好了到了這里然后交易就提交完畢啦,然后有人問咧,那么給其它節點傳呢,這里不是只有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/
總結
以上是生活随笔為你收集整理的以太坊源码分析-交易的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android 8.0 中如何读取内部和
- 下一篇: go-ethereum-code-ana