在过去的几个月里,我们和来自block42的人一起做了一个项目,我们谈到,如何在现实世界的项目中使用像以太坊这样的技术,每个潜在的用户都需要有一个拥有一些以太坊的账户,以便能够与区块链互动,从而与产品互动。
这构成了一个真正的问题,因为无论是以太坊还是比特币或任何其他区块链技术,都还没有接近主流的采用,在可预见的未来也不会。
另外,即使用户已经涉足加密货币,也不能保证用户持有以太坊或当前平台上所需的货币。为了解决这个问题,在以太坊上有一个提案,让合约为交易付费成为可能。不幸的是,这种变化最早会在明年左右随着第二个Metropolis版本 "Constantinople "的发布而发生。
在此期间,有几个变通方法可以做,每个人都有不同的权衡。这篇文章将展示一个用Go实现这些变通方法的概念验证。
概念
这个概念很简单。用户请求一些东西(例如:某种协议),如果服务器同意,服务器将其放在区块链上的智能合约中。现在服务器希望用户在区块链上签署协议,以便事后证明双方都同意。
用户没有任何以太坊,所以服务器发送足够的以太坊给用户(最低的交易费用)。当然,用户需要一个以太坊地址,以便能够收到它并能够签署协议。为此,可能会有一个应用程序或WebApp,为用户创建一个账户,并提供验证保存在区块链上的协议的能力,并使用收到的交易费来签署它。
例子
- 用户安装App,并为用户创建Ethereum账户(地址)。
- 用户向服务器发送一份协议和公钥
- 服务器将该协议放在智能合约上
- 服务器向用户发送一个计算好的交易费用,这足以让用户签署该协议
- 用户现在可以查询智能合约上的协议,并检查它是否正常。
- 一旦交易费用到达,用户可以在智能合约上签署协议。
这种方法在很多情况下是行不通的,需要有一些反欺诈的机制才能使用。此外,它还引入了一些中心化,这在区块链应用中总是不受欢迎的。但是,这种方法可以帮助双方,即用户和服务器,在区块链上有持久的证明,证明他们在某些时候达成了协议,而不需要双方处理加密货币。
可以用一个Webserver作为服务器,一个WebApp作为客户端来实现这个概念验证,testrpc可以用来进行本地测试。
PoC实现
首先,我们使用Docker启动一个本地testrpc实例。
docker pull harshjv/testrpc
docker run -d --name=preprpc -p 8545:8545 harshjv/testrpc --account="0xb4087f10eacc3a032a2d550c02ae7a3ff88bc62eb0d9f6c02c9d5ef4d1562862, 1000000000000000000000000" --account="0xd2a99b289915eb11ea50a51247e1cef2c4583ae1d9699a3bb0154c2792bda339,0"
这将使testrpc有两个账户,一个有资金(服务器),一个没有资金(用户)。
然后,我们需要智能合约。对于这个简单的例子来说,它并没有太多的内容。
pragma solidity ^0.4.6;
contract Signer {
address public owner = msg.sender;
struct Agreement {
string stringToAgreeOn;
bool signed;
bool initialized;
}
mapping (address=> Agreement) agreements;
modifier onlyBy(address _account)
{
require(msg.sender == _account);
_;
}
function createAgreement(string _stringToAgreeOn, address customer) payable public onlyBy(owner) returns (bool success) {
agreements[customer] = Agreement(_stringToAgreeOn, false, true);
return true;
}
function signAgreement() payable public returns (bool success) {
var agreement = agreements[msg.sender];
require(agreement.initialized == true);
require(agreement.signed == false);
agreement.signed = true;
return true;
}
function getAgreement(address addr) public constant returns(string stringToAgreeOn, bool signed, bool initialized) {
var agreement = agreements[addr];
return (agreement.stringToAgreeOn, agreement.signed, agreement.initialized);
}
}
基本上,合同持有一个从address 到Agreement 的映射,所以在任何时候,每个地址最多只有一个协议。协议只能由合同的所有者(onlyBy )创建,但任何人都可以调用getAgreement ,以查看某个地址的协议。
然后是signAgreement 交易方法。在这种方法中,用户只需发送一个交易,如果有活动协议(initialized==true),如果还没有签署(signed == false),就会签署。这就是交易,用户需要交易费。这里发生的事情不多,所以会很便宜。
我没有花任何时间来优化或确保这个合同,所以不要把它当作你正在建立的任何东西的模板。它只是用来展示工作流程的。
接下来,有一个基于HTML的用户界面和一些JavaScript,我不会详细介绍。WebApp包括web3,在合同上调用getAgreement 和signAgreement 函数,并为用户获取余额。
签署协议。
var signbutton = document.getElementById("signbutton");
signbutton.addEventListener("click", function(event) {
var tx = {
from: currentAccount,
value: 0,
gas: 100000
}
instance.signAgreement.sendTransaction(tx, function(err, res) {
if (err) {
return alert(err)
}
})
});
刷新活动的协议:
var refresh = document.getElementById("refresh");
refresh.addEventListener("click", function(event) {
instance.getAgreement(currentAccount, function(err, results) {
if (err || !results[2]) {
agreement.innerText = ""
signed.innerText = "";
} else {
agreement.innerText = results[0];
signed.innerText = results[1];
}
});
});
刷新余额:
var refreshbalance = document.getElementById("refreshbalance");
refreshbalance.addEventListener("click", function(event) {
var newBalance = web3.eth.getBalance(currentAccount);
var balanceElement = document.getElementById("balance");
balanceElement.innerText = newBalance.toNumber();
});
UI还可以使用服务器上的REST API创建新的协议。
var agreementButton = document.getElementById("agreementButton");
agreementButton.addEventListener("click", function(event) {
var agreementInput = document.getElementById("agreementInput");
agreementValue = agreementInput.value
superagent.post(host + "/agreement").send({
account: currentAccount,
agreement: agreementValue,
}).end(function(err, res) {
if (err) {
return alert(res.text)
}
console.log(res);
});
});
好了,有了合同和简单的用户界面,让我们来看看这个PoC的重点--在智能合同上创建协议和向用户发送交易费用的逻辑。
首先,我们需要将智能合约转换为Go-API,如我之前的文章中所述。
abigen --sol=signer.sol --pkg=main --out=signer.g
然后,需要进行一些设置,以连接到testrpc ,并设置一个账户来部署和与合同互动。
var key = "b4087f10eacc3a032a2d550c02ae7a3ff88bc62eb0d9f6c02c9d5ef4d1562862" // don't hardcode keys in production y'all!
privKey, err := crypto.HexToECDSA(key)
if err != nil {
log.Fatalf("Failed to convert private key: %v", err)
}
conn, err := ethclient.Dial("http://localhost:8545")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum client: %v", err)
}
auth := bind.NewKeyedTransactor(privKey)
if err != nil {
log.Fatalf("Failed to create Transactor: %v", err)
}
在这种情况下,我们使用一个硬编码的私钥,它也将被提供给testrpc以创建一个账户。然后,这个密钥被转换为ECDSA Private Key ,并创建一个Transactor ,这是做交易所需要的。
我们还使用Dial 来打开与本地testrpc 实例的连接。随着区块链连接和证书的建立和运行,让我们来部署合约。
addr, _, contract, err := DeploySigner(auth, conn)
if err != nil {
log.Fatalf("Failed to deploy contract: %v", err)
}
fmt.Println("Contract Deployed to: ", addr.String())
好了,现在我们可以使用chi设置WebServer,为了方便起见,设置了CORS头信息。
r := chi.NewRouter()
corsOption := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
AllowCredentials: true,
MaxAge: 300,
})
r.Use(corsOption.Handler)
r.Use(middleware.Logger)
r.Post("/agreement", createAgreementHandler(contract, auth, conn, privKey))
log.Println("Server started on localhost:8080")
log.Fatal(http.ListenAndServe(":8080", r))
我们在这里添加的唯一路线是createAgreementHandler 。你可能会争辩说,如果只是使用go标准lib来设置这个Webserver,那就完全可以了,而且你也是完全正确的。事实是,当我开始的时候,我认为这个PoC会更复杂,我准备的太多了。)
POST 处理程序(/agreement )是服务器和用户之间唯一的交互,所以这就是所有魔法发生的地方。让我们一步步来看看。
// Agreement is an agreement
type Agreement struct {
Account string `json:"account"`
Agreement string `json:"agreement"`
}
// Bind binds the request parameters
func (a *Agreement) Bind(r *http.Request) error {
return nil
}
func createAgreementHandler(contract *Signer, auth *bind.TransactOpts, conn *ethclient.Client, privKey *ecdsa.PrivateKey) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
agreement := &Agreement{}
if err := render.Bind(r, agreement); err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Invalid Request, Account and Agreement need to be set"))
return
}
if agreement.Account == "" || agreement.Agreement == "" {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Account and Agreement need to be set"))
return
}
})
首先,我们需要处理请求。chi 有一个很好的方法来处理这个问题,它的render.Bind 函数试图将有效载荷与给定的 json-struct 结合起来。然后,我们验证账户和协议都已设置,否则就会发送一个错误。
下一步是在智能合约上创建协议。
_, err := contract.CreateAgreement(&bind.TransactOpts{
From: auth.From,
Signer: auth.Signer,
GasLimit: big.NewInt(200000),
Value: big.NewInt(0),
}, agreement.Agreement, common.HexToAddress(agreement.Account))
if err != nil {
log.Fatalf("Failed to create agreement: %v", err)
}
fmt.Println("Agreement created: ", agreement.Agreement)
基本上,我们只是调用我们的智能合约中生成的CreateAgreement 方法和交易选项。我们还需要将给定的账户从十六进制转换为实际的以太坊地址。在这一步之后,该协议就会在区块链上持久存在。
现在是向用户发送交易费用的问题。
gasPrice, err := conn.SuggestGasPrice(context.Background())
if err != nil {
log.Fatalf("Failed to get gas price: %v", err)
}
signer := types.HomesteadSigner{}
tx := types.NewTransaction(nonce, common.HexToAddress(agreement.Account), big.NewInt(100000), big.NewInt(21000), gasPrice, nil)
signed, err := types.SignTx(tx, signer, privKey)
if err != nil {
log.Fatalf("Failed to sign transaction: %v", err)
}
err = conn.SendTransaction(context.Background(), signed)
if err != nil {
log.Fatalf("Failed to send transaction: %v", err)
}
fmt.Println("Transaction Fee sent to client!")
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
在这里,我们只是执行一个正常的交易到用户的转换地址。这有点涉及到SuggestGasPrice 调用和HomesteadSigner 。这在web3中会容易得多,我不确定这是否是Go的建议方式,但我在文档中找不到任何其他方式。
基本上,我们创建一个新的交易,向用户发送100000 wei。然后我们签署该交易并发送。之后,如果一切顺利,我们返回200 OK 。
就这样了!
这个例子的完整代码可以在这里找到
不幸的是,在写这篇文章的时候,似乎有一个与Go的testrpc 工作的小错误。我没有深入研究它,但它似乎与如何在testrpc 和go-ethereum 之间发送nonces有关。在这个PoC中,我简单地将nonces硬编码为从0开始,并手动计算它们。这并不漂亮,而且当Go服务器重启时,会出现尴尬的中断,但我不想浪费时间,所以例子中出现了一些黑客式的nonce-updating代码。
结论
所概述的概念不会对很多应用起作用,但对于简单的签名用例,也就是适合区块链平台的用例,它似乎已经足够了。
该解决方案运行良好,我相信甚至可以作为这种机制在现实世界中实施的基础。当然,需要有一些严肃的预防措施,甚至需要有一个向任意用户发送交易费用的人工过程,但总体概念似乎是合理的。
我很好奇区块链领域未来将如何处理这个问题,以及平台将提供哪些安全和反欺诈机制。我今天可以肯定的是,智能合约平台将需要像自我支付合约这样的机制,以屏蔽用户与加密货币打交道的需要。
请不要把这个简单的实现用于任何严肃的事情,因为这肯定会以眼泪告终,但也许可以用它来激发灵感或学习关于使用Go与Ethereum区块链互动的可能性。)