1. 程式人生 > >以太坊轉賬離線簽名

以太坊轉賬離線簽名

首先是錢包本地將轉賬資訊進行離線簽名,然後通過以太坊JSON-RPC方法傳送到以太坊節點,其中以太坊和其他ERC20代幣的簽名是不同的.

轉賬

ETH轉賬簽名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
 * ETH 轉賬離線簽名
 * @param to 轉入的錢包地址
 * @param nonce 以太坊nonce
 * @param gasPrice gasPrice
 * @param gasLimit gasLimit
 * @param amount 轉賬的eth數量
 * @param wallet 錢包物件
 * @param password 密碼
 * @return 簽名data
 */
public String signedEthTransactionData(String to,
                                          BigInteger nonce,
                                          BigInteger gasPrice,
                                          BigInteger gasLimit,
                                          BigDecimal amount,
                                          HLWallet wallet,
                                          String password) throws Exception {
    // 把十進位制的轉換成ETH的Wei, 1ETH = 10^18 Wei
    BigDecimal amountInWei = Convert.toWei(amount.toString(), Convert.Unit.ETHER);
    RawTransaction rawTransaction = RawTransaction.createEtherTransaction(nonce, gasPrice, gasLimit, to, amountInWei.toBigInteger());
    return signData(rawTransaction,wallet,password);
}

private String signData(RawTransaction rawTransaction,
                        HLWallet wallet,
                        String password) throws Exception {
    Credentials credentials = Credentials.create(LWallet.decrypt(password, wallet.walletFile));
    byte[] signMessage = TransactionEncoder.signMessage(rawTransaction, ChainId.MAINNET, credentials);
    return Numeric.toHexString(signMessage);

}

nonce

為了防止交易的重播攻擊,每筆交易必須有一個nonce隨機數,針對每一個賬戶nonce都是從0開始,當nonce為0的交易處理完之後,才會處理nonce為1的交易,並依次加1的交易才會被處理.以下是nonce使用的幾條規則:

  • 當nonce太小,交易會被直接拒絕。
  • 當nonce太大,交易會一直處於佇列之中,這也就是導致我們上面描述的問題的原因;
  • 當傳送一個比較大的nonce值,然後補齊開始nonce到那個值之間的nonce,那麼交易依舊可以被執行。
  • 當交易處於queue中時停止geth客戶端,那麼交易queue中的交易會被清除掉。

ERC-20代幣轉賬簽名

ERC-20代幣與以太坊轉賬是不同的,需要走智慧合約.

以下是手動拼接的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public String signedContractTransactionData(String contractAddress,
                                               String to,
                                               BigInteger nonce,
                                               BigInteger gasPrice,
                                               BigInteger gasLimit,
                                               BigDecimal amount,
                                               BigDecimal decimal,
                                               HLWallet wallet,
                                               String password) throws Exception {
    //因為每個代幣可以規定自己的小數位, 所以實際的轉賬值=數值 * 10^小數位
    BigDecimal realValue = amount.multiply(decimal);
    //0xa9059cbb代表某個代幣的轉賬方法hex(transfer) + 對方的轉賬地址hex + 轉賬的值的hex
    String data = Params.Abi.transfer + // 0xa9059cbb
            Numeric.toHexStringNoPrefixZeroPadded(Numeric.toBigInt(to), 64) +
            Numeric.toHexStringNoPrefixZeroPadded(realValue.toBigInteger(), 64);

    RawTransaction rawTransaction = RawTransaction.createTransaction(
            nonce,
            gasPrice,
            gasLimit,
            contractAddress,
            data);
    return signData(rawTransaction, wallet, password);
}

此時我們需要將轉賬的指令資訊通過簽名時的data傳送出去.代幣轉賬的data組成部分有三個.

  1. 首先是固定頭0xa9059cbb
  2. 然後是轉入錢包地址
  3. 再是轉賬的數量,以wei作為單位

這裡我們說明下,為什麼固定頭是0xa9059cbb.是因為它是函式原型transfer(address,uint256)的MethodId,相當於以太坊用來分辨指令的標識,具體轉換方法為

1
2
3
4
5
6
7
// transfer -> 0xa9059cbb

 String transfer = "transfer(address,uint256)";
 byte[] bytes = transfer.getBytes();
 byte[] bytes1 = org.web3j.crypto.Hash.sha3(bytes);
 String hex = Numeric.toHexString(bytes1, 0, 4, true);
 ShadowLog.i("transfer", hex);

然後對於轉入地址及轉賬數量,我們是將其轉化為16進位制,然後左邊補0至64位.三個引數字串拼接在一起即為最後簽名時用到的data. 那麼可能有人會問,是不是其他指令引數都是這麼拼接? 當然不是,這裡涉及到以太坊合約ABI,有興趣的童鞋可以看看.這裡因為轉入地址的型別address和轉賬數量的型別uint256都是靜態的,因此是上述的拼接方式,如果是動態的則不一樣了,具體可以參考ABI文件.

這裡我們提供另外一種web3j既有的封裝實現,不用關心內部引數是如何拼接的.(推薦)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public String signContractTransaction(String contractAddress,
                                      String to,
                                      BigInteger nonce,
                                      BigInteger gasPrice,
                                      BigInteger gasLimit,
                                      BigDecimal amount,
                                      BigDecimal decimal,
                                      HLWallet wallet,
                                      String password) throws IOException, CipherException {
    BigDecimal realValue = amount.multiply(decimal);
    Function function = new Function("transfer",
            Arrays.asList(new Address(to), new Uint256(realValue.toBigInteger())),
            Collections.emptyList());
    String data = FunctionEncoder.encode(function);
    RawTransaction rawTransaction = RawTransaction.createTransaction(
            nonce,
            gasPrice,
            gasLimit,
            contractAddress,
            data);
    return signData(rawTransaction, wallet, password);
}

傳送請求

ETH轉賬最終在以太坊瀏覽器上的轉賬結果 link ,可在頁面最下方檢視Input Data

1
0x3078

代幣轉賬最終在以太坊瀏覽器上的轉賬結果 link,Input Data

1
2
3
4
5
Function: transfer(address _to, uint256 _value)

MethodID: 0xa9059cbb
[0]:  000000000000000000000000b7bb6c45800f4531cc1581637868373a06367b48
[1]:  000000000000000000000000000000000000000000000002d1a51c7e00500000

FAQ

Q: 為什麼呼叫sendRawTransaction介面後,轉賬記錄在以太坊瀏覽器或者其他錢包查詢不到?

A: 介面呼叫後會返回一個字串hash,我們通常稱為txHash.此時是不知道結果的,因為擁堵或者其他原因以太坊不一定確認,因此查不到是正常的.在app中,一種做法是在傳送請求後,主動通知自己的中繼relay,在中繼relay中維護這個txHash狀態,以處理中的狀態呈現給使用者.

Q:sendRawTransaction後接口報錯’invalid sender’

A: 這個報錯是以太坊環境錯誤導致的,程式碼上表現為chainId沒對應.

1
2
3
4
5
6
7
8
9
private String signData(byte chainId,
                            RawTransaction rawTransaction,
                            HDWallet wallet,
                            String password) throws Exception {
            Credentials credentials;
            credentials = Credentials.create(Wallet.decrypt(password, wallet.getWalletFile()));
            byte[] signMessage = TransactionEncoder.signMessage(rawTransaction, chainId, credentials);
            return Numeric.toHexString(signMessage);
    }

簽名需要制定環境chainId,若使用不帶chainId的方法,則預設是主網.

友鏈:

參考: