이더리움 거래 서명 과정 원본 코드 분석

12495 단어 이태방블록체인
이더리움 네트워크에 거래를 시작할 때 개인 키를 사용하여 거래에 서명해야 한다. 그러면 원시적인 요청 데이터에서 최종 서명 후의 데이터, 이 사이의 데이터 흐름이 어떠한지, 어떤 과정을 거쳤는지, 오늘 go-ethereum 원본에서 착안하여 데이터의 전환을 분석한다.

1. 준비 작업


나는 간단한 계약을 예로 들면 계약의 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 인코딩
  • 구조 Transaction 거래 대상
  • 거래 대상 RLP 코드
  • 인코딩 후 거래 데이터에 대해 개인 키를 사용하여 타원 곡선 서명을 하고 서명열
  • 서명열에 따라 서명 후 거래 대상
  • 서명된 거래 대상에 대한 RLP 코딩 서명된 거래 데이터
  • 2. ABI 인코딩 요청 매개 변수

    setA(123) ABI 인코딩을 통해 얻은 데이터: 0xee919d50000000000000000000000000000000000000000000000000000000000000007b이 데이터에는
  • methodId, 함수 표지 코드(4바이트), setA(uint256)에 Keccak256을 구하고 4위를 취한다. 값은 ee919d50이다.
  • paramValue, 함수 매개 변수(32바이트), 123의 BigInt 유형에 대해byte, 000000000000000000000000000000000000000000000000000000000000007b
  • 3. 구조 Transaction 대상


    거래 객체를 구성하는 데 필요한 매개 변수는 다음과 같습니다.
  • nonce, 요청 계정 nonce 값
  • address, 계약 주소
  • value, 이체의 이더리움 화폐 개수, 단위 wei
  • gasLimit, 최대 소모gas
  • gasPrice,gas가격
  • 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}
    }
    
    txdataV, 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"}
    

    4. 거래 서명


    거래 서명 핵심은 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 서명 방식은 EIP155Signer HomesteadSignerFrontierSigner을 포함하고 그 중에서 HomesteadSignerFrontierSigner을 계승한다.이 필드가 필요한 이유는 EIP155에서 단순 반복 공격 취약점을 복구한 후 기존 블록체인의 서명 방식을 그대로 유지해야 하지만 새로운 버전의 서명 방식을 제공해야 하기 때문이다.따라서 블록 높이에 따라 다른 서명기를 만듭니다.
  • prv *ecdsa.PrivateKey, secp256k1 표준 개인 키
  • SignTx 방법의 서명 과정은 세 단계로 나뉜다.
  • 거래 정보 계산 rlpHash
  • rlpHash에 개인 키로 서명
  • 거래 대상의 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, 10진수 27
  • EIP155SignerSignatureValues 방법에서 체인 ID에 따라 V값을 다시 계산했다. 여기서 체인 ID는 1이고 다시 계산한 V값의 10진법 결과는 37이다.
    서명 후 거래 대상 결과: {"nonce":"0x18","gasPrice":"0x4a817c800","gas":"0x2dc6c0","to":"0x05e56888360ae54acf2a389bab39bd41e3934d2b","value":"0x0","input":"0xee919d50000000000000000000000000000000000000000000000000000000000000007b","v":"0x25","r":"0x41c4a2eb073e6df89c3f467b3516e9c313590d8d57f7c217fe7e72a7b4a6b8ed","s":"0x5f20a758396a5e681ce1ab4cec749f8560e28c9eb91072ec7a8acc002a11bb1d","hash":"0xf8a3bf13828d50b107da40188c8e772b83a613f0044593a4e49438a214a79c83"}

    5. 거래 발송


    거래 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이다

    6. 총결산


    이로써 거래의 서명이 완료되어 서명 데이터를 얻었다.원본 데이터에서 서명 데이터까지 핵심 기술 포인트는 다음과 같습니다.
  • ABI 인코딩
  • 거래 정보 rpl 인코딩
  • 타원 곡선
  • 서명 결과에 따라 secp256k1, V, R
  • 참조:https://learnblockchain.cn/books/geth/part3/sign-and-valid.html

    좋은 웹페이지 즐겨찾기