用Go进行Ethereum预付交易的方法

196 阅读10分钟

在过去的几个月里,我们和来自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);
    }
}

基本上,合同持有一个从addressAgreement 的映射,所以在任何时候,每个地址最多只有一个协议。协议只能由合同的所有者(onlyBy )创建,但任何人都可以调用getAgreement ,以查看某个地址的协议。

然后是signAgreement 交易方法。在这种方法中,用户只需发送一个交易,如果有活动协议(initialized==true),如果还没有签署(signed == false),就会签署。这就是交易,用户需要交易费。这里发生的事情不多,所以会很便宜。

我没有花任何时间来优化或确保这个合同,所以不要把它当作你正在建立的任何东西的模板。它只是用来展示工作流程的。

接下来,有一个基于HTML的用户界面和一些JavaScript,我不会详细介绍。WebApp包括web3,在合同上调用getAgreementsignAgreement 函数,并为用户获取余额。

签署协议。

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 工作的小错误。我没有深入研究它,但它似乎与如何在testrpcgo-ethereum 之间发送nonces有关。在这个PoC中,我简单地将nonces硬编码为从0开始,并手动计算它们。这并不漂亮,而且当Go服务器重启时,会出现尴尬的中断,但我不想浪费时间,所以例子中出现了一些黑客式的nonce-updating代码。

结论

所概述的概念不会对很多应用起作用,但对于简单的签名用例,也就是适合区块链平台的用例,它似乎已经足够了。

该解决方案运行良好,我相信甚至可以作为这种机制在现实世界中实施的基础。当然,需要有一些严肃的预防措施,甚至需要有一个向任意用户发送交易费用的人工过程,但总体概念似乎是合理的。

我很好奇区块链领域未来将如何处理这个问题,以及平台将提供哪些安全和反欺诈机制。我今天可以肯定的是,智能合约平台将需要像自我支付合约这样的机制,以屏蔽用户与加密货币打交道的需要。

请不要把这个简单的实现用于任何严肃的事情,因为这肯定会以眼泪告终,但也许可以用它来激发灵感或学习关于使用Go与Ethereum区块链互动的可能性。)

资源