Skip to content

Commit

Permalink
Use RLp serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastijankuzner committed Dec 10, 2024
1 parent 3012eb4 commit 0d9f79d
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 204 deletions.
4 changes: 1 addition & 3 deletions packages/contracts/source/contracts/crypto/transactions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BigNumber, ByteBuffer } from "@mainsail/utils";
import { BigNumber } from "@mainsail/utils";

import type { EcdsaSignature, KeyPair } from "./identities.js";
import type { SchemaValidationResult } from "./validator.js";
Expand Down Expand Up @@ -109,8 +109,6 @@ export interface TransactionSerializer {

export interface TransactionDeserializer {
deserialize(serialized: string | Buffer): Promise<Transaction>;

deserializeCommon(transaction: TransactionData, buf: ByteBuffer): void;
}

export interface TransactionFactory {
Expand Down
102 changes: 28 additions & 74 deletions packages/crypto-transaction/source/deserializer.ts
Original file line number Diff line number Diff line change
@@ -1,98 +1,52 @@
import { inject, injectable, tagged } from "@mainsail/container";
import { inject, injectable } from "@mainsail/container";
import { Contracts, Identifiers } from "@mainsail/contracts";
import { BigNumber, ByteBuffer } from "@mainsail/utils";
import { BigNumber } from "@mainsail/utils";
import { decodeRlp } from "ethers";

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

@inject(Identifiers.Cryptography.Identity.Address.Serializer)
private readonly addressSerializer!: Contracts.Crypto.AddressSerializer;
public async deserialize(serialized: Buffer | string): Promise<Contracts.Crypto.Transaction> {
const data = {} as Contracts.Crypto.TransactionData;

@inject(Identifiers.Cryptography.Identity.Address.Factory)
private readonly addressFactory!: Contracts.Crypto.AddressFactory;
const encodedRlp =
"0x" + (typeof serialized === "string" ? serialized.slice(2) : serialized.toString("hex").slice(2));

@inject(Identifiers.Cryptography.Signature.Size)
@tagged("type", "wallet")
private readonly signatureSize!: number;
const decoded = decodeRlp(encodedRlp);

public async deserialize(serialized: string | Buffer): Promise<Contracts.Crypto.Transaction> {
const data = {} as Contracts.Crypto.TransactionData;
data.network = Number(decoded[0]);
data.nonce = BigNumber.make(this.#parseNumber(decoded[1].toString()));
data.gasPrice = this.#parseNumber(decoded[3].toString());
data.gasLimit = this.#parseNumber(decoded[4].toString());
data.recipientAddress = this.#parseAddress(decoded[5].toString());
data.value = BigNumber.make(this.#parseNumber(decoded[6].toString()));
data.data = this.#parseData(decoded[7].toString());

const buff: ByteBuffer = this.#getByteBuffer(serialized);
this.deserializeCommon(data, buff);
if (decoded.length === 12) {
data.v = this.#parseNumber(decoded[9].toString()) + 27;
data.r = decoded[10].toString().slice(2);
data.s = decoded[11].toString().slice(2);
}

const instance: Contracts.Crypto.Transaction = this.transactionTypeFactory.create(data);
await this.#deserializeBody(instance.data, buff);

this.#deserializeSignatures(instance.data, buff);

instance.serialized = buff.getResult();
const eip1559Prefix = "02"; // marker for Type 2 (EIP1559) transaction which is the standard nowadays
instance.serialized = Buffer.from(`${eip1559Prefix}${encodedRlp.slice(2)}`, "hex");

return instance;
}

public deserializeCommon(transaction: Contracts.Crypto.TransactionData, buf: ByteBuffer): void {
transaction.network = buf.readUint8();
transaction.nonce = BigNumber.make(buf.readUint64());
transaction.gasPrice = buf.readUint32();
transaction.gasLimit = buf.readUint32();
transaction.value = BigNumber.ZERO;
#parseNumber(value: string): number {
return value === "0x" ? 0 : Number(value);
}

async #deserializeBody(transaction: Contracts.Crypto.TransactionData, buf: ByteBuffer): Promise<void> {
transaction.value = BigNumber.make(buf.readUint256());

const recipientMarker = buf.readUint8();
if (recipientMarker === 1) {
transaction.recipientAddress = await this.addressFactory.fromBuffer(
this.addressSerializer.deserialize(buf),
);
}

const dataLength = buf.readUint32();
const dataBytes = buf.readBytes(dataLength);

transaction.data = dataBytes.toString("hex");
#parseAddress(value: string): string | undefined {
return value === "0x" ? undefined : value;
}

#deserializeSignatures(transaction: Contracts.Crypto.TransactionData, buf: ByteBuffer): void {
if (buf.getRemainderLength() && buf.getRemainderLength() % this.signatureSize === 0) {
transaction.v = buf.readUint8();
transaction.r = buf.readBytes(32).toString("hex");
transaction.s = buf.readBytes(32).toString("hex");
}

// if (buf.getRemainderLength()) {
// if (buf.getRemainderLength() % (this.signatureSize + 1) === 0) {
// transaction.signatures = [];

// const count: number = buf.getRemainderLength() / (this.signatureSize + 1);
// const publicKeyIndexes: { [index: number]: boolean } = {};
// for (let index = 0; index < count; index++) {
// const multiSignaturePart: string = buf.readBytes(this.signatureSize + 1).toString("hex");
// const publicKeyIndex: number = Number.parseInt(multiSignaturePart.slice(0, 2), 16);

// if (!publicKeyIndexes[publicKeyIndex]) {
// publicKeyIndexes[publicKeyIndex] = true;
// } else {
// throw new Exceptions.DuplicateParticipantInMultiSignatureError();
// }

// transaction.signatures.push(multiSignaturePart);
// }
// } else {
// throw new Exceptions.InvalidTransactionBytesError("signature buffer not exhausted");
// }
// }
}

#getByteBuffer(serialized: Buffer | string): ByteBuffer {
if (!(serialized instanceof Buffer)) {
serialized = Buffer.from(serialized, "hex");
}

return ByteBuffer.fromBuffer(serialized);
#parseData(value: string): string {
return value === "0x" ? "" : value;
}
}
146 changes: 21 additions & 125 deletions packages/crypto-transaction/source/serializer.ts
Original file line number Diff line number Diff line change
@@ -1,139 +1,35 @@
import { inject, injectable, tagged } from "@mainsail/container";
import { Contracts, Exceptions, Identifiers } from "@mainsail/contracts";
import { ByteBuffer } from "@mainsail/utils";
import { injectable } from "@mainsail/container";
import { Contracts } from "@mainsail/contracts";
import { encodeRlp, toBeArray } from "ethers";

@injectable()
export class Serializer implements Contracts.Crypto.TransactionSerializer {
@inject(Identifiers.Cryptography.Configuration)
private readonly configuration!: Contracts.Crypto.Configuration;

@inject(Identifiers.Cryptography.Signature.Size)
@tagged("type", "wallet")
private readonly signatureSize!: number;

@inject(Identifiers.Cryptography.Identity.Address.Factory)
protected readonly addressFactory!: Contracts.Crypto.AddressFactory;

@inject(Identifiers.Cryptography.Identity.Address.Serializer)
private readonly addressSerializer!: Contracts.Crypto.AddressSerializer;

@inject(Identifiers.Cryptography.Identity.Address.Size)
private readonly addressSize!: number;

public commonSize(transaction: Contracts.Crypto.Transaction): number {
return (
1 + // network
8 + // nonce
4 + // gasLimit
4 // gasPrice in gwei
);
}

public signaturesSize(
transaction: Contracts.Crypto.Transaction,
options: Contracts.Crypto.SerializeOptions = {},
): number {
let size = 0;

const { data } = transaction;
if (data.v && data.r && data.s && !options.excludeSignature) {
size += this.signatureSize;
}

// if (data.signatures && !options.excludeMultiSignature) {
// size += data.signatures.length * (1 + this.signatureSize) /* 1 additional byte for index */;
// }

return size;
}

public assetSize(transaction: Contracts.Crypto.Transaction): number {
return (
32 + // value
1 + // recipient marker
(transaction.data.recipientAddress ? this.addressSize : 0) + // recipient
4 + // payload length
Buffer.byteLength(transaction.data.data, "hex")
);
}

public totalSize(
transaction: Contracts.Crypto.Transaction,
options: Contracts.Crypto.SerializeOptions = {},
): number {
return this.commonSize(transaction) + this.assetSize(transaction) + this.signaturesSize(transaction, options);
}

public async serialize(
transaction: Contracts.Crypto.Transaction,
options: Contracts.Crypto.SerializeOptions = {},
): Promise<Buffer> {
const bufferSize = this.totalSize(transaction, options);
const buff: ByteBuffer = ByteBuffer.fromSize(bufferSize);

this.#serializeCommon(transaction.data, buff);

const serialized = await this.#serializeBody(transaction.data);
buff.writeBytes(serialized.getResult());

this.#serializeSignatures(transaction.data, buff, options);

const bufferBuffer = buff.getResult();
if (bufferBuffer.length !== bufferSize) {
throw new Exceptions.InvalidTransactionBytesError(
`expected size ${bufferSize} actual size: ${bufferBuffer.length}`,
);
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.gasLimit), // gasLimit - 4
transaction.data.recipientAddress || "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
];

if (transaction.data.v && transaction.data.r && transaction.data.s && !options.excludeSignature) {
fields.push(toBeArray(transaction.data.v - 27), `0x${transaction.data.r}`, `0x${transaction.data.s}`);
}

transaction.serialized = bufferBuffer;

return bufferBuffer;
}

#serializeCommon(transaction: Contracts.Crypto.TransactionData, buff: ByteBuffer): void {
buff.writeUint8(transaction.network || this.configuration.get("network.pubKeyHash"));
buff.writeUint64(transaction.nonce.toBigInt());
buff.writeUint32(transaction.gasPrice);
buff.writeUint32(transaction.gasLimit);
}

async #serializeBody(transaction: Contracts.Crypto.TransactionData): Promise<ByteBuffer> {
const dataBytes = Buffer.from(transaction.data, "hex");
const rlpEncoded = encodeRlp(fields);

const recipient = transaction.recipientAddress;
const eip1559Prefix = "02"; // marker for Type 2 (EIP1559) transaction which is the standard nowadays

const buff: ByteBuffer = ByteBuffer.fromSize(
32 + 1 + (recipient ? this.addressSize : 0) + 4 + dataBytes.byteLength,
);

buff.writeUint256(transaction.value.toBigInt());

if (recipient) {
buff.writeUint8(1);
this.addressSerializer.serialize(buff, await this.addressFactory.toBuffer(recipient));
} else {
buff.writeUint8(0);
}

buff.writeUint32(dataBytes.byteLength);
buff.writeBytes(dataBytes);

return buff;
}

#serializeSignatures(
transaction: Contracts.Crypto.TransactionData,
buff: ByteBuffer,
options: Contracts.Crypto.SerializeOptions = {},
): void {
if (transaction.v && transaction.r && transaction.s && !options.excludeSignature) {
buff.writeUint8(transaction.v);
buff.writeBytes(Buffer.from(transaction.r, "hex"));
buff.writeBytes(Buffer.from(transaction.s, "hex"));
}
transaction.serialized = Buffer.from(`${eip1559Prefix}${rlpEncoded.slice(2)}`, "hex");

// if (transaction.signatures && !options.excludeMultiSignature) {
// buff.writeBytes(Buffer.from(transaction.signatures.join(""), "hex"));
// }
return transaction.serialized;
}
}
4 changes: 2 additions & 2 deletions packages/crypto-transaction/source/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class Utils implements Contracts.Crypto.TransactionUtils {
const fields = [
toBeArray(transaction.network),
toBeArray(transaction.nonce.toBigInt()),
toBeArray(transaction.gasPrice), // maxPriorityFeePerGas
toBeArray(0), // maxPriorityFeePerGas
toBeArray(transaction.gasPrice), // maxFeePerGas
toBeArray(transaction.gasLimit),
transaction.recipientAddress || "0x",
Expand All @@ -40,7 +40,7 @@ export class Utils implements Contracts.Crypto.TransactionUtils {
AppUtils.assert.defined<string>(transaction.r);
AppUtils.assert.defined<string>(transaction.s);

fields.push(toBeArray(transaction.v), `0x${transaction.r}`, `0x${transaction.s}`);
fields.push(toBeArray(transaction.v - 27), `0x${transaction.r}`, `0x${transaction.s}`);
}

const eip1559Prefix = "02"; // marker for Type 2 (EIP1559) transaction which is the standard nowadays
Expand Down

0 comments on commit 0d9f79d

Please sign in to comment.