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

Support native transfer #505

Merged
merged 5 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 25 additions & 7 deletions crates/indexer/src/helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub struct PolyjuiceArgs {
pub gas_price: u128,
pub value: u128,
pub input: Option<Vec<u8>>,
pub to_address_when_native_transfer: Option<Vec<u8>>,
}

impl PolyjuiceArgs {
Expand All @@ -39,19 +40,36 @@ impl PolyjuiceArgs {
input_size
));
}
if args.len() < 52 + input_size as usize {
return Err(anyhow!(
"polyjuice args input data too short: 0x{}",
hex(args)?
));

let input: Option<Vec<u8>>;
let to_address_when_native_transfer: Option<Vec<u8>>;
match args.len() {
_ if args.len() == 52 + input_size as usize => {
input = Some(args[52..(52 + input_size as usize)].to_vec());
to_address_when_native_transfer = None;
}
_ if args.len() == 52 + input_size as usize + 20 => {
input = Some(args[52..(52 + input_size as usize)].to_vec());
to_address_when_native_transfer = Some(
args[(52 + input_size as usize)..(52 + input_size as usize + 20)].to_vec(),
);
}
_ => {
return Err(anyhow!(
"unrecognizable polyjuice args: 0x{}, args_size: {}, input_size: {}",
hex(args)?,
args.len(),
input_size
));
}
}
let input: Vec<u8> = args[52..(52 + input_size as usize)].to_vec();
Ok(PolyjuiceArgs {
is_create,
gas_limit,
gas_price,
value,
input: Some(input),
input,
to_address_when_native_transfer,
})
}
}
Expand Down
9 changes: 8 additions & 1 deletion crates/indexer/src/indexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,16 @@ impl Web3Indexer {
if to_script.code_hash().as_slice() == self.polyjuice_type_script_hash.0 {
let l2_tx_args = l2_transaction.raw().args();
let polyjuice_args = PolyjuiceArgs::decode(l2_tx_args.raw_data().as_ref())?;
// to_address is null if it's a contract deployment transaction

// For CREATE contracts, tx.to_address = null;
// for native transfers, tx.to_address = last 20 bytes of polyjuice_args;
// otherwise, tx.to_address equals to the eth_address of tx.to_id
let (to_address, _polyjuice_chain_id) = if polyjuice_args.is_create {
(None, to_id)
} else if let Some(ref address_vec) = polyjuice_args.to_address_when_native_transfer {
RetricSu marked this conversation as resolved.
Show resolved Hide resolved
let mut address = [0u8; 20];
address.copy_from_slice(&address_vec[..]);
(Some(address), to_id)
} else {
let args: gw_types::bytes::Bytes = to_script.args().unpack();
let address = {
Expand Down
21 changes: 2 additions & 19 deletions docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,7 @@

## RPC compatibility

### 1. `transaction.to` MUST be a Contract Address

The `to` member of a Godwoken transaction must be a contract.

#### Result

- In the following RPCs, the `to` parameter can only be a contract address and **CANNOT** be an EOA address:
- eth_call
- eth_estimateGas
- eth_sendRawTransaction

#### Recommend workaround

- **Transfer Value From EOA To EOA**: Use the `transfer function` in [pCKB_ERC20_Proxy](https://github.com/nervosnetwork/godwoken-polyjuice/blob/ae65ef551/solidity/erc20/README.md) contract [combined](https://github.com/nervosnetwork/godwoken-polyjuice/blob/3f1ad5b322/solidity/erc20/SudtERC20Proxy_UserDefinedDecimals.sol#L154) with sUDT_ID = 1 (CKB a.k.a. [pCKB](https://github.com/nervosnetwork/godwoken/blob/develop/docs/life_of_a_polyjuice_transaction.md#pckb)).
- mainnet_v1 pCKB_ERC20_Proxy contract: 0x7538C85caE4E4673253fFd2568c1F1b48A71558a (pCKB)
- testnet_v1 pCKB_ERC20_Proxy contract: 0xE05d380839f32bC12Fb690aa6FE26B00Bd982613 (pCKB)

### 2. ZERO ADDRESS
### 1. ZERO ADDRESS

Godwoken does not have the corresponding "zero address"(0x0000000000000000000000000000000000000000) concept, so Polyjuice won't be able to handle zero address as well.

Expand All @@ -33,7 +16,7 @@ known issue: #246

- if you are trying to use zero address as a black hole to burn ethers, you can use `transfer function` in `CKB_ERC20_Proxy` to send ethers to zero address. more info can be found in the above section `Transfer Value From EOA To EOA`.

### 3. GAS LIMIT
### 2. GAS LIMIT

Godwoken limit the transaction execution resource in CKB-VM with [Cycle Limit](https://docs-xi-two.vercel.app/docs/rfcs/0014-vm-cycle-limits/0014-vm-cycle-limits), we set the `RPC_GAS_LIMIT` to `50000000` for max compatibility with Ethereum toolchain, but the real gas limit you can use depends on such Cycle Limit.

Expand Down
111 changes: 71 additions & 40 deletions packages/api-server/src/convert-tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { gwConfig } from "./base";
import { logger } from "./base/logger";
import {
MAX_TRANSACTION_SIZE,
COMPATIBLE_DOCS_URL,
AUTO_CREATE_ACCOUNT_FROM_ID,
} from "./methods/constant";
import {
Expand Down Expand Up @@ -52,7 +51,7 @@ export function polyjuiceRawTransactionToApiTransaction(
tipBlockNumber: bigint,
fromEthAddress: HexString
): EthTransaction {
const tx: PolyjuiceTransaction = decodeRawTransactionData(rawTx);
const tx: PolyjuiceTransaction = ethRawTxToPolyTx(rawTx);

const pendingBlockHash = bumpHash(tipBlockHash);
const pendingBlockNumber = new Uint64(tipBlockNumber + 1n).toHex();
Expand Down Expand Up @@ -81,38 +80,41 @@ export function calcEthTxHash(encodedSignedTx: HexString): Hash {
return ethTxHash;
}

export async function generateRawTransaction(
/**
* Convert the ETH raw transaction to Godwoken L2Transaction
*/
export async function ethRawTxToGwTx(
data: HexString,
rpc: GodwokenClient
): Promise<[L2Transaction, [string, string] | undefined]> {
logger.debug("convert-tx, origin data:", data);
const polyjuiceTx: PolyjuiceTransaction = decodeRawTransactionData(data);
const polyjuiceTx: PolyjuiceTransaction = ethRawTxToPolyTx(data);
logger.debug("convert-tx, decoded polyjuice tx:", polyjuiceTx);
const [godwokenTx, cacheKeyAndValue] = await parseRawTransactionData(
const [godwokenTx, cacheKeyAndValue] = await polyTxToGwTx(
polyjuiceTx,
rpc,
data
);
return [godwokenTx, cacheKeyAndValue];
}

export function decodeRawTransactionData(
dataParams: HexString
): PolyjuiceTransaction {
const result: Buffer[] = rlp.decode(dataParams) as Buffer[];
// todo: r might be "0x" which cause inconvenient for down-stream
const resultHex = result.map((r) => "0x" + Buffer.from(r).toString("hex"));

/**
* Convert ETH raw transaction to PolyjuiceTransaction
*/
export function ethRawTxToPolyTx(ethRawTx: HexString): PolyjuiceTransaction {
const result: Buffer[] = rlp.decode(ethRawTx) as Buffer[];
if (result.length !== 9) {
throw new Error("decode raw transaction data error");
throw new Error("decode eth raw transaction data error");
}

// todo: r might be "0x" which cause inconvenient for down-stream
const resultHex = result.map((r) => "0x" + Buffer.from(r).toString("hex"));
const [nonce, gasPrice, gasLimit, to, value, data, v, r, s] = resultHex;

// r & s is integer in RLP, convert to 32-byte hex string (add leading zeros)
const rWithLeadingZeros: HexString = "0x" + r.slice(2).padStart(64, "0");
const sWithLeadingZeros: HexString = "0x" + s.slice(2).padStart(64, "0");
const tx: PolyjuiceTransaction = {
return {
nonce,
gasPrice,
gasLimit,
Expand All @@ -123,8 +125,6 @@ export function decodeRawTransactionData(
r: rWithLeadingZeros,
s: sWithLeadingZeros,
};

return tx;
}

export function getSignature(tx: PolyjuiceTransaction): HexString {
Expand Down Expand Up @@ -206,10 +206,13 @@ function encodePolyjuiceTransaction(tx: PolyjuiceTransaction) {
return "0x" + result.toString("hex");
}

export async function parseRawTransactionData(
/**
* Convert Polyjuice transaction to Godwoken transaction
*/
export async function polyTxToGwTx(
rawTx: PolyjuiceTransaction,
rpc: GodwokenClient,
polyjuiceRawTx: HexString
ethRawTx: HexString
): Promise<[L2Transaction, [string, string] | undefined]> {
const { nonce, gasPrice, gasLimit, to, value, data, v } = rawTx;

Expand All @@ -224,16 +227,12 @@ export async function parseRawTransactionData(

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

const gasPriceErr = verifyGasPrice(gasPrice === "0x" ? "0x0" : gasPrice, 0);
if (gasPriceErr) {
throw gasPriceErr.padContext(
`eth_sendRawTransaction ${parseRawTransactionData.name}`
);
throw gasPriceErr.padContext(`eth_sendRawTransaction ${polyTxToGwTx.name}`);
}

const signature: HexString = getSignature(rawTx);
Expand Down Expand Up @@ -268,7 +267,7 @@ export async function parseRawTransactionData(
}
const key = autoCreateAccountCacheKey(ethTxHash);
const cacheValue: AutoCreateAccountCacheValue = {
tx: polyjuiceRawTx,
tx: ethRawTx,
fromAddress: fromEthAddress,
};
cacheKeyAndValue = [key, JSON.stringify(cacheValue)];
Expand All @@ -285,7 +284,7 @@ export async function parseRawTransactionData(
);
if (intrinsicGasErr) {
throw intrinsicGasErr.padContext(
`eth_sendRawTransaction ${parseRawTransactionData.name}`
`eth_sendRawTransaction ${polyTxToGwTx.name}`
);
}

Expand All @@ -299,7 +298,7 @@ export async function parseRawTransactionData(
);
if (enoughBalanceErr) {
throw enoughBalanceErr.padContext(
`eth_sendRawTransaction ${parseRawTransactionData.name}`
`eth_sendRawTransaction ${polyTxToGwTx.name}`
);
}

Expand All @@ -324,31 +323,32 @@ export async function parseRawTransactionData(
const args_48_52 = UInt32ToLeBytes(dataByteLength);
// data
const args_data = data;
let args_7 = to === DEPLOY_TO_ADDRESS ? "0x03" : "0x00";

let args_7 = "";
const isEthNativeTransfer_ = await isEthNativeTransfer(rawTx, rpc);
let toId: HexNumber | undefined;
if (to === DEPLOY_TO_ADDRESS) {
args_7 = "0x03";
toId = gwConfig.accounts.polyjuiceCreator.id;
} else if (isEthNativeTransfer_) {
toId = gwConfig.accounts.polyjuiceCreator.id;
} else {
args_7 = "0x00";
toId = await getAccountIdByEthAddress(to, rpc);
}

if (toId == null) {
throw new Error(`to id not found by address: ${to}`);
}

// disable to address is eoa case
const toScriptHash = await rpc.getScriptHash(Number(toId));
const eoaScriptHash = ethEoaAddressToScriptHash(to);
if (toScriptHash === eoaScriptHash) {
throw new Error(
`to_address can not be EOA address! more info: ${COMPATIBLE_DOCS_URL}`
);
}

const args =
// When it is a native transfer transaction, we do some modifications to Godwoken transaction:
//
// We make some convention for native transfer transaction:
// - Set `gwTx.to_id = <Polyjuice Creator Account ID>`, to mark the transaction is native transfer
// - Append the original `tx.to` address to `gwTx.args`, to pass the recipient information to Polyjuice
//
// See also:
// - https://github.com/nervosnetwork/godwoken/pull/784
// - https://github.com/nervosnetwork/godwoken-polyjuice/pull/173
let args: HexString =
"0x" +
args_0_7.slice(2) +
args_7.slice(2) +
Expand All @@ -357,6 +357,9 @@ export async function parseRawTransactionData(
args_32_48.slice(2) +
args_48_52.slice(2) +
args_data.slice(2);
if (isEthNativeTransfer_) {
args = args + rawTx.to.slice(2);
}

let chainId = gwConfig.web3ChainId;
// When `v = 27` or `v = 28`, the transaction is considered a non-eip155 transaction.
Expand Down Expand Up @@ -447,3 +450,31 @@ function publicKeyToEthAddress(publicKey: HexString): HexString {
export function autoCreateAccountCacheKey(ethTxHash: string) {
return `${AUTO_CREATE_ACCOUNT_PREFIX_KEY}:${ethTxHash}`;
}

/**
* Determine whether the transaction is a native transfer transaction.
*
* When tx.to refers to an EOA account or an account that doesn't exist, it is considered a native transfer transaction.
*/
export async function isEthNativeTransfer(
{ to: toAddress }: { to: HexString },
keroro520 marked this conversation as resolved.
Show resolved Hide resolved
rpc: GodwokenClient
): Promise<boolean> {
if (toAddress.length === 42) {
const toId = await getAccountIdByEthAddress(toAddress, rpc);
return toId == null || isEthEOA(toAddress, toId, rpc);
}
return false;
}

/**
* Determine whether the account is EOA account
*/
export async function isEthEOA(
ethAddress: HexString,
accountId: HexNumber,
rpc: GodwokenClient
): Promise<boolean> {
const scriptHash = await rpc.getScriptHash(Number(accountId));
return ethEoaAddressToScriptHash(ethAddress) === scriptHash;
}
7 changes: 7 additions & 0 deletions packages/api-server/src/filter-web3-tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,17 @@ export async function filterWeb3Transaction(
const l2TxArgs: HexNumber = l2Tx.raw.args;
const polyjuiceArgs = decodePolyjuiceArgs(l2TxArgs);

// For CREATE contracts, tx.to_address = null;
// for native transfers, tx.to_address = last 20 bytes of polyjuice_args;
// otherwise, tx.to_address equals to the eth_address of tx.to_id.
let toAddress: HexString | undefined;

// let polyjuiceChainId: HexNumber | undefined;
if (polyjuiceArgs.isCreate) {
toAddress = undefined;
// polyjuiceChainId = toIdHex;
} else if (polyjuiceArgs.toAddressWhenNativeTransfer != null) {
toAddress = polyjuiceArgs.toAddressWhenNativeTransfer;
} else {
// 74 = 2 + (32 + 4) * 2
toAddress = "0x" + toScript.args.slice(74);
Expand Down
Loading