离线发送签名交易
何为离线?
指你的机器上并没有运行着以太坊客户端,也就没有节点可言了
何为签名交易?
即利用keystore文件和解锁账户所需的密码重新计算出一个对应着账户地址的私钥,利用这个私钥对想要发起的交易进行签名,签名后会得到一串16进制形式的字符串,即可通过广播将该交易发布到区块链中,以待被矿工打包确认。
应用场景?
一些以太坊Dapp、以及应用程序并不希望其用户也在终端上运行以太坊节点,(用户肯定也不想这么麻烦),那就在用户终端上计算出用户的私钥(为了安全,这个私钥并不传到服务端,而是直接在用户终端本地对交易进行签名,最后将已经签名过的交易发送到服务端,由服务端进行广播,从而达到离线发送交易。
实现框架
网页端:
- html
- js/ethereumjs-all-2018-1-17.min.js
- js/keythereum.min.js
服务端:
- web3.js 1.0
下面的步骤为了简化,我将服务端部分也整合到网页端,以作展示。
实现过程
一、在html中引入js文件
js文件下载路径:
- https://github.com/ethereumjs/browser-builds/tree/master/dist/ethereumjs-all
- https://github.com/ethereumjs/keythereum
<script src="js/web3.min.js" type="text/javascript"></script>
<script src="js/ethereumjs-all-2018-1-17.min.js" type="text/javascript"></script>
<script src="js/keythereum.min.js" type="text/javascript"></script>
<script src="eth4Remote.js" type="text/javascript"></script>
二、新建一个js文件,名为eth4Remote.js
(1)先给出利用keystore文件和解锁密码计算出私钥Privatekey的部分
//新建tx对象,签名交易对象
var tx = new ethereumjs.Tx();
//新建私钥
var privatekey;
let web3;
//定义一个计算私钥的函数
function getPrivateKey(){
//jsonStr为节点目录下keystore文件的内容
var jsonStr='{"address":"ade50c7dd4a010063059eeaadf73aa016d9892f9","crypto":{"cipher":"aes-128-ctr","ciphertext":"c17459c90e981f455509dd0257b8642a428c6b4029a0b606c2f4442b68dda602","cipherparams":{"iv":"3bb79f892f8971dde7a03bbf0fefd047"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"d06768ba21ea2474779fdde7a868378751619cbc2ae75324423477383d3d9dd1"},"mac":"14bb7150ad442949e7676813b074ef859f4693fda59a96268b1f6c414e3a0d5a"},"id":"bf3b1b34-8dcf-4828-91ce-dfaecafb2368","version":3}';
//转换为json对象
var keyObject = JSON.parse(jsonStr);
//利用keythereum计算privatekey(私钥)
//传入参数为("解锁账户时所需的密码",keyObject)
var privatekey = keythereum.recover("123",keyObject);
console.log("the private key is:",privatekey.toString('hex'));
return privatekey;
}
$(document).ready(function() {
privatekey=getPrivateKey();
}
上面这一段,在html打开时变开始计算私钥了,计算过程需要一段时间,最好做点提示标志。
输出的privatekey如下:
(2)有了privatekey,接下来实现交易的签名
从官方说明:https://github.com/ethereumjs/ethereumjs-tx
可以看到,要实现交易的签名,需要先构造一个tx
tx的一些参数说明如下:
- nonce:账户发起的交易数,这个值不能过大,也不能过小,需要利用web3.js的getTransactionCount() 获取,如果设置过大,就会发现交易虽然发起,但是却不会被矿工打包确认,因为前面的nonce的交易还没发起呢!!这点是要注意的
- gasPrice:设置为'0x09184e72a000' 即可
- gasLimit:设置为跟Remix里面大小一样即可,"0x2dc6c0"
- to:这个是关键参数,应设置为要转账的以太坊地址或者是要调用函数的合约地址
- value:要发送的以太币或者是调用合约方法时要传入的以太币
- data:如果是转账以太币,则可为空;如果是调用合约里面的方法,则需要传入abi编码格式的参数;1)官方关于abi的说明:https://solidity.readthedocs.io/en/develop/abi-spec.html#types 2)也可以利用官方集成的abi库进行简化调用:https://github.com/ethereumjs/ethereumjs-abi
//新建tx对象,签名交易对象
var tx = new ethereumjs.Tx();
//这里要调用合约中的方法,所以
//这里的地址是合约地址
tx.to="0x73e52e2bdeda481ac5ba92e24ec59521d2d75a01";
//data可以直接从remix中调用方法时的回执中复制
tx.data="0x950887d7000000000000000000000000000000000000000000000000000000000000000a";
//gas设置
tx.gasLimit="0x2dc6c0";
tx.gasPrice='0x09184e72a000';
由于要设置tx的nonce属性,这就要先获取该账户的交易数,这里不展开,具体思路为:(1)先从服务端得到账户的交易数,从而设置tx对象的nonce属性(2)得到nonce属性和在第一步中获取到的privatekey之后,就可以签名交易了。
为了简化流程,假设我已经获取到nonce为828,转化为16进制则为:0x33c
进一步的,设置tx属性
//设置nonce属性
tx.nonce="0x33c";
//开始签名交易
tx.sign(privatekey);
var serializedTx = tx.serialize();
//输出签名后的字符串
console.log('0x' + serializedTx.toString('hex'));
(3)全部代码为:
//新建tx对象,签名交易对象
var tx = new ethereumjs.Tx();
//新建私钥
var privatekey;
let web3;
//定义一个计算私钥的函数
function getPrivateKey(){
//jsonStr为节点目录下keystore文件的内容
var jsonStr='{"address":"ade50c7dd4a010063059eeaadf73aa016d9892f9","crypto":{"cipher":"aes-128-ctr","ciphertext":"c17459c90e981f455509dd0257b8642a428c6b4029a0b606c2f4442b68dda602","cipherparams":{"iv":"3bb79f892f8971dde7a03bbf0fefd047"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"d06768ba21ea2474779fdde7a868378751619cbc2ae75324423477383d3d9dd1"},"mac":"14bb7150ad442949e7676813b074ef859f4693fda59a96268b1f6c414e3a0d5a"},"id":"bf3b1b34-8dcf-4828-91ce-dfaecafb2368","version":3}';
//转换为json对象
var keyObject = JSON.parse(jsonStr);
//利用keythereum计算privatekey(私钥)
//传入参数为("解锁账户时所需的密码",keyObject)
var privatekey = keythereum.recover("123",keyObject);
console.log("the private key is:",privatekey.toString('hex'));
return privatekey;
}
$(document).ready(function() {
//获取私钥Privatekey
privatekey=getPrivateKey();
//新建tx对象,签名交易对象
var tx = new ethereumjs.Tx();
//这里要调用合约中的方法,所以
//这里的地址是合约地址
tx.to="0x73e52e2bdeda481ac5ba92e24ec59521d2d75a01";
//data可以直接从remix中调用方法时的回执中复制
tx.data="0x950887d7000000000000000000000000000000000000000000000000000000000000000a";
//gas设置
tx.gasLimit="0x2dc6c0";
tx.gasPrice='0x09184e72a000';
//设置nonce属性
tx.nonce="0x33c";
//开始签名交易
tx.sign(privatekey);
var serializedTx = tx.serialize();
//输出签名后的字符串
console.log('0x' + serializedTx.toString('hex'));
}
最终,可以看到输出签名后的字符串:
(4)得到这个签名后的交易,那么就可以直接使用web3.js的eth.senSignedTransaction()方法实现广播,具体可参见api:http://cw.hubwiz.com/card/c/web3.js-1.0/1/2/22/ ,或者也可以直接在geth客户端里面使用eth.sendRawTransaction("签名后的交易字符串")进行广播,这样就不需要解锁账户,也可以实现发起交易了。
发起交易后,在geth客户端可以看到交易已提交:
这里给出的是直接在网页端实现的纯前端的发送离线签名交易javascript文件内容,记得先在html文件中引入所需的js文件:
<script src="js/web3.min.js" type="text/javascript"></script>
<script src="js/ethereumjs-all-2018-1-17.min.js" type="text/javascript"></script>
<script src="js/keythereum.min.js" type="text/javascript"></script>
<script src="eth4Remote.js" type="text/javascript"></script>
eth4Remote.js如下:
var tx = new ethereumjs.Tx();
tx.to="0x73e52e2bdeda481ac5ba92e24ec59521d2d75a01";
tx.data="0x950887d7000000000000000000000000000000000000000000000000000000000000000a";
tx.gasLimit="0x2dc6c0";
tx.gasPrice='0x09184e72a000';
//var abi=new ethereumjs.ABI();
let web3;
var privatekey;
$(document).ready(function() {
if (typeof web3 !== 'undefined') { //检查是否已有web3实例
web3 = new Web3(web3.currentProvider);
} else {
//否则就连接到给出节点
web3 = new Web3();
web3.setProvider(new Web3.providers.WebsocketProvider("ws://localhost:8546")); //注意这里注意端口不用一致,直接默认8546即可(若刚刚启动节点的rpc端口是8545的情况下)
}
//判断Web与节点的连接状态
web3.eth.getBlock(0, function(error, result) {
if (!error){
console.log("connect should be success");
}
else{
console.log("something wrong,the connection might be failed");
alert("连接节点失败");
}
})
console.log(tx);
privatekey=getPrivateKey();
});
$("#Btn").click(function() {
//先获取账户nonce
web3.eth.getTransactionCount("0xade50c7dd4a010063059eeaadf73aa016d9892f9",function(error,result)
{
if(!error){
//设置tx的nonce属性
console.log("TransactionCount is:",result);
tx.nonce="0x"+(Number(result)).toString(16);
//签名交易
tx.sign(privatekey);
console.log("tx from :",tx.from.toString('hex'));
//序列化
var serializedTx = tx.serialize();
console.log('0x' + serializedTx.toString('hex'));
//发起签名交易
web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'))
.on('receipt', console.log)
}else{
console.log(error);
}
});
});
//定义一个计算私钥的函数
function getPrivateKey(){
//jsonStr为节点目录下keystore文件的内容
var jsonStr='{"address":"ade50c7dd4a010063059eeaadf73aa016d9892f9","crypto":{"cipher":"aes-128-ctr","ciphertext":"c17459c90e981f455509dd0257b8642a428c6b4029a0b606c2f4442b68dda602","cipherparams":{"iv":"3bb79f892f8971dde7a03bbf0fefd047"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"d06768ba21ea2474779fdde7a868378751619cbc2ae75324423477383d3d9dd1"},"mac":"14bb7150ad442949e7676813b074ef859f4693fda59a96268b1f6c414e3a0d5a"},"id":"bf3b1b34-8dcf-4828-91ce-dfaecafb2368","version":3}';
//转换为json对象
var keyObject = JSON.parse(jsonStr);
//利用keythereum计算privatekey(私钥)
//传入参数为("解锁账户时所需的密码",keyObject)
var privatekey = keythereum.recover("123",keyObject);
console.log("the private key is:",privatekey.toString('hex'));
return privatekey;
}