Skip to content
Merged
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,14 @@ serial_test = "3.2.0"
sha2 = "0.10.9"
sol_rpc_client = "6.0.0"
sol_rpc_types = "3.1.2"
solana-account-decoder-client-types = "3.1.11"
solana-address = "2.3.0"
solana-client = "3.1.8"
solana-hash = "4.1.0"
solana-keypair = "3.1.2"
solana-message = "3.1.0"
solana-native-token = "3.0.0"
solana-nonce = { version = "3.1.0", features = ["serde"] }
solana-sdk-ids = "3.1.0"
solana-signature = "3.1.0"
solana-system-interface = "3.1.0"
Expand Down
34 changes: 25 additions & 9 deletions docs/design.md

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion integration_tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use pocket_ic::{PocketIcBuilder, RejectResponse, nonblocking::PocketIc};
use serde::de::DeserializeOwned;
use sol_rpc_client::SolRpcClient;
use sol_rpc_types::{Lamport, RpcAccess};
use solana_address::address;
use solana_transaction::Transaction;
use std::{default::Default, env::var, fs, ops::Deref, path::PathBuf, time::Duration, vec};

Expand Down Expand Up @@ -128,7 +129,8 @@ impl Setup {
pub const DEFAULT_MINIMUM_WITHDRAWAL_AMOUNT: Lamport = 2_000_000; // 0.002 SOL
pub const DEFAULT_CALLER: Principal =
Principal::from_slice(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x0, 0x3, 0x1, 0x1]);
pub const DEFAULT_NONCE_ACCOUNT: &'static str = "US517G5965aydkZ46HS38QLi7UQiSojurfbQfKCELFx";
pub const DEFAULT_NONCE_ACCOUNT: solana_address::Address =
address!("US517G5965aydkZ46HS38QLi7UQiSojurfbQfKCELFx");

pub async fn new(
make_live: PocketIcMode,
Expand Down
2 changes: 2 additions & 0 deletions minter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,11 @@ scopeguard = { workspace = true }
serde = { workspace = true }
sol_rpc_client = { workspace = true }
sol_rpc_types = { workspace = true }
solana-account-decoder-client-types = { workspace = true }
solana-address = { workspace = true }
solana-hash = { workspace = true }
solana-message = { workspace = true }
solana-nonce = { workspace = true }
solana-sdk-ids = { workspace = true }
solana-signature = { workspace = true }
solana-system-interface = { workspace = true, features = ["bincode"] }
Expand Down
9 changes: 9 additions & 0 deletions minter/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ pub const GET_BALANCE_CYCLES: u128 = 10_000_000_000;
/// Cycles to attach for `getSignatureStatuses` RPC calls.
pub const GET_SIGNATURE_STATUSES_CYCLES: u128 = 1_000_000_000_000;

/// Cycles to attach for `getAccountInfo` RPC calls.
///
/// The SOL RPC canister charges about 2.1B cycles for a `getAccountInfo`
/// request with the default 3-out-of-4 provider consensus, comparable to
/// `getBalance` since a nonce account holds only 80 bytes of state. The
/// attached amount leaves a wide margin for provider or price changes; the
/// unused part is refunded.
pub const GET_ACCOUNT_INFO_CYCLES: u128 = 10_000_000_000;

/// Cost in lamports per signature included in a Solana transaction.
///
/// See <https://solana.com/docs/core/fees#base-fee>.
Expand Down
89 changes: 86 additions & 3 deletions minter/src/rpc/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
constants::{
GET_BALANCE_CYCLES, GET_RECENT_BLOCK_MAX_TRIES, GET_SIGNATURE_STATUSES_CYCLES,
GET_TRANSACTION_CYCLES, MAX_HTTP_OUTCALL_RESPONSE_BYTES,
GET_ACCOUNT_INFO_CYCLES, GET_BALANCE_CYCLES, GET_RECENT_BLOCK_MAX_TRIES,
GET_SIGNATURE_STATUSES_CYCLES, GET_TRANSACTION_CYCLES, MAX_HTTP_OUTCALL_RESPONSE_BYTES,
},
runtime::CanisterRuntime,
state::read_state,
Expand All @@ -11,10 +11,14 @@ use derive_more::From;
use ic_canister_runtime::IcError;
use minicbor::{Decode, Encode};
use sol_rpc_types::{
CommitmentLevel, GetTransactionEncoding, Lamport, MultiRpcResult, RpcError, Slot,
CommitmentLevel, GetAccountInfoEncoding, GetTransactionEncoding, Lamport, MultiRpcResult,
RpcError, Slot,
};
use solana_account_decoder_client_types::UiAccount;
use solana_address::Address;
use solana_hash::Hash;
use solana_nonce::{state::State as NonceState, versions::Versions as NonceVersions};
use solana_sdk_ids::system_program;
use solana_signature::Signature;
use solana_transaction::{Transaction, versioned::VersionedTransaction};
use solana_transaction_status_client_types::{
Expand Down Expand Up @@ -177,6 +181,85 @@ pub enum SubmitTransactionError {
InconsistentRpcResults,
}

pub async fn get_nonce_account<R: CanisterRuntime>(
runtime: &R,
address: Address,
) -> Result<NonceAccount, GetNonceAccountError> {
let result = read_state(|state| state.sol_rpc_client(runtime.inter_canister_call_runtime()))
.get_account_info(address)
.with_encoding(GetAccountInfoEncoding::Base64)
.with_commitment(CommitmentLevel::Finalized)
.with_cycles(GET_ACCOUNT_INFO_CYCLES)
.try_send()
.await;
match result? {
MultiRpcResult::Consistent(Ok(Some(account))) => NonceAccount::try_from(account),
MultiRpcResult::Consistent(Ok(None)) => Err(GetNonceAccountError::AccountNotFound),
MultiRpcResult::Consistent(Err(e)) => Err(GetNonceAccountError::RpcError(e)),
MultiRpcResult::Inconsistent(_) => Err(GetNonceAccountError::InconsistentRpcResults),
Comment thread
gregorydemay marked this conversation as resolved.
}
}

/// The on-chain state of an initialized durable nonce account.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct NonceAccount {
pub authority: Address,
pub nonce: Hash,
}

impl TryFrom<UiAccount> for NonceAccount {
type Error = GetNonceAccountError;

fn try_from(account: UiAccount) -> Result<Self, Self::Error> {
if account.owner != system_program::ID.to_string() || account.executable {
return Err(GetNonceAccountError::UnexpectedAccountMetadata {
owner: account.owner,
executable: account.executable,
});
}
let data = account.data.decode().ok_or_else(|| {
GetNonceAccountError::NotAnInitializedNonceAccount(
"undecodable account data".to_string(),
)
})?;
let versions: NonceVersions = bincode::deserialize(&data)
.map_err(|e| GetNonceAccountError::NotAnInitializedNonceAccount(e.to_string()))?;
Comment thread
gregorydemay marked this conversation as resolved.
let state = match versions {
NonceVersions::Legacy(_) => return Err(GetNonceAccountError::LegacyNonceAccount),
NonceVersions::Current(state) => state,
};
match *state {
NonceState::Uninitialized => Err(GetNonceAccountError::NotAnInitializedNonceAccount(
"the nonce account is uninitialized".to_string(),
)),
NonceState::Initialized(data) => Ok(Self {
authority: data.authority,
nonce: data.blockhash(),
}),
}
}
}

#[derive(Debug, PartialEq, Error)]
pub enum GetNonceAccountError {
#[error("Error while calling SOL RPC canister: {0}")]
IcError(#[from] IcError),
#[error("RPC error while fetching nonce account: {0}")]
RpcError(RpcError),
#[error("Inconsistent RPC results for getAccountInfo")]
InconsistentRpcResults,
#[error("Nonce account not found")]
AccountNotFound,
#[error(
"Expected a non-executable account owned by the system program, got owner {owner} (executable: {executable})"
)]
UnexpectedAccountMetadata { owner: String, executable: bool },
#[error("Not an initialized nonce account: {0}")]
NotAnInitializedNonceAccount(String),
#[error("Legacy nonce account, which cannot back a durable transaction")]
LegacyNonceAccount,
}

pub async fn get_recent_block<R: CanisterRuntime>(
runtime: &R,
) -> Result<Block, GetRecentBlockError> {
Expand Down
147 changes: 143 additions & 4 deletions minter/src/rpc/tests.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
use crate::{
constants::GET_RECENT_BLOCK_MAX_TRIES,
rpc::{
Block, BlockHeight, GetBalanceError, GetRecentBlockError, GetTransactionError,
SubmitTransactionError, get_balance, get_recent_block, get_transaction, submit_transaction,
Block, BlockHeight, GetBalanceError, GetNonceAccountError, GetRecentBlockError,
GetTransactionError, NonceAccount, SubmitTransactionError, get_balance, get_nonce_account,
get_recent_block, get_transaction, submit_transaction,
},
test_fixtures::{
confirmed_block, confirmed_block_at_height,
MINTER_ADDRESS, confirmed_block, confirmed_block_at_height,
deposit::{
DEPOSIT_ADDRESS, legacy_deposit_transaction, legacy_deposit_transaction_signature,
},
fetched, init_state,
durable_nonce, fetched, init_state, legacy_nonce_account_info, nonce_account_address,
nonce_account_info,
runtime::TestCanisterRuntime,
uninitialized_nonce_account_info,
},
};
use assert_matches::assert_matches;
Expand Down Expand Up @@ -325,6 +328,142 @@ mod submit_transaction_tests {
}
}

mod get_nonce_account_tests {
use super::*;
use sol_rpc_types::{AccountData, AccountEncoding};

type GetAccountInfoResult = sol_rpc_types::MultiRpcResult<Option<sol_rpc_types::AccountInfo>>;

#[tokio::test]
async fn should_return_the_authority_and_nonce_value() {
init_state();

let runtime = TestCanisterRuntime::new().add_stub_response(
GetAccountInfoResult::Consistent(Ok(Some(nonce_account_info(MINTER_ADDRESS, 1)))),
);

let result = get_nonce_account(&runtime, nonce_account_address()).await;

assert_eq!(
result,
Ok(NonceAccount {
authority: MINTER_ADDRESS,
nonce: durable_nonce(1),
})
);
}

#[tokio::test]
async fn should_fail_if_account_not_found() {
init_state();

let runtime = TestCanisterRuntime::new()
.add_stub_response(GetAccountInfoResult::Consistent(Ok(None)));

let result = get_nonce_account(&runtime, nonce_account_address()).await;

assert_eq!(result, Err(GetNonceAccountError::AccountNotFound));
}

#[tokio::test]
async fn should_fail_if_call_fails_or_results_are_wrong() {
init_state();
let rpc_error = RpcError::ValidationError("Error 1".to_string());
let inconsistent = vec![(
RpcSource::Supported(SupportedRpcProviderId::AnkrMainnet),
Err(rpc_error.clone()),
)];

for (runtime, expected) in [
(
TestCanisterRuntime::new().add_stub_error(IcError::CallPerformFailed),
GetNonceAccountError::IcError(IcError::CallPerformFailed),
),
(
TestCanisterRuntime::new()
.add_stub_response(GetAccountInfoResult::Consistent(Err(rpc_error.clone()))),
GetNonceAccountError::RpcError(rpc_error.clone()),
),
(
TestCanisterRuntime::new()
.add_stub_response(GetAccountInfoResult::Inconsistent(inconsistent.clone())),
GetNonceAccountError::InconsistentRpcResults,
),
] {
let result = get_nonce_account(&runtime, nonce_account_address()).await;

assert_eq!(result, Err(expected));
}
}

#[tokio::test]
async fn should_fail_if_account_is_not_a_non_executable_system_program_account() {
init_state();

let foreign_owner_account = sol_rpc_types::AccountInfo {
owner: MINTER_ADDRESS.to_string(),
..nonce_account_info(MINTER_ADDRESS, 1)
};
let executable_account = sol_rpc_types::AccountInfo {
executable: true,
..nonce_account_info(MINTER_ADDRESS, 1)
};

for account in [foreign_owner_account, executable_account] {
let runtime = TestCanisterRuntime::new()
.add_stub_response(GetAccountInfoResult::Consistent(Ok(Some(account))));

let result = get_nonce_account(&runtime, nonce_account_address()).await;

assert_matches!(
result,
Err(GetNonceAccountError::UnexpectedAccountMetadata { .. })
);
}
}

#[tokio::test]
async fn should_fail_if_account_is_a_legacy_nonce_account() {
init_state();

let runtime = TestCanisterRuntime::new().add_stub_response(
GetAccountInfoResult::Consistent(Ok(Some(legacy_nonce_account_info(MINTER_ADDRESS)))),
);

let result = get_nonce_account(&runtime, nonce_account_address()).await;

assert_eq!(result, Err(GetNonceAccountError::LegacyNonceAccount));
}

#[tokio::test]
async fn should_fail_if_account_is_not_an_initialized_nonce_account() {
init_state();

let account_with_data = |data: &str| sol_rpc_types::AccountInfo {
data: AccountData::Binary(data.to_string(), AccountEncoding::Base64),
..nonce_account_info(MINTER_ADDRESS, 1)
};
let invalid_base64_account = account_with_data("not base64!");
let invalid_nonce_state_account = account_with_data("AAAA");

for account in [
uninitialized_nonce_account_info(),
invalid_base64_account,
invalid_nonce_state_account,
] {
let runtime = TestCanisterRuntime::new()
.add_stub_response(GetAccountInfoResult::Consistent(Ok(Some(account))));

let result = get_nonce_account(&runtime, nonce_account_address()).await;

assert_matches!(
result,
Err(GetNonceAccountError::NotAnInitializedNonceAccount(_))
);
}
}
}

mod get_recent_block_tests {
use super::*;

Expand Down
Loading
Loading