以太坊源碼探究之交易與簽名

與比特幣相比,以太坊中的交易結(jié)構(gòu)有相當(dāng)明顯的不同。下面是以太坊中Transaction數(shù)據(jù)結(jié)構(gòu)的UML圖:
以太坊交易類(lèi)圖

右邊的txdata才是實(shí)際的交易數(shù)據(jù),它在core/types/transaction.go里是這樣聲明的:

type txdata struct {
    AccountNonce uint64          `json:"nonce"    gencodec:"required"`
    Price        *big.Int        `json:"gasPrice" gencodec:"required"`
    GasLimit     uint64          `json:"gas"      gencodec:"required"`
    Recipient    *common.Address `json:"to"       rlp:"nil"` // nil means contract creation
    Amount       *big.Int        `json:"value"    gencodec:"required"`
    Payload      []byte          `json:"input"    gencodec:"required"`

    // Signature values
    V *big.Int `json:"v" gencodec:"required"`
    R *big.Int `json:"r" gencodec:"required"`
    S *big.Int `json:"s" gencodec:"required"`

    // This is only used when marshaling to JSON.
    Hash *common.Hash `json:"hash" rlp:"-"`
}

第一個(gè)字段AccountNonce,直譯就是賬戶(hù)隨機(jī)數(shù)。它是以太坊中很小但也很重要的一個(gè)細(xì)節(jié)。以太坊為每個(gè)賬戶(hù)和交易都創(chuàng)建了一個(gè)Nonce,當(dāng)從賬戶(hù)發(fā)起交易的時(shí)候,當(dāng)前賬戶(hù)的Nonce值就被作為交易的Nonce。這里,如果是普通賬戶(hù)那么Nonce就是它發(fā)出的交易數(shù),如果是合約賬戶(hù)就是從它的創(chuàng)建合約數(shù)。

為什么要使用這個(gè)Nonce呢?其主要目的就是為了防止重復(fù)攻擊(Replay Attack)。因?yàn)榻灰锥际切枰灻模俣](méi)有Nonce,那么只要交易數(shù)據(jù)和發(fā)起人是確定的,簽名就一定是相同的,這樣攻擊者就能在收到一個(gè)交易數(shù)據(jù)后,重新生成一個(gè)完全相同的交易并再次提交,比如A給B發(fā)了個(gè)交易,因?yàn)榻灰资怯泻灻?,B雖然不能改動(dòng)這個(gè)交易數(shù)據(jù),但只要反復(fù)提交一模一樣的交易數(shù)據(jù),就能把A賬戶(hù)的所有資金都轉(zhuǎn)到B手里。

當(dāng)使用賬戶(hù)Nonce之后,每次發(fā)起一個(gè)交易,A賬戶(hù)的Nonce值就會(huì)增加,當(dāng)B重新提交時(shí),因?yàn)镹once對(duì)不上了,交易就會(huì)被拒絕。這樣就可以防止重復(fù)攻擊。當(dāng)然,事情還沒(méi)有完,因?yàn)檫€能跨鏈實(shí)施攻擊,直到EIP-155引入了chainID,才實(shí)現(xiàn)了不同鏈之間的交易數(shù)據(jù)不兼容。事實(shí)上,Nonce并不能真正防止重復(fù)攻擊,比如A向B買(mǎi)東西,發(fā)起交易T1給B,緊接著又提交另一個(gè)交易T2,T2的Gas價(jià)格更高、優(yōu)先級(jí)更高將被優(yōu)先處理,如果恰好T2處理完成后剩余資金已經(jīng)不足以支付T1,那么T1就會(huì)被拒絕。這時(shí)如果B已經(jīng)把東西給了A,那A也就攻擊成功了。所以說(shuō),就算交易被處理了也還要再等待一定時(shí)間,確保生成足夠深度的區(qū)塊,才能保證交易的不可逆。

Price指的是單位Gas的價(jià)格,所謂Gas就是交易的消耗,Price就是單位Gas要消耗多少以太幣(Ether),Gas * Price就是處理交易需要消耗多少以太幣,它就相當(dāng)于比特幣中的交易手續(xù)費(fèi)。

GasLimit限定了本次交易允許消耗資源的最高上限,換句話(huà)說(shuō),以太坊中的交易不可能無(wú)限制地消耗資源,這也是以太坊的安全策略之一,防止攻擊者惡意占用資源。

Recipient是交易接收者,它是common.Address指針類(lèi)型,代表一個(gè)地址。這個(gè)值也可以是空的,這時(shí)在交易執(zhí)行時(shí),會(huì)通過(guò)智能合約創(chuàng)建一個(gè)地址來(lái)完成交易。

Amount是交易額。這個(gè)簡(jiǎn)單,不用解釋。

Payload比較重要,它是一個(gè)字節(jié)數(shù)組,可以用來(lái)作為創(chuàng)建合約的指令數(shù)組,這時(shí)每個(gè)字節(jié)都是一個(gè)單獨(dú)的指令;也可以作為數(shù)據(jù)數(shù)組,由合約指令來(lái)進(jìn)行操作。合約由以太坊虛擬機(jī)(Ethereum Virtual Machine,EVM)創(chuàng)建并執(zhí)行。

V、R、S是交易的簽名數(shù)據(jù)。以太坊當(dāng)中,交易經(jīng)過(guò)數(shù)字簽名之后,生成的signature是一個(gè)長(zhǎng)度65的字節(jié)數(shù)組,它被截成三段,前32字節(jié)被放進(jìn)R,再32字節(jié)放進(jìn)S,最后1個(gè)字節(jié)放進(jìn)V。那么為什么要被截成3段呢?以太坊用的是ECDSA算法,R和S就是ECSDA簽名輸出,V則是Recovery ID??聪旅娴膉avascript代碼:

var sig = secp256k1.sign(msgHash, privateKey)
  var ret = {}
  ret.r = sig.signature.slice(0, 32)
  ret.s = sig.signature.slice(32, 64)
  ret.v = sig.recovery + 27

在早前的版本中,根據(jù)R的奇偶性取值27或28。在EIP-155之后,為了防范Replay Attack,V被調(diào)整為CHAIN_ID * 2 + 35/36,確保不同的鏈中V值不相同。來(lái)看一下core/types/transaction_signing.go末尾定義的deriveChainId函數(shù):

func deriveChainId(v *big.Int) *big.Int {
    if v.BitLen() <= 64 {
        v := v.Uint64()
        if v == 27 || v == 28 {
            return new(big.Int)
        }
        return new(big.Int).SetUint64((v - 35) / 2)
    }
    v = new(big.Int).Sub(v, big.NewInt(35))
    return v.Div(v, big.NewInt(2))
}

OK,下面仔細(xì)研究一下以太坊交易是怎么簽名的。在core/types/transaction_signing.go當(dāng)中,定義了Signer這個(gè)簽名接口,以及幾個(gè)實(shí)現(xiàn)簽名的類(lèi),其UML類(lèi)圖如下:
以太坊交易簽名UML類(lèi)圖

如何確定用哪個(gè)Signer實(shí)施簽名操作?這是MakeSigner函數(shù)的任務(wù):

func MakeSigner(config *params.ChainConfig, blockNumber *big.Int) Signer {
    var signer Signer
    switch {
    case config.IsEIP155(blockNumber):
        signer = NewEIP155Signer(config.ChainID)
    case config.IsHomestead(blockNumber):
        signer = HomesteadSigner{}
    default:
        signer = FrontierSigner{}
    }
    return signer
}

Signer接口定義了4個(gè)函數(shù),其作用分別如下:

  • Sender返回交易發(fā)起方,也是付款方的地址。
  • SignatureValues根據(jù)給定簽名返回原始的R、S、V值。
  • Hash返回一個(gè)交易的哈希值,用于簽名操作。
  • Equal用于判斷兩個(gè)Signer是否相同。

我們知道,以太坊發(fā)布分成為四個(gè)階段,分別是Frontier、Homestead、Metropolis和Serenity。所以在幾個(gè)不同的簽名類(lèi)中,F(xiàn)rontierSigner是最早出來(lái)的,然后是HomesteadSigner,之后EIP155推出時(shí)才有EIP155Signer。我們依次來(lái)看一下。

type FrontierSigner struct{}

func (s FrontierSigner) Equal(s2 Signer) bool {
    _, ok := s2.(FrontierSigner)
    return ok
}

所以實(shí)際上FrontierSigner就是一個(gè)空類(lèi),是最基礎(chǔ)的實(shí)現(xiàn)。來(lái)看它的另外兩個(gè)函數(shù):

func (fs FrontierSigner) SignatureValues(tx *Transaction, sig []byte) (r, s, v *big.Int, err error) {
    if len(sig) != 65 {
        panic(fmt.Sprintf("wrong size for signature: got %d, want 65", len(sig)))
    }
    r = new(big.Int).SetBytes(sig[:32])
    s = new(big.Int).SetBytes(sig[32:64])
    v = new(big.Int).SetBytes([]byte{sig[64] + 27})
    return r, s, v, nil
}

func (fs FrontierSigner) 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, })
}

都比較簡(jiǎn)單直觀。其中Hash函數(shù)采用了RLP編碼過(guò)程。最后來(lái)看Sender函數(shù)的實(shí)現(xiàn):

func (fs FrontierSigner) Sender(tx *Transaction) (common.Address, error) {
    //注意最后一個(gè)homestead參數(shù),這里是false
    return recoverPlain(fs.Hash(tx), tx.data.R, tx.data.S, tx.data.V, false)
}

func recoverPlain(sighash common.Hash, R, S, Vb *big.Int, homestead bool) (common.Address, error) {
    if Vb.BitLen() > 8 {
        return common.Address{}, ErrInvalidSig
    }
    V := byte(Vb.Uint64() - 27)
    if !crypto.ValidateSignatureValues(V, R, S, homestead) {
        return common.Address{}, ErrInvalidSig
    }
    //合成sig
    r, s := R.Bytes(), S.Bytes()
    sig := make([]byte, 65)
    copy(sig[32-len(r):32], r)
    copy(sig[64-len(s):64], s)
    sig[64] = V
    //恢復(fù)公鑰
    pub, err := crypto.Ecrecover(sighash[:], sig)
    if err != nil {
        return common.Address{}, err
    }
    if len(pub) == 0 || pub[0] != 4 {
        return common.Address{}, errors.New("invalid public key")
    }
    var addr common.Address
    copy(addr[:], crypto.Keccak256(pub[1:])[12:])
    return addr, nil
}

這里用到的加密算法,將來(lái)如果有機(jī)會(huì)再深入剖析。接著看HomesteadSigner,它的相關(guān)代碼是這樣的:

type HomesteadSigner struct{ FrontierSigner }

func (hs HomesteadSigner) SignatureValues(tx *Transaction, sig []byte) (r, s, v *big.Int, err error) {
    return hs.FrontierSigner.SignatureValues(tx, sig)
}

func (hs HomesteadSigner) Sender(tx *Transaction) (common.Address, error) {
    return recoverPlain(hs.Hash(tx), tx.data.R, tx.data.S, tx.data.V, true)
}

很簡(jiǎn)單是不是?跟FrontierSigner的區(qū)別就是在調(diào)用recoverPlain的時(shí)候,改動(dòng)了末尾最后一個(gè)參數(shù),內(nèi)部實(shí)現(xiàn)上的差別就是多了一步驗(yàn)證,這里不再多述。

最后看EIP155Signer。代碼不多,不再分拆了,詳看注釋?zhuān)?/p>

type EIP155Signer struct {
    chainId, chainIdMul *big.Int  //EIP155對(duì)不同的鏈?zhǔn)亲隽藚^(qū)分的
}

func (s EIP155Signer) Equal(s2 Signer) bool {
    eip155, ok := s2.(EIP155Signer)
    return ok && eip155.chainId.Cmp(s.chainId) == 0  //不同的鏈,不相等
}

var big8 = big.NewInt(8)

func (s EIP155Signer) Sender(tx *Transaction) (common.Address, error) {
    if !tx.Protected() {  //如果還是早前的交易,直接調(diào)用Homestead版的方法
        return HomesteadSigner{}.Sender(tx)
    }
    if tx.ChainId().Cmp(s.chainId) != 0 {  //鏈號(hào)不對(duì),報(bào)錯(cuò)
        return common.Address{}, ErrInvalidChainId
    }
    V := new(big.Int).Sub(tx.data.V, s.chainIdMul)  //chainIdMul = 2 * chainId
    V.Sub(V, big8)  //35 - 8 = 27。EIP155就在這里有所差別
    return recoverPlain(s.Hash(tx), tx.data.R, tx.data.S, V, true)
}

func (s EIP155Signer) SignatureValues(tx *Transaction, sig []byte) (R, S, V *big.Int, err error) {
    R, S, V, err = HomesteadSigner{}.SignatureValues(tx, sig)
    if err != nil {
        return nil, nil, nil, err
    }
    if s.chainId.Sign() != 0 {
        V = big.NewInt(int64(sig[64] + 35))
        V.Add(V, s.chainIdMul)  // 2 * chainId + 35/36
    }
    return R, S, V, nil
}

func (s EIP155Signer) Hash(tx *Transaction) common.Hash {
    //注意這里的區(qū)別,Hash的時(shí)候多增加了一個(gè)chainId
    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), })
}

全文完。


將來(lái)的你,一定會(huì)感謝今天拼命的自己。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 原文:Transactions 交易是由外部擁有的賬戶(hù)發(fā)起的簽名消息,由以太坊網(wǎng)絡(luò)傳輸,并記錄(挖掘)在以太坊區(qū)塊...
    Jisen閱讀 3,903評(píng)論 0 8
  • 這篇文章主要講解以太坊的基本原理,對(duì)技術(shù)感興趣的朋友可以看看。 翻譯作者:許莉 原文地址:How does Eth...
    藍(lán)肥仔閱讀 1,860評(píng)論 0 15
  • 簡(jiǎn)介 不管你們知不知道以太坊(Ethereum blockchain)是什么,但是你們大概都聽(tīng)說(shuō)過(guò)以太坊。最近在新...
    Lilymoana閱讀 4,000評(píng)論 1 22
  • 概念 以太坊是一個(gè)可編程區(qū)塊鏈,那么允許用戶(hù)創(chuàng)建屬于他們自己的復(fù)雜的操作,且作為一個(gè)去中介化的平臺(tái),提供不同的區(qū)塊...
    磨鏈社區(qū)閱讀 1,504評(píng)論 0 1
  • 001 好父母的參考建議 給孩子提供一個(gè)支持和幫助的環(huán)境:當(dāng)孩子做得好時(shí),及時(shí)肯定他們;當(dāng)孩子受挫時(shí),給予支持并鼓...
    蘇_8ab1閱讀 301評(píng)論 2 2

友情鏈接更多精彩內(nèi)容