From 7fb4fcc511c58ed35147b452859b8ce5ed93bad6 Mon Sep 17 00:00:00 2001 From: Yair Bakalchuk Date: Tue, 16 Jul 2024 15:34:55 +0300 Subject: [PATCH] feat(gateway): create error type that matches the spec --- Cargo.lock | 16 + Cargo.toml | 3 +- crates/gateway/Cargo.toml | 2 + .../gateway/resources/starknet_write_api.json | 291 ++++++++++++++++++ crates/gateway/src/errors.rs | 54 ++++ crates/gateway/src/gateway_test.rs | 36 +++ 6 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 crates/gateway/resources/starknet_write_api.json diff --git a/Cargo.lock b/Cargo.lock index 0f175579d..b84512c6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1855,6 +1855,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "enum-assoc" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24247b89d37b9502dc5a4b80d369aab1a12106067776e440094c786dae5b9d07" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -5451,6 +5462,7 @@ dependencies = [ "blockifier 0.8.0-rc.0 (git+https://github.com/starkware-libs/blockifier.git?rev=32191d41)", "cairo-lang-starknet-classes", "cairo-vm", + "enum-assoc", "hyper", "mempool_test_utils", "mockall", @@ -5470,6 +5482,7 @@ dependencies = [ "starknet_mempool_infra", "starknet_mempool_types", "starknet_sierra_compile", + "strum 0.24.1", "thiserror", "tokio", "tracing", @@ -5638,6 +5651,9 @@ name = "strum" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros 0.24.3", +] [[package]] name = "strum" diff --git a/Cargo.toml b/Cargo.toml index c7530e3b4..a354c9b38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ clap = "4.3.10" colored = "2.1.0" const_format = "0.2.30" derive_more = "0.99" +enum-assoc = "1.1.0" futures = "0.3.30" hyper = { version = "0.14", features = ["client", "http1", "http2", "server", "tcp"] } indexmap = "2.1.0" @@ -70,7 +71,7 @@ starknet_api = "0.13.0-dev.9" # starknet-api version. This should be removed once we have a mono-repo. starknet-types-core = { version = "0.1.5", features = ["hash", "prime-bigint", "std"] } starknet_client = { git = "https://github.com/starkware-libs/papyrus.git", rev = "ca83fd42" } -strum = "0.24.1" +strum = { version = "0.24.1", features = ["derive"] } tempfile = "3.3.0" thiserror = "1.0" tokio = { version = "1.37.0", features = ["full"] } diff --git a/crates/gateway/Cargo.toml b/crates/gateway/Cargo.toml index 0d93cbfdb..07e22a745 100644 --- a/crates/gateway/Cargo.toml +++ b/crates/gateway/Cargo.toml @@ -17,6 +17,7 @@ axum.workspace = true blockifier= { workspace = true, features = ["testing"] } cairo-lang-starknet-classes.workspace = true cairo-vm.workspace = true +enum-assoc.workspace = true hyper.workspace = true num-traits.workspace = true papyrus_config.workspace = true @@ -29,6 +30,7 @@ starknet_mempool_infra = { path = "../mempool_infra", version = "0.0" } starknet_mempool_types = { path = "../mempool_types", version = "0.0" } starknet_sierra_compile = { path = "../starknet_sierra_compile", version = "0.0" } starknet-types-core.workspace = true +strum.workspace = true mempool_test_utils = { path = "../mempool_test_utils", version = "0.0"} thiserror.workspace = true tokio.workspace = true diff --git a/crates/gateway/resources/starknet_write_api.json b/crates/gateway/resources/starknet_write_api.json new file mode 100644 index 000000000..ae23029a5 --- /dev/null +++ b/crates/gateway/resources/starknet_write_api.json @@ -0,0 +1,291 @@ +{ + "openrpc": "1.0.0-rc1", + "info": { + "version": "0.7.1", + "title": "StarkNet Node Write API", + "license": {} + }, + "servers": [], + "methods": [ + { + "name": "starknet_addInvokeTransaction", + "summary": "Submit a new transaction to be added to the chain", + "params": [ + { + "name": "invoke_transaction", + "description": "The information needed to invoke the function (or account, for version 1 transactions)", + "required": true, + "schema": { + "$ref": "#/components/schemas/BROADCASTED_INVOKE_TXN" + } + } + ], + "result": { + "name": "result", + "description": "The result of the transaction submission", + "schema": { + "type": "object", + "properties": { + "transaction_hash": { + "title": "The hash of the invoke transaction", + "$ref": "#/components/schemas/TXN_HASH" + } + }, + "required": ["transaction_hash"] + } + }, + "errors": [ + { + "$ref": "#/components/errors/INSUFFICIENT_ACCOUNT_BALANCE" + }, + { + "$ref": "#/components/errors/INSUFFICIENT_MAX_FEE" + }, + { + "$ref": "#/components/errors/INVALID_TRANSACTION_NONCE" + }, + { + "$ref": "#/components/errors/VALIDATION_FAILURE" + }, + { + "$ref": "#/components/errors/NON_ACCOUNT" + }, + { + "$ref": "#/components/errors/DUPLICATE_TX" + }, + { + "$ref": "#/components/errors/UNSUPPORTED_TX_VERSION" + }, + { + "$ref": "#/components/errors/UNEXPECTED_ERROR" + } + ] + }, + { + "name": "starknet_addDeclareTransaction", + "summary": "Submit a new class declaration transaction", + "params": [ + { + "name": "declare_transaction", + "description": "Declare transaction required to declare a new class on Starknet", + "required": true, + "schema": { + "title": "Declare transaction", + "$ref": "#/components/schemas/BROADCASTED_DECLARE_TXN" + } + } + ], + "result": { + "name": "result", + "description": "The result of the transaction submission", + "schema": { + "type": "object", + "properties": { + "transaction_hash": { + "title": "The hash of the declare transaction", + "$ref": "#/components/schemas/TXN_HASH" + }, + "class_hash": { + "title": "The hash of the declared class", + "$ref": "#/components/schemas/FELT" + } + }, + "required": ["transaction_hash", "class_hash"] + } + }, + "errors": [ + { + "$ref": "#/components/errors/CLASS_ALREADY_DECLARED" + }, + { + "$ref": "#/components/errors/COMPILATION_FAILED" + }, + { + "$ref": "#/components/errors/COMPILED_CLASS_HASH_MISMATCH" + }, + { + "$ref": "#/components/errors/INSUFFICIENT_ACCOUNT_BALANCE" + }, + { + "$ref": "#/components/errors/INSUFFICIENT_MAX_FEE" + }, + { + "$ref": "#/components/errors/INVALID_TRANSACTION_NONCE" + }, + { + "$ref": "#/components/errors/VALIDATION_FAILURE" + }, + { + "$ref": "#/components/errors/NON_ACCOUNT" + }, + { + "$ref": "#/components/errors/DUPLICATE_TX" + }, + { + "$ref": "#/components/errors/CONTRACT_CLASS_SIZE_IS_TOO_LARGE" + }, + { + "$ref": "#/components/errors/UNSUPPORTED_TX_VERSION" + }, + { + "$ref": "#/components/errors/UNSUPPORTED_CONTRACT_CLASS_VERSION" + }, + { + "$ref": "#/components/errors/UNEXPECTED_ERROR" + } + ] + }, + { + "name": "starknet_addDeployAccountTransaction", + "summary": "Submit a new deploy account transaction", + "params": [ + { + "name": "deploy_account_transaction", + "description": "The deploy account transaction", + "required": true, + "schema": { + "$ref": "#/components/schemas/BROADCASTED_DEPLOY_ACCOUNT_TXN" + } + } + ], + "result": { + "name": "result", + "description": "The result of the transaction submission", + "schema": { + "type": "object", + "properties": { + "transaction_hash": { + "title": "The hash of the deploy transaction", + "$ref": "#/components/schemas/TXN_HASH" + }, + "contract_address": { + "title": "The address of the new contract", + "$ref": "#/components/schemas/FELT" + } + }, + "required": ["transaction_hash", "contract_address"] + } + }, + "errors": [ + { + "$ref": "#/components/errors/INSUFFICIENT_ACCOUNT_BALANCE" + }, + { + "$ref": "#/components/errors/INSUFFICIENT_MAX_FEE" + }, + { + "$ref": "#/components/errors/INVALID_TRANSACTION_NONCE" + }, + { + "$ref": "#/components/errors/VALIDATION_FAILURE" + }, + { + "$ref": "#/components/errors/NON_ACCOUNT" + }, + { + "$ref": "#/components/errors/CLASS_HASH_NOT_FOUND" + }, + { + "$ref": "#/components/errors/DUPLICATE_TX" + }, + { + "$ref": "#/components/errors/UNSUPPORTED_TX_VERSION" + }, + { + "$ref": "#/components/errors/UNEXPECTED_ERROR" + } + ] + } + ], + "components": { + "contentDescriptors": {}, + "schemas": { + "NUM_AS_HEX": { + "title": "An integer number in hex format (0x...)", + "type": "string", + "pattern": "^0x[a-fA-F0-9]+$" + }, + "SIGNATURE": { + "$ref": "./starknet_api_openrpc.json#/components/schemas/SIGNATURE" + }, + "FELT": { + "$ref": "./starknet_api_openrpc.json#/components/schemas/FELT" + }, + "TXN_HASH": { + "$ref": "./starknet_api_openrpc.json#/components/schemas/TXN_HASH" + }, + "BROADCASTED_INVOKE_TXN": { + "$ref": "./api/starknet_api_openrpc.json#/components/schemas/BROADCASTED_INVOKE_TXN" + }, + "BROADCASTED_DECLARE_TXN": { + "$ref": "./api/starknet_api_openrpc.json#/components/schemas/BROADCASTED_DECLARE_TXN" + }, + "BROADCASTED_DEPLOY_ACCOUNT_TXN": { + "$ref": "./api/starknet_api_openrpc.json#/components/schemas/BROADCASTED_DEPLOY_ACCOUNT_TXN" + }, + "FUNCTION_CALL": { + "$ref": "./starknet_api_openrpc.json#/components/schemas/FUNCTION_CALL" + } + }, + "errors": { + "CLASS_HASH_NOT_FOUND": { + "code": 28, + "message": "Class hash not found" + }, + "CLASS_ALREADY_DECLARED": { + "code": 51, + "message": "Class already declared" + }, + "INVALID_TRANSACTION_NONCE": { + "code": 52, + "message": "Invalid transaction nonce" + }, + "INSUFFICIENT_MAX_FEE": { + "code": 53, + "message": "Max fee is smaller than the minimal transaction cost (validation plus fee transfer)" + }, + "INSUFFICIENT_ACCOUNT_BALANCE": { + "code": 54, + "message": "Account balance is smaller than the transaction's max_fee" + }, + "VALIDATION_FAILURE": { + "code": 55, + "message": "Account validation failed", + "data": "string" + }, + "COMPILATION_FAILED": { + "code": 56, + "message": "Compilation failed" + }, + "CONTRACT_CLASS_SIZE_IS_TOO_LARGE": { + "code": 57, + "message": "Contract class size it too large" + }, + "NON_ACCOUNT": { + "code": 58, + "message": "Sender address in not an account contract" + }, + "DUPLICATE_TX": { + "code": 59, + "message": "A transaction with the same hash already exists in the mempool" + }, + "COMPILED_CLASS_HASH_MISMATCH": { + "code": 60, + "message": "the compiled class hash did not match the one supplied in the transaction" + }, + "UNSUPPORTED_TX_VERSION": { + "code": 61, + "message": "the transaction version is not supported" + }, + "UNSUPPORTED_CONTRACT_CLASS_VERSION": { + "code": 62, + "message": "the contract class version is not supported" + }, + "UNEXPECTED_ERROR": { + "code": 63, + "message": "An unexpected error occurred", + "data": "string" + } + } + } +} diff --git a/crates/gateway/src/errors.rs b/crates/gateway/src/errors.rs index 75c30c0d6..78c106011 100644 --- a/crates/gateway/src/errors.rs +++ b/crates/gateway/src/errors.rs @@ -5,12 +5,15 @@ use blockifier::execution::errors::ContractClassError; use blockifier::state::errors::StateError; use blockifier::transaction::errors::TransactionExecutionError; use cairo_vm::types::errors::program_errors::ProgramError; +use enum_assoc::Assoc; +use serde::Serialize; use serde_json::{Error as SerdeError, Value}; use starknet_api::block::{BlockNumber, GasPrice}; use starknet_api::core::CompiledClassHash; use starknet_api::transaction::{Resource, ResourceBounds}; use starknet_api::StarknetApiError; use starknet_sierra_compile::errors::CompilationUtilError; +use strum::EnumIter; use thiserror::Error; use tokio::task::JoinError; @@ -53,6 +56,57 @@ impl IntoResponse for GatewayError { } } +#[derive(Error, Debug, Assoc, Clone, EnumIter, Serialize)] +#[func(pub fn code(&self) -> u16)] +#[func(pub fn data(&self) -> Option<&str>)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum GatewaySpecError { + #[error("Class hash not found")] + #[assoc(code = 28)] + ClassHashNotFound, + #[error("Class already declared")] + #[assoc(code = 51)] + ClassAlreadyDeclared, + #[error("Invalid transaction nonce")] + #[assoc(code = 52)] + InvalidTransactionNonce, + #[error("Max fee is smaller than the minimal transaction cost (validation plus fee transfer)")] + #[assoc(code = 53)] + InsufficientMaxFee, + #[error("Account balance is smaller than the transaction's max_fee")] + #[assoc(code = 54)] + InsufficientAccountBalance, + #[error("Account validation failed")] + #[assoc(code = 55)] + #[assoc(data = _0)] + ValidationFailure(String), + #[error("Compilation failed")] + #[assoc(code = 56)] + CompilationFailed, + #[error("Contract class size it too large")] + #[assoc(code = 57)] + ContractClassSizeIsTooLarge, + #[error("Sender address in not an account contract")] + #[assoc(code = 58)] + NonAccount, + #[error("A transaction with the same hash already exists in the mempool")] + #[assoc(code = 59)] + DuplicateTx, + #[error("the compiled class hash did not match the one supplied in the transaction")] + #[assoc(code = 60)] + CompiledClassHashMismatch, + #[error("the transaction version is not supported")] + #[assoc(code = 61)] + UnsupportedTxVersion, + #[error("the contract class version is not supported")] + #[assoc(code = 62)] + UnsupportedContractClassVersion, + #[error("An unexpected error occurred")] + #[assoc(code = 63)] + #[assoc(data = _0)] + UnexpectedError(String), +} + #[derive(Debug, Error)] #[cfg_attr(test, derive(PartialEq))] pub enum StatelessTransactionValidatorError { diff --git a/crates/gateway/src/gateway_test.rs b/crates/gateway/src/gateway_test.rs index d88d55cbb..2b998b3bf 100644 --- a/crates/gateway/src/gateway_test.rs +++ b/crates/gateway/src/gateway_test.rs @@ -1,3 +1,4 @@ +use std::fs::File; use std::sync::Arc; use axum::body::{Bytes, HttpBody}; @@ -6,6 +7,7 @@ use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use blockifier::context::ChainInfo; use blockifier::test_utils::CairoVersion; +use mempool_test_utils::get_absolute_path; use mempool_test_utils::starknet_api_test_utils::invoke_tx; use mockall::predicate::eq; use starknet_api::core::ContractAddress; @@ -13,11 +15,13 @@ use starknet_api::rpc_transaction::RPCTransaction; use starknet_api::transaction::TransactionHash; use starknet_mempool_types::communication::MockMempoolClient; use starknet_mempool_types::mempool_types::{Account, AccountState, MempoolInput, ThinTransaction}; +use strum::IntoEnumIterator; use crate::compilation::GatewayCompiler; use crate::config::{ GatewayCompilerConfig, StatefulTransactionValidatorConfig, StatelessTransactionValidatorConfig, }; +use crate::errors::GatewaySpecError; use crate::gateway::{add_tx, AppState, SharedMempoolClient}; use crate::state_reader_test_utils::{local_test_state_reader_factory, TestStateReaderFactory}; use crate::stateful_transaction_validator::StatefulTransactionValidator; @@ -107,3 +111,35 @@ fn calculate_hash(external_tx: &RPCTransaction) -> TransactionHash { .unwrap(); get_tx_hash(&account_tx) } + +#[test] +fn test_errors_match_spec() { + let spec: serde_json::Value = serde_json::from_reader( + File::open(get_absolute_path("crates/gateway/resources/starknet_write_api.json")).unwrap(), + ) + .unwrap(); + let spec_errors = &spec["components"]["errors"].as_object().unwrap(); + + for err in GatewaySpecError::iter() { + // Use the error serialization to get the error name, and then use it to get the error + // schema. + let err_schema = match serde_json::to_value(&err).unwrap() { + serde_json::Value::String(err_name) => &spec_errors[&err_name], + // Errors that contain data. + serde_json::Value::Object(mapping) => { + assert_eq!(mapping.len(), 1); + let err_name = mapping.keys().next().unwrap().as_str(); + &spec_errors[err_name] + } + _ => panic!("Unexpected error type"), + }; + + let expected_code: u16 = err_schema["code"].as_u64().unwrap().try_into().unwrap(); + assert_eq!(err.code(), expected_code); + + let expected_message = err_schema["message"].as_str().unwrap(); + assert_eq!(err.to_string(), expected_message); + + assert_eq!(err_schema.get("data").is_some(), err.data().is_some()); + } +}