以太坊交易签名过程源码解析

栏目: IT技术 · 发布时间: 4年前

内容简介:向以太坊网络发起一笔交易时,需要使用私钥对交易进行签名,那么从原始的请求数据到最终的签名后的数据,这中间的数据流转是怎样的,经过了什么过程,今天从go-ethereum源码入手,解析下数据的转换。我以一个简单合约为例,调用合约的调用代码如下所示。

向以太坊网络发起一笔交易时,需要使用私钥对交易进行签名,那么从原始的请求数据到最终的签名后的数据,这中间的数据流转是怎样的,经过了什么过程,今天从go-ethereum源码入手,解析下数据的转换。

一、准备工作

我以一个简单合约为例,调用合约的 setA 方法,参数为 123 。合约代码如下。

pragma solidity >=0.4.22 <0.6.0;
contract Test {
    uint256 internal a;
    event SetA(address indexed _from, uint256 _value);
    
    function setA(uint256 _a) public {
        a = _a;
        emit SetA(msg.sender, _a);
    }
    
    function getA() public view returns (uint256) {
        return a;
    }
}

调用代码如下所示。

package main
import (
	"context"
	"fmt"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/math"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/ethclient"
	"math/big"
)

func main() {
	// 一、ABI编码请求参数
	methodId := crypto.Keccak256([]byte("setA(uint256)"))[:4]
	fmt.Println("methodId: ", common.Bytes2Hex(methodId))
	paramValue := math.U256Bytes(new(big.Int).Set(big.NewInt(123)))
	fmt.Println("paramValue: ", common.Bytes2Hex(paramValue))
	input := append(methodId, paramValue...)
	fmt.Println("input: ", common.Bytes2Hex(input))

	// 二、构造交易对象
	nonce := uint64(24)
	value := big.NewInt(0)
	gasLimit := uint64(3000000)
	gasPrice := big.NewInt(20000000000)
	rawTx := types.NewTransaction(nonce, common.HexToAddress("0x05e56888360ae54acf2a389bab39bd41e3934d2b"), value, gasLimit, gasPrice, input)
	jsonRawTx, _ := rawTx.MarshalJSON()
	fmt.Println("rawTx: ", string(jsonRawTx))

	// 三、交易签名
	signer := types.NewEIP155Signer(big.NewInt(1))
	key, err := crypto.HexToECDSA("e8e14120bb5c085622253540e886527d24746cd42d764a5974be47090d3cbc42")
	if err != nil {
		fmt.Println("crypto.HexToECDSA failed: ", err.Error())
		return
	}
	sigTransaction, err := types.SignTx(rawTx, signer, key)
	if err != nil {
		fmt.Println("types.SignTx failed: ", err.Error())
		return
	}
	jsonSigTx, _ := sigTransaction.MarshalJSON()
	fmt.Println("sigTransaction: ", string(jsonSigTx))

	// 四、发送交易
	ethClient, err := ethclient.Dial("http://127.0.0.1:7545")
	if err != nil {
		fmt.Println("ethclient.Dial failed: ", err.Error())
		return
	}
	err = ethClient.SendTransaction(context.Background(), sigTransaction)
	if err != nil {
		fmt.Println("ethClient.SendTransaction failed: ", err.Error())
		return
	}
	fmt.Println("send transaction success,tx: ", sigTransaction.Hash().Hex())
}

二、ABI编码请求参数

setA(123) 经过ABI编码后得到的数据是: 0xee919d50000000000000000000000000000000000000000000000000000000000000007b

这个数据包含两部分:

  • methodId ,函数标识码(4个字节),对 setA(uint256) 求Keccak256,然后取前4位,值为: ee919d50
  • paramValue ,函数参数(32字节),对值为123的BigInt类型转byte,值为:, 000000000000000000000000000000000000000000000000000000000000007b

三、构造 Transaction 对象

构造交易对象需要的参数包括:

nonce
address
value
gasLimit
gasPrice
input

如果是部署合约时, address 为空。 如果是以太币转账交易, input 为空, address 为接收者地址。

交易的核心数据结构是 txdata

// go-ethereum/core/types/transaction.go
type Transaction struct {
	data txdata
	// caches
	hash atomic.Value
	size atomic.Value
	from atomic.Value
}

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:"-"`
}

func newTransaction(nonce uint64, to *common.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, data []byte) *Transaction {
	if len(data) > 0 {
		data = common.CopyBytes(data)
	}
	d := txdata{
		AccountNonce: nonce,
		Recipient:    to,
		Payload:      data,
		Amount:       new(big.Int),
		GasLimit:     gasLimit,
		Price:        new(big.Int),
		V:            new(big.Int),
		R:            new(big.Int),
		S:            new(big.Int),
	}
	if amount != nil {
		d.Amount.Set(amount)
	}
	if gasPrice != nil {
		d.Price.Set(gasPrice)
	}

	return &Transaction{data: d}
}

txdata 中的 V , R , S 三个字段是与签名相关。 构造后的交易对象输出结果为(此时v、r、s为默认空值):

rawTx:  {"nonce":"0x18","gasPrice":"0x4a817c800","gas":"0x2dc6c0","to":"0x05e56888360ae54acf2a389bab39bd41e3934d2b","value":"0x0","input":"0xee919d50000000000000000000000000000000000000000000000000000000000000007b","v":"0x0","r":"0x0","s":"0x0","hash":"0x629d42fd16be0b5dc22d53d63dcce8144d5fc843e056465bc2bea25f4ebe8249"}

四、交易签名

交易签名核心调用 types.SignTx 方法,源码如下所示。

// go-ethereum/core/types/transaction_signing.go
// SignTx signs the transaction using the given signer and private key
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)
}

SignTx 方法有三个参数:

  • tx *Transaction ,构造 Transaction 对象
  • s Signer ,signer签名方式,包括 EIP155SignerHomesteadSignerFrontierSigner ,其中 HomesteadSigner 继承 FrontierSigner 。之所以需要该字段,是因为在EIP155中修复了简单重复攻击漏洞后,需要保持旧区块链的签名方式不变,但又需要提供新版本的签名方式。因此根据区块高度创建不同的签名器。
  • prv *ecdsa.PrivateKey ,secp256k1标准的私钥

SignTx 方法的签名过程分为三步:

  1. 对交易信息计算rlpHash
  2. 对rlpHash使用私钥进行签名
  3. 填充交易对象中的 V , R , S 字段

4.1 计算rlpHash

EIP155Signer 实现的hash算法相比 FrontierSigner 多了一个链ID和两个uint空值,这样的话,一笔已签名的交易只可能属于一条链。

Hash计算代码如下所示。

// go-ethereum/core/types/transaction_signing.go
func (s EIP155Signer) 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,
		s.chainId, uint(0), uint(0),
	})
}

rlpHash 的计算结果为: 0x9ef7f101dae55081553998d52d0ce57c4cf37271f800b70c0863c4a749977ef1

4.2 私钥签名

crypto.Sign(h[:], prv) 源代码如下所示。

// go-ethereum/crypto/signature_cgo.go
func Sign(hash []byte, prv *ecdsa.PrivateKey) (sig []byte, err error) {
	if len(hash) != 32 {
		return nil, fmt.Errorf("hash is required to be exactly 32 bytes (%d)", len(hash))
	}
	seckey := math.PaddedBigBytes(prv.D, prv.Params().BitSize/8)
	defer zeroBytes(seckey)
	return secp256k1.Sign(hash, seckey)
}

Sign 方法调用 secp256k1 的椭圆曲线算法进行签名,签名后返回结果为: 41c4a2eb073e6df89c3f467b3516e9c313590d8d57f7c217fe7e72a7b4a6b8ed5f20a758396a5e681ce1ab4cec749f8560e28c9eb91072ec7a8acc002a11bb1d00

4.3 填充交易对象中的 V , R , S 字段

tx.WithSignature(s, sig) 源代码如下所示。

// go-ethereum/core/types/transaction_signing.go
func (tx *Transaction) WithSignature(signer Signer, sig []byte) (*Transaction, error) {
	r, s, v, err := signer.SignatureValues(tx, sig)
	if err != nil {
		return nil, err
	}
	cpy := &Transaction{data: tx.data}
	cpy.data.R, cpy.data.S, cpy.data.V = r, s, v
	return cpy, nil
}

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)
	}
	return R, S, V, nil
}
func (hs HomesteadSigner) SignatureValues(tx *Transaction, sig []byte) (r, s, v *big.Int, err error) {
	return hs.FrontierSigner.SignatureValues(tx, sig)
}
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])
	if tx.IsPrivate() {
		v = new(big.Int).SetBytes([]byte{sig[64] + 37})
	} else {
		v = new(big.Int).SetBytes([]byte{sig[64] + 27})
	}
	return r, s, v, nil
}

WithSignature 方法中,核心调用了 SignatureValues 方法。 EIP155SignerSignatureValues 方法相比 FrontierSigner 的方法,区别是在计算 V 值上。

FrontierSignerSignatureValues 方法中,将签名结果 41c4a2eb073e6df89c3f467b3516e9c313590d8d57f7c217fe7e72a7b4a6b8ed5f20a758396a5e681ce1ab4cec749f8560e28c9eb91072ec7a8acc002a11bb1d00 分为三份,分别是:

  • 前32字节的 R , 41c4a2eb073e6df89c3f467b3516e9c313590d8d57f7c217fe7e72a7b4a6b8ed
  • 中间32字节的 S , 5f20a758396a5e681ce1ab4cec749f8560e28c9eb91072ec7a8acc002a11bb1d
  • 最后一个字节 00 加上27,得到 V ,十进制为27

EIP155SignerSignatureValues 方法中,根据链ID重新计算 V 值,我这里的链ID是1,重新计算得到的 V 值十进制结果是37。

签名后的交易对象结果为: {"nonce":"0x18","gasPrice":"0x4a817c800","gas":"0x2dc6c0","to":"0x05e56888360ae54acf2a389bab39bd41e3934d2b","value":"0x0","input":"0xee919d50000000000000000000000000000000000000000000000000000000000000007b","v":"0x25","r":"0x41c4a2eb073e6df89c3f467b3516e9c313590d8d57f7c217fe7e72a7b4a6b8ed","s":"0x5f20a758396a5e681ce1ab4cec749f8560e28c9eb91072ec7a8acc002a11bb1d","hash":"0xf8a3bf13828d50b107da40188c8e772b83a613f0044593a4e49438a214a79c83"}

五、发送交易

发送交易 SendTransaction 方法首先会对具有签名信息的交易对象进行rlp编码,编码后调用的jsonrpc的 eth_sendRawTransaction 方法发送交易。 源代码如下所示:

// go-ethereum/ethclient/ethclient.go
func (ec *Client) SendTransaction(ctx context.Context, tx *types.Transaction) error {
	data, err := rlp.EncodeToBytes(tx)
	if err != nil {
		return err
	}
	return ec.c.CallContext(ctx, nil, "eth_sendRawTransaction", common.ToHex(data))
}

最终计算得到的签名后的交易数据为: 0xf889188504a817c800832dc6c09405e56888360ae54acf2a389bab39bd41e3934d2b80a4ee919d50000000000000000000000000000000000000000000000000000000000000007b25a041c4a2eb073e6df89c3f467b3516e9c313590d8d57f7c217fe7e72a7b4a6b8eda05f20a758396a5e681ce1ab4cec749f8560e28c9eb91072ec7a8acc002a11bb1d

六、总结

至此,交易的签名已完成,得到了签名数据。从原始数据到签名数据,核心的技术点包括:

  • ABI编码
  • 交易信息rpl编码
  • 椭圆曲线 secp256k1 签名
  • 根据签名结果计算 V , R , S

参考: https://learnblockchain.cn/books/geth/part3/sign-and-valid.html


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

创业就是要细分垄断

创业就是要细分垄断

李开复、汪华、傅盛 / 文化发展出版社 / 2017-5-1 / CNY 45.00

对各方面资源极为有限的创业公司而言,想在激烈的市场竞争中站立下来的第一步是:成为细分市场的垄断者。不管是资本还是尖端人才,追逐的永远是行业里尖端的企业,第二名毫无意义。 首先,要精准定位潜在市场。这个市场的需求仍没有被满足,并且潜力巨大。其次,抓住时代和行业的红利,通过高速增长实现“小垄断”,抢滩登陆。最后,在细分领域里建立起自己的竞争壁垒,应对巨头和竞争对手的复制,去扩展更大的市场,从而扩......一起来看看 《创业就是要细分垄断》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

随机密码生成器
随机密码生成器

多种字符组合密码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具