右边的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类图如下:
如何确定用哪个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), })
}
全文完。
将来的你,一定会感谢今天拼命的自己。