理解Geth--签名

交易的构成

回顾一下交易结构,分析一下这些字段分别是什么含义,如何填充的,以legacyTx为例:

type LegacyTx struct {
	Nonce    uint64          // nonce of sender account
	GasPrice *big.Int        // wei per gas
	Gas      uint64          // gas limit
	To       *common.Address `rlp:"nil"` // nil means contract creation
	Value    *big.Int        // wei amount
	Data     []byte          // contract invocation input data
	V, R, S  *big.Int        // signature values
}

以要mint nft为例:

  • nonce是发送方的交易序号
  • gasPrice是指定的燃油费
  • gas是消耗上限
  • to是目标地址,这里即合约地址
  • value是要发送的ether数目
  • data是对交易内容(”mint(uint256 tokenId)”)abi编码后的数据

至此,交易内容构建完成,还差V R S三个字段,即交易签名,签名的具体实现由signer控制

Signer定义:signer是一个接口,需要实现以下方法:

type Signer interface {
	// Sender returns the sender address of the transaction.
	Sender(tx *Transaction) (common.Address, error) // 恢复出交易签名者地址

	// SignatureValues returns the raw R, S, V values corresponding to the
	// given signature.
	SignatureValues(tx *Transaction, sig []byte) (r, s, v *big.Int, err error) // 根据签名结果解析出r s v
	ChainID() *big.Int // 返回当前chainid

	// Hash returns 'signature hash', i.e. the transaction hash that is signed by the
	// private key. This hash does not uniquely identify the transaction.
	Hash(tx *Transaction) common.Hash // 计算交易结构的hash值

	// Equal returns true if the given signer is the same as the receiver.
	Equal(Signer) bool // 比对signer是否一致
}

直接看最关键的签名函数:

与上面的例子结合起来,参数tx即已构造好但未签名的结构,s是signer接口,prv是私钥

  • 先调用signer的Hash函数,得出tx的序列化结果
  • 调用crypto.Sign,使用私钥对tx的序列化结果进行签名
  • 调用tx.WithSignature,对签名结果sig进行解析,得出R S V三个字段并设置到tx结构中
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 tx.WithSignature(s, sig)
}

至此,完整的tx结构就构建好了,我们可以发送给矿工进行处理

Signer的实现

回到signer接口,随着EIP的提出及升级,具体的实现如下:

  • cancunSigner:坎昆升级
  • londonSigner:伦敦升级
  • eip2930Signer:柏林升级
  • EIP155Signer:EIP155
  • HomesteadSigner:升级
  • FrontierSigner:第一版

分析SignTx函数可知,不同的signer最关键的区别就是Hash方法的实现:

  • FrontierSigner
func (fs FrontierSigner) Hash(tx *Transaction) common.Hash {
	return rlpHash([]interface{}{
		tx.Nonce(),
		tx.GasPrice(),
		tx.Gas(),
		tx.To(),
		tx.Value(),
		tx.Data(),
	})
}
  • HomesteadSignerFrontierSigner 相同
  • EIP155Signer:追加了chainId和两个0
func (s EIP155Signer) Hash(tx *Transaction) common.Hash {
	return rlpHash([]interface{}{
		tx.Nonce(),
		tx.GasPrice(),
		tx.Gas(),
		tx.To(),
		tx.Value(),
		tx.Data(),
		s.chainId, uint(0), uint(0),
	})
}
  • eip2930Signer:如果是accesslist类型的交易,前面加上chainid,后面追加accesslist
func (s eip2930Signer) Hash(tx *Transaction) common.Hash {
	switch tx.Type() {
	case LegacyTxType:
		return rlpHash([]interface{}{
			tx.Nonce(),
			tx.GasPrice(),
			tx.Gas(),
			tx.To(),
			tx.Value(),
			tx.Data(),
			s.chainId, uint(0), uint(0),
		})
	case AccessListTxType:
		return prefixedRlpHash(
			tx.Type(),
			[]interface{}{
				s.chainId,
				tx.Nonce(),
				tx.GasPrice(),
				tx.Gas(),
				tx.To(),
				tx.Value(),
				tx.Data(),
				tx.AccessList(),
			})
	default:
		// This _should_ not happen, but in case someone sends in a bad
		// json struct via RPC, it's probably more prudent to return an
		// empty hash instead of killing the node with a panic
		//panic("Unsupported transaction type: %d", tx.typ)
		return common.Hash{}
	}
}
  • londonSigner:把签名的gasPrice改为了GasTipCap和GasFeeCap
func (s londonSigner) Hash(tx *Transaction) common.Hash {
	if tx.Type() != DynamicFeeTxType {
		return s.eip2930Signer.Hash(tx)
	}
	return prefixedRlpHash(
		tx.Type(),
		[]interface{}{
			s.chainId,
			tx.Nonce(),
			tx.GasTipCap(),
			tx.GasFeeCap(),
			tx.Gas(),
			tx.To(),
			tx.Value(),
			tx.Data(),
			tx.AccessList(),
		})
}
  • cancunSigner:新增了BlobGasFeeCap和BlobHashes字段
func (s cancunSigner) Hash(tx *Transaction) common.Hash {
	if tx.Type() != BlobTxType {
		return s.londonSigner.Hash(tx)
	}
	return prefixedRlpHash(
		tx.Type(),
		[]interface{}{
			s.chainId,
			tx.Nonce(),
			tx.GasTipCap(),
			tx.GasFeeCap(),
			tx.Gas(),
			tx.To(),
			tx.Value(),
			tx.Data(),
			tx.AccessList(),
			tx.BlobGasFeeCap(),
			tx.BlobHashes(),
		})
}

可以看到,随着以太坊升级,交易结构和Signer是同步变化的,目的是为了把升级变更后的交易字段也添加到待签名数据中

创建Signer对外暴露了三个方法,由繁到简:

  • func MakeSigner(config *params.ChainConfig, blockNumber *big.Int) Signer
  • func LatestSigner(config *params.ChainConfig) Signer
  • func LatestSignerForChainID(chainID *big.Int) Signer

这三个方法实现的功能是一致的,但目标对象不一样

最简单的是LatestSignerForChainID,在用户签名的客户端都使用这个方法,实现也比较简单,只要客户端sdk升级,就默认选择最新的signer,例如最新的geth代码中已经使用了cancunSigner

func LatestSignerForChainID(chainID *big.Int) Signer {
	if chainID == nil {
		return HomesteadSigner{}
	}
	return NewCancunSigner(chainID)
}

上面两个方法的主要使用对象是节点自身,最主要使用的是Signer接口的Sender 方法,从交易签名中恢复出地址,具体的签名函数和实现细节在这里不做赘述

comments powered by Disqus