主页 > imtoken官网地址打不开 > 以太坊源码探索之交易与签名

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

imtoken官网地址打不开 2023-10-21 05:12:03

与比特币相比,以太坊的交易结构截然不同。 下面是以太坊中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那里买了东西,向B发起了一笔交易T1以太坊的两种账户,然后又提交了另一笔交易T2。 T2的Gas price更高,优先级更高。 如果碰巧T2处理完后,剩余的资金不足以支付T1,那么T1就会被拒绝。 这时,如果B已经把物品给了A,那么A就攻击成功了。 因此,即使交易被处理,你也必须等待一定的时间,以确保产生足够深度的区块,以确保交易的不可逆性。

价格是指单位Gas的价格。 所谓Gas就是交易的消耗。 价格是单位 Gas 消耗的以太币数量。 Gas * Price 是处理交易消耗的以太币数量。 相当于比特币的交易。 手续费。

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

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

金额为交易金额。 这很简单,不需要解释。

有效载荷更重要。 它是一个字节数组,可以用作创建合约的指令数组。 此时,每个字节都是一条单独的指令; 也可以作为合约指令操作的数据数组。 合约由以太坊虚拟机 (EVM) 创建和执行。

V、R、S为交易的签名数据。 在以太坊中,交易经过数字签名后,生成的签名是一个长度为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个函数,其作用如下:

我们知道以太坊的发布分为四个阶段,分别是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), }) }

全文结束。

以后的你,一定会感谢今天辛勤付出的自己。