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 #568 from godwokenrises/url-feature
Browse files Browse the repository at this point in the history
refactor: instant-finality feature via special RPC url
  • Loading branch information
RetricSu authored Nov 15, 2022
2 parents f511003 + 9d97119 commit 8c1c75d
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 83 deletions.
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

A Web3 RPC compatible layer build upon Godwoken/Polyjuice.

Checkout [additional feature](docs/addtional-feature.md).

## Development

### Config database
Expand Down Expand Up @@ -121,9 +123,26 @@ docker exec -it <CONTAINER NAME> /bin/bash
$ root@ec562fe2172b:/godwoken-web3# pm2 monit
```

Http url: http://your-url/
#### URLs

```sh
# Http
http://example_web3_rpc_url

# WebSocket
ws://example_web3_rpc_url/ws
```

WebSocket url: ws://your-url/ws
With instant-finality feature turn on:

```sh
# Http
http://example_web3_rpc_url?instant-finality-hack=true
http://example_web3_rpc_url/instant-finality-hack

# WebSocket
ws://example_web3_rpc_url/ws?instant-finality-hack=true
```

### Docker Prebuilds

Expand Down
22 changes: 22 additions & 0 deletions docs/addtional-feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Additional Feature

## Instant Finality

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

Godwoken provide a quicker way to confirm transaction. Once the transaction is validated in mempool, users can get instant transaction receipt. This feature is called Instant Finality.

If your want to build a low latency user experience for on-chain interaction in your dapp, you can turn on such feature by using the RPC with additional path or query parameter:

```sh
# http
https://example_web3_rpc_url?instant-finality-hack=true
https://example_web3_rpc_url/instant-finality-hack

# websocket
ws://example_web3_rpc_url/ws?instant-finality-hack=true
```

Environment 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 such mode, there might have 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 consider to be most compatible with Ethereum.
5 changes: 5 additions & 0 deletions packages/api-server/src/methods/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,9 @@ function getMethods(argsList: ModConstructorArgs = {}) {
return methods;
}

const instantFinalityHackMode = true;

export const methods = getMethods();
export const instantFinalityHackMethods = getMethods({
eth: [instantFinalityHackMode],
});
94 changes: 55 additions & 39 deletions packages/api-server/src/methods/modules/eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,18 @@ type GodwokenBlockParameter = U64 | undefined;
export class Eth {
private query: Query;
private rpc: GodwokenClient;
private instantFinalityHackMode: boolean;
private filterManager: FilterManager;
private cacheStore: Store;
private ethNormalizer: EthNormalizer;

constructor() {
constructor(instantFinalityHackMode: boolean = false) {
this.query = new Query();
this.rpc = new GodwokenClient(
envConfig.godwokenJsonRpc,
envConfig.godwokenReadonlyJsonRpc
);
this.instantFinalityHackMode = instantFinalityHackMode;
this.filterManager = new FilterManager(true);
this.cacheStore = new Store(true, CACHE_EXPIRED_TIME_MILSECS);
this.ethNormalizer = new EthNormalizer(this.rpc);
Expand Down Expand Up @@ -718,19 +720,19 @@ export class Eth {

async getTransactionByHash(args: [string]): Promise<EthTransaction | null> {
const ethTxHash: Hash = args[0];
const cacheKey = autoCreateAccountCacheKey(ethTxHash);
const acaCacheKey = autoCreateAccountCacheKey(ethTxHash);

// 1. Find in db
const tx = await this.query.getTransactionByEthTxHash(ethTxHash);
if (tx != null) {
// no need await
// delete auto create account tx if already in db
this.cacheStore.delete(cacheKey);
this.cacheStore.delete(acaCacheKey);
const apiTx = toApiTransaction(tx);
return apiTx;
}

// 2. If null, find pending transactions
// 2. Find pending tx from gw mempool block
const ethTxHashKey = ethTxHashCacheKey(ethTxHash);
const gwTxHash: Hash | null = await this.cacheStore.get(ethTxHashKey);
if (gwTxHash != null) {
Expand Down Expand Up @@ -768,7 +770,7 @@ export class Eth {
// 3. Find by auto create account tx
// TODO: delete cache store if dropped by godwoken
// convert to tx hash mapping store if account id generated ?
const polyjuiceRawTx = await this.cacheStore.get(cacheKey);
const polyjuiceRawTx = await this.cacheStore.get(acaCacheKey);
if (polyjuiceRawTx != null) {
const tipBlock = await this.query.getTipBlock();
if (tipBlock == null) {
Expand Down Expand Up @@ -797,7 +799,7 @@ export class Eth {
return apiTransaction;
} else {
// If not found, means dropped by godwoken, should delete cache
this.cacheStore.delete(cacheKey);
this.cacheStore.delete(acaCacheKey);
}
}

Expand Down Expand Up @@ -861,6 +863,7 @@ export class Eth {
return null;
}

// 1. Find in db
const data = await this.query.getTransactionAndLogsByHash(gwTxHash);
if (data != null) {
const [tx, logs] = data;
Expand All @@ -869,37 +872,43 @@ export class Eth {
return transactionReceipt;
}

const godwokenTxWithStatus = await this.rpc.getTransaction(gwTxHash);
if (godwokenTxWithStatus == null) {
return null;
}
const godwokenTxReceipt = await this.rpc.getTransactionReceipt(gwTxHash);
if (godwokenTxReceipt == null) {
return null;
}
const tipBlock = await this.query.getTipBlock();
if (tipBlock == null) {
throw new Error(`tip block not found`);
}
let ethTxInfo = undefined;
try {
ethTxInfo = await filterWeb3Transaction(
ethTxHash,
this.rpc,
tipBlock.number,
tipBlock.hash,
godwokenTxWithStatus.transaction,
godwokenTxReceipt
// 2. If under instant-finality hack mode, build receipt from gw mempool block
if (this.instantFinalityHackMode) {
logger.debug(
`[eth_getTransactionReceipt] find with instant-finality hack`
);
} catch (err) {
logger.error("filterWeb3Transaction:", err);
logger.info("godwoken tx:", godwokenTxWithStatus);
logger.info("godwoken receipt:", godwokenTxReceipt);
throw err;
}
if (ethTxInfo != null) {
const ethTxReceipt = ethTxInfo[1]!;
return ethTxReceipt;
const godwokenTxWithStatus = await this.rpc.getTransaction(gwTxHash);
if (godwokenTxWithStatus == null) {
return null;
}
const godwokenTxReceipt = await this.rpc.getTransactionReceipt(gwTxHash);
if (godwokenTxReceipt == null) {
return null;
}
const tipBlock = await this.query.getTipBlock();
if (tipBlock == null) {
throw new Error(`tip block not found`);
}
let ethTxInfo = undefined;
try {
ethTxInfo = await filterWeb3Transaction(
ethTxHash,
this.rpc,
tipBlock.number,
tipBlock.hash,
godwokenTxWithStatus.transaction,
godwokenTxReceipt
);
} catch (err) {
logger.error("filterWeb3Transaction:", err);
logger.info("godwoken tx:", godwokenTxWithStatus);
logger.info("godwoken receipt:", godwokenTxReceipt);
throw err;
}
if (ethTxInfo != null) {
const ethTxReceipt = ethTxInfo[1]!;
return ethTxReceipt;
}
}

return null;
Expand Down Expand Up @@ -1099,7 +1108,11 @@ export class Eth {
): Promise<GodwokenBlockParameter> {
switch (blockParameter) {
case "latest":
return undefined;
if (this.instantFinalityHackMode) {
// under instant-finality hack, we treat latest as pending
return undefined;
}
return await this.getTipNumber();
case "earliest":
return 0n;
// It's supposed to be filtered in the validator, so throw an error if matched
Expand Down Expand Up @@ -1143,6 +1156,9 @@ export class Eth {
return blockNumber;
}

// Some RPCs does not support pending parameter
// eth_getBlockByNumber/eth_getBlockTransactionCountByNumber/eth_getTransactionByBlockNumberAndIndex
// TODO: maybe we should support for those as well?
private async blockParameterToBlockNumber(
blockParameter: BlockParameter
): Promise<U64> {
Expand Down Expand Up @@ -1468,7 +1484,7 @@ function serializeEthCallParameters(
gasPrice: ethCallObj.gasPrice || "0x",
data: ethCallObj.data || "0x",
value: ethCallObj.value || "0x",
blockNumber: blockNumber ? "0x" + blockNumber?.toString(16) : "0x", // undefined means latest block, the key contains tipBlockHash, so there is no need to diff latest height
blockNumber: blockNumber ? "0x" + blockNumber?.toString(16) : "0x", // undefined means pending block, the key contains tipBlockHash, so there is no need to diff pending height
};
return JSON.stringify(toSerializeObj);
}
Expand Down Expand Up @@ -1498,7 +1514,7 @@ function serializeEstimateGasParameters(
gasPrice: estimateGasObj.gasPrice || "0x",
data: estimateGasObj.data || "0x",
value: estimateGasObj.value || "0x",
blockNumber: blockNumber ? "0x" + blockNumber?.toString(16) : "0x", // undefined means latest block, the key contains tipBlockHash, so there is no need to diff latest height
blockNumber: blockNumber ? "0x" + blockNumber?.toString(16) : "0x", // undefined means pending block, the key contains tipBlockHash, so there is no need to diff pending height
};
return JSON.stringify(toSerializeObj);
}
Expand Down
11 changes: 10 additions & 1 deletion packages/api-server/src/middlewares/jayson.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import jayson from "jayson";
import { methods } from "../methods/index";
import { instantFinalityHackMethods, methods } from "../methods/index";
import { Request, Response, NextFunction } from "express";
import createServer from "connect";
import { isInstantFinalityHackMode } from "../util";

const server = new jayson.Server(methods);
const instantFinalityHackServer = new jayson.Server(instantFinalityHackMethods);

export const jaysonMiddleware = (
req: Request,
Expand All @@ -16,6 +18,13 @@ export const jaysonMiddleware = (
req.body.params = [] as any[];
}

// enable additional feature for special URL
if (isInstantFinalityHackMode(req)) {
const middleware =
instantFinalityHackServer.middleware() as createServer.NextHandleFunction;
return middleware(req, res, next);
}

const middleware = server.middleware() as createServer.NextHandleFunction;
return middleware(req, res, next);
};
9 changes: 9 additions & 0 deletions packages/api-server/src/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { HexString } from "@ckb-lumos/base";
import { Request } from "express";
import {
TX_DATA_NONE_ZERO_GAS,
TX_DATA_ZERO_GAS,
Expand Down Expand Up @@ -121,6 +122,14 @@ export function calcFee(serializedL2Tx: HexString, feeRate: bigint) {
return byteLen * feeRate;
}

// WEB3_RPC_URL/instant-finality-hack or WEB3_RPC_URL?instant-finality-hack=true
export function isInstantFinalityHackMode(req: Request): boolean {
return (
req.url == "/instant-finality-hack" ||
(req.query && req.query["instant-finality-hack"] == "true")
);
}

export async function asyncSleep(ms = 0) {
return new Promise((r) => setTimeout(() => r("ok"), ms));
}
Loading

0 comments on commit 8c1c75d

Please sign in to comment.