diff --git a/packages/contracts/source/contracts/crypto/identities.ts b/packages/contracts/source/contracts/crypto/identities.ts index e624b49beb..9a0da4897b 100644 --- a/packages/contracts/source/contracts/crypto/identities.ts +++ b/packages/contracts/source/contracts/crypto/identities.ts @@ -69,4 +69,5 @@ export interface SignatureEcdsa { signRecoverable(message: Buffer, privateKey: Buffer): Promise; verifyRecoverable(signature: EcdsaSignature, message: Buffer, publicKey: Buffer): Promise; recoverPublicKey(message: Buffer, signature: EcdsaSignature): string; + isLowS(signature: EcdsaSignature): boolean; } diff --git a/packages/contracts/source/contracts/evm/evm.ts b/packages/contracts/source/contracts/evm/evm.ts index 4604d67869..5979b1845b 100644 --- a/packages/contracts/source/contracts/evm/evm.ts +++ b/packages/contracts/source/contracts/evm/evm.ts @@ -101,7 +101,6 @@ export interface PreverifyTransactionContext { readonly nonce: bigint; readonly data: Buffer; readonly txHash: string; - readonly index?: number; readonly specId: SpecId; readonly blockGasLimit: bigint; } diff --git a/packages/contracts/source/contracts/transactions.ts b/packages/contracts/source/contracts/transactions.ts index b4c2dd3d42..db4b2e7d0d 100644 --- a/packages/contracts/source/contracts/transactions.ts +++ b/packages/contracts/source/contracts/transactions.ts @@ -1,9 +1,7 @@ -import type { Transaction, TransactionData } from "./crypto/index.js"; +import type { Transaction } from "./crypto/index.js"; import type { BlockContext, Instance, TransactionReceipt } from "./evm/index.js"; import type { Wallet } from "./state/index.js"; -export type TransactionHandlerConstructor = new () => TransactionHandler; - export type TransactionHandlerContext = { evm: { instance: Instance; @@ -12,27 +10,7 @@ export type TransactionHandlerContext = { }; export interface TransactionHandler { - verify(transaction: Transaction): Promise; - throwIfCannotBeApplied(transaction: Transaction, sender: Wallet, evm: Instance): Promise; apply(context: TransactionHandlerContext, transaction: Transaction): Promise; } - -export interface TransactionHandlerRegistry { - getRegisteredHandlers(): TransactionHandler[]; - - getRegisteredHandlerByType(type: number, version?: number): TransactionHandler; - - getActivatedHandlers(): Promise; - - getActivatedHandlerByType(type: number, version?: number): Promise; - - getActivatedHandlerForData(transactionData: TransactionData): Promise; -} - -export interface TransactionHandlerProvider { - isRegistrationRequired(): boolean; - - registerHandlers(): void; -} diff --git a/packages/crypto-signature-ecdsa/source/signature.ts b/packages/crypto-signature-ecdsa/source/signature.ts index 55531db0eb..1db086cae7 100644 --- a/packages/crypto-signature-ecdsa/source/signature.ts +++ b/packages/crypto-signature-ecdsa/source/signature.ts @@ -20,12 +20,15 @@ export class Signature implements Contracts.Crypto.SignatureEcdsa { message: Buffer, publicKey: Buffer, ): Promise { - const signatureRS = Buffer.from(signature.r + signature.s, "hex"); - if (!secp256k1.isLowS(signatureRS)) { + if (!this.isLowS(signature)) { return false; } - return secp256k1.verify(message, signatureRS, publicKey); + return secp256k1.verify(message, Buffer.from(signature.r + signature.s, "hex"), publicKey); + } + + public isLowS(signature: Contracts.Crypto.EcdsaSignature): boolean { + return secp256k1.isLowS(Buffer.from(signature.r + signature.s, "hex")); } public recoverPublicKey(message: Buffer, signature: Contracts.Crypto.EcdsaSignature): string { diff --git a/packages/crypto-transaction/source/factory.ts b/packages/crypto-transaction/source/factory.ts index 43b805313e..7bac5f8e51 100644 --- a/packages/crypto-transaction/source/factory.ts +++ b/packages/crypto-transaction/source/factory.ts @@ -21,7 +21,7 @@ export class TransactionFactory implements Contracts.Crypto.TransactionFactory { @inject(Identifiers.Cryptography.Signature.Instance) @tagged("type", "wallet") - private readonly signatureSerializer!: Contracts.Crypto.SignatureEcdsa; + private readonly signatureEcdsa!: Contracts.Crypto.SignatureEcdsa; @inject(Identifiers.Cryptography.Transaction.Deserializer) private readonly deserializer!: Contracts.Crypto.TransactionDeserializer; @@ -101,10 +101,14 @@ export class TransactionFactory implements Contracts.Crypto.TransactionFactory { assert.string(data.r); assert.string(data.s); + if (!this.signatureEcdsa.isLowS({ r: data.r, s: data.s, v: data.v })) { + throw new InvalidTransactionBytesError("non-canonical signature (high S value)"); + } + const unsignedHash = await this.hashFactory.toHashUnsigned(data); const hash = await this.hashFactory.toHash(data); - const senderPublicKey = this.signatureSerializer.recoverPublicKey(unsignedHash, { + const senderPublicKey = this.signatureEcdsa.recoverPublicKey(unsignedHash, { r: data.r, s: data.s, v: data.v, diff --git a/packages/evm/bindings/src/ctx.rs b/packages/evm/bindings/src/ctx.rs index d14224ef64..4a0895d06a 100644 --- a/packages/evm/bindings/src/ctx.rs +++ b/packages/evm/bindings/src/ctx.rs @@ -29,7 +29,6 @@ pub struct JsTransactionContext { pub nonce: BigInt, pub data: Buffer, pub tx_hash: String, - pub index: Option, pub block_context: JsBlockContext, pub spec_id: String, } diff --git a/packages/exceptions/source/crypto.ts b/packages/exceptions/source/crypto.ts index ec4c99517e..6e1a7e6610 100644 --- a/packages/exceptions/source/crypto.ts +++ b/packages/exceptions/source/crypto.ts @@ -168,6 +168,12 @@ export class EvmCallMissingRecipientError extends Exception { } } +export class EvmCallFailedError extends Exception { + public constructor(transaction: Contracts.Crypto.Transaction, error: Error) { + super(`invalid EVM call for tx ${transaction.hash}: ${error.message}`); + } +} + export class InvalidMilestoneConfigurationError extends Exception { public constructor(message: string) { super(message); @@ -334,12 +340,6 @@ export class NotEnoughValidatorsError extends Exception { } } -export class InvalidSignatureError extends Exception { - public constructor() { - super(`Failed to apply transaction, because signature is invalid.`); - } -} - export class MultiPaymentAmountMismatchError extends Exception { public constructor() { super(`Payment amounts mismatch.`); diff --git a/packages/exceptions/source/pool.ts b/packages/exceptions/source/pool.ts index 720e4c616f..c110acfb43 100644 --- a/packages/exceptions/source/pool.ts +++ b/packages/exceptions/source/pool.ts @@ -66,11 +66,8 @@ export class TransactionPoolFullError extends PoolError { } export class TransactionFailedToPreverifyError extends PoolError { - public readonly error: Error; - - public constructor(transaction: Contracts.Crypto.Transaction, error: Error) { - super(`tx ${transaction.hash} cannot be preverified: ${error.message}`, "ERR_PREVERIFY"); - this.error = error; + public constructor(transaction: Contracts.Crypto.Transaction, reason: string) { + super(`tx ${transaction.hash} cannot be preverified: ${reason}`, "ERR_PREVERIFY"); } } @@ -83,12 +80,6 @@ export class TransactionFailedToApplyError extends PoolError { } } -export class TransactionFailedToVerifyError extends PoolError { - public constructor(transaction: Contracts.Crypto.Transaction) { - super(`tx ${transaction.hash} didn't pass verification`, "ERR_BAD_DATA"); - } -} - export class TransactionFromWrongNetworkError extends PoolError { public currentNetwork: number; diff --git a/packages/processor/source/transaction-processor.ts b/packages/processor/source/transaction-processor.ts index 83d66368a4..93934e8b7b 100644 --- a/packages/processor/source/transaction-processor.ts +++ b/packages/processor/source/transaction-processor.ts @@ -3,7 +3,6 @@ import type { Contracts } from "@mainsail/contracts"; import { formatCurrency } from "@mainsail/blockchain-utils"; import { Identifiers } from "@mainsail/constants"; import { inject, injectable, tagged } from "@mainsail/container"; -import { InvalidSignatureError } from "@mainsail/exceptions"; @injectable() export class TransactionProcessor implements Contracts.Processor.TransactionProcessor { @@ -52,10 +51,6 @@ export class TransactionProcessor implements Contracts.Processor.TransactionProc }, }; - if (!(await this.transactionHandler.verify(transaction))) { - throw new InvalidSignatureError(); - } - const receipt = await this.transactionHandler.apply(transactionHandlerContext, transaction); const feeConsumed = this.feeCalculator.calculateConsumed(transaction.gasPrice, receipt.gasUsed); diff --git a/packages/transaction-pool-service/source/actions/index.ts b/packages/transaction-pool-service/source/actions/index.ts deleted file mode 100644 index 438af59fb6..0000000000 --- a/packages/transaction-pool-service/source/actions/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./throw-if-cannot-be-applied.js"; -export * from "./verify-transaction.js"; diff --git a/packages/transaction-pool-service/source/actions/throw-if-cannot-be-applied.ts b/packages/transaction-pool-service/source/actions/throw-if-cannot-be-applied.ts deleted file mode 100644 index 8945cffbb0..0000000000 --- a/packages/transaction-pool-service/source/actions/throw-if-cannot-be-applied.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Contracts } from "@mainsail/contracts"; - -import { injectable } from "@mainsail/container"; -import { Services } from "@mainsail/kernel"; - -@injectable() -export class ThrowIfCannotBeAppliedAction extends Services.Triggers.Action { - public async execute( - arguments_: Contracts.Kernel.ActionArguments<{ - handler: Contracts.Transactions.TransactionHandler; - transaction: Contracts.Crypto.Transaction; - sender: Contracts.State.Wallet; - evm: Contracts.Evm.Instance; - }>, - ): Promise { - const handler = arguments_.handler; - const transaction = arguments_.transaction; - const sender = arguments_.sender; - const evm = arguments_.evm; - - await handler.throwIfCannotBeApplied(transaction, sender, evm); - } -} diff --git a/packages/transaction-pool-service/source/actions/verify-transaction.ts b/packages/transaction-pool-service/source/actions/verify-transaction.ts deleted file mode 100644 index f01731bc79..0000000000 --- a/packages/transaction-pool-service/source/actions/verify-transaction.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Contracts } from "@mainsail/contracts"; - -import { injectable } from "@mainsail/container"; -import { Services } from "@mainsail/kernel"; - -@injectable() -export class VerifyTransactionAction extends Services.Triggers.Action { - public async execute( - arguments_: Contracts.Kernel.ActionArguments<{ - handler: Contracts.Transactions.TransactionHandler; - transaction: Contracts.Crypto.Transaction; - }>, - ): Promise { - const handler = arguments_.handler; - const transaction = arguments_.transaction; - - return handler.verify(transaction); - } -} diff --git a/packages/transaction-pool-service/source/errors.test.ts b/packages/transaction-pool-service/source/errors.test.ts index 8d51df5597..fb7d5cb640 100644 --- a/packages/transaction-pool-service/source/errors.test.ts +++ b/packages/transaction-pool-service/source/errors.test.ts @@ -74,14 +74,6 @@ describe<{ assert.equal(error.message, `tx ${context.transaction.hash} cannot be applied: Something went horribly wrong`); }); - it("TransactionFailedToVerifyError", (context) => { - const error = new Exceptions.TransactionFailedToVerifyError(context.transaction); - - assert.instance(error, Exceptions.PoolError); - assert.equal(error.type, "ERR_BAD_DATA"); - assert.equal(error.message, `tx ${context.transaction.hash} didn't pass verification`); - }); - it("TransactionFromWrongNetworkError", (context) => { const error = new Exceptions.TransactionFromWrongNetworkError(context.transaction, 23); diff --git a/packages/transaction-pool-service/source/index.ts b/packages/transaction-pool-service/source/index.ts index 693659e4fa..32838a9091 100644 --- a/packages/transaction-pool-service/source/index.ts +++ b/packages/transaction-pool-service/source/index.ts @@ -1,4 +1,3 @@ -export * from "./actions/index.js"; export * from "./mempool.js"; export * from "./processor.js"; export * from "./query.js"; diff --git a/packages/transaction-pool-service/source/processor.test.ts b/packages/transaction-pool-service/source/processor.test.ts index 8c88c3041b..3400b1b2a7 100644 --- a/packages/transaction-pool-service/source/processor.test.ts +++ b/packages/transaction-pool-service/source/processor.test.ts @@ -1,9 +1,7 @@ import { Container } from "@mainsail/container"; import { Identifiers } from "@mainsail/constants"; import * as Exceptions from "@mainsail/exceptions"; -import { Configuration } from "@mainsail/crypto-config"; -import crypto from "../../core/bin/config/devnet/core/crypto.json"; import { describe } from "@mainsail/test-runner"; import { Processor } from "./processor"; @@ -15,7 +13,6 @@ describe<{ transaction1: any; transaction2: any; factory: any; - blockSerializer: any; }>("Processor", ({ it, assert, beforeAll, stub, spy }) => { beforeAll((context) => { context.pool = { @@ -24,32 +21,21 @@ describe<{ context.extensions = [{ throwIfCannotBroadcast: () => {} }, { throwIfCannotBroadcast: () => {} }]; - context.broadcaster = { - broadcastTransactions: () => Promise.resolve(), - }; - context.factory = { fromBytes: (bytes) => ({}), fromJson: (tx) => tx, }; - context.blockSerializer = { - headerSize: () => 152, - }; context.broadcaster = { broadcastTransactions: async () => {}, }; context.container = new Container(); - context.container.bind(Identifiers.Cryptography.Block.Serializer).toConstantValue(context.blockSerializer); - context.container.bind(Identifiers.Cryptography.Configuration).to(Configuration).inSingletonScope(); - context.container.get(Identifiers.Cryptography.Configuration).setConfig(crypto); context.container.bind(Identifiers.TransactionPool.ProcessorExtension).toConstantValue(context.extensions[0]); context.container.bind(Identifiers.TransactionPool.ProcessorExtension).toConstantValue(context.extensions[1]); context.container.bind(Identifiers.TransactionPool.Service).toConstantValue(context.pool); context.container.bind(Identifiers.Cryptography.Transaction.Factory).toConstantValue(context.factory); - context.container.bind(Identifiers.Cryptography.Transaction.Deserializer).toConstantValue({}); context.container.bind(Identifiers.TransactionPool.Broadcaster).toConstantValue(context.broadcaster); context.container.bind(Identifiers.Services.Log.Service).toConstantValue({ error: () => {}, diff --git a/packages/transaction-pool-service/source/sender-state.ts b/packages/transaction-pool-service/source/sender-state.ts index 0cc18aa879..60953d3e35 100644 --- a/packages/transaction-pool-service/source/sender-state.ts +++ b/packages/transaction-pool-service/source/sender-state.ts @@ -6,11 +6,9 @@ import { InsufficientBalanceError, TransactionExceedsMaximumByteSizeError, TransactionFailedToApplyError, - TransactionFailedToVerifyError, TransactionFromWrongNetworkError, UnexpectedNonceError, } from "@mainsail/exceptions"; -import { Services } from "@mainsail/kernel"; import { Wallets } from "@mainsail/state"; @injectable() @@ -32,9 +30,6 @@ export class SenderState implements Contracts.TransactionPool.SenderState { @inject(Identifiers.Transaction.Handler) private readonly transactionHandler!: Contracts.Transactions.TransactionHandler; - @inject(Identifiers.Services.Trigger.Service) - private readonly triggers!: Services.Triggers.Triggers; - @inject(Identifiers.BlockchainUtils.FeeCalculator) private readonly feeCalculator!: Contracts.BlockchainUtils.FeeCalculator; @@ -118,24 +113,10 @@ export class SenderState implements Contracts.TransactionPool.SenderState { throw new InsufficientBalanceError(); } - if ( - await this.triggers.call("verifyTransaction", { - handler: this.transactionHandler, - transaction, - }) - ) { - try { - await this.triggers.call("throwIfCannotBeApplied", { - evm: this.evm, - handler: this.transactionHandler, - sender: this.#wallet, - transaction, - }); - } catch (error) { - throw new TransactionFailedToApplyError(transaction, error); - } - } else { - throw new TransactionFailedToVerifyError(transaction); + try { + await this.transactionHandler.throwIfCannotBeApplied(transaction, this.#wallet, this.evm); + } catch (error) { + throw new TransactionFailedToApplyError(transaction, error); } } } diff --git a/packages/transaction-pool-service/source/service-provider.ts b/packages/transaction-pool-service/source/service-provider.ts index d964207046..919bb2e890 100644 --- a/packages/transaction-pool-service/source/service-provider.ts +++ b/packages/transaction-pool-service/source/service-provider.ts @@ -2,10 +2,9 @@ import type { Contracts } from "@mainsail/contracts"; import { Identifiers } from "@mainsail/constants"; import { injectable } from "@mainsail/container"; -import { Providers, Services } from "@mainsail/kernel"; +import { Providers } from "@mainsail/kernel"; import Joi from "joi"; -import { ThrowIfCannotBeAppliedAction, VerifyTransactionAction } from "./actions/index.js"; import { Mempool } from "./mempool.js"; import { Processor } from "./processor.js"; import { Query } from "./query.js"; @@ -19,7 +18,6 @@ import { Storage } from "./storage.js"; export class ServiceProvider extends Providers.ServiceProvider { public async register(): Promise { this.#registerServices(); - this.#registerActions(); } public async boot(): Promise { @@ -67,14 +65,4 @@ export class ServiceProvider extends Providers.ServiceProvider { this.app.bind(Identifiers.TransactionPool.Storage).to(Storage).inSingletonScope(); this.app.bind(Identifiers.TransactionPool.Selector).to(Selector).inSingletonScope(); } - - #registerActions(): void { - this.app - .get(Identifiers.Services.Trigger.Service) - .bind("verifyTransaction", this.app.resolve(VerifyTransactionAction)); - - this.app - .get(Identifiers.Services.Trigger.Service) - .bind("throwIfCannotBeApplied", this.app.resolve(ThrowIfCannotBeAppliedAction)); - } } diff --git a/packages/transactions/package.json b/packages/transactions/package.json index 26b23df765..b70a257323 100644 --- a/packages/transactions/package.json +++ b/packages/transactions/package.json @@ -24,8 +24,7 @@ "@mainsail/constants": "workspace:*", "@mainsail/container": "workspace:*", "@mainsail/exceptions": "workspace:*", - "@mainsail/kernel": "workspace:*", - "@mainsail/utils": "workspace:*" + "@mainsail/kernel": "workspace:*" }, "devDependencies": { "@mainsail/contracts": "workspace:*", diff --git a/packages/transactions/source/handlers/index.ts b/packages/transactions/source/handlers/index.ts deleted file mode 100644 index 53523708cd..0000000000 --- a/packages/transactions/source/handlers/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type { TransactionHandlerConstructor } from "./transaction.js"; -export { TransactionHandler } from "./transaction.js"; diff --git a/packages/transactions/source/handlers/transaction.ts b/packages/transactions/source/handlers/transaction.ts deleted file mode 100644 index 57e880e276..0000000000 --- a/packages/transactions/source/handlers/transaction.ts +++ /dev/null @@ -1,118 +0,0 @@ -import type { Contracts } from "@mainsail/contracts"; - -import { Events, Identifiers } from "@mainsail/constants"; -import { inject, injectable } from "@mainsail/container"; -import { TransactionFailedToPreverifyError, UnexpectedLegacySecondSignatureError } from "@mainsail/exceptions"; -import { assert } from "@mainsail/utils"; - -@injectable() -export class TransactionHandler implements Contracts.Transactions.TransactionHandler { - @inject(Identifiers.Application.Instance) - protected readonly app!: Contracts.Kernel.Application; - - @inject(Identifiers.Services.Log.Service) - protected readonly logger!: Contracts.Kernel.Logger; - - @inject(Identifiers.Cryptography.Configuration) - protected readonly configuration!: Contracts.Crypto.Configuration; - - @inject(Identifiers.Cryptography.Transaction.Verifier) - protected readonly verifier!: Contracts.Crypto.TransactionVerifier; - - @inject(Identifiers.BlockchainUtils.FeeCalculator) - protected readonly feeCalculator!: Contracts.BlockchainUtils.FeeCalculator; - - @inject(Identifiers.Services.EventDispatcher.Service) - private readonly events!: Contracts.Kernel.EventDispatcher; - - @inject(Identifiers.State.State) - private readonly state!: Contracts.State.State; - - public async verify(transaction: Contracts.Crypto.Transaction): Promise { - assert.string(transaction.from); - return this.verifier.verifyHash(transaction); - } - - public async throwIfCannotBeApplied( - transaction: Contracts.Crypto.Transaction, - sender: Contracts.State.Wallet, - evm: Contracts.Evm.Instance, - ): Promise { - // Legacy - if (sender.hasLegacySecondPublicKey()) { - await this.verifier.verifyLegacySecondSignature(transaction, sender.legacySecondPublicKey()); - } else { - if (transaction.legacySecondSignature) { - throw new UnexpectedLegacySecondSignatureError(); - } - } - - const milestone = this.configuration.getMilestone(); - - const preverified = await evm.preverifyTransaction({ - blockGasLimit: BigInt(milestone.block.maxGasLimit), - data: Buffer.from(transaction.data.slice(2), "hex"), - from: transaction.from, - gasLimit: BigInt(transaction.gasLimit), - gasPrice: BigInt(transaction.gasPrice), - legacyAddress: transaction.senderLegacyAddress, - nonce: transaction.nonce, - specId: milestone.evmSpec, - to: transaction.to, - txHash: transaction.hash, - value: transaction.value, - }); - - if (!preverified.success) { - throw new TransactionFailedToPreverifyError(transaction, new Error(preverified.error)); - } - } - - public async apply( - context: Contracts.Transactions.TransactionHandlerContext, - transaction: Contracts.Crypto.Transaction, - ): Promise { - assert.string(transaction.hash); - - const { evmSpec } = this.configuration.getMilestone(); - - try { - const { blockContext, instance } = context.evm; - const data = { - blockContext, - data: Buffer.from(transaction.data.slice(2), "hex"), - from: transaction.from, - gasLimit: BigInt(transaction.gasLimit), - gasPrice: BigInt(transaction.gasPrice), - legacyAddress: transaction.senderLegacyAddress, - nonce: transaction.nonce, - specId: evmSpec, - to: transaction.to, - txHash: transaction.hash, - value: transaction.value, - }; - - const { receipt } = await instance.process(data); - - void this.#emit(Events.EvmEvent.TransactionReceipt, { - receipt, - sender: transaction.from, - transactionId: transaction.hash, - }); - - return receipt; - } catch (error) { - throw new Error(`invalid EVM call: ${error.message}`); - } - } - - async #emit(event: string, data?: T): Promise { - if (this.state.isBootstrap()) { - return; - } - - return this.events.dispatch(event, data); - } -} - -export type TransactionHandlerConstructor = new () => TransactionHandler; diff --git a/packages/transactions/source/index.ts b/packages/transactions/source/index.ts index 3e28948d63..793704e5dc 100644 --- a/packages/transactions/source/index.ts +++ b/packages/transactions/source/index.ts @@ -1,2 +1 @@ -export * as Handlers from "./handlers/index.js"; export * from "./service-provider.js"; diff --git a/packages/transactions/source/service-provider.ts b/packages/transactions/source/service-provider.ts index 874c45f107..3781e09f63 100644 --- a/packages/transactions/source/service-provider.ts +++ b/packages/transactions/source/service-provider.ts @@ -2,7 +2,7 @@ import { Identifiers } from "@mainsail/constants"; import { injectable } from "@mainsail/container"; import { Providers } from "@mainsail/kernel"; -import { TransactionHandler } from "./handlers/index.js"; +import { TransactionHandler } from "./transaction.js"; @injectable() export class ServiceProvider extends Providers.ServiceProvider { diff --git a/packages/transactions/source/transaction.ts b/packages/transactions/source/transaction.ts new file mode 100644 index 0000000000..45ccbb8052 --- /dev/null +++ b/packages/transactions/source/transaction.ts @@ -0,0 +1,84 @@ +import type { Contracts } from "@mainsail/contracts"; + +import { Identifiers } from "@mainsail/constants"; +import { inject, injectable } from "@mainsail/container"; +import { + EvmCallFailedError, + TransactionFailedToPreverifyError, + UnexpectedLegacySecondSignatureError, +} from "@mainsail/exceptions"; + +@injectable() +export class TransactionHandler implements Contracts.Transactions.TransactionHandler { + @inject(Identifiers.Cryptography.Configuration) + private readonly configuration!: Contracts.Crypto.Configuration; + + @inject(Identifiers.Cryptography.Transaction.Verifier) + private readonly verifier!: Contracts.Crypto.TransactionVerifier; + + public async throwIfCannotBeApplied( + transaction: Contracts.Crypto.Transaction, + sender: Contracts.State.Wallet, + evm: Contracts.Evm.Instance, + ): Promise { + await this.#verifyLegacySecondSignature(transaction, sender); + await this.#preverifyEvm(transaction, evm); + } + + async #verifyLegacySecondSignature( + transaction: Contracts.Crypto.Transaction, + sender: Contracts.State.Wallet, + ): Promise { + if (sender.hasLegacySecondPublicKey()) { + await this.verifier.verifyLegacySecondSignature(transaction, sender.legacySecondPublicKey()); + } else if (transaction.legacySecondSignature) { + throw new UnexpectedLegacySecondSignatureError(); + } + } + + async #preverifyEvm(transaction: Contracts.Crypto.Transaction, evm: Contracts.Evm.Instance): Promise { + const milestone = this.configuration.getMilestone(); + + const preverified = await evm.preverifyTransaction({ + ...this.#toEvmTransactionFields(transaction, milestone.evmSpec), + blockGasLimit: BigInt(milestone.block.maxGasLimit), + }); + + if (!preverified.success) { + throw new TransactionFailedToPreverifyError(transaction, preverified.error ?? "unknown"); + } + } + + public async apply( + context: Contracts.Transactions.TransactionHandlerContext, + transaction: Contracts.Crypto.Transaction, + ): Promise { + const { evmSpec } = this.configuration.getMilestone(); + const { blockContext, instance } = context.evm; + + try { + const { receipt } = await instance.process({ + ...this.#toEvmTransactionFields(transaction, evmSpec), + blockContext, + }); + return receipt; + } catch (error) { + throw new EvmCallFailedError(transaction, error); + } + } + + #toEvmTransactionFields(transaction: Contracts.Crypto.Transaction, specId: Contracts.Evm.SpecId) { + return { + data: Buffer.from(transaction.data.slice(2), "hex"), + from: transaction.from, + gasLimit: BigInt(transaction.gasLimit), + gasPrice: BigInt(transaction.gasPrice), + legacyAddress: transaction.senderLegacyAddress, + nonce: transaction.nonce, + specId, + to: transaction.to, + txHash: transaction.hash, + value: transaction.value, + }; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f9ba0be94..d5a7485d41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3195,9 +3195,6 @@ importers: '@mainsail/kernel': specifier: workspace:* version: link:../kernel - '@mainsail/utils': - specifier: workspace:* - version: link:../utils devDependencies: '@mainsail/contracts': specifier: workspace:* @@ -6296,6 +6293,7 @@ packages: basic-ftp@5.0.5: resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} engines: {node: '>=10.0.0'} + deprecated: Security vulnerability fixed in 5.2.1, please upgrade bcrypto@5.5.2: resolution: {integrity: sha512-k3PF755oJM0+25iOVuraNedF5XneykxRwl+oBoMeQPfYee4qX8hHQhKCsNZWLthNYgi41GH2ysopd/8sDQDhEw==} @@ -10831,6 +10829,7 @@ packages: uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uvu@0.5.6: