Skip to content
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
551 changes: 354 additions & 197 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ magicblock-committor-program = { path = "./magicblock-committor-program", featur
magicblock-committor-service = { path = "./magicblock-committor-service" }
magicblock-config = { path = "./magicblock-config" }
magicblock-core = { path = "./magicblock-core" }
magicblock-delegation-program = { git = "https://github.com/magicblock-labs/delegation-program.git", rev = "2cb491032f372", features = [
"no-entrypoint",
magicblock-delegation-program = { path = "../delegation-program", default-features = false, features = [
"sdk",
] }
Comment on lines +105 to 107
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Local path dependency ../delegation-program will break CI and isolated checkouts.

path = "../delegation-program" references a sibling directory outside this repository. Any developer or CI runner without that exact directory layout will get a build failure. The previous git-based reference (with rev) was portable. This should either stay as a git dependency (possibly pointing to a local branch) or the delegation-program should be vendored/subtree'd into this repo before merging.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` around lines 105 - 107, The dependency entry for
magicblock-delegation-program currently uses a local path ("path =
\"../delegation-program\"") which will break CI and isolated checkouts; change
it back to a VCS-based dependency (restore the previous git + rev or git +
tag/branch) or vendor/subtree the delegation-program into this repo and update
the Cargo.toml entry accordingly so CI can fetch it; locate and update the
magicblock-delegation-program dependency line in Cargo.toml to use the git URL
and fixed rev (or replace the path with the vendored crate name) instead of the
local path.

magicblock-ledger = { path = "./magicblock-ledger" }
magicblock-magic-program-api = { path = "./magicblock-magic-program-api" }
Expand Down
5 changes: 4 additions & 1 deletion magicblock-account-cloner/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ impl ChainlinkCloner {
message,
);
// Defined positive commit frequency means commits should be scheduled
let ixs = match request.commit_frequency_ms {
let mut ixs = match request.commit_frequency_ms {
// TODO(GabrielePicco): Hotfix. Do not schedule frequency commits until we impose limits.
// 1. Allow configuring a higher minimum.
// 2. Stop committing accounts if they have been committed more than X times,
Expand Down Expand Up @@ -162,6 +162,9 @@ impl ChainlinkCloner {
}
_ => vec![modify_ix],
};
if !request.delegation_actions.is_empty() {
ixs.extend(request.delegation_actions.clone());
}
Comment on lines +165 to +167
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

No transaction size guard before appending delegation_actions.

Appending arbitrary instructions from delegation_actions to the existing ixs slice can push the serialized Transaction past Solana's 1232-byte limit, which will cause a runtime send failure. There is no size check before Transaction::new_with_payer or send_transaction.

Consider computing the serialized size of ixs after extension and returning an error (or chunking into a separate transaction) when the limit would be exceeded.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@magicblock-account-cloner/src/lib.rs` around lines 165 - 167, Before
extending ixs with request.delegation_actions, compute the serialized size that
would result (e.g., temporarily clone ixs, extend with
request.delegation_actions.clone(), serialize a Transaction built with those
instructions or compute serialized instruction bytes) and if it exceeds Solana's
1232-byte limit return an error or split the instructions into a separate
transaction; specifically update the block that currently does
ixs.extend(request.delegation_actions.clone()) to perform this size check prior
to calling Transaction::new_with_payer or send_transaction and either reject the
request with a clear error or chunk delegation_actions into additional
transactions so Transaction::new_with_payer/send_transaction never receives an
oversized transaction.

🧹 Nitpick | 🔵 Trivial

Unnecessary full-Vec clone; prefer extend_from_slice.

request.delegation_actions.clone() allocates a full Vec copy before iterating. Since request is already a reference, extend_from_slice avoids the intermediate allocation.

♻️ Proposed refactor
-        if !request.delegation_actions.is_empty() {
-            ixs.extend(request.delegation_actions.clone());
-        }
+        ixs.extend_from_slice(&request.delegation_actions);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if !request.delegation_actions.is_empty() {
ixs.extend(request.delegation_actions.clone());
}
ixs.extend_from_slice(&request.delegation_actions);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@magicblock-account-cloner/src/lib.rs` around lines 165 - 167, The code
currently clones the entire Vec with request.delegation_actions.clone() before
extending ixs, which causes an unnecessary allocation; change the extension to
use ixs.extend_from_slice(&request.delegation_actions) (or
ixs.extend(request.delegation_actions.iter().cloned()) if types differ) so you
avoid the intermediate Vec allocation—update the block that checks
request.delegation_actions and replace the clone-based extend with
extend_from_slice referencing ixs and request.delegation_actions.


let mut tx =
Transaction::new_with_payer(&ixs, Some(&validator_authority_id()));
Expand Down
2 changes: 1 addition & 1 deletion magicblock-accounts/src/scheduled_commits_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ impl ScheduledCommitsProcessorImpl {
.map(|finalize| vec![commit, finalize])
.unwrap_or(vec![commit])
})
.unwrap_or(vec![])
.unwrap_or_default()
}
};
let patched_errors = result
Expand Down
2 changes: 2 additions & 0 deletions magicblock-chainlink/src/chainlink/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub enum ChainlinkError {

#[error("Delegation record could not be decoded: {0} ({1:?})")]
InvalidDelegationRecord(Pubkey, ProgramError),
#[error("Delegation actions could not be decoded: {0} ({1})")]
InvalidDelegationActions(Pubkey, String),

#[error("Failed to resolve one or more accounts {0} when getting delegation records")]
DelegatedAccountResolutionsFailed(String),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use tracing::*;

use super::{delegation, types::AccountWithCompanion, FetchCloner};
use crate::{
cloner::{AccountCloneRequest, Cloner},
cloner::{AccountCloneRequest, Cloner, DelegationActions},
remote_account_provider::{ChainPubsubClient, ChainRpcClient},
};

Expand Down Expand Up @@ -127,15 +127,16 @@ where
)
.await
{
let (deleg_record, _delegation_actions) = deleg;
delegated_to_other =
delegation::get_delegated_to_other(this, &deleg);
commit_frequency_ms = Some(deleg.commit_frequency_ms);
delegation::get_delegated_to_other(this, &deleg_record);
commit_frequency_ms = Some(deleg_record.commit_frequency_ms);

if let Some(projected_ata) = this
.maybe_project_delegated_ata_from_eata(
ata_account.account_shared_data(),
&eata_shared,
&deleg,
&deleg_record,
)
{
account_to_clone = projected_ata;
Expand All @@ -147,6 +148,7 @@ where
pubkey: ata_pubkey,
account: account_to_clone,
commit_frequency_ms,
delegation_actions: DelegationActions::default(),
delegated_to_other,
});
}
Expand Down
39 changes: 34 additions & 5 deletions magicblock-chainlink/src/chainlink/fetch_cloner/delegation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,60 @@ use magicblock_accounts_db::traits::AccountsBank;
use magicblock_core::token_programs::EATA_PROGRAM_ID;
use magicblock_metrics::metrics;
use solana_account::ReadableAccount;
use solana_instruction::Instruction;
use solana_program::program_error::ProgramError;
use solana_pubkey::Pubkey;
use tracing::*;

use super::FetchCloner;
use crate::{
chainlink::errors::{ChainlinkError, ChainlinkResult},
cloner::Cloner,
cloner::{Cloner, DelegationActions},
remote_account_provider::{
ChainPubsubClient, ChainRpcClient, MatchSlotsConfig,
ResolvedAccountSharedData,
},
};

/// Parses a delegation record from account data bytes.
/// Returns the parsed DelegationRecord, or InvalidDelegationRecord error
/// if parsing fails.
Comment on lines +23 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update the parser doc comment to include InvalidDelegationActions

The comment currently documents only InvalidDelegationRecord, but this function can now also fail with ChainlinkError::InvalidDelegationActions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@magicblock-chainlink/src/chainlink/fetch_cloner/delegation.rs` around lines
25 - 27, Update the parser doc comment (the one starting "Parses a delegation
record from account data bytes.") to document that the function can return not
only InvalidDelegationRecord but also ChainlinkError::InvalidDelegationActions;
explicitly mention both error variants and the conditions under which
InvalidDelegationActions is produced so readers know the parser may fail for
invalid delegation actions as well as malformed records.

pub(crate) fn parse_delegation_record(
data: &[u8],
delegation_record_pubkey: Pubkey,
) -> ChainlinkResult<DelegationRecord> {
DelegationRecord::try_from_bytes_with_discriminator(data)
) -> ChainlinkResult<(DelegationRecord, Option<DelegationActions>)> {
let delegation_record_size = DelegationRecord::size_with_discriminator();
if data.len() < delegation_record_size {
return Err(ChainlinkError::InvalidDelegationRecord(
delegation_record_pubkey,
ProgramError::InvalidAccountData,
));
}
let record =
DelegationRecord::try_from_bytes_with_discriminator(
&data[..delegation_record_size],
)
.copied()
.map_err(|err| {
ChainlinkError::InvalidDelegationRecord(
delegation_record_pubkey,
err,
)
})
})?;

if data.len() <= delegation_record_size {
Ok((record, None))
} else {
let actions_data = &data[delegation_record_size..];
let actions = bincode::deserialize::<Vec<Instruction>>(actions_data)
.map_err(|err| {
ChainlinkError::InvalidDelegationActions(
delegation_record_pubkey,
err.to_string(),
)
})?;
Ok((record, Some(actions.into())))
}
}

pub(crate) fn apply_delegation_record_to_account<T, U, V, C>(
Expand Down Expand Up @@ -87,7 +116,7 @@ pub(crate) async fn fetch_and_parse_delegation_record<T, U, V, C>(
account_pubkey: Pubkey,
min_context_slot: u64,
fetch_origin: metrics::AccountFetchOrigin,
) -> Option<DelegationRecord>
) -> Option<(DelegationRecord, Option<DelegationActions>)>
where
T: ChainRpcClient,
U: ChainPubsubClient,
Expand Down
61 changes: 45 additions & 16 deletions magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ use crate::{
account_still_undelegating_on_chain::account_still_undelegating_on_chain,
blacklisted_accounts::blacklisted_accounts,
},
cloner::{errors::ClonerResult, AccountCloneRequest, Cloner},
cloner::{
errors::ClonerResult, AccountCloneRequest, Cloner,
DelegationActions,
},
remote_account_provider::{
program_account::get_loaderv3_get_program_data_address,
ChainPubsubClient, ChainRpcClient, ForwardedSubscriptionUpdate,
Expand Down Expand Up @@ -167,7 +170,7 @@ where
// record fetches. This allows multiple updates to be processed in parallel.
let this = Arc::clone(&self);
tokio::spawn(async move {
let (resolved_account, deleg_record) =
let (resolved_account, deleg_record, delegation_actions) =
this.resolve_account_to_clone_from_forwarded_sub_with_unsubscribe(update)
.await;
if let Some(account) = resolved_account {
Expand Down Expand Up @@ -277,6 +280,7 @@ where
pubkey,
account,
commit_frequency_ms: None,
delegation_actions,
delegated_to_other,
})
.await
Expand Down Expand Up @@ -332,7 +336,11 @@ where
async fn resolve_account_to_clone_from_forwarded_sub_with_unsubscribe(
&self,
update: ForwardedSubscriptionUpdate,
) -> (Option<AccountSharedData>, Option<DelegationRecord>) {
) -> (
Option<AccountSharedData>,
Option<DelegationRecord>,
DelegationActions,
) {
let ForwardedSubscriptionUpdate { pubkey, account } = update;
let owned_by_delegation_program =
account.is_owned_by_delegation_program();
Expand Down Expand Up @@ -376,8 +384,8 @@ where
let account = if let Some(delegation_record) =
delegation_record
{
let delegation_record =
match Self::parse_delegation_record(
let delegation_record_with_actions =
match delegation::parse_delegation_record(
delegation_record.data(),
delegation_record_pubkey,
) {
Expand All @@ -394,7 +402,11 @@ where

// If the delegation record is valid we set the owner and delegation
// status on the account
if let Some(delegation_record) = delegation_record {
if let Some((
delegation_record,
delegation_actions,
)) = delegation_record_with_actions
{
if tracing::enabled!(tracing::Level::TRACE) {
let delegation_record_display =
format!("{:?}", delegation_record);
Expand Down Expand Up @@ -438,17 +450,26 @@ where
(
Some(account.into_account_shared_data()),
Some(delegation_record),
delegation_actions.unwrap_or_default(),
)
} else {
// If the delegation record is invalid we cannot clone the account
// since something is corrupt and we wouldn't know what owner to
// use, etc.
(None, None)
(
None,
None,
DelegationActions::default(),
)
}
} else {
// If no delegation record exists we must assume the account itself is
// a delegation record or metadata
(Some(account.into_account_shared_data()), None)
(
Some(account.into_account_shared_data()),
None,
DelegationActions::default(),
)
};

if !subs_to_remove.is_empty() {
Expand All @@ -467,28 +488,32 @@ where
error = %err,
"Failed to fetch delegation record"
);
(None, None)
(None, None, DelegationActions::default())
}
Err(err) => {
error!(
pubkey = %pubkey,
error = %err,
"Failed to fetch delegation record"
);
(None, None)
(None, None, DelegationActions::default())
}
}
} else {
let (account, deleg_record) = self
.maybe_project_ata_from_subscription_update(pubkey, account)
.await;
(Some(account), deleg_record)
(
Some(account),
deleg_record,
DelegationActions::default(),
)
}
} else {
// This should not happen since we call this method with sub updates which always hold
// a fresh remote account
error!(pubkey = %pubkey, account = ?account, "BUG: Received subscription update without fresh account");
(None, None)
(None, None, DelegationActions::default())
}
}

Expand Down Expand Up @@ -547,6 +572,7 @@ where
pubkey: ata_pubkey,
account: projected_ata,
commit_frequency_ms: None,
delegation_actions: DelegationActions::default(),
delegated_to_other: None,
})
}
Expand Down Expand Up @@ -614,6 +640,7 @@ where
let Some(deleg_record) = deleg_record else {
return (ata_account, None);
};
let (deleg_record, _delegation_actions) = deleg_record;

if let Some(projected_ata) = self.maybe_project_delegated_ata_from_eata(
&ata_account,
Expand Down Expand Up @@ -655,7 +682,7 @@ where
fn parse_delegation_record(
data: &[u8],
delegation_record_pubkey: Pubkey,
) -> ChainlinkResult<DelegationRecord> {
) -> ChainlinkResult<(DelegationRecord, Option<DelegationActions>)> {
delegation::parse_delegation_record(data, delegation_record_pubkey)
}

Expand Down Expand Up @@ -691,7 +718,7 @@ where
account_pubkey: Pubkey,
min_context_slot: u64,
fetch_origin: metrics::AccountFetchOrigin,
) -> Option<DelegationRecord> {
) -> Option<(DelegationRecord, Option<DelegationActions>)> {
delegation::fetch_and_parse_delegation_record(
self,
account_pubkey,
Expand Down Expand Up @@ -936,9 +963,10 @@ where
}

let delegated_on_chain = deleg_record.as_ref().is_some_and(|dr| {
dr.authority.eq(&self.validator_pubkey)
|| dr.authority.eq(&Pubkey::default())
dr.0.authority.eq(&self.validator_pubkey)
|| dr.0.authority.eq(&Pubkey::default())
});
let deleg_record = deleg_record.map(|el| el.0);
if !account_still_undelegating_on_chain(
pubkey,
delegated_on_chain,
Expand Down Expand Up @@ -1367,6 +1395,7 @@ where
pubkey,
account,
commit_frequency_ms: None,
delegation_actions: DelegationActions::default(),
delegated_to_other: None,
})
.await?;
Expand Down
Loading