Skip to content
This repository has been archived by the owner on Mar 24, 2023. It is now read-only.

Commit

Permalink
Merge pull request #581 from godwokenrises/gasless-tx
Browse files Browse the repository at this point in the history
feat: optionally support gasless transaction
  • Loading branch information
RetricSu authored Dec 15, 2022
2 parents e5a67e7 + 6149153 commit c456a58
Show file tree
Hide file tree
Showing 14 changed files with 642 additions and 49 deletions.
84 changes: 79 additions & 5 deletions docs/addtional-feature.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

## Instant Finality

Ethereum requires a transaction to be on-chain (meaning the transaction is included in the latest block) before returning a transaction receipt with final status to users, so they know whether the transaction is success or not.
Ethereum requires a transaction to be on-chain (meaning the transaction is included in the latest block) before returning a transaction receipt with final status to users, so they know whether the transaction is a success or not.

Godwoken provide a faster way to confirm transaction. Once a transaction is verified in the mempool, the instant transaction receipt will be generated immediately. This feature is called `Instant Finality`.
Godwoken provides a faster way to confirm transactions. Once a transaction is verified in the mem-pool, the instant transaction receipt will be generated immediately. This feature is called `Instant Finality`.

If you want to build a low latency user experience for on-chain interactions in your dApp, you could turn on `Instant Finality` feature by using the RPC **with additional path or query parameter**:
If you want to build a low-latency user experience for on-chain interactions in your dApp, you could turn on `Instant Finality` feature by using the RPC **with an additional path or query parameters**:

```bash
# http
Expand All @@ -17,6 +17,80 @@ https://example_web3_rpc_url/instant-finality-hack
ws://example_web3_rpc_url/ws?instant-finality-hack=true
```

**Note**: Environments like [Hardhat](https://github.com/NomicFoundation/hardhat) will swallow the http url's query parameter, so you might want to use the `/instant-finality-hack` path to overcome that.
**Note**: Environments like [Hardhat](https://github.com/NomicFoundation/hardhat) will swallow the HTTP URL's query parameter, so you might want to use the `/instant-finality-hack` path to overcome that.

Also notice that under `instant-finality-hack` mode, there might be some [compatibility issue](https://github.com/godwokenrises/godwoken-web3/issues/283) with Ethereum toolchain like `ether.js`. If you care more about the compatibility, please use the bare RPC url `https://example_web3_rpc_url`, which is considered to be most compatible with Ethereum.
Also notice that under `instant-finality-hack` mode, there might be some [compatibility issues](https://github.com/godwokenrises/godwoken-web3/issues/283) with Ethereum toolchain like `ether.js`. If you care more about compatibility, please use the bare RPC URL `https://example_web3_rpc_url`, which is considered to be the most compatible with Ethereum.

## Gasless Transaction

The gas fee is preventing new users step into the web3 world. Users must learn to get the native token(CKB or ETH) before playing a blockchain game or exchanging tokens with a DEX. The gasless feature can provide a way for developers to sponsor transaction fees for users to give them a smooth experience.

The gas feature is based on the [ERC-4337 solution](https://eips.ethereum.org/EIPS/eip-4337) but way simpler. To use a such feature, users sign and send a special gasless transaction to call a specific smart contract named `Entrypoint`, then `Entrypoint` will call another smart contract named `Paymaster` deployed by developers to check if they are willing to pay the gas fee for this transaction. The special gasless transaction must satisfy the requirements:

- `tx.data` must be the call data of calling `Entrypoint`'s `handleOp(UserOperation calldata op)` function, which contains the target contract and the paymaster address.
- Must set `tx.gasPrice` to 0
- The `tx.to` must be set to the `Entrypoint` contract.

```sh
struct UserOperation {
address callContract; # address of the target contract
bytes callData; # call data of the target contract
uint256 callGasLimit; # gas used to execute the call
uint256 verificationGasLimit; # gas used to verification
uint256 maxFeePerGas; # gas price
uint256 maxPriorityFeePerGas; # must equals to maxFeePerGas, reserved for EIP-1559
bytes paymasterAndData; # pay master address and extra data
}
```

More specs can be found [here](https://github.com/godwokenrises/godwoken/discussions/860#discussion-4568687)

### Example: How to use gasless transaction for your dapp

Let's say we have the entrypoint contract at `0x9a11f47c0729fc56d9c44c059987d40703249569` and as a game developer, we want to pay the gas fee for some whitelist users, so we wrote a paymaster contract just like [this one](https://github.com/godwokenrises/account-abstraction/blob/gw-gasless/contracts/samples/GaslessDemoPaymaster.sol) and deployed it at `0x6b019795aa36dd19eb4a4d76f3b9a40239b7c19f`.

dapp frontend using ethers.js

```ts
// define UserOp
const userOp: UserOperationStruct = {
callContract: realGameContract.address,
callData: realGameContractCallData,
callGasLimit: gasToExecuteRealGameContractCallData,
verificationGasLimit: gasToVerifyPaymaster,
maxFeePerGas: gasPrice,
maxPriorityFeePerGas: gasPrice,
paymasterAndData: "0x6b019795aa36dd19eb4a4d76f3b9a40239b7c19f"
}

// 1. construct and send gasless transaction
const abiCoder = new ethers.utils.AbiCoder();
const userOp = abiCoder.encode(["tuple(address callContract, bytes callData, uint256 callGasLimit, uint256 verificationGasLimit, uint256 maxFeePerGas, uint256 maxPriorityFeePerGas, bytes paymasterAndData) UserOperation"], [userOp]);
// first 4 bytes of keccak hash of handleOp((address,bytes,uint256,uint256,uint256,uint256,bytes))
const fnSelector = "fb4350d8";
// gasless payload = ENTRYPOINT_HANDLE_OP_SELECTOR + abiEncode(UserOperation)
const payload = "0x" + fnSelector + userOp.slice(2);

const gaslessTx = {
from: whitelistUser.address,
to: '0x9a11f47c0729fc56d9c44c059987d40703249569',
data: payload,
gasPrice: 0,
gasLimit: 1000000,
value: 0,
}

const signer = new ethers.Wallet("private key");
const tx = await signer.sendTransaction(gaslessTx);
await tx.wait();


// 2. or just use ethers contract factory
{
// Send tx with a valid user.
const EntryPoint = await ethers.getContractFactory("EntryPoint");
const entrypoint = await EntryPoint.attach('0x9a11f47c0729fc56d9c44c059987d40703249569');
const tx = await entryPoint.connect(whitelistUser).handleOp(userOp, {gasLimit: 100000, gasPrice: 0});
await tx.wait();
}
```
108 changes: 79 additions & 29 deletions docs/poly-apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,67 @@

## Table of Contents

* [RPC Methods](#rpc-methods)
* [Method `poly_getCreatorId`](#method-poly_getCreatorId)
* [Method `poly_getDefaultFromId`](#method-poly_getDefaultFromId)
* [Method `poly_getContractValidatorTypeHash`](#method-poly_getContractValidatorTypeHash)
* [Method `poly_getRollupTypeHash`](#method-poly_getRollupTypeHash)
* [Method `poly_getEthAccountLockHash`](#method-poly_getEthAccountLockHash)
* [Method `poly_version`](#method-poly_version)

* [RPC Types](#rpc-types)
* [Type `Uint32`](#type-uint32)
* [Type `Uint64`](#type-uint64)
* [Type `H256`](#type-h256)
* [Type `JsonBytes`](#type-jsonbytes)
* [Type `PolyVersionInfo`](#type-polyversioninfo)
* [Type `Versions`](#type-versions)
* [Type `NodeInfo`](#type-NodeInfo)
* [Type `RollupCell`](#type-rollupcell)
* [Type `RollupConfig`](#type-rollupconfig)
* [Type `GwScripts`](#type-gwscripts)
* [Type `EoaScripts`](#type-eoascripts)
* [Type `Backends`](#type-backends)
* [Type `Accounts`](#type-accounts)
* [Type `Script`](#type-scripts)
* [Type `ScriptHashType`](#type-scripthashtype)
* [Type `ScriptInfo`](#type-scriptinfo)
* [Type `BackendInfo`](#type-backendinfo)
* [Type `AccountInfo`](#type-accountinfo)
* [Type `HealthStatus`](#type-healthstatus)
- [Poly RPCs](#poly-rpcs)
- [Table of Contents](#table-of-contents)
- [RPC Methods](#rpc-methods)
- [Method `poly_getCreatorId`](#method-poly_getcreatorid)
- [Examples](#examples)
- [Method `poly_getDefaultFromId`](#method-poly_getdefaultfromid)
- [Examples](#examples-1)
- [Method `poly_getContractValidatorTypeHash`](#method-poly_getcontractvalidatortypehash)
- [Examples](#examples-2)
- [Method `poly_getRollupTypeHash`](#method-poly_getrolluptypehash)
- [Examples](#examples-3)
- [Method `poly_getEthAccountLockHash`](#method-poly_getethaccountlockhash)
- [Examples](#examples-4)
- [Method `poly_version`](#method-poly_version)
- [Examples](#examples-5)
- [Method `poly_getEthTxHashByGwTxHash`](#method-poly_getethtxhashbygwtxhash)
- [Examples](#examples-6)
- [Method `poly_getGwTxHashByEthTxHash`](#method-poly_getgwtxhashbyethtxhash)
- [Examples](#examples-7)
- [Method `poly_getHealthStatus`](#method-poly_gethealthstatus)
- [Examples](#examples-8)
- [RPC Types](#rpc-types)
- [Type `Uint32`](#type-uint32)
- [Examples](#examples-9)
- [Type `Uint64`](#type-uint64)
- [Examples](#examples-10)
- [Type `H256`](#type-h256)
- [Examples](#examples-11)
- [Type `PolyVersionInfo`](#type-polyversioninfo)
- [Fields](#fields)
- [Type `BackendInfo`](#type-backendinfo)
- [Fields](#fields-1)
- [Type `ScriptInfo`](#type-scriptinfo)
- [Fields](#fields-2)
- [Type `Script`](#type-script)
- [Type `ScriptHashType`](#type-scripthashtype)
- [Type `JsonBytes`](#type-jsonbytes)
- [Example](#example)
- [Type `Versions`](#type-versions)
- [Examples](#examples-12)
- [Fields](#fields-3)
- [Type `RollupCell`](#type-rollupcell)
- [Fields](#fields-4)
- [Type `RollupConfig`](#type-rollupconfig)
- [Fields](#fields-5)
- [Type `NodeInfo`](#type-nodeinfo)
- [Fields](#fields-6)
- [Type `GwScripts`](#type-gwscripts)
- [Fields](#fields-7)
- [Type `EoaScripts`](#type-eoascripts)
- [Fields](#fields-8)
- [Type `Backends`](#type-backends)
- [Fields](#fields-9)
- [Type `AccountInfo`](#type-accountinfo)
- [Fields](#fields-10)
- [Type `Accounts`](#type-accounts)
- [Fields](#fields-11)
- [Type `HealthStatus`](#type-healthstatus)
- [Fields](#fields-12)
- [Type `GaslessTx`](#type-gaslesstx)
- [Fields](#fields-13)

## RPC Methods

Expand Down Expand Up @@ -346,7 +379,11 @@ Response
"scriptHash": "0xffe2e575a9c327f160e09d142bf21bcedbf79f23d585be3b87dacde843e171a4"
}
},
"chainId": "0x116e8"
"chainId": "0x116e8",
"gaslessTx": {
"support": true,
"entrypointAddress": "0x954dcfc2b81446bc83254c1fa36a037613bd2481"
}
}
}
}
Expand Down Expand Up @@ -626,6 +663,7 @@ Info of Godwoken & Web3 node.

* `chainId`: [`Uint64`](#type-uint64) - Chain ID, more info: [EIP155](https://eips.ethereum.org/EIPS/eip-155)

* `gaslessTx`: [`GaslessTx`](#type-gaslessTx) - Gasless Tx feature, more info: [additional feature](/docs/addtional-feature.md#gasless-transaction)

### Type `GwScripts`

Expand Down Expand Up @@ -728,3 +766,15 @@ Describes the web3 server health status.
* `syncBlocksDiff`: `number` - Web3 sync behind godwoken blocks count, eg 2 means sync behind 2 blocks, 0 means sync to the latest

* `ckbOraclePrice`: `string` - CKBPriceOracle updating value or "PriceOracleNotEnabled" if it is turned off, should not be null

### Type `GaslessTx`

Describes the accounts web3 used.

#### Fields

`GaslessTx` is a JSON object with the following fields.

* `support`: `boolean` - Weather the feature is turned on or not

* `entrypointAddress`: `string` - the entrypoint contract account address
1 change: 1 addition & 0 deletions packages/api-server/src/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ async function startServer(port: number): Promise<void> {
logger.error("godwoken config initialize failed:", err);
process.exit(1);
}

server = app.listen(port, () => {
const addr = (server as Server).address();
const bind =
Expand Down
16 changes: 16 additions & 0 deletions packages/api-server/src/base/gw-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { CKB_SUDT_ID } from "../methods/constant";
import { Uint32 } from "./types/uint";
import { snakeToCamel } from "../util";
import { EntryPointContract } from "../gasless/entrypoint";

// source: https://github.com/nervosnetwork/godwoken/commit/d6c98d8f8a199b6ec29bc77c5065c1108220bb0a#diff-c56fda2ca3b1366049c88e633389d9b6faa8366151369fd7314c81f6e389e5c7R5
const BUILTIN_ETH_ADDR_REG_ACCOUNT_ID = 2;
Expand All @@ -34,6 +35,7 @@ export class GwConfig {
private iRollupCell: RollupCell | undefined;
private iNodeMode: NodeMode | undefined;
private iNodeVersion: string | undefined;
private iEntryPointContract: EntryPointContract | undefined;

constructor(rpcOrUrl: GodwokenClient | string) {
if (typeof rpcOrUrl === "string") {
Expand Down Expand Up @@ -66,6 +68,16 @@ export class GwConfig {
this.iNodeMode = this.nodeInfo.mode;
this.iNodeVersion = this.nodeInfo.version;

const entrypointAddr = this.nodeInfo.gaslessTxSupport?.entrypointAddress;
if (entrypointAddr != null) {
this.iEntryPointContract = new EntryPointContract(
this.rpc,
entrypointAddr,
ethAddrReg.id
);
await this.iEntryPointContract.init();
}

return this;
}

Expand Down Expand Up @@ -105,6 +117,10 @@ export class GwConfig {
return this.iNodeVersion!;
}

public get entrypointContract(): EntryPointContract | undefined {
return this.iEntryPointContract;
}

private get nodeInfo(): NodeInfo {
return this.iNodeInfo!;
}
Expand Down
8 changes: 7 additions & 1 deletion packages/api-server/src/base/types/node-info.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Hash, HexNumber, Script } from "@ckb-lumos/base";
import { Hash, HexNumber, HexString, Script } from "@ckb-lumos/base";
import {
EoaScriptType,
BackendType,
Expand Down Expand Up @@ -37,6 +37,11 @@ export interface RollupConfig {
rewardBurnRate: HexNumber;
chainId: HexNumber;
}

export interface GaslessTxSupport {
entrypointAddress: HexString;
}

export interface NodeInfo {
backends: Array<BackendInfo>;
eoaScripts: Array<EoaScript>;
Expand All @@ -45,4 +50,5 @@ export interface NodeInfo {
rollupConfig: RollupConfig;
version: string;
mode: NodeMode;
gaslessTxSupport?: GaslessTxSupport;
}
42 changes: 34 additions & 8 deletions packages/api-server/src/convert-tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import {
checkBalance,
verifyEnoughBalance,
verifyGaslessTransaction,
verifyGasLimit,
verifyGasPrice,
verifyIntrinsicGas,
Expand All @@ -31,6 +32,7 @@ import { bumpHash, PENDING_TRANSACTION_INDEX } from "./filter-web3-tx";
import { Uint64 } from "./base/types/uint";
import { AutoCreateAccountCacheValue } from "./cache/types";
import { TransactionCallObject } from "./methods/types";
import { isGaslessTransaction } from "./gasless/utils";

export const DEPLOY_TO_ADDRESS = "0x";

Expand Down Expand Up @@ -367,19 +369,43 @@ export async function polyTxToGwTx(
);
}

// Check gas limit
const gasLimitErr = verifyGasLimit(gasLimit === "0x" ? "0x0" : gasLimit, 0);
if (gasLimitErr) {
throw gasLimitErr.padContext(`eth_sendRawTransaction ${polyTxToGwTx.name}`);
}

const minGasPrice = await readonlyPriceOracle.minGasPrice();
const gasPriceErr = verifyGasPrice(
gasPrice === "0x" ? "0x0" : gasPrice,
minGasPrice,
0
);
if (gasPriceErr) {
throw gasPriceErr.padContext(`eth_sendRawTransaction ${polyTxToGwTx.name}`);
// only check if it is gasless transaction when entrypointContract is configured
if (
gwConfig.entrypointContract != null &&
isGaslessTransaction(
{ to, gasPrice: gasPrice === "0x" ? "0x0" : gasPrice, data },
gwConfig.entrypointContract
)
) {
const err = verifyGaslessTransaction(
to,
data,
gasPrice === "0x" ? "0x0" : gasPrice,
gasLimit === "0x" ? "0x0" : gasLimit,
0
);
if (err != null) {
throw err.padContext(`eth_sendRawTransaction ${polyTxToGwTx.name}`);
}
} else {
// not gasless transaction, check gas price
const minGasPrice = await readonlyPriceOracle.minGasPrice();
const gasPriceErr = verifyGasPrice(
gasPrice === "0x" ? "0x0" : gasPrice,
minGasPrice,
0
);
if (gasPriceErr) {
throw gasPriceErr.padContext(
`eth_sendRawTransaction ${polyTxToGwTx.name}`
);
}
}

const signature: HexString = getSignature(rawTx);
Expand Down
Loading

0 comments on commit c456a58

Please sign in to comment.