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: add expires_at field to request swap parameter encoding #5506

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
15 changes: 15 additions & 0 deletions bouncer/tests/btc_vault_swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface VaultSwapDetails {
chain: string;
nulldata_payload: string;
deposit_address: string;
expires_at: number;
}

interface Beneficiary {
Expand Down Expand Up @@ -66,6 +67,20 @@ async function buildAndSendBtcVaultSwap(
assert.strictEqual(vaultSwapDetails.chain, 'Bitcoin');
testBtcVaultSwap.debugLog('nulldata_payload:', vaultSwapDetails.nulldata_payload);
testBtcVaultSwap.debugLog('deposit_address:', vaultSwapDetails.deposit_address);
testBtcVaultSwap.debugLog('expires_at:', vaultSwapDetails.expires_at);

// Calculate expected expiry time assuming block time is 6 secs, expires_at = time left to next rotation
const epochDuration = (await chainflip.rpc(`cf_epoch_duration`)) as number;
const epochStartedAt = (await chainflip.rpc(`cf_current_epoch_started_at`)) as number;
const currentBlockNumber = (await chainflip.rpc.chain.getHeader()).number.toNumber();
const blocksUntilNextRotation = epochDuration + epochStartedAt - currentBlockNumber;
const expectedExpiresAt = Date.now() + blocksUntilNextRotation * 6000;
// Check that expires_at field is correct (within 20 secs drift)
assert(
Math.abs(expectedExpiresAt - vaultSwapDetails.expires_at) <= 20 * 1000,
`VaultSwapDetails expiry timestamp is not within a 20 secs drift of the expected expiry time.
expectedExpiresAt = ${expectedExpiresAt} and actualExpiresAt = ${vaultSwapDetails.expires_at}`,
);

const txid = await sendVaultTransaction(
vaultSwapDetails.nulldata_payload,
Expand Down
25 changes: 24 additions & 1 deletion state-chain/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,10 @@ use sp_runtime::{
BoundedVec,
};

use frame_support::genesis_builder_helper::build_state;
use frame_support::{
genesis_builder_helper::build_state,
traits::{EstimateNextSessionRotation, Time},
};
#[cfg(any(feature = "std", test))]
pub use sp_runtime::BuildStorage;
use sp_runtime::{
Expand Down Expand Up @@ -2241,6 +2244,23 @@ impl_runtime_apis! {
})
.map_err(|_| pallet_cf_swapping::Error::<Runtime>::InvalidDestinationAddress)?;

// Payload expiry time is set to time left to next rotation.
// * For BTC: the actual expiry time is 2 rotations, but we deliberately set expires_at to be the time left to next
// rotation to cater for the case when a forced rotation happens between when the payload was requested and
// before expires_at. BTC funds can be lost in 3 cases:
// * User makes the vault transaction after expires_at, and it happens that we did a forced rotation in between
// * User submits the vault transaction 3 days (1 epoch) after expires_at, and with no forced rotations in between
// * We do 2 forced rotations in between payload request and expires_at
// * For SOLANA: The actual expiry time is indeed time left to next rotation. If a forced rotation
// happens in between as explained before, it is actually not a problem as the user won't lose any funds.
// * For ETH: payload never expires hence we don't send expires_at
let current_block = System::block_number();
let (Some(next_rotation_block), _) = Validator::estimate_next_session_rotation(current_block) else {
Err(pallet_cf_validator::Error::<Runtime>::InvalidEpochDuration)?
};
let blocks_until_next_rotation = next_rotation_block.saturating_sub(current_block);
let expires_at = Timestamp::now() + blocks_until_next_rotation as u64 * SLOT_DURATION;

// Encode swap
match ForeignChain::from(source_asset) {
ForeignChain::Bitcoin => {
Expand Down Expand Up @@ -2286,9 +2306,12 @@ impl_runtime_apis! {
},
};



Ok(VaultSwapDetails::Bitcoin {
nulldata_payload: encode_swap_params_in_nulldata_payload(params),
deposit_address: derive_btc_vault_deposit_address(private_channel_id),
expires_at
})
},
_ => Err(pallet_cf_swapping::Error::<Runtime>::UnsupportedSourceAsset.into()),
Expand Down
10 changes: 8 additions & 2 deletions state-chain/runtime/src/runtime_apis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ pub enum VaultSwapDetails<BtcAddress> {
#[serde(with = "sp_core::bytes")]
nulldata_payload: Vec<u8>,
deposit_address: BtcAddress,
/// Payload expiry time, expressed as timestamp since the UNIX_EPOCH in milliseconds
expires_at: u64,
},
}

Expand All @@ -52,8 +54,12 @@ impl<BtcAddress> VaultSwapDetails<BtcAddress> {
F: FnOnce(BtcAddress) -> T,
{
match self {
VaultSwapDetails::Bitcoin { nulldata_payload, deposit_address } =>
VaultSwapDetails::Bitcoin { nulldata_payload, deposit_address: f(deposit_address) },
VaultSwapDetails::Bitcoin { nulldata_payload, deposit_address, expires_at } =>
VaultSwapDetails::Bitcoin {
nulldata_payload,
deposit_address: f(deposit_address),
expires_at,
},
}
}
}
Expand Down
Loading