Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Deposit channel recycling for Solana #5411

Merged
merged 3 commits into from
Nov 13, 2024
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
254 changes: 220 additions & 34 deletions state-chain/cf-integration-tests/src/solana.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ use cf_chains::{
SolAddress, SolApiEnvironment, SolCcmAccounts, SolCcmAddress, SolHash, SolPubkey,
SolanaCrypto,
},
CcmChannelMetadata, CcmDepositMetadata, CcmFailReason, Chain, ExecutexSwapAndCallError,
ForeignChainAddress, RequiresSignatureRefresh, SetAggKeyWithAggKey, SetAggKeyWithAggKeyError,
Solana, SwapOrigin, TransactionBuilder,
CcmChannelMetadata, CcmDepositMetadata, CcmFailReason, Chain, DepositChannel,
ExecutexSwapAndCallError, ForeignChainAddress, RequiresSignatureRefresh, SetAggKeyWithAggKey,
SetAggKeyWithAggKeyError, Solana, SwapOrigin, TransactionBuilder,
};
use cf_primitives::{AccountRole, AuthorityCount, ForeignChain, SwapRequestId};
use cf_test_utilities::{assert_events_match, assert_has_matching_event};
Expand All @@ -27,6 +27,10 @@ use frame_support::{
traits::{OnFinalize, UnfilteredDispatchable},
};
use pallet_cf_elections::{
electoral_systems::{
blockchain::delta_based_ingress::ChannelTotalIngressedFor,
composite::tuple_6_impls::CompositeElectionIdentifierExtra,
},
vote_storage::{composite::tuple_6_impls::CompositeVote, AuthorityVote},
CompositeAuthorityVoteOf, CompositeElectionIdentifierOf, MAXIMUM_VOTES_PER_EXTRINSIC,
};
Expand Down Expand Up @@ -57,13 +61,17 @@ const BOB: AccountId = AccountId::new([0x44; 32]);
const DEPOSIT_AMOUNT: u64 = 5_000_000_000u64; // 5 Sol
const FALLBACK_ADDRESS: SolAddress = SolAddress([0xf0; 32]);

type SolanaCompositeVote = CompositeAuthorityVoteOf<
<Runtime as pallet_cf_elections::Config<SolanaInstance>>::ElectoralSystemRunner,
>;
type SolanaCompositeElectionIdentifier = CompositeElectionIdentifierOf<
<Runtime as pallet_cf_elections::Config<SolanaInstance>>::ElectoralSystemRunner,
>;
type SolanaChannelIngressed = ChannelTotalIngressedFor<SolanaIngressEgress>;

type SolanaElectionVote = BoundedBTreeMap<
CompositeElectionIdentifierOf<
<Runtime as pallet_cf_elections::Config<SolanaInstance>>::ElectoralSystemRunner,
>,
CompositeAuthorityVoteOf<
<Runtime as pallet_cf_elections::Config<SolanaInstance>>::ElectoralSystemRunner,
>,
SolanaCompositeElectionIdentifier,
SolanaCompositeVote,
ConstU32<MAXIMUM_VOTES_PER_EXTRINSIC>,
>;

Expand All @@ -84,6 +92,11 @@ fn setup_sol_environments() {
.map(|nonce| (nonce, sol_test_values::TEST_DURABLE_NONCE))
.collect::<Vec<_>>(),
);

// Enable voting for all validators
for v in Validator::current_authorities() {
assert_ok!(SolanaElections::stop_ignoring_my_votes(RuntimeOrigin::signed(v.clone()),));
}
}

fn schedule_deposit_to_swap(
Expand Down Expand Up @@ -147,6 +160,65 @@ fn schedule_deposit_to_swap(
=> swap_request_id)
}

/// Helper functions to make voting in Solana Elections easier
enum SolanaState {
BlockHeight(u64),
Ingressed(Vec<(SolAddress, SolanaChannelIngressed)>),
Egress(TransactionSuccessDetails),
}

impl SolanaState {
fn is_of_type(&self, target: &SolanaCompositeElectionIdentifier) -> bool {
match self {
SolanaState::BlockHeight(..) =>
matches!(*target.extra(), CompositeElectionIdentifierExtra::A(..)),
SolanaState::Ingressed(..) =>
matches!(*target.extra(), CompositeElectionIdentifierExtra::C(..)),
SolanaState::Egress(..) =>
matches!(*target.extra(), CompositeElectionIdentifierExtra::EE(..)),
}
}
}

impl From<SolanaState> for SolanaCompositeVote {
fn from(value: SolanaState) -> Self {
match value {
SolanaState::BlockHeight(block_height) =>
AuthorityVote::Vote(CompositeVote::A(block_height)),
SolanaState::Ingressed(channel_ingresses) => AuthorityVote::Vote(CompositeVote::C(
channel_ingresses
.into_iter()
.collect::<BTreeMap<_, _>>()
.try_into()
.expect("Too many ingress channels per election."),
)),
SolanaState::Egress(transaction_success_details) =>
AuthorityVote::Vote(CompositeVote::EE(transaction_success_details)),
}
}
}

#[track_caller]
fn witness_solana_state(state: SolanaState) {
// Get the election identifier of the Solana egress.
let election_id = SolanaElections::with_election_identifiers(|election_identifiers| {
Ok(election_identifiers
.into_iter()
.find(|id| state.is_of_type(id))
.expect("Election must exists to be voted on."))
})
.unwrap();

let vote = state.into();

// Submit vote to witness: transaction success, but execution failure
let votes: SolanaElectionVote = BTreeMap::from_iter([(election_id, vote)]).try_into().unwrap();

for v in Validator::current_authorities() {
assert_ok!(SolanaElections::vote(RuntimeOrigin::signed(v), votes.clone()));
}
}

#[test]
fn can_build_solana_batch_all() {
const EPOCH_BLOCKS: u32 = 100;
Expand Down Expand Up @@ -728,31 +800,10 @@ fn solana_ccm_execution_error_can_trigger_fallback() {
assert_eq!(pallet_cf_broadcast::PendingBroadcasts::<Runtime, SolanaInstance>::get().len(), 1);
let ccm_broadcast_id = pallet_cf_broadcast::PendingBroadcasts::<Runtime, SolanaInstance>::get().into_iter().next().unwrap();

// Get the election identifier of the Solana egress.
let election_id = SolanaElections::with_election_identifiers(
|election_identifiers| {
Ok(election_identifiers.last().cloned().unwrap())
},
).unwrap();

// Submit vote to witness: transaction success, but execution failure
let vote: SolanaElectionVote = BTreeMap::from_iter([(election_id,
AuthorityVote::Vote(CompositeVote::EE(TransactionSuccessDetails {
tx_fee: 1_000,
transaction_successful: false,
}))
)]).try_into().unwrap();

for v in Validator::current_authorities() {
assert_ok!(SolanaElections::stop_ignoring_my_votes(
RuntimeOrigin::signed(v.clone()),
));

assert_ok!(SolanaElections::vote(
RuntimeOrigin::signed(v),
vote.clone()
));
}
witness_solana_state(SolanaState::Egress(TransactionSuccessDetails {
tx_fee: 1_000,
transaction_successful: false,
}));

// Egress queue should be empty
assert_eq!(pallet_cf_ingress_egress::ScheduledEgressFetchOrTransfer::<Runtime, SolanaInstance>::decode_len(), Some(0));
Expand All @@ -776,3 +827,138 @@ fn solana_ccm_execution_error_can_trigger_fallback() {
assert!(!pallet_cf_broadcast::PendingApiCalls::<Runtime, SolanaInstance>::contains_key(ccm_broadcast_id));
});
}

#[test]
fn solana_can_recycle_deposit_channels() {
const EPOCH_BLOCKS: u32 = 100;
const MAX_AUTHORITIES: AuthorityCount = 10;
super::genesis::with_test_defaults()
.blocks_per_epoch(EPOCH_BLOCKS)
.max_authorities(MAX_AUTHORITIES)
.with_additional_accounts(&[
(DORIS, AccountRole::LiquidityProvider, 5 * FLIPPERINOS_PER_FLIP),
(ZION, AccountRole::Broker, 5 * FLIPPERINOS_PER_FLIP),
])
.build()
.execute_with(|| {
setup_sol_environments();

let (mut testnet, _, _) = network::fund_authorities_and_join_auction(MAX_AUTHORITIES);
assert_ok!(RuntimeCall::SolanaVault(
pallet_cf_vaults::Call::<Runtime, SolanaInstance>::initialize_chain {}
)
.dispatch_bypass_filter(pallet_cf_governance::RawOrigin::GovernanceApproval.into()));
setup_pool_and_accounts(vec![Asset::Sol, Asset::SolUsdc], OrderType::LimitOrder);
testnet.move_to_the_next_epoch();
System::reset_events();
witness_solana_state(SolanaState::BlockHeight(0u64));
testnet.move_forward_blocks(1);

let destination_address = EncodedAddress::Eth([0x11; 20]);
const SOL_SOL: cf_chains::assets::sol::Asset = cf_chains::assets::sol::Asset::Sol;
const SOL_USDC: cf_chains::assets::sol::Asset = cf_chains::assets::sol::Asset::SolUsdc;

// Request some deposit channels
assert_ok!(Swapping::request_swap_deposit_address(
RuntimeOrigin::signed(ZION),
Asset::Sol,
Asset::Usdc,
destination_address.clone(),
Default::default(),
None,
Default::default(),
));

let (channel_address, channel_id, source_chain_expiry_block) = assert_events_match!(
Runtime,
RuntimeEvent::Swapping(
pallet_cf_swapping::Event::SwapDepositAddressReady {
deposit_address,
channel_id,
source_chain_expiry_block,
..
}
) => (deposit_address, channel_id, source_chain_expiry_block)
);
let deposit_address = channel_address.try_into().expect("Deposit channel generated must be on the Solana Chain");

assert!(pallet_cf_ingress_egress::DepositChannelLookup::<Runtime, SolanaInstance>::contains_key(deposit_address));

// Generated deposit channel address should be correct.
let (generated_address, generated_bump) = <AddressDerivation as AddressDerivationApi<Solana>>::generate_address_and_state(
SOL_SOL,
channel_id,
)
.expect("Must be able to derive Solana deposit channel.");
assert_eq!(deposit_address, generated_address);

// Witness Solana ingress in the deposit channel
testnet.move_forward_blocks(10);
witness_solana_state(SolanaState::Ingressed(vec![(deposit_address, ChannelTotalIngressedFor::<SolanaIngressEgress> {
block_number: source_chain_expiry_block - 1,
amount: 1_000_000_000_000,
})]));

// Move forward so the deposit channels expire
witness_solana_state(SolanaState::BlockHeight(source_chain_expiry_block));
witness_solana_state(SolanaState::Ingressed(vec![(deposit_address, ChannelTotalIngressedFor::<SolanaIngressEgress> {
block_number: source_chain_expiry_block,
amount: 1_000_000_000_000,
})]));
testnet.move_forward_blocks(2);

// Expired deposit channels should be recycled.
assert!(!pallet_cf_ingress_egress::DepositChannelLookup::<Runtime, SolanaInstance>::contains_key(deposit_address));
assert_eq!(pallet_cf_ingress_egress::DepositChannelPool::<Runtime, SolanaInstance>::get(channel_id),
Some(DepositChannel {
channel_id,
address: deposit_address,
asset: SOL_SOL,
state: generated_bump,
})
);

// Reuse the same deposit channel for a SolUsdc deposit.
assert_ok!(Swapping::request_swap_deposit_address(
RuntimeOrigin::signed(ZION),
Asset::SolUsdc,
Asset::Usdc,
destination_address,
Default::default(),
None,
Default::default(),
));

let usdc_expiry_block = assert_events_match!(
Runtime,
RuntimeEvent::Swapping(
pallet_cf_swapping::Event::SwapDepositAddressReady {
deposit_address: deposit_address_sol_usdc,
channel_id: channel_id_sol_usdc,
source_chain_expiry_block,
..
}
) if EncodedAddress::Sol(deposit_address.into()) == deposit_address_sol_usdc && channel_id == channel_id_sol_usdc => source_chain_expiry_block
);

// Witness Solana ingress in the deposit channel
testnet.move_forward_blocks(10);
witness_solana_state(SolanaState::BlockHeight(usdc_expiry_block));
witness_solana_state(SolanaState::Ingressed(vec![(deposit_address, ChannelTotalIngressedFor::<SolanaIngressEgress> {
block_number: usdc_expiry_block,
amount: 1_000_000_000_000,
})]));
testnet.move_forward_blocks(2);

// Channel is ready to be recycled again.
assert!(!pallet_cf_ingress_egress::DepositChannelLookup::<Runtime, SolanaInstance>::contains_key(deposit_address));
assert_eq!(pallet_cf_ingress_egress::DepositChannelPool::<Runtime, SolanaInstance>::get(channel_id),
Some(DepositChannel {
channel_id,
address: deposit_address,
asset: SOL_USDC,
state: generated_bump,
})
);
});
}
10 changes: 10 additions & 0 deletions state-chain/chains/src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,16 @@ impl TryFrom<ForeignChainAddress> for ScriptPubkey {
}
}

impl TryFrom<EncodedAddress> for SolAddress {
type Error = AddressError;

fn try_from(address: EncodedAddress) -> Result<Self, Self::Error> {
match address {
EncodedAddress::Sol(addr) => Ok(addr.into()),
_ => Err(AddressError::InvalidAddress),
}
}
}
pub trait IntoForeignChainAddress<C: Chain> {
fn into_foreign_chain_address(address: C::ChainAccount) -> ForeignChainAddress;
}
Expand Down
6 changes: 5 additions & 1 deletion state-chain/chains/src/sol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,11 @@ impl address::ToHumanreadableAddress for SolAddress {
}
}

impl crate::ChannelLifecycleHooks for AccountBump {}
impl crate::ChannelLifecycleHooks for AccountBump {
fn maybe_recycle(self) -> Option<Self> {
Some(self)
}
}

#[derive(Encode, Decode, TypeInfo, Clone, PartialEq, Eq, Copy, Debug)]
pub struct SolanaDepositFetchId {
Expand Down
2 changes: 1 addition & 1 deletion state-chain/pallets/cf-ingress-egress/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ pub mod pallet {

/// Stores address ready for use.
#[pallet::storage]
pub(crate) type DepositChannelPool<T: Config<I>, I: 'static = ()> =
pub type DepositChannelPool<T: Config<I>, I: 'static = ()> =
StorageMap<_, Twox64Concat, ChannelId, DepositChannel<T::TargetChain>>;

/// Defines the minimum amount of Deposit allowed for each asset.
Expand Down
Loading