diff --git a/rs/rosetta-api/icp/src/convert.rs b/rs/rosetta-api/icp/src/convert.rs index 419a68ae90a9..c2c66e79e82d 100644 --- a/rs/rosetta-api/icp/src/convert.rs +++ b/rs/rosetta-api/icp/src/convert.rs @@ -1,5 +1,6 @@ mod state; +use crate::errors::Details; use crate::{ convert, convert::state::State, @@ -42,6 +43,7 @@ use on_wire::{FromWire, IntoWire}; use rosetta_core::convert::principal_id_from_public_key; use serde_json::{Number, Value, from_value, map::Map}; use std::convert::{TryFrom, TryInto}; +use tracing::log::debug; /// This module converts from ledger_canister data structures to Rosetta data /// structures @@ -111,8 +113,15 @@ pub fn operations_to_requests( if o.coin_change.is_some() { return Err(op_error(o, "Coin changes are not permitted".into())); } - let account = from_model_account_identifier(o.account.as_ref().unwrap()) - .map_err(|e| op_error(o, e))?; + let account = from_model_account_identifier(o.account.as_ref().unwrap()).map_err(|e| { + op_error( + o, + format!( + "error converting '{:?}' to account identifier: {e:?}", + o.account.as_ref() + ), + ) + })?; let validate_neuron_management_op = || { if o.amount.is_some() && o.type_.parse::()? != OperationType::Disburse { @@ -136,13 +145,25 @@ pub fn operations_to_requests( } }; - match o.type_.parse::()? { + let parsed_type = o.type_.parse::().map_err(|e| { + let err_msg = format!("Error parsing operation type '{}': {e}", o.type_); + debug!("{}", err_msg); + ApiError::InvalidTransaction(false, Details::from(err_msg)) + })?; + match parsed_type { OperationType::Transaction => { let amount = o .amount .as_ref() .ok_or_else(|| op_error(o, "Amount must be populated".into()))?; - let amount = from_amount(amount, token_name).map_err(|e| op_error(o, e))?; + let amount = from_amount(amount, token_name).map_err(|e| { + let err_msg = format!( + "Transaction - Error converting amount (value: {}, currency: {:?}): {e}", + amount.value, amount.currency + ); + debug!("{}", err_msg); + op_error(o, err_msg) + })?; state.transaction(account, amount)?; } OperationType::Approve => { @@ -156,13 +177,25 @@ pub fn operations_to_requests( .amount .as_ref() .ok_or_else(|| op_error(o, "Amount must be populated".into()))?; - let amount = from_amount(amount, token_name).map_err(|e| op_error(o, e))?; + let amount = from_amount(amount, token_name).map_err(|e| { + let err_msg = format!( + "Fee - Error converting amount (value: {}, currency: {:?}): {e}", + amount.value, amount.currency + ); + debug!("{}", err_msg); + op_error(o, err_msg) + })?; state.fee(account, Tokens::from_e8s((-amount) as u64))?; } OperationType::Stake => { validate_neuron_management_op()?; let NeuronIdentifierMetadata { neuron_index, .. } = - o.metadata.clone().try_into()?; + NeuronIdentifierMetadata::try_from(o.metadata.clone()).map_err(|e| { + let err_msg = + format!("Stake - Failed to parse neuron identifier metadata: {e:?}"); + debug!("{}", err_msg); + e + })?; state.stake(account, neuron_index)?; } OperationType::SetDissolveTimestamp => { @@ -170,7 +203,11 @@ pub fn operations_to_requests( let SetDissolveTimestampMetadata { neuron_index, timestamp, - } = o.metadata.clone().try_into()?; + } = SetDissolveTimestampMetadata::try_from(o.metadata.clone()).map_err(|e| { + let err_msg = format!("SetDissolveTimestamp - Failed to parse metadata: {e:?}"); + debug!("{}", err_msg); + e + })?; state.set_dissolve_timestamp(account, neuron_index, timestamp)?; } OperationType::ChangeAutoStakeMaturity => { @@ -178,7 +215,7 @@ pub fn operations_to_requests( let ChangeAutoStakeMaturityMetadata { neuron_index, requested_setting_for_auto_stake_maturity, - } = o.metadata.clone().try_into()?; + } = ChangeAutoStakeMaturityMetadata::try_from(o.metadata.clone())?; state.change_auto_stake_maturity( account, neuron_index, @@ -212,11 +249,20 @@ pub fn operations_to_requests( let DisburseMetadata { neuron_index, recipient, - } = o.metadata.clone().try_into()?; + } = o.metadata.clone().try_into().map_err(|e| { + let err_msg = format!("Disburse - Failed to parse metadata: {e:?}"); + debug!("{}", err_msg); + e + })?; validate_neuron_management_op()?; let amount = if let Some(ref amount) = o.amount { Some(ledgeramount_from_amount(amount, token_name).map_err(|e| { - ApiError::internal_error(format!("Could not convert Amount {e:?}")) + let err_msg = format!( + "Disburse - Could not convert amount (value: {}, currency: {:?}): {e:?}", + amount.value, amount.currency + ); + debug!("{}", err_msg); + ApiError::internal_error(err_msg) })?) } else { None @@ -243,7 +289,11 @@ pub fn operations_to_requests( controller, percentage_to_spawn, spawned_neuron_index, - } = o.metadata.clone().try_into()?; + } = o.metadata.clone().try_into().map_err(|e| { + let err_msg = format!("Spawn - Failed to parse metadata: {e:?}"); + debug!("{}", err_msg); + e + })?; validate_neuron_management_op()?; state.spawn( account, @@ -301,7 +351,11 @@ pub fn operations_to_requests( followees, controller, neuron_index, - } = o.metadata.clone().try_into()?; + } = o.metadata.clone().try_into().map_err(|e| { + let err_msg = format!("Follow - Failed to parse metadata: {e:?}"); + debug!("{}", err_msg); + e + })?; validate_neuron_management_op()?; // convert from pkp in operation to principal in request. let pid = match controller { @@ -433,8 +487,14 @@ pub fn principal_id_from_public_key_or_principal( ) -> Result { match pkp { PublicKeyOrPrincipal::Principal(p) => Ok(p), - PublicKeyOrPrincipal::PublicKey(pk) => principal_id_from_public_key(&pk) - .map_err(|err| ApiError::InvalidPublicKey(false, err.into())), + PublicKeyOrPrincipal::PublicKey(pk) => principal_id_from_public_key(&pk).map_err(|err| { + let err_msg = format!( + "Failed to derive principal ID from public key (curve_type: {:?}): {err:?}", + pk.curve_type + ); + debug!("{}", err_msg); + ApiError::InvalidPublicKey(false, Details::from(err_msg)) + }), } } diff --git a/rs/rosetta-api/icp/src/request_handler.rs b/rs/rosetta-api/icp/src/request_handler.rs index 351eba673458..39657a9fd187 100644 --- a/rs/rosetta-api/icp/src/request_handler.rs +++ b/rs/rosetta-api/icp/src/request_handler.rs @@ -36,6 +36,7 @@ use ic_nns_common::pb::v1::NeuronId; use ic_nns_governance_api::manage_neuron::NeuronIdOrSubaccount; use ic_types::{CanisterId, crypto::DOMAIN_IC_REQUEST, messages::MessageId}; use icp_ledger::{Block, BlockIndex}; +use rosetta_core::metrics::RosettaMetrics; use rosetta_core::{ objects::ObjectMap, response_types::{MempoolResponse, MempoolTransactionResponse, NetworkListResponse}, @@ -46,8 +47,7 @@ use std::{ sync::Arc, }; use strum::IntoEnumIterator; - -use rosetta_core::metrics::RosettaMetrics; +use tracing::log::debug; /// The maximum amount of blocks to retrieve in a single search. const MAX_SEARCH_LIMIT: usize = 10_000; @@ -920,22 +920,31 @@ impl RosettaRequestHandler { fn verify_network_id(canister_id: &CanisterId, net_id: &NetworkIdentifier) -> Result<(), ApiError> { verify_network_blockchain(net_id)?; - let id: CanisterId = net_id - .try_into() - .map_err(|err| ApiError::InvalidNetworkId(false, format!("{err:?}").into()))?; + let id = CanisterId::try_from(net_id).map_err(|err| { + let err_msg = format!("Invalid network ID ('{net_id:?}'): {err:?}"); + debug!("{err_msg}"); + ApiError::InvalidNetworkId(false, Details::from(err_msg)) + })?; if *canister_id != id { - return Err(ApiError::InvalidNetworkId(false, "unknown network".into())); + let err_msg = format!("Invalid canister ID (expected '{canister_id}', received '{id}')"); + debug!("{err_msg}"); + return Err(ApiError::InvalidNetworkId(false, Details::from(err_msg))); } Ok(()) } fn verify_network_blockchain(net_id: &NetworkIdentifier) -> Result<(), ApiError> { + const EXPECTED_BLOCKCHAIN: &str = "Internet Computer"; match net_id.blockchain.as_str() { - "Internet Computer" => Ok(()), - _ => Err(ApiError::InvalidNetworkId( - false, - "unknown blockchain".into(), - )), + EXPECTED_BLOCKCHAIN => Ok(()), + _ => { + let err_msg = format!( + "Unknown blockchain (expected '{EXPECTED_BLOCKCHAIN}', received '{}')", + net_id.blockchain + ); + debug!("{err_msg}"); + Err(ApiError::InvalidNetworkId(false, Details::from(err_msg))) + } } } diff --git a/rs/rosetta-api/icp/src/request_handler/construction_payloads.rs b/rs/rosetta-api/icp/src/request_handler/construction_payloads.rs index 22bd94840c92..bc9dae25d44b 100644 --- a/rs/rosetta-api/icp/src/request_handler/construction_payloads.rs +++ b/rs/rosetta-api/icp/src/request_handler/construction_payloads.rs @@ -1,17 +1,7 @@ -use candid::Encode; -use ic_nns_common::pb::v1::NeuronId; -use ic_types::{ - PrincipalId, - messages::{Blob, HttpCanisterUpdate, MessageId}, -}; -use icp_ledger::{Memo, Operation, SendArgs, Tokens}; -use rand::Rng; -use std::{collections::HashMap, sync::Arc, time::Duration}; - use crate::{ convert, convert::{make_read_state_from_update, to_arg, to_model_account_identifier}, - errors::ApiError, + errors::{ApiError, Details}, ledger_client::LedgerAccess, models, models::{ @@ -28,11 +18,21 @@ use crate::{ StopDissolve, }, }; +use candid::Encode; +use ic_nns_common::pb::v1::NeuronId; use ic_nns_governance_api::{ ClaimOrRefreshNeuronFromAccount, ManageNeuron, manage_neuron::{self, Command, NeuronIdOrSubaccount, configure}, }; +use ic_types::{ + PrincipalId, + messages::{Blob, HttpCanisterUpdate, MessageId}, +}; +use icp_ledger::{Memo, Operation, SendArgs, Tokens}; +use rand::Rng; use rosetta_core::convert::principal_id_from_public_key; +use std::{collections::HashMap, sync::Arc, time::Duration}; +use tracing::log::debug; impl RosettaRequestHandler { /// Generate an Unsigned Transaction and Signing Payloads. @@ -49,7 +49,9 @@ impl RosettaRequestHandler { let ops = msg.operations.clone(); let pks = msg.public_keys.clone().ok_or_else(|| { - ApiError::internal_error("Expected field 'public_keys' to be populated") + const NO_PUBLIC_KEYS: &str = "Expected field 'public_keys' to be populated"; + debug!("{NO_PUBLIC_KEYS}"); + ApiError::internal_error(NO_PUBLIC_KEYS) })?; let transactions = convert::operations_to_requests(&ops, false, self.ledger.token_symbol())?; @@ -61,7 +63,13 @@ impl RosettaRequestHandler { .metadata .as_ref() .map(|m| ConstructionPayloadsRequestMetadata::try_from(m.clone())) - .transpose()?; + .transpose() + .map_err(|e| { + let err_msg = + format!("Failed to parse construction payloads request metadata: {e:?}"); + debug!("{}", err_msg); + e + })?; let ingress_start = meta .as_ref() @@ -104,8 +112,15 @@ impl RosettaRequestHandler { let pks_map = pks .iter() .map(|pk| { - let pid: PrincipalId = principal_id_from_public_key(pk) - .map_err(|err| ApiError::InvalidPublicKey(false, err.into()))?; + let pid: PrincipalId = principal_id_from_public_key(pk).map_err(|err| { + let err_msg = format!( + "Failed to derive principal ID from public key (curve_type: {:?}, hex_bytes: {}): {err:?}", + pk.curve_type, + pk.hex_bytes + ); + debug!("{}", err_msg); + ApiError::InvalidPublicKey(false, Details::from(err_msg)) + })?; let account: icp_ledger::AccountIdentifier = pid.into(); Ok((account, pk)) }) @@ -313,9 +328,9 @@ fn handle_transfer_operation( ingress_expiries: &[u64], ) -> Result<(), ApiError> { let pk = pks_map.get(&from).ok_or_else(|| { - ApiError::internal_error(format!( - "Cannot find public key for account identifier {from}", - )) + let err_msg = format!("Transfer - Cannot find public key for account identifier {from}"); + debug!("{}", err_msg); + ApiError::internal_error(err_msg) })?; // The argument we send to the canister @@ -338,7 +353,14 @@ fn handle_transfer_operation( nonce: None, sender: Blob( principal_id_from_public_key(pk) - .map_err(|err| ApiError::InvalidPublicKey(false, err.into()))? + .map_err(|err| { + let err_msg = format!( + "Transfer - Failed to derive principal ID from public key (curve_type: {:?}): {err:?}", + pk.curve_type + ); + debug!("{}", err_msg); + ApiError::InvalidPublicKey(false, Details::from(err_msg)) + })? .into_vec(), ), ingress_expiry: 0, @@ -366,17 +388,23 @@ fn handle_neuron_info( let account = req.account; let controller = req.controller; let neuron_index = req.neuron_index; - let neuron_subaccount = neuron_subaccount(account, controller, neuron_index, pks_map); + let neuron_subaccount = neuron_subaccount(account, controller, neuron_index, pks_map)?; // In the case of an hotkey, account will be derived from the hotkey so // we can use the same logic for controller or hotkey. let pk = pks_map.get(&account).ok_or_else(|| { - ApiError::internal_error(format!( - "NeuronInfo - Cannot find public key for account {account}", - )) + let err_msg = format!("NeuronInfo - Cannot find public key for account {account}"); + debug!("{}", err_msg); + ApiError::internal_error(err_msg) + })?; + let sender = principal_id_from_public_key(pk).map_err(|err| { + let err_msg = format!( + "NeuronInfo - Failed to derive principal ID from public key (curve_type: {:?}): {err:?}", + pk.curve_type + ); + debug!("{}", err_msg); + ApiError::InvalidPublicKey(false, Details::from(err_msg)) })?; - let sender = principal_id_from_public_key(pk) - .map_err(|err| ApiError::InvalidPublicKey(false, err.into()))?; // Argument for the method called on the governance canister. let args = NeuronIdOrSubaccount::Subaccount(neuron_subaccount.to_vec()); @@ -418,12 +446,18 @@ fn handle_list_neurons( // In the case of an hotkey, account will be derived from the hotkey so // we can use the same logic for controller or hotkey. let pk = pks_map.get(&account).ok_or_else(|| { - ApiError::internal_error(format!( - "NeuronInfo - Cannot find public key for account {account}", - )) + let err_msg = format!("ListNeurons - Cannot find public key for account {account}"); + debug!("{}", err_msg); + ApiError::internal_error(err_msg) + })?; + let sender = principal_id_from_public_key(pk).map_err(|err| { + let err_msg = format!( + "ListNeurons - Failed to derive principal ID from public key (curve_type: {:?}): {err:?}", + pk.curve_type + ); + debug!("{}", err_msg); + ApiError::InvalidPublicKey(false, Details::from(err_msg)) })?; - let sender = principal_id_from_public_key(pk) - .map_err(|err| ApiError::InvalidPublicKey(false, err.into()))?; // Argument for the method called on the governance canister. let args = ic_nns_governance_api::ListNeurons { @@ -532,9 +566,9 @@ fn handle_stake( let account = req.account; let neuron_index = req.neuron_index; let pk = pks_map.get(&account).ok_or_else(|| { - ApiError::internal_error(format!( - "Cannot find public key for account identifier {account}", - )) + let err_msg = format!("Stake - Cannot find public key for account identifier {account}"); + debug!("{}", err_msg); + ApiError::internal_error(err_msg) })?; // What we send to the governance canister @@ -561,7 +595,14 @@ fn handle_stake( )), sender: Blob( principal_id_from_public_key(pk) - .map_err(|err| ApiError::InvalidPublicKey(false, err.into()))? + .map_err(|err| { + let err_msg = format!( + "Stake - Failed to derive principal ID from public key (curve_type: {:?}): {err:?}", + pk.curve_type + ); + debug!("{}", err_msg); + ApiError::InvalidPublicKey(false, Details::from(err_msg)) + })? .into_vec(), ), ingress_expiry: 0, @@ -712,8 +753,14 @@ fn handle_add_hotkey( let key = req.key; let pid = match key { PublicKeyOrPrincipal::Principal(p) => p, - PublicKeyOrPrincipal::PublicKey(pk) => principal_id_from_public_key(&pk) - .map_err(|err| ApiError::InvalidPublicKey(false, err.into()))?, + PublicKeyOrPrincipal::PublicKey(pk) => principal_id_from_public_key(&pk).map_err(|err| { + let err_msg = format!( + "AddHotKey - Failed to derive principal ID from public key (curve_type: {:?}): {err:?}", + pk.curve_type + ); + debug!("{}", err_msg); + ApiError::InvalidPublicKey(false, Details::from(err_msg)) + })?, }; let command = Command::Configure(manage_neuron::Configure { operation: Some(configure::Operation::AddHotKey(manage_neuron::AddHotKey { @@ -747,8 +794,14 @@ fn handle_remove_hotkey( let key = req.key; let pid = match key { PublicKeyOrPrincipal::Principal(p) => p, - PublicKeyOrPrincipal::PublicKey(pk) => principal_id_from_public_key(&pk) - .map_err(|err| ApiError::InvalidPublicKey(false, err.into()))?, + PublicKeyOrPrincipal::PublicKey(pk) => principal_id_from_public_key(&pk).map_err(|err| { + let err_msg = format!( + "RemoveHotKey - Failed to derive principal ID from public key (curve_type: {:?}): {err:?}", + pk.curve_type + ); + debug!("{}", err_msg); + ApiError::InvalidPublicKey(false, Details::from(err_msg)) + })?, }; let command = Command::Configure(manage_neuron::Configure { operation: Some(configure::Operation::RemoveHotKey( @@ -931,14 +984,14 @@ fn add_neuron_management_payload( pks_map: &HashMap, ingress_expiries: &[u64], ) -> Result<(), ApiError> { - let neuron_subaccount = neuron_subaccount(account, controller, neuron_index, pks_map); + let neuron_subaccount = neuron_subaccount(account, controller, neuron_index, pks_map)?; // In the case of an hotkey, account will be derived from the hotkey so // we can use the same logic for controller or hotkey. let pk = pks_map.get(&account).ok_or_else(|| { - ApiError::internal_error(format!( - "Neuron management - Cannot find public key for account {account}", - )) + let err_msg = format!("Neuron management - Cannot find public key for account {account}"); + debug!("{}", err_msg); + ApiError::internal_error(err_msg) })?; let manage_neuron = ManageNeuron { @@ -958,7 +1011,14 @@ fn add_neuron_management_payload( )), sender: Blob( principal_id_from_public_key(pk) - .map_err(|err| ApiError::InvalidPublicKey(false, err.into()))? + .map_err(|err| { + let err_msg = format!( + "Neuron management - Failed to derive principal ID from public key (curve_type: {:?}): {err:?}", + pk.curve_type + ); + debug!("{}", err_msg); + ApiError::InvalidPublicKey(false, Details::from(err_msg)) + })? .into_vec(), ), ingress_expiry: 0, @@ -1014,24 +1074,32 @@ fn neuron_subaccount( controller: Option, neuron_index: u64, pks_map: &HashMap, -) -> [u8; 32] { +) -> Result<[u8; 32], ApiError> { match controller { Some(neuron_controller) => { // Hotkey (or any explicit controller). - crate::convert::neuron_subaccount_bytes_from_principal(&neuron_controller, neuron_index) + Ok(crate::convert::neuron_subaccount_bytes_from_principal( + &neuron_controller, + neuron_index, + )) } None => { // Default controller. - let pk = pks_map - .get(&account) - .ok_or_else(|| { - ApiError::internal_error(format!( - "Cannot find public key for account {account}", - )) - }) - .unwrap(); - crate::convert::neuron_subaccount_bytes_from_public_key(pk, neuron_index) - .expect("Error while processing neuron subaccount") + let pk = pks_map.get(&account).ok_or_else(|| { + let err_msg = + format!("Neuron subaccount - Cannot find public key for account {account}"); + debug!("{}", err_msg); + ApiError::internal_error(err_msg) + })?; + crate::convert::neuron_subaccount_bytes_from_public_key(pk, neuron_index).map_err( + |err| { + let err_msg = format!( + "Neuron subaccount - Error processing neuron subaccount for account {account}, neuron_index {neuron_index}: {err:?}" + ); + debug!("{}", err_msg); + ApiError::internal_error(err_msg) + }, + ) } } }