Skip to content

Commit f80c598

Browse files
authored
feat: detecting accounts stuck in undelegating state and fixing that (#664)
1 parent 91c606b commit f80c598

File tree

12 files changed

+494
-165
lines changed

12 files changed

+494
-165
lines changed

magicblock-chainlink/src/accounts_bank.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ pub mod mock {
5151
}
5252

5353
pub fn undelegate(&self, pubkey: &Pubkey) -> &Self {
54+
self.set_undelegating(pubkey, true);
5455
self.set_delegated(pubkey, false)
5556
}
5657

@@ -64,6 +65,21 @@ pub mod mock {
6465
self.set_owner(pubkey, dlp::id()).undelegate(pubkey);
6566
}
6667

68+
pub fn set_undelegating(
69+
&self,
70+
pubkey: &Pubkey,
71+
undelegating: bool,
72+
) -> &Self {
73+
trace!("Set account {pubkey} undelegating={undelegating}");
74+
let mut accounts = self.accounts.lock().unwrap();
75+
if let Some(account) = accounts.get_mut(pubkey) {
76+
account.set_undelegating(undelegating);
77+
} else {
78+
panic!("Account not found in bank: {pubkey}");
79+
}
80+
self
81+
}
82+
6783
#[allow(dead_code)]
6884
pub fn dump_account_keys(&self, include_blacklisted: bool) -> String {
6985
let mut output = String::new();
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
use dlp::state::DelegationRecord;
2+
use log::*;
3+
use solana_pubkey::Pubkey;
4+
5+
/// Decides if an account that is undelegating should be updated
6+
/// (overwritten) by the remote account state and the `undelegating` flag cleared.
7+
///
8+
/// The only case when an account should not be updated is when the following is true:
9+
///
10+
/// - account is still delegated to us on chain
11+
/// - the delegation slot is older than the slot at which we last fetched
12+
/// the account state from chain.
13+
///
14+
/// # Arguments
15+
/// - `pubkey`: the account pubkey
16+
/// - `is_delegated_on_chain`: whether the account is currently delegated to us on chain
17+
/// - `remote_slot_in_bank`: the chain slot at which we last fetched and cloned state
18+
/// of the account in our bank
19+
/// - `delegation_record`: the delegation record associated with the account in our bank, if found
20+
/// - `validator_auth`: the validator authority pubkey
21+
/// - returns `true` if the account is still undelegating, `false` otherwise.
22+
pub(crate) fn account_still_undelegating_on_chain(
23+
pubkey: &Pubkey,
24+
is_delegated_to_us_on_chain: bool,
25+
remote_slot_in_bank: u64,
26+
deleg_record: Option<DelegationRecord>,
27+
validator_auth: &Pubkey,
28+
) -> bool {
29+
// In the case of a subscription update for an account that was undelegating
30+
// we know that the undelegation or associated commit or possibly a previous
31+
// commit made after we subscribed to the account was completed, otherwise
32+
// there would be no update.
33+
//
34+
// Now the account could be in one the following states:
35+
//
36+
// A) the account was undelegated and remained so
37+
// B) the account was undelegated and was re-delegated to us or the system
38+
// program (broadcast account)
39+
// C) the account was undelegated and was re-delegated to another validator
40+
// D) the account's undelegation request did not complete.
41+
// In case of a subscription update the commit (or a commit scheduled previously) did trigger an update.
42+
// Alternatively someone may be accessing the account while undelegation is still pending.
43+
// Thus the account is still delegated to us on chain.
44+
//
45+
// In the case of D) we want to keep the bank version of the account.
46+
//
47+
// In all other cases we want to clone the remote version of the account into
48+
// our bank which will automatically set the correct delegated state and
49+
// untoggle the undelegating flag.
50+
if is_delegated_to_us_on_chain {
51+
// B) or D)
52+
// Since the account was found to be delegated we must have
53+
// found a delegation record and thus have the delegation slot.
54+
let delegation_slot = deleg_record
55+
.as_ref()
56+
.map(|d| d.delegation_slot)
57+
.unwrap_or_default();
58+
if delegation_slot < remote_slot_in_bank {
59+
// The last update of the account was after the last delegation
60+
// Therefore the account was not redelegated which indicates
61+
// that the undelegation is still not completed. Case (D))
62+
debug!(
63+
"Undelegation for {pubkey} is still pending. Keeping bank account.",
64+
);
65+
true
66+
} else {
67+
// This is a re-delegation to us after undelegation completed.
68+
// Case (B))
69+
debug!(
70+
"Undelegation completed for account {pubkey} and it was re-delegated to us at slot: ({delegation_slot}).",
71+
);
72+
magicblock_metrics::metrics::inc_undelegation_completed();
73+
false
74+
}
75+
} else if let Some(deleg_record) = deleg_record {
76+
// Account delegated to other (Case C)) -> clone as is
77+
debug!(
78+
"Account {pubkey} was undelegated and re-delegated to another validator. authority: {}, delegated_to: {}",
79+
validator_auth, deleg_record.authority
80+
);
81+
magicblock_metrics::metrics::inc_undelegation_completed();
82+
false
83+
} else {
84+
// Account no longer delegated (Case A)) -> clone as is
85+
debug!("Account {pubkey} was undelegated and remained so");
86+
magicblock_metrics::metrics::inc_undelegation_completed();
87+
false
88+
}
89+
}
90+
91+
#[cfg(test)]
92+
mod tests {
93+
use dlp::state::DelegationRecord;
94+
use solana_pubkey::Pubkey;
95+
96+
use super::*;
97+
98+
fn create_delegation_record(delegation_slot: u64) -> DelegationRecord {
99+
DelegationRecord {
100+
authority: Pubkey::default(),
101+
owner: Pubkey::default(),
102+
delegation_slot,
103+
lamports: 1000,
104+
commit_frequency_ms: 100,
105+
}
106+
}
107+
108+
#[test]
109+
fn test_account_was_undelegated_and_remained_so() {
110+
// Case A: The account was undelegated and remained so.
111+
// Conditions:
112+
// - is_delegated: false (account is not delegated to us on chain)
113+
// - deleg_record: None (no delegation record associated)
114+
// Expected: true (should override/clone as is)
115+
116+
let pubkey = Pubkey::default();
117+
let is_delegated = false;
118+
let remote_slot = 100;
119+
let deleg_record = None;
120+
121+
assert!(!account_still_undelegating_on_chain(
122+
&pubkey,
123+
is_delegated,
124+
remote_slot,
125+
deleg_record,
126+
&Pubkey::default(),
127+
));
128+
}
129+
130+
#[test]
131+
fn test_account_was_undelegated_and_redelegated_to_us() {
132+
// Case B: The account was undelegated and was re-delegated to us.
133+
// Conditions:
134+
// - is_delegated: true (account is delegated to us on chain)
135+
// - delegation_slot >= remote_slot (delegation happend after we last updated the account)
136+
// Expected: true (should override/update)
137+
138+
let pubkey = Pubkey::default();
139+
let is_delegated = true;
140+
let remote_slot = 100;
141+
142+
// Subcase B1: delegation_slot == remote_slot
143+
let delegation_slot = 100;
144+
let deleg_record = Some(create_delegation_record(delegation_slot));
145+
assert!(!account_still_undelegating_on_chain(
146+
&pubkey,
147+
is_delegated,
148+
remote_slot,
149+
deleg_record,
150+
&Pubkey::default(),
151+
));
152+
153+
// Subcase B2: delegation_slot > remote_slot
154+
let delegation_slot = 101;
155+
let deleg_record = Some(create_delegation_record(delegation_slot));
156+
assert!(!account_still_undelegating_on_chain(
157+
&pubkey,
158+
is_delegated,
159+
remote_slot,
160+
deleg_record,
161+
&Pubkey::default(),
162+
));
163+
}
164+
165+
#[test]
166+
fn test_account_was_undelegated_and_redelegated_to_another() {
167+
// Case C: The account was undelegated and then re-delegated to another validator.
168+
// Conditions:
169+
// - is_delegated: false (not delegated to us on chain)
170+
// - deleg_record: Some(...) (but record exists, maybe describing delegation to another)
171+
// Expected: true (should override/clone as is)
172+
173+
let pubkey = Pubkey::default();
174+
let is_delegated = false;
175+
let remote_slot = 100;
176+
// Value doesn't matter for this path
177+
let delegation_slot = 90;
178+
let deleg_record = Some(create_delegation_record(delegation_slot));
179+
180+
assert!(!account_still_undelegating_on_chain(
181+
&pubkey,
182+
is_delegated,
183+
remote_slot,
184+
deleg_record,
185+
&Pubkey::default(),
186+
));
187+
}
188+
189+
#[test]
190+
fn test_account_undelegation_pending() {
191+
// Case D: The account's undelegation request did not complete.
192+
// Conditions:
193+
// - is_delegated: true
194+
// - delegation_slot < remote_slot (delegation is older than remote update, implying pending undelegation)
195+
// Expected: false (should NOT override, keep bank account)
196+
197+
let pubkey = Pubkey::default();
198+
let is_delegated = true;
199+
let remote_slot = 100;
200+
let delegation_slot = 99;
201+
let deleg_record = Some(create_delegation_record(delegation_slot));
202+
203+
assert!(account_still_undelegating_on_chain(
204+
&pubkey,
205+
is_delegated,
206+
remote_slot,
207+
deleg_record,
208+
&Pubkey::default(),
209+
));
210+
}
211+
}

0 commit comments

Comments
 (0)