Skip to content
Closed
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
3 changes: 3 additions & 0 deletions packages/contracts/source/contracts/crypto/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export interface TransactionData {
v?: number;
r?: string;
s?: string;
rlpPrefix?: number;

legacySecondSignature?: string;

transactionIndex?: number;
Expand Down Expand Up @@ -64,6 +66,7 @@ export interface TransactionJson {
v?: number;
r?: string;
s?: string;
rlpPrefix?: number;

transactionIndex?: number;
gasUsed?: number;
Expand Down
84 changes: 75 additions & 9 deletions packages/crypto-transaction/source/deserializer.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,55 @@
import { inject, injectable } from "@mainsail/container";
import { Contracts, Identifiers } from "@mainsail/contracts";
import { BigNumber } from "@mainsail/utils";
import { decodeRlp, ethers, getAddress } from "ethers";
import { decodeRlp, ethers, getAddress, RlpStructuredData } from "ethers";

@injectable()
export class Deserializer implements Contracts.Crypto.TransactionDeserializer {
@inject(Identifiers.Cryptography.Transaction.TypeFactory)
private readonly transactionTypeFactory!: Contracts.Transactions.TransactionTypeFactory;

@inject(Identifiers.Cryptography.Configuration)
private readonly configuration!: Contracts.Crypto.Configuration;

public async deserialize(serialized: Buffer | string): Promise<Contracts.Crypto.Transaction> {
const data = {} as Contracts.Crypto.TransactionData;

const encodedRlp =
"0x" + (typeof serialized === "string" ? serialized.slice(2) : serialized.toString("hex").slice(2));
let rlpBuffer =
typeof serialized === "string"
? Buffer.from(serialized.startsWith("0x") ? serialized.slice(2) : serialized, "hex")
: serialized;

// Remove type prefix (e.g. 02) if it's a EIP1559 tx (`decodeRlp` expects input to be without)
let prefix: number | undefined = undefined;
if (rlpBuffer[0] < 0xc0) {
prefix = rlpBuffer[0];
if (prefix !== 0x02) {
throw new Error("expected EIP1559 transaction");
}

rlpBuffer = rlpBuffer.subarray(1);
}

const decoded = decodeRlp(new Uint8Array(rlpBuffer));

if (prefix !== undefined) {
this.#decodeEIP1559Transaction(decoded, data);
serialized = Buffer.concat([Buffer.from([prefix]), rlpBuffer]);
} else {
this.#decodeLegacyTransaction(decoded, data);
serialized = rlpBuffer;
}

const decoded = decodeRlp(encodedRlp);
const instance: Contracts.Crypto.Transaction = this.transactionTypeFactory.create(data);
instance.serialized = serialized;

return instance;
}

#decodeEIP1559Transaction(decoded: RlpStructuredData, data: Contracts.Crypto.TransactionData): void {
if (decoded.length < 9 || decoded.length > 13) {
throw new Error("RLP data out of range");
}

const recipientAddressRaw = this.#parseAddress(decoded[5].toString());

Expand All @@ -31,25 +66,56 @@ export class Deserializer implements Contracts.Crypto.TransactionDeserializer {
data.to = recipientAddressRaw ? getAddress(recipientAddressRaw) : undefined;
data.value = this.#parseBigNumber(decoded[6].toString());
data.data = this.#parseData(decoded[7].toString());
data.rlpPrefix = 0x02;

// Signature
if (decoded.length >= 12) {
data.v = this.#parseNumber(decoded[9].toString());
data.r = decoded[10].toString().slice(2);
data.s = decoded[11].toString().slice(2);

// Legacy second signature
// Legacy second signature is only supported for EIP-1559 transactions
if (decoded.length === 13) {
data.legacySecondSignature = decoded[12].toString().slice(2);
}
}
}

const instance: Contracts.Crypto.Transaction = this.transactionTypeFactory.create(data);
#decodeLegacyTransaction(decoded: RlpStructuredData, data: Contracts.Crypto.TransactionData): void {
if (decoded.length < 6 || decoded.length > 9) {
throw new Error("legacy RLP data out of range");
}

const eip1559Prefix = "02"; // marker for Type 2 (EIP1559) transaction which is the standard nowadays
instance.serialized = Buffer.from(`${eip1559Prefix}${encodedRlp.slice(2)}`, "hex");
// [nonce, gasPrice, gasLimit, to, value, data, v, r, s];
data.nonce = BigNumber.make(this.#parseNumber(decoded[0].toString()));
data.gasPrice = this.#parseNumber(decoded[1].toString());
data.gas = this.#parseNumber(decoded[2].toString());

return instance;
const recipientAddressRaw = this.#parseAddress(decoded[3].toString());
data.to = recipientAddressRaw ? getAddress(recipientAddressRaw) : undefined;

data.value = this.#parseBigNumber(decoded[4].toString());
data.data = this.#parseData(decoded[5].toString());

// NOTE:
// The chainId is encoded in 'v' which is part of the optional signature.
// In the case of absence default to the config for the chainId.

// Signature
if (decoded.length >= 9) {
const v = this.#parseNumber(decoded[6].toString());
const chainId = Math.floor((v - 35) / 2);

data.network = chainId;

const normalizedV = v - (chainId * 2 + 35);
data.v = normalizedV;

data.r = decoded[7].toString().slice(2);
data.s = decoded[8].toString().slice(2);
} else {
data.network = this.configuration.get("network.chainId");
}
}

#parseNumber(value: string): number {
Expand Down
76 changes: 56 additions & 20 deletions packages/crypto-transaction/source/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,69 @@ export class Serializer implements Contracts.Crypto.TransactionSerializer {
transaction: Contracts.Crypto.Transaction,
options: Contracts.Crypto.SerializeOptions = {},
): Promise<Buffer> {
const fields = [
toBeArray(transaction.data.network), // chainId - 0
toBeArray(transaction.data.nonce.toBigInt()), // nonce - 1
toBeArray(0), // maxPriorityFeePerGas - 2
toBeArray(transaction.data.gasPrice), // maxFeePerGas - 3
toBeArray(transaction.data.gas), // gasLimit - 4
transaction.data.to || "0x", // to - 5
toBeArray(transaction.data.value.toBigInt()), // value - 6
transaction.data.data.startsWith("0x") ? transaction.data.data : `0x${transaction.data.data}`, // data - 7
[], //accessList - 8
];
let fields: Array<any> = [];

if (transaction.data.v !== undefined && transaction.data.r && transaction.data.s && !options.excludeSignature) {
// 9, 10, 11
fields.push(toBeArray(transaction.data.v), `0x${transaction.data.r}`, `0x${transaction.data.s}`);
const { rlpPrefix } = transaction.data;

if (transaction.data.legacySecondSignature) {
// 12
fields.push(`0x${transaction.data.legacySecondSignature}`);
if (rlpPrefix) {
if (rlpPrefix !== 0x02) {
throw new Error("expected EIP1559 transaction");
}

fields = [
toBeArray(transaction.data.network), // chainId - 0
toBeArray(transaction.data.nonce.toBigInt()), // nonce - 1
toBeArray(0), // maxPriorityFeePerGas - 2
toBeArray(transaction.data.gasPrice), // maxFeePerGas - 3
toBeArray(transaction.data.gas), // gasLimit - 4
transaction.data.to || "0x", // to - 5
toBeArray(transaction.data.value.toBigInt()), // value - 6
transaction.data.data.startsWith("0x") ? transaction.data.data : `0x${transaction.data.data}`, // data - 7
[], //accessList - 8
];
} else {
// Legacy encoding
fields = [
toBeArray(transaction.data.nonce.toBigInt()),
toBeArray(transaction.data.gasPrice),
toBeArray(transaction.data.gas),
transaction.data.to || "0x",
toBeArray(transaction.data.value.toBigInt()),
transaction.data.data.startsWith("0x") ? transaction.data.data : `0x${transaction.data.data}`,

// EIP-155 also requires chainId (`v`)
toBeArray(transaction.data.network), // v
toBeArray(0), // r
toBeArray(0), // s
];
}

const rlpEncoded = encodeRlp(fields);
if (transaction.data.v !== undefined && transaction.data.r && transaction.data.s && !options.excludeSignature) {
if (rlpPrefix) {
// 9, 10, 11
fields.push(toBeArray(transaction.data.v), `0x${transaction.data.r}`, `0x${transaction.data.s}`);

if (transaction.data.legacySecondSignature) {
// 12
fields.push(`0x${transaction.data.legacySecondSignature}`);
}
} else {
// Legacy with EIP-155
const normalizedV = transaction.data.v;
const v = transaction.data.network * 2 + 35 + normalizedV;

const eip1559Prefix = "02"; // marker for Type 2 (EIP1559) transaction which is the standard nowadays
fields[6] = toBeArray(v);
fields[7] = `0x${transaction.data.r}`;
fields[8] = `0x${transaction.data.s}`;
}
}

let encoded = Buffer.from(encodeRlp(fields).slice(2), "hex");
if (rlpPrefix) {
encoded = Buffer.concat([Buffer.from([rlpPrefix]), encoded]);
}

transaction.serialized = Buffer.from(`${eip1559Prefix}${rlpEncoded.slice(2)}`, "hex");
transaction.serialized = encoded;

return transaction.serialized;
}
Expand Down
67 changes: 52 additions & 15 deletions packages/crypto-transaction/source/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,68 @@ export class Utils implements Contracts.Crypto.TransactionUtilities {
transaction: Contracts.Crypto.TransactionData,
options?: Contracts.Crypto.SerializeOptions,
): Promise<Buffer> {
let fields: Array<any> = [];

const { rlpPrefix } = transaction;

// based on EIP1559 encoding
const fields = [
toBeArray(transaction.network),
toBeArray(transaction.nonce.toBigInt()),
toBeArray(0), // maxPriorityFeePerGas
toBeArray(transaction.gasPrice), // maxFeePerGas
toBeArray(transaction.gas),
transaction.to || "0x",
toBeArray(transaction.value.toBigInt()),
transaction.data.startsWith("0x") ? transaction.data : `0x${transaction.data}`,
[], // accessList is unused
];
if (rlpPrefix) {
if (rlpPrefix !== 0x02) {
throw new Error("expected EIP1559 transaction");
}

fields = [
toBeArray(transaction.network),
toBeArray(transaction.nonce.toBigInt()),
toBeArray(0), // maxPriorityFeePerGas
toBeArray(transaction.gasPrice), // maxFeePerGas
toBeArray(transaction.gas),
transaction.to || "0x",
toBeArray(transaction.value.toBigInt()),
transaction.data.startsWith("0x") ? transaction.data : `0x${transaction.data}`,
[], // accessList is unused
];
} else {
// Legacy encoding
fields = [
toBeArray(transaction.nonce.toBigInt()),
toBeArray(transaction.gasPrice),
toBeArray(transaction.gas),
transaction.to || "0x",
toBeArray(transaction.value.toBigInt()),
transaction.data.startsWith("0x") ? transaction.data : `0x${transaction.data}`,

// EIP-155 also requires chainId (`v`)
toBeArray(transaction.network), // v
toBeArray(0), // r
toBeArray(0), // s
];
}

if (options && !options.excludeSignature) {
assert.number(transaction.v);
assert.string(transaction.r);
assert.string(transaction.s);

fields.push(toBeArray(transaction.v), `0x${transaction.r}`, `0x${transaction.s}`);
if (rlpPrefix) {
fields.push(toBeArray(transaction.v), `0x${transaction.r}`, `0x${transaction.s}`);
} else {
// Legacy with EIP-155
const normalizedV = transaction.v;
const v = transaction.network * 2 + 35 + normalizedV;

fields[6] = toBeArray(v);
fields[7] = `0x${transaction.r}`;
fields[8] = `0x${transaction.s}`;
}
}

const eip1559Prefix = "02"; // marker for Type 2 (EIP1559) transaction which is the standard nowadays
const encoded = encodeRlp(fields).slice(2); // remove 0x prefix
let encoded = Buffer.from(encodeRlp(fields).slice(2), "hex");
if (rlpPrefix) {
encoded = Buffer.concat([Buffer.from([rlpPrefix]), encoded]);
}

return Buffer.from(keccak256(Buffer.from(`${eip1559Prefix}${encoded}`, "hex")).slice(2), "hex");
return Buffer.from(keccak256(encoded).slice(2), "hex");
}

public async getHash(transaction: Contracts.Crypto.Transaction): Promise<string> {
Expand Down
2 changes: 2 additions & 0 deletions packages/crypto-transaction/source/validation/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export const transactionBaseSchema: SchemaObject = {

r: { type: "string" },

rlpPrefix: { maximum: 2, minimum: 0, type: "number" },

// TODO: prefixed hex
s: { type: "string" },

Expand Down
Loading