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