以太坊源码探究之交易与签名

与比特币相比,以太坊中的交易结构有相当明显的不同。下面是以太坊中Transaction数据结构的UML图:
以太坊交易类图

右边的txdata才是实际的交易数据,它在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:"-"`
}

第一个字段AccountNonce,直译就是账户随机数。它是以太坊中很小但也很重要的一个细节。以太坊为每个账户和交易都创建了一个Nonce,当从账户发起交易的时候,当前账户的Nonce值就被作为交易的Nonce。这里,如果是普通账户那么Nonce就是它发出的交易数,如果是合约账户就是从它的创建合约数。

为什么要使用这个Nonce呢?其主要目的就是为了防止重复攻击(Replay Attack)。因为交易都是需要签名的,假定没有Nonce,那么只要交易数据和发起人是确定的,签名就一定是相同的,这样攻击者就能在收到一个交易数据后,重新生成一个完全相同的交易并再次提交,比如A给B发了个交易,因为交易是有签名的,B虽然不能改动这个交易数据,但只要反复提交一模一样的交易数据,就能把A账户的所有资金都转到B手里。

当使用账户Nonce之后,每次发起一个交易,A账户的Nonce值就会增加,当B重新提交时,因为Nonce对不上了,交易就会被拒绝。这样就可以防止重复攻击。当然,事情还没有完,因为还能跨链实施攻击,直到EIP-155引入了chainID,才实现了不同链之间的交易数据不兼容。事实上,Nonce并不能真正防止重复攻击,比如A向B买东西,发起交易T1给B,紧接着又提交另一个交易T2,T2的Gas价格更高、优先级更高将被优先处理,如果恰好T2处理完成后剩余资金已经不足以支付T1,那么T1就会被拒绝。这时如果B已经把东西给了A,那A也就攻击成功了。所以说,就算交易被处理了也还要再等待一定时间,确保生成足够深度的区块,才能保证交易的不可逆。

Price指的是单位Gas的价格,所谓Gas就是交易的消耗,Price就是单位Gas要消耗多少以太币(Ether),Gas * Price就是处理交易需要消耗多少以太币,它就相当于比特币中的交易手续费。

GasLimit限定了本次交易允许消耗资源的最高上限,换句话说,以太坊中的交易不可能无限制地消耗资源,这也是以太坊的安全策略之一,防止攻击者恶意占用资源。

Recipient是交易接收者,它是common.Address指针类型,代表一个地址。这个值也可以是空的,这时在交易执行时,会通过智能合约创建一个地址来完成交易。

Amount是交易额。这个简单,不用解释。

Payload比较重要,它是一个字节数组,可以用来作为创建合约的指令数组,这时每个字节都是一个单独的指令;也可以作为数据数组,由合约指令来进行操作。合约由以太坊虚拟机(Ethereum Virtual Machine,EVM)创建并执行。

V、R、S是交易的签名数据。以太坊当中,交易经过数字签名之后,生成的signature是一个长度65的字节数组,它被截成三段,前32字节被放进R,再32字节放进S,最后1个字节放进V。那么为什么要被截成3段呢?以太坊用的是ECDSA算法,R和S就是ECSDA签名输出,V则是Recovery ID。看下面的javascript代码:

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

在早前的版本中,根据R的奇偶性取值27或28。在EIP-155之后,为了防范Replay Attack,V被调整为CHAIN_ID * 2 + 35/36,确保不同的链中V值不相同。来看一下core/types/transaction_signing.go末尾定义的deriveChainId函数:

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,下面仔细研究一下以太坊交易是怎么签名的。在core/types/transaction_signing.go当中,定义了Signer这个签名接口,以及几个实现签名的类,其UML类图如下:
以太坊交易签名UML类图

如何确定用哪个Signer实施签名操作?这是MakeSigner函数的任务:

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个函数,其作用分别如下:

  • Sender返回交易发起方,也是付款方的地址。
  • SignatureValues根据给定签名返回原始的R、S、V值。
  • Hash返回一个交易的哈希值,用于签名操作。
  • Equal用于判断两个Signer是否相同。

我们知道,以太坊发布分成为四个阶段,分别是Frontier、Homestead、Metropolis和Serenity。所以在几个不同的签名类中,FrontierSigner是最早出来的,然后是HomesteadSigner,之后EIP155推出时才有EIP155Signer。我们依次来看一下。

type FrontierSigner struct{}

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

所以实际上FrontierSigner就是一个空类,是最基础的实现。来看它的另外两个函数:

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, })
}

都比较简单直观。其中Hash函数采用了RLP编码过程。最后来看Sender函数的实现:

func (fs FrontierSigner) Sender(tx *Transaction) (common.Address, error) {
    //注意最后一个homestead参数,这里是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
    //恢复公钥
    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
}

这里用到的加密算法,将来如果有机会再深入剖析。接着看HomesteadSigner,它的相关代码是这样的:

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)
}

很简单是不是?跟FrontierSigner的区别就是在调用recoverPlain的时候,改动了末尾最后一个参数,内部实现上的差别就是多了一步验证,这里不再多述。

最后看EIP155Signer。代码不多,不再分拆了,详看注释:

type EIP155Signer struct {
    chainId, chainIdMul *big.Int  //EIP155对不同的链是做了区分的
}

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() {  //如果还是早前的交易,直接调用Homestead版的方法
        return HomesteadSigner{}.Sender(tx)
    }
    if tx.ChainId().Cmp(s.chainId) != 0 {  //链号不对,报错
        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 {
    //注意这里的区别,Hash的时候多增加了一个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), })
}

全文完。


将来的你,一定会感谢今天拼命的自己。

你可能感兴趣的:(以太坊源码探究之交易与签名)