Miễn phí gas cho giao dịch Ethereum...liệu có thể?

Bất cứ ai muốn giao dịch trên nền tảng của Ethereum đều cần ETH để trả phí (gas). Điều này là rào cản lớn với đa số user khi mới tham gia vào mạng Ethereum và sử dụng 1 Dapp do quá trình trao đổi ETH không hề đơn giản. Bài viết này sẽ mở ra khái niệm gasless (còn gọi là meta) transactions, giúp người dùng không nhất thiết cần ETH để giao dịch và đồng thời giới thiệu về Gas Station Network được sinh ra để giải quyết vấn đề này.

1. Meta-transaction là gì?

Mọi giao dịch trên mạng Ethereum đều yêu cầu gas, và người gửi cần có đủ Ether để trả phí gas này. Mặc dù, số Ether phải trả là rất nhỏ (vài nghìn VND ở thời điểm bài viết), nhưng quá trình thực hiện thì lại phức tạp bao gồm: đáp ứng chính sách KYC & AML và mua đồng ETH trước khi sử dụng bất cứ Dapp nào, không chỉ tốn thời gian mà còn có nguy cơ lộ thông tin cá nhân trên Internet. Ngoài với những người dùng "hardcore" ra, đây là trở ngại lớn với đa số người có ít kinh nghiệm với tiền kỹ thuật số.
meta-transactions, một ý tưởng đơn giản ra đời nhằm giải quyết vấn đề: Một tổ chức (được gọi là relayer) sẽ thanh toán phí giao dịch thay cho người dùng. Người dùng chỉ cần ký một thông điệp bao chứa thông tin về giao dịch. Sau đó, Relayer sẽ lo các bước còn lại: ký giao dịch (nếu hợp lệ) đi kèm với các thông tin đó, lan truyền lên mạng Ethereum và trả phí gas. Bằng cách này, người dùng không cần thiết phải có ví điện tử (chứa Ether) và không cần thiết phải cung cấp thông tin các nhân.

Note: Vậy vấn đề về tính tin cậy của Relayer sẽ được giải quyết thể nào?

2. Gas Station Network (GSN)

GSN là một mạng phân tán gồm các relayer, cho phép bạn triển khai những Dapp và trả phí giao dịch thay cho người dùng, giảm bớt rào cản với người dùng mới.

Note: GSN là ý tưởng của TabooKey, bao gồm nhiều công ty hợp tác cùng nhau nhằm giải quyết khó khăn khi mới bước chân vào mạng Ethereum.

Tuy nhiên, các relayer không hoạt động miễn phí. Chính nhà cung cấp đã triển khai Dapp sẽ phải hoàn lại cho họ phí giao dịch (gas fee) cùng với một khoản nhỏ trả phí dịch vụ.
Nghe có vẻ kỳ quặc nhưng đây lại là một cách hợp lý để lôi kéo người dùng. Đó chỉ là 1 khoản nhỏ so với sô tiền cho việc quảng cáo, miễn phí dùng thử, giảm giá.... Hoặc nếu không, nhà cung cấp có thể yêu cầu trả phí gas thông qua "off-chain" (ví dụ:thông qua credit card), được tính từ số lần GSN được thực thi.

Giải quyết câu hỏi phần 1: "Relayer liệu có đáng tin?": Mặc dù relayer có toàn quyền quyết định xử lý request của bạn thế nào, nhưng những hành vi xấu của relayer sẽ bị hạn chế bởi cơ chế đánh giá xử phạt của GSN. Tất cả hoàn toàn tự động, nên bạn không cần lo lắng khi sử dụng dịch vụ của GSN.

3. Kiến trúc của Ethereum Gas Station Network (GSN)

3.1. Các thành phần trong GSN:

  • Sender: Một địa chỉ chứa cặp khóa hợp lệ nhưng không có ETH để trả gas.
  • Relay: một node chứa ETH, có nhiệm vụ thực hiện giao dịch và trả phí gas.
  • Recipient contract: Là smart contract mà được deploy.
  • RelayHub: Là một contract kết nối relay và smart contract, điều hướng sender đến relay và phạt các relay có hành vi xấu,.... Ngoài ra, nó được cọc một khoản tiền từ nhà cung cấp của Dapp để hoàn lại cho relay.

3.2. Luồng hoạt động

Luồng hoạt động

  1. Các ứng viên đăng ký làm Relayer với RelayHub, và cọc 1 khoản tiền (nhằm phạt nếu relayer có hành vi xấu).
  2. Khi có giao dịch từ người dùng, RelayHub sẽ lựa chọn Relayer phù hợp.
  3. Sender tạo và gửi thông điệp bao gồm các thông tin về giao dịch muốn thực hiện cho Relayer
  4. Với các thông tin trong lời gọi, Relayer xác nhận lại với RelayHub contract xem giao dịch này có được chấp thuận.
  5. Với các thông tin trong lời gọi, RelayHub xác nhận với Recipient Contract xem giao dịch này có được chấp thuận để trả gas thay cho người dùng.
  6. Relayer tạo ra một giao dịch bao gồm các thông tin mà người dùng đã gửi (vd: chữ ký, nonce). Đặc biệt, msgSender sẽ là người dùng chứ không phải relayer. Và relayer sẽ lan truyền giao dịch này lên mạng Blockchain, trả phí giao dịch.
  7. Relayer lan truyền giao dịch lên mạng Ethereum bằng cách gửi raw Transaction đến RelayHub.
  8. RelayHub xác nhận lại xem giao dịch này đã chấp thuận.
  9. RelayHub gọi các hàm thực hiện trước lời gọi chính từ Recipient Contract.
  10. RelayHub gọi các hàm người dùng muốn thực hiệm từ Recipient Contract.
  11. RelayHub gọi các hàm thực hiện sau lời gọi chính từ Recipient Contract.
  12. RelayHub hoàn lại phí từ gas cho Relayer và trigger sự kiện TransactionRelayed.
  13. Raw transaction được trả về cho Sender.
  14. (Không bắt buộc) Gửi lại raw transaction đến RelayHub. Giao dịch này có thể sẽ không thành công do giống giao dịch trước, nhưng điều đó xác nhận rằng giao dịch trước đã được lan truyền.

4. Triển khai Dapp với GSN

Phần này sẽ hướng dẫn việc triển khai Dapp với GSN và đẩy lên testnet, ta sẽ cần:

4.1. Thiết lập môi trường

Khởi đầu bằng việc tạo npm project và cài đặt các thư viện

$ mkdir gsn-dapp && cd gsn-dapp
$ npm init -y
$ npm install @openzeppelin/network
$ npm install --save-dev @openzeppelin/contracts-ethereum-package @openzeppelin/upgrades

Sử dụng CLI nhằm thiết lập project:

$ npx oz init

Note: Nếu chưa rõ về câu lệnh npx, có thể đọc thêm tại Npm vs Npx whats the difference.

4.2. Khởi tạo Contract

Ta sẽ tạo một contract tên Counter trong thư mục đã được tạo sẵn contracts.

// contracts/Counter.sol
pragma solidity ^0.5.0;

contract Counter {
    uint256 public value;

    function increase() public {
        value += 1;
    }
}

Contract này chỉ đơn giản đếm số lượt hàm increase được gọi. Hãy chỉnh sửa lại để thêm GSN. Contract này sẽ kế thừa GSNRecipient và cần có thêm hàm acceptRelayedCall để quyết định xem sẽ có trả phí gas thay cho người dùng không. Tuy nhiên, trong bài viết này, mọi giao dịch sẽ được chấp thuận trả phí thay cho người dùng.

// contracts/Counter.sol
pragma solidity ^0.5.0;

import "@openzeppelin/contracts-ethereum-package/contracts/GSN/GSNRecipient.sol";

contract Counter is GSNRecipient {
    uint256 public value;

    function increase() public {
        value += 1;
    }

    function acceptRelayedCall(
        address relay,
        address from,
        bytes calldata encodedFunction,
        uint256 transactionFee,
        uint256 gasPrice,
        uint256 gasLimit,
        uint256 nonce,
        bytes calldata approvalData,
        uint256 maxPossibleCharge
    ) external view returns (uint256, bytes memory) {
        return _approveRelayedCall();
    }

    // We won't do any pre or post processing, so leave _preRelayedCall and _postRelayedCall empty
    function _preRelayedCall(bytes memory context) internal returns (bytes32) {
    }

    function _postRelayedCall(bytes memory context, bool, uint256 actualCharge, bytes32) internal {
    }
}

Trong thực tế, không nên làm vậy vì một số người dùng xấu có thể sẽ hút hết Ether của bạn. Có thể tham khảo GSN payment strategies để tìm hiểu các cách tiếp cận khác.

Bước tiếp, ta sẽ triển khai Dapp lên Rinkeby testnet. Bạn sẽ cần config lại file networks.js, và cần có 1 tài khoản chứ chút ETH ở mạng Rinkeby. Có thể tham khảo thêm hướng dẫn cách triển khai contract lên testnet.

rinkeby: {
      provider: () =>
        new HDWalletProvider(
          mnemonic,
          `https://rinkeby.infura.io/v3/${projectId}`
        ),
      networkId: 4,
      gasPrice: 5e9,
    },

Ta sẽ tạo contract sử dụng OpenZeppelin CLI:

npx oz create

sau đó, lựa chọn theo chỉ dẫn:

$ npx oz create
✓ Compiled contracts with solc 0.5.9 (commit.e560f70d)
? Pick a contract to instantiate: Counter
? Pick a network: rinkeby
✓ Added contract Counter
✓ Contract Counter deployed
? Call a function to initialize the instance after creating it?: Yes
? Select which function * initialize()
✓ Setting everything up to create contract instances
✓ Instance created at 0xCfEB869F69431e42cdB54A4F4f105C19C080A601

Note: Bạn cần lưu lại địa chỉ của instance, nó sẽ được sử dụng sau.

Note: hàm initialize() cần được gọi để khởi tạo contract.

4.3. Xây dựng Dapp

Tạo sample:

npm create-react-app client

Ta cần tạo 1 simlink để có thể truy cập file biên dịch của contract .json. Từ thư mục client/src, chạy:

$ ln -ns ../../build

Thay thế client/src/App.js bằng đoạn code sau:

// client/src/App.js
import React, { useState, useEffect, useCallback } from 'react';
import { useWeb3Network, useEphemeralKey } from '@openzeppelin/network/react';

const PROVIDER_URL =
  `https://rinkeby.infura.io/v3/${process.env.REACT_APP_PROJECT_ID}`;

function App() {
  // get GSN web3
  const context = useWeb3Network(PROVIDER_URL, {
    gsn: { signKey: useEphemeralKey() },
  });

  const { accounts, lib } = context;

  // load Counter json artifact
  const counterJSON = require('./build/contracts/Counter.json');

  // load Counter Instance
  const [counterInstance, setCounterInstance] = useState(undefined);

  if (!counterInstance && context && context.networkId) {
    console.log(context.networkId);
    const deployedNetwork = counterJSON.networks[context.networkId.toString()];
    const instance = new context.lib.eth.Contract(
      counterJSON.abi,
      deployedNetwork.address
    );
    setCounterInstance(instance);
  }

  const [count, setCount] = useState(0);

  const getCount = useCallback(async () => {
    if (counterInstance) {
      // Get the value from the contract to prove it worked.
      const response = await counterInstance.methods.value().call();
      // Update state with the result.
      setCount(response);
    }
  }, [counterInstance]);

  useEffect(() => {
    getCount();
  }, [counterInstance, getCount]);

  const increase = async () => {
    await counterInstance.methods.increase().send({ from: accounts[0] });
    getCount();
  };

  return (
    <div>
      <h3> Counter counterInstance </h3>
      {lib && !counterInstance && (
        <React.Fragment>
          <div>Contract Instance or network not loaded.</div>
        </React.Fragment>
      )}
      {lib && counterInstance && (
        <React.Fragment>
          <div>
            <div>Counter Value:</div>
            <div>{count}</div>
          </div>
          <div>Counter Actions</div>
          <button onClick={() => increase()} size="small">
            Increase Counter by 1
          </button>
        </React.Fragment>
      )}
    </div>
  );
}

export default App;

Note: Ở đây sử dụng dịch vụ của Infura để truy cập vào mạng Rinkeby. Bạn có thể lựa chọn dịch vụ khác hoặc nếu cũng dùng Infura, cần thay REACT_APP_PROJECT_ID của riêng bạn.

Đoạn code này sử dụng OpenZeppelin Network JS để kết nối với một node của mạng Ethereum. Ta có thể bắt đầu sử dụng Dapp. Từ client, chạy npm start.

Bạn có thể sử dụng Dapp rồi quay lại đây đọc tiếp. Bạn sẽ phát hiện ra, biến đếm không hề tăng. Đó là bởi Counter contract chưa được "cấp vốn". Bạn có thể sử dụng GSN-online tool, pasting địa chỉ instace đã lưu ở trên vào đây. Ngoài ra, cần có ví Metamask và Ether ở mạng Rinkeby nhằm đặt cọc cho contract này ở Relayer.

Đến lúc này, bạn đã có thể giao dịch từ browser mà không cần đến ví Metamask.

Tổng kết

Vậy là bài viết này đã giới thiệu về khái niệm Meta-transaction và Gas Station Network để giải quyết những khó khăn của người mới với mạng Ethereum. Tiếp đó, một Dapp đơn giản sử dụng thư viện của Openzeppelin để làm rõ các bước của quá trình triển khai với Gas Station Network. Cảm ơn bạn đọc đã theo dõi đến cuối bài viết. Toàn bộ source-code có thể tìm thấy treen Github.

Tài liệu tham khảo


All Rights Reserved