背景描述
以太坊为作为一个公有链,允许任何人发布智能合约,但使用以太坊网络的成本很高,无论是普通交易或是智能合约都需要一定费用。尤其对于大批量的小额交易来讲,由于这些交易是需要全网共识的,如果频繁的执行智能合约,不但会增加以太坊网络的负担,光交易手续费一项,就让人望而却步。
状态通道为此提供了一种新的思路,通过将部分流程移出到链外来提高区块链的效率,但这并不会增加参与者的风险。
举一个简单的比方:区块链相当于银行,而状态通道相当于支付宝。假设Alice跟Bob各自有10000块钱。之前,如果Alice跟Bob如果要互相转账,都要去银行排队。若是大额的还好,如果他们之间每次转1分钱,假设Alice给Bob转了60000次,Bob给Alice转了80000次,那么他们各自得去银行60000次与80000次,而且每转1分钱可能需要付出比1分钱还要高的手续费。现在有了支付宝就不一样了,Alice跟Bob把这10000块钱转到支付宝,然后他们分别转的60000次与80000次都在支付宝里面进行,等转完之后,Alice或者Bob想要把钱提出来的时候,支付宝只要告诉银行说,Alice 现在的金额是 10000 - 60000 * 0.1 + 80000 * 0.1 = 12000
元,Bob现在的金额是 10000 + 60000 * 0.1 - 80000 * 0.1 = 8000
。这样,我们需要在银行(区块链)中执行的 60000 + 80000
次,减少为只要做 1
次。其他的 60000 + 80000 - 1
都在支付宝(状态通道)里面完成了。
后台简要实现
状态通道结合了支付通道和Aeternity通道的特点,将私有privateState和公共publicState(区块链)数据完全分离,也就是在后台会存在两份LevelDB数据库。私有交易和链上交易分开执行的方式,一方面保护链上数据的安全性,同时私有交易可明显提高交易执行效率以及降低交易执行的开销。
其中privateState上的所有交易只需要在交易相关方节点上执行的交易,交易执行结果只记录在交易相关方privateState上,各个节点只维护自己的privateState账本,交易只广播给相关方。为了保证链上交易不受影响,私有交易有单独的rpc入口。此处privateState上的交易类似上面描述在支付宝里面的交易。
而publicState上的交易需要在区块链上执行并确认的交易,交易执行结果记录在区块链的State上,每个节点都维护相同的账本,交易需要在整个链内广播。此处publicState上的交易类似上面描述在银行里面的交易。
合约接口简要描述
StateChannelManager.sol
状态通道管理合约,区块链上用于管理状态通道的合约,存储在区块链publicState上。主要接口有如下:
function open(address _contractAddr, string _participants, uint _expireHeight)
建立通道,记录状态通道入口合约地址,参与方,通道超时高度等信息。
入参:
_contractAddr: address
通道合约地址。_participants: string
通道参与方地址列表。_expireHeight: uint
超时高度。
出参:无
function deposit(address _contractAddr, uint _amount)
账户金额锁定。
入参:
_contractAddr: address
通道合约地址。_amount: uint
锁定金额。
出参:无
function commit(address _contractAddr, uint _seqNo, string _balanceList, string _signatureList, string _storageRoot)
提交确认。
入参:
_contractAddr: address
通道合约地址。_seqNo: uint
私有合约序列号。_balanceList: string
按顺序的余额列表 ,号分隔。_signatureList: string
按顺序的签名列表 ,号分隔。_storageRoot: string
私有合约的storageRoot。
出参:无
function close(address _contractAddr, uint _amount)
关闭通道。
入参:
_contractAddr: address
通道合约地址。
出参:无
function getChannelInfo(address _contractAddr) returns(string _json)
获取通道信息。
入参:
_contractAddr: address
通道合约地址。
出参:
_json: string
通道信息。一个string的JSON字符串。
PayChannel.sol
状态通道入口合约,具体业务逻辑功能合约,依具体业务场景不同而逻辑不同。存储在区块链privateState上。主要接口有如下:
function deposit(uint _amount)
账户金额充值。
入参:
_amount: uint
锁定金额。
出参:无
function transfer(address _to, uint _amount)
转账。
入参:
_to: address
目标账户地址。_amount: uint
转账金额。
出参:无
function query() returns(uint)
当前余额查询。
入参:无
出参:
_amount: uint
当前账户余额。
function seqno() constant returns(uint)
当前序列号。
入参:无
出参:seqNo: uint
序列号。
Tickets.sol
状态通道入口合约阶段确认合约,目的是收集相关方签名确认,以便确认状态能同步到区块链上,存储在区块链privateState上。主要接口有如下:
function confirm(address _addr, uint _seqNo, uint _balance, string _storageRoot, string _signature)
账户金额充值。
入参:
_addr: address
合约地址。_seqNo: uint
序列号。_balance: uint
确认者的余额。_storageRoot: string
合约账户storageRoot。_signature: string
对storageRoot的签名值。
出参:无
function queryTicket(address _addr, uint _seqNo) view returns(string _json)
查询签名信息。
入参:
_to: address
合约地址。_seqNo: uint
序列号。
出参:
_json: string
签名信息。一个string的JSON字符串。
状态通道使用流程概述
- 设置参与方列表。
- 在私链privateState发布入口合约。
- 在公链publicState打开状态通道。
- 在公链publicState锁定资产。
- 在私链privateState上充值。
- 在私链privateState上执行若干次业务。
- 在私链privateState上提交确认结果。
- 参与方全部确认之后,任意方获取私链入口合约的状态树根storageRoot与签名列表,在公链publicState执行确认。
- 公链区块高度大于最后一次确认高度时,任意参与方关闭状态通道。
测试流程
- 环境搭建:首先搭建主链,然后使用命令
./jutools private create --address '0x00b20b6b6fe489a749baedb7aa389bf6806341d7' --outdir ../data/ --balance 1000000000000
初始化私链。此命令主要完成私链的创世区块初始化,以及将合约PayChannel.sol
,Tickets.sol
作为系统合约写入私链。其中--address
后参数用非admin账户创建。 - 安装Redis:以Ubuntu为例,执行命令
sudo apt-get install redis-server
。 - 设置状态通道参与方:在私链使用JSON-RPC接口
admin_nodeInfo
获取各自私链enode,将enode里面的0.0.0.0
替换成自己的IP。然后通过JSON-RPC接口ju_setStateChannelPeers
设置参与方列表。所有JSON-RPC调用均可使用网页以太坊开发工具集。示例如下:// 获取一个参与方enode curl -X POST --data '{"jsonrpc":"2.0","method":"admin_nodeInfo","params":[],"id":1}' { "enode": "enode://5da1a7b5282917d443ae93939fa8ae0aa7a503846a958c7ed8b7f4fe436ef62a77265b0c3e782c7057a778842cb0684e04648d90a8162e60a045978a60d7c91a@0.0.0.0:16789", // 其他无关数据简化不写 }
// 获取另外一方enode(省略...)
ju_setStateChannelPeers 使用说明
参数:
string - 入口合约地址
string - 参与方enode列表,用","隔开。
返回值:
true 设置成功,false 设置失败。
// 使用 ju_setStateChannelPeers 设置参与方列表
curl -X POST --data '{"jsonrpc":"2.0","method":"ju_setStateChannelPeers","params":["0x1100000000000000000000000000000000000002","enode://5da1a7b5282917d443ae93939fa8ae0aa7a503846a958c7ed8b7f4fe436ef62a77265b0c3e782c7057a778842cb0684e04648d90a8162e60a045978a60d7c91a@192.168.10.1:16789,enode://7097031c7d4d81155f8c8aebd79b79c2c422e486f1fb0a03f778c1d28c6404746a598a1eef48af357339bccbe78c9171a2a5c7cfd7bf2e9774290d45d5f2502d@192.168.10.2:39290"],"id":1}'
// 调用完毕可使用JSON-RPC接口admin_peers查看是否参与方加入成功
curl -X POST --data '{"jsonrpc":"2.0","method":"admin_peers","params":[],"id":1}'
如果返回的列表里面有加入的enode,那么表示此步骤成功。
* 在公链调用合约 StateChannelManager 的 open
函数登记一个状态通道。具体参数调用见上述说明。
* 在公链调用合约 StateChannelManager 的 deposit
函数锁定资产。
* 做完上诉两步,在公链可使用 StateChannelManager 的 getChannelInfo
函数查看上诉等级通道与资产锁定是否成功。
* 在私链调用合约 PayChannel 的 deposit
方法给私链各个账号充值(也可不充值),需要注意的是,私链上的所有账号金额之和必须等于公链上调用getChannelInfo
返回的锁定的资产。充值完成之后,可调用合约 PayChannel 的 query
方法查看是否充值成功。
* 在私链上调用合约 PayChannel 的 transfer
函数进行多次转账。转账之后,可调用合约 PayChannel 的 query
方法查看是否转账成功。
* 各自在私链上调用合约 Tickets 的 confirm
函数进行提交状态确认。提交之后,可使用queryTicket
函数查看提交结果。
* 其中第二个参数seqNo 使用合约 PayChannel 的 seqno
函数查到。
* 其中第三个参数_storageRoot,在私链上使用JSON-RPC接口 eth_getStorageRoot 获取私链入口合约的状态树哈希。示例如下:
curl -X POST --data '{"jsonrpc":"2.0","method":"eth_getStorageRoot","params":["0x1100000000000000000000000000000000000002", "latest"],"id":1}'
eth_getStorageRoot 使用说明
参数:
string - 入口合约地址
QUANTITY|TAG - 整数块编号,或者字符串"latest", "earliest" 或 "pending",获取最新请使用 "latest" 即可。
返回值:
合约地址storageRoot
* 其中第四个参数_signature,可调用JSON-RPC接口 personal_signData 获取。示例如下:
curl -X POST --data '{"jsonrpc":"2.0","method":"personal_signData","params":["0xe6e1d884a49be0e189a6886234c4bc1c7f72c3d0759df31e871fbfa1c4ea8e3b", "0x63f215ab36d2ad1e7769a6125123afb6621df7391531eca47816265f7c2710a8"],"id":1}'
personal_signData 使用说明
参数:
string - 待使用私钥签名的数据
string - 参与方账号的私钥(可使用以太坊开发工具集以太坊工具的账号管理获取账号私钥。将后台目录keys账号json数据复制进去,输入密码按确定。)
返回值:
使用账号的私钥签名的数据合约地址storageRoot
* 在公链调用合约 StateChannelManager 的 commit
函数提交最终结果。
* 在公链调用合约 StateChannelManager 的 close
函数关闭状态通道。
* 在公链调用合约 StateChannelManager 的 getChannelInfo
函数查看上诉所有操作是否生效。
## 测试脚本
请结合上述流程,查看该脚本。可在Node.js环境下或者以太坊工具集JavaScript模块运行。
```javascript
(async () => {
const Web3 = require("web3");
const Client = require("axios");
let sleep = time => {
return new Promise(resolve => setTimeout(resolve, time));
};
const ipA = "10.10.8.168";
const ipB = "10.10.8.169";
const urlA = `http://${ipA}:6789`;
const urlB = `http://${ipB}:6789`;
const PrivateurlA = `http://${ipA}:5789`;
const PrivateurlB = `http://${ipB}:5789`;
const pwd = "12345678";
const stateChannelManagerAddress = "0x7767408a6e129342c27b2bc831bdd4e42b82126b";
const payChannelAddress = "0x1100000000000000000000000000000000000002";
const ticketsAddress = "0x1100000000000000000000000000000000000028";
const stateChannelManagerAbi = [
{
constant: false,
inputs: [
{
name: "_contractAddr",
type: "address"
}
],
name: "close",
outputs: [],
payable: false,
type: "function"
},
{
constant: true,
inputs: [
{
name: "_contractAddr",
type: "address"
}
],
name: "getChannelInfo",
outputs: [
{
name: "_json",
type: "string"
}
],
payable: false,
type: "function"
},
{
constant: false,
inputs: [
{
name: "_contractAddr",
type: "address"
},
{
name: "_amount",
type: "uint256"
}
],
name: "deposit",
outputs: [],
payable: false,
type: "function"
},
{
constant: false,
inputs: [
{
name: "_contractAddr",
type: "address"
},
{
name: "_seqNo",
type: "uint256"
},
{
name: "_balanceList",
type: "string"
},
{
name: "_signatureList",
type: "string"
},
{
name: "_storageRoot",
type: "string"
}
],
name: "commit",
outputs: [],
payable: false,
type: "function"
},
{
constant: false,
inputs: [
{
name: "_contractAddr",
type: "address"
},
{
name: "_participants",
type: "string"
},
{
name: "_expireHeight",
type: "uint256"
}
],
name: "open",
outputs: [],
payable: false,
type: "function"
}
];
const payChannelAbi = [
{
constant: false,
inputs: [],
name: "query",
outputs: [
{
name: "",
type: "uint256"
}
],
payable: false,
type: "function"
},
{
constant: false,
inputs: [
{
name: "_to",
type: "address"
},
{
name: "_amount",
type: "uint256"
}
],
name: "transfer",
outputs: [],
payable: false,
type: "function"
},
{
constant: false,
inputs: [
{
name: "_amount",
type: "uint256"
}
],
name: "deposit",
outputs: [],
payable: false,
type: "function"
},
{
constant: true,
inputs: [],
name: "seqno",
outputs: [
{
name: "",
type: "uint256"
}
],
payable: false,
type: "function"
}
];
const ticketsAbi = [
{
constant: false,
inputs: [
{
name: "_addr",
type: "address"
},
{
name: "_seqNo",
type: "uint256"
},
{
name: "_balance",
type: "uint256"
},
{
name: "_storageRoot",
type: "string"
},
{
name: "_signature",
type: "string"
}
],
name: "confirm",
outputs: [],
payable: false,
type: "function"
},
{
constant: true,
inputs: [
{
name: "_addr",
type: "address"
},
{
name: "_seqNo",
type: "uint256"
}
],
name: "queryTicket",
outputs: [
{
name: "jsonResult",
type: "string"
}
],
payable: false,
type: "function"
}
];
Date.prototype.Format = function (fmt) {
var o = {
"M+": this.getMonth() + 1, //月份
"d+": this.getDate(), //日
"h+": this.getHours(), //小时
"m+": this.getMinutes(), //分
"s+": this.getSeconds(), //秒
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
S: this.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
for (var k in o) if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length));
return fmt;
};
function getId() {
let id = parseInt(new Date().Format("hhmmss"));
return id;
}
let clientA = Client.create({
baseURL: urlA
});
let clientB = Client.create({
baseURL: urlB
});
let privateClientA = Client.create({
baseURL: PrivateurlA
});
let privateClientB = Client.create({
baseURL: PrivateurlB
});
const web3A = new Web3(urlA);
const web3B = new Web3(urlB);
const privateWeb3A = new Web3(PrivateurlA);
const privateWeb3B = new Web3(PrivateurlB);
let initClientDispatch = client => {
client.dispatch = async data => {
let replay = await client.post("", data);
return new Promise((resolve, reject) => {
if (replay.status === 200) {
resolve(replay.data);
} else {
reject("request error");
}
});
};
};
initClientDispatch(clientA);
initClientDispatch(clientB);
initClientDispatch(privateClientA);
initClientDispatch(privateClientB);
let rpc = async (client, method, params) => {
let data = {
method: method,
jsonrpc: "2.0",
id: parseInt(getId()),
params: params
};
try {
let replay = await client.dispatch(data);
return Promise.resolve(replay.result);
} catch (error) {
return Promise.reject(error);
}
};
let contractExcute = async (web3, abi, address, from, funcName, params, send = true) => {
let contract = new web3.eth.Contract(abi, address, null);
let func = contract.methods[funcName].apply(contract.methods, params);
try {
if (send) {
let options = {
gas: "0xe8d4a50fff",
gasPrice: "0x174876e800",
from: from
};
return Promise.resolve(await func.send(options));
} else {
return Promise.resolve(await func.call({ from: from }));
}
} catch (error) {
return Promise.reject(error);
}
};
let ret = {};
try {
const SEND = true;
const CALL = false;
const contractAddress = "0x00000000000000000000000000" + new Date().Format("yyyyMMddhhmmss");
ret.contractAddress = contractAddress;
let result = null;
let params = null;
// let accounts1 = await rpc(clientA, "eth_accounts", []);
// let accounts2 = await rpc(clientB, "eth_accounts", []);
let accountA = "0x00b20b6b6fe489a749baedb7aa389bf6806341d7";
let accountB = "0x002566a7a94203639b08ddb4e12fc1680942564c";
let accountPrivateKeyA = "0x63f215ab36d2ad1e7769a6125123afb6621df7391531eca47816265f7c2710a8";
let accountPrivateKeyB = "0x07e1f6fe69cd66672bd9a5ccea5b0251887209dfc63c245752d988a3b1513cfe";
let peers = await rpc(privateClientA, "admin_peers", []);
if (!(peers && peers.length >= 1)) {
// 获取节点
let nodeInfo1 = await rpc(privateClientA, "admin_nodeInfo", []);
let nodeInfo2 = await rpc(privateClientB, "admin_nodeInfo", []);
ret.enode1 = nodeInfo1.enode.replace("0.0.0.0", ipA);
ret.enode2 = nodeInfo2.enode.replace("0.0.0.0", ipB);
// 设置参与方(参与方有bug,不会跳过自己)
result = await rpc(privateClientA, "ju_setStateChannelPeers", [payChannelAddress, ret.enode2]);
if (!result) throw "ju_setStateChannelPeers failed!";
while (true) {
await sleep(3000);
console.log("confirm ju_setStateChannelPeers......");
let peers = await rpc(privateClientA, "admin_peers", []);
if (peers && peers.length >= 1) break;
}
console.log("ju_setStateChannelPeers success");
} else {
console.log("ju_setStateChannelPeers has set");
}
// 解锁账号,为打开通道做准备
console.log("personal_unlockAccount");
await rpc(clientA, "personal_unlockAccount", [accountA, pwd, 24 * 60 * 60]);
await rpc(clientB, "personal_unlockAccount", [accountB, pwd, 24 * 60 * 60]);
await rpc(privateClientA, "personal_unlockAccount", [accountA, pwd, 24 * 60 * 60]);
await rpc(privateClientB, "personal_unlockAccount", [accountB, pwd, 24 * 60 * 60]);
// 调用open函数打开状态通道
console.log("stateChannelManager open");
const expireHeight = 26;
params = [contractAddress, accountA + "," + accountB, expireHeight];
await contractExcute(web3A, stateChannelManagerAbi, stateChannelManagerAddress, accountA, "open", params, SEND);
// 获取私链A, B总额
console.log("payChannel query");
params = [];
ret.balanceAExcept = ret.balanceABegin = parseInt(await contractExcute(privateWeb3A, payChannelAbi, payChannelAddress, accountA, "query", params, CALL));
params = [];
ret.balanceBExcept = ret.balanceBBegin = parseInt(await contractExcute(privateWeb3B, payChannelAbi, payChannelAddress, accountB, "query", params, CALL));
// 资产锁定
console.log("stateChannelManager deposit");
ret.depositAmountTotal = ret.balanceABegin + ret.balanceBBegin;
if (ret.depositAmountTotal === 0) {
ret.depositAmountTotal = 10000;
}
params = [contractAddress, ret.depositAmountTotal];
await contractExcute(web3A, stateChannelManagerAbi, stateChannelManagerAddress, accountA, "deposit", params, SEND);
// 获取私链序列号
console.log("ju_getTransactionSeqNo");
params = [contractAddress];
ret.transactionSeqNo = await rpc(privateClientA, "ju_getTransactionSeqNo", params);
// 充值或者追加保证金
let deposit = ret.depositAmountTotal - ret.balanceABegin - ret.balanceBBegin;
if (deposit > 0) {
console.log("payChannel deposit " + deposit);
params = [deposit];
ret.balanceAExcept += deposit;
await contractExcute(privateWeb3A, payChannelAbi, payChannelAddress, accountA, "deposit", params, SEND);
ret.balanceABegin += deposit;
} else if (deposit < 0) {
deposit *= -1;
console.log("stateChannelManager deposit " + deposit);
ret.depositAmountTotal += deposit
params = [contractAddress, deposit];
await contractExcute(web3A, stateChannelManagerAbi, stateChannelManagerAddress, accountA, "deposit", params, SEND);
} else {
console.log("stateChannelManager deposit == balanceA + balanceB");
}
// 获取状态通道信息
console.log("stateChannelManager getChannelInfo");
params = [contractAddress];
result = await contractExcute(web3A, stateChannelManagerAbi, stateChannelManagerAddress, accountA, "getChannelInfo", params, CALL);
console.log("stateChannelManager getChannelInfo result = ", result);
ret.channelInfoBegin = JSON.parse(result);
// 执行私链转账给另外账户
let count = 10;
ret.transfetBalanceLogs = [];
while (count--) {
await sleep(3000);
let num = Math.random();
let transfetBalance = parseInt(Math.random() * 10 + 1) * 100;
let log = (num < 0.5 ? "A -> B" : "B -> A") + " payChannel transfer " + transfetBalance;
console.log(log);
ret.transfetBalanceLogs.push(log);
if (num < 0.5) {
params = [accountB, transfetBalance];
await contractExcute(privateWeb3A, payChannelAbi, payChannelAddress, accountA, "transfer", params, SEND);
ret.balanceAExcept -= transfetBalance;
ret.balanceBExcept += transfetBalance;
} else {
params = [accountA, transfetBalance];
await contractExcute(privateWeb3B, payChannelAbi, payChannelAddress, accountB, "transfer", params, SEND);
ret.balanceAExcept += transfetBalance;
ret.balanceBExcept -= transfetBalance;
}
}
// 再次查询余额
console.log("payChannel query");
params = [];
ret.balanceALast = parseInt(await contractExcute(privateWeb3A, payChannelAbi, payChannelAddress, accountA, "query", params, CALL));
params = [];
ret.balanceBLast = parseInt(await contractExcute(privateWeb3B, payChannelAbi, payChannelAddress, accountB, "query", params, CALL));
// 查询合约storageRoot
console.log("eth_getStorageRoot");
params = [payChannelAddress, "latest"];
ret.storageRoot = await rpc(privateClientA, "eth_getStorageRoot", params);
// 对storageRoot签名
console.log("personal_signData");
params = [ret.storageRoot, accountPrivateKeyA];
ret.signstorageRootA = await rpc(clientA, "personal_signData", params);
params = [ret.storageRoot, accountPrivateKeyB];
ret.signstorageRootB = await rpc(clientA, "personal_signData", params);
// 获取seqNo
console.log("payChannel seqno");
ret.seqNo = await contractExcute(privateWeb3A, payChannelAbi, payChannelAddress, accountA, "seqno", [], CALL);
// 各自提交结果
console.log("tickets confirm");
params = [contractAddress, ret.seqNo, ret.balanceALast, ret.storageRoot, ret.signstorageRootA];
await contractExcute(privateWeb3A, ticketsAbi, ticketsAddress, accountA, "confirm", params, SEND);
params = [contractAddress, ret.seqNo, ret.balanceBLast, ret.storageRoot, ret.signstorageRootB];
await contractExcute(privateWeb3B, ticketsAbi, ticketsAddress, accountB, "confirm", params, SEND);
// 查询签名
console.log("tickets queryTicket");
params = [contractAddress, ret.seqNo];
result = await contractExcute(privateWeb3A, ticketsAbi, ticketsAddress, accountA, "queryTicket", params, CALL);
ret.ticket = JSON.parse(result);
// 提交最终结果
console.log("stateChannelManager commit");
params = [contractAddress, ret.seqNo, `${ret.balanceALast},${ret.balanceBLast}`, `${ret.signstorageRootA},${ret.signstorageRootB}`, ret.storageRoot];
console.log("commit params", params);
await contractExcute(web3A, stateChannelManagerAbi, stateChannelManagerAddress, accountA, "commit", params, SEND);
// 关闭状态通道
console.log("stateChannelManager close");
params = [contractAddress];
await contractExcute(web3A, stateChannelManagerAbi, stateChannelManagerAddress, accountA, "close", params, SEND);
// 获取状态通道关闭状态
console.log("stateChannelManager getChannelInfo");
params = [contractAddress];
result = await contractExcute(web3A, stateChannelManagerAbi, stateChannelManagerAddress, accountA, "getChannelInfo", params, CALL);
ret.channelInfoEnd = JSON.parse(result);
console.log(ret);
} catch (error) {
console.log("error :" + error);
}
})();