理解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:EIP155HomesteadSigner:升级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(),
})
}
HomesteadSigner与FrontierSigner相同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) Signerfunc LatestSigner(config *params.ChainConfig) Signerfunc 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 方法,从交易签名中恢复出地址,具体的签名函数和实现细节在这里不做赘述