Skip to content

Commit 3afdf37

Browse files
feat(IC-1579): Governance: TLA Codelink for refresh_neuron (dfinity#3547)
Instrument refresh_neuron and its tests for TLA Code Link, add TLA model for refresh neuron. --------- Co-authored-by: oggy-dfin <[email protected]>
1 parent f41794f commit 3afdf37

File tree

6 files changed

+262
-2
lines changed

6 files changed

+262
-2
lines changed

rs/nns/governance/src/governance.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,9 @@ use crate::storage::with_voting_state_machines_mut;
154154
#[cfg(feature = "tla")]
155155
pub use tla::{
156156
tla_update_method, InstrumentationState, ToTla, CLAIM_NEURON_DESC, DISBURSE_NEURON_DESC,
157-
DISBURSE_TO_NEURON_DESC, MERGE_NEURONS_DESC, SPAWN_NEURONS_DESC, SPAWN_NEURON_DESC,
158-
SPLIT_NEURON_DESC, TLA_INSTRUMENTATION_STATE, TLA_TRACES_LKEY, TLA_TRACES_MUTEX,
157+
DISBURSE_TO_NEURON_DESC, MERGE_NEURONS_DESC, REFRESH_NEURON_DESC, SPAWN_NEURONS_DESC,
158+
SPAWN_NEURON_DESC, SPLIT_NEURON_DESC, TLA_INSTRUMENTATION_STATE, TLA_TRACES_LKEY,
159+
TLA_TRACES_MUTEX,
159160
};
160161

161162
// 70 KB (for executing NNS functions that are not canister upgrades)
@@ -6077,6 +6078,7 @@ impl Governance {
60776078
}
60786079

60796080
/// Refreshes the stake of a given neuron by checking it's account.
6081+
#[cfg_attr(feature = "tla", tla_update_method(REFRESH_NEURON_DESC.clone()))]
60806082
async fn refresh_neuron(
60816083
&mut self,
60826084
nid: NeuronId,
@@ -6099,6 +6101,7 @@ impl Governance {
60996101
)?;
61006102

61016103
// Get the balance of the neuron from the ledger canister.
6104+
tla_log_locals! { neuron_id: nid.id };
61026105
let balance = self.ledger.account_balance(account).await?;
61036106
let min_stake = self.economics().neuron_minimum_stake_e8s;
61046107
if balance.get_e8s() < min_stake {

rs/nns/governance/src/governance/tla/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ mod claim_neuron;
2828
mod disburse_neuron;
2929
mod disburse_to_neuron;
3030
mod merge_neurons;
31+
mod refresh_neuron;
3132
mod spawn_neuron;
3233
mod spawn_neurons;
3334
mod split_neuron;
@@ -36,6 +37,7 @@ pub use claim_neuron::CLAIM_NEURON_DESC;
3637
pub use disburse_neuron::DISBURSE_NEURON_DESC;
3738
pub use disburse_to_neuron::DISBURSE_TO_NEURON_DESC;
3839
pub use merge_neurons::MERGE_NEURONS_DESC;
40+
pub use refresh_neuron::REFRESH_NEURON_DESC;
3941
pub use spawn_neuron::SPAWN_NEURON_DESC;
4042
pub use spawn_neurons::SPAWN_NEURONS_DESC;
4143
pub use split_neuron::SPLIT_NEURON_DESC;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
use lazy_static::lazy_static;
2+
use tla_instrumentation::{Label, TlaConstantAssignment, ToTla, Update, VarAssignment};
3+
4+
use super::{extract_common_constants, post_process_trace};
5+
6+
lazy_static! {
7+
pub static ref REFRESH_NEURON_DESC: Update = {
8+
const PID: &str = "Refresh_Neuron";
9+
let default_locals = VarAssignment::new().add("neuron_id", 0_u64.to_tla_value());
10+
11+
Update {
12+
default_start_locals: default_locals.clone(),
13+
default_end_locals: default_locals,
14+
start_label: Label::new("RefreshNeuron1"),
15+
end_label: Label::new("Done"),
16+
process_id: PID.to_string(),
17+
canister_name: "governance".to_string(),
18+
post_process: |trace| {
19+
let constants = TlaConstantAssignment {
20+
constants: extract_common_constants(PID, trace).into_iter().collect(),
21+
};
22+
post_process_trace(trace);
23+
constants
24+
},
25+
}
26+
};
27+
}

rs/nns/governance/tests/governance.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5039,13 +5039,15 @@ fn refresh_neuron_by_memo(owner: PrincipalId, caller: PrincipalId) {
50395039

50405040
/// Tests that a neuron can be refreshed by memo by it's controller.
50415041
#[test]
5042+
#[cfg_attr(feature = "tla", with_tla_trace_check)]
50425043
fn test_refresh_neuron_by_memo_by_controller() {
50435044
let owner = *TEST_NEURON_1_OWNER_PRINCIPAL;
50445045
refresh_neuron_by_memo(owner, owner);
50455046
}
50465047

50475048
/// Tests that a neuron can be refreshed by memo by proxy.
50485049
#[test]
5050+
#[cfg_attr(feature = "tla", with_tla_trace_check)]
50495051
fn test_refresh_neuron_by_memo_by_proxy() {
50505052
let owner = *TEST_NEURON_1_OWNER_PRINCIPAL;
50515053
let caller = *TEST_NEURON_2_OWNER_PRINCIPAL;
@@ -5121,12 +5123,14 @@ fn refresh_neuron_by_id_or_subaccount(
51215123
}
51225124

51235125
#[test]
5126+
#[cfg_attr(feature = "tla", with_tla_trace_check)]
51245127
fn test_refresh_neuron_by_id_by_controller() {
51255128
let owner = *TEST_NEURON_1_OWNER_PRINCIPAL;
51265129
refresh_neuron_by_id_or_subaccount(owner, owner, RefreshBy::NeuronId);
51275130
}
51285131

51295132
#[test]
5133+
#[cfg_attr(feature = "tla", with_tla_trace_check)]
51305134
fn test_refresh_neuron_by_id_by_proxy() {
51315135
let owner = *TEST_NEURON_1_OWNER_PRINCIPAL;
51325136
let caller = *TEST_NEURON_1_OWNER_PRINCIPAL;
@@ -5136,12 +5140,14 @@ fn test_refresh_neuron_by_id_by_proxy() {
51365140
/// Tests that a neuron can be refreshed by subaccount, and that anyone can do
51375141
/// it.
51385142
#[test]
5143+
#[cfg_attr(feature = "tla", with_tla_trace_check)]
51395144
fn test_refresh_neuron_by_subaccount_by_controller() {
51405145
let owner = *TEST_NEURON_1_OWNER_PRINCIPAL;
51415146
refresh_neuron_by_id_or_subaccount(owner, owner, RefreshBy::Subaccount);
51425147
}
51435148

51445149
#[test]
5150+
#[cfg_attr(feature = "tla", with_tla_trace_check)]
51455151
fn test_refresh_neuron_by_subaccount_by_proxy() {
51465152
let owner = *TEST_NEURON_1_OWNER_PRINCIPAL;
51475153
let caller = *TEST_NEURON_1_OWNER_PRINCIPAL;
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
------------ MODULE Refresh_Neuron ------------
2+
EXTENDS TLC, Sequences, Naturals, FiniteSets, Variants
3+
4+
CONSTANT
5+
FRESH_NEURON_ID(_)
6+
7+
CONSTANTS
8+
Governance_Account_Ids,
9+
Neuron_Ids
10+
11+
CONSTANTS
12+
Refresh_Neuron_Process_Ids
13+
14+
CONSTANTS
15+
\* Minimum stake a neuron can have
16+
MIN_STAKE,
17+
\* The transfer fee charged by the ledger canister
18+
TRANSACTION_FEE
19+
20+
DUMMY_ACCOUNT == ""
21+
22+
\* @type: (a -> b, Set(a)) => a -> b;
23+
Remove_Arguments(f, S) == [ x \in (DOMAIN f \ S) |-> f[x]]
24+
25+
request(caller, request_args) == [caller |-> caller, method_and_args |-> request_args]
26+
account_balance(account) == Variant("AccountBalance", [account |-> account])
27+
28+
29+
(* --algorithm Governance_Ledger_Refresh_Neuron {
30+
31+
variables
32+
33+
neuron \in [{} -> {}];
34+
\* Used to decide whether we should refresh or claim a neuron
35+
neuron_id_by_account \in [{} -> {}];
36+
\* The set of currently locked neurons
37+
locks = {};
38+
\* The queue of messages sent from the governance canister to the ledger canister
39+
governance_to_ledger = <<>>;
40+
ledger_to_governance = {};
41+
spawning_neurons = FALSE;
42+
43+
macro refresh_neuron_reset_local_vars() {
44+
neuron_id := 0;
45+
}
46+
47+
48+
\* Modified from formal-models/tla/governance-ledger
49+
\* A Refresh_Neuron process simulates a call to refresh_neuron
50+
process ( Refresh_Neuron \in Refresh_Neuron_Process_Ids )
51+
variable
52+
\* There are two ways that the user can invoke a neuron refresh:
53+
\* 1. by specifying an account ID
54+
\* 2. by specifying an existing neuron ID
55+
\* We only model the second option; the second should follow from the invariant that
56+
\* \A nid aid : neuron_id_by_account[aid] = nid <=> neuron[nid].account = aid
57+
58+
\* The neuron_id is an argument; we let it be chosen non-deteministically
59+
neuron_id = 0;
60+
{
61+
RefreshNeuron1:
62+
either {
63+
\* Simulate calls that just fail early and don't change the state.
64+
\* Not so useful for model checking, but needed to follow the code traces.
65+
goto Done;
66+
} or {
67+
with(nid \in DOMAIN(neuron) \ locks) {
68+
neuron_id := nid;
69+
locks := locks \union {neuron_id};
70+
governance_to_ledger := Append(governance_to_ledger, request(self, account_balance(neuron[nid].account)));
71+
};
72+
};
73+
WaitForBalanceQuery:
74+
\* Note that the "with" construct implicitly awaits until the set of values to draw from is non-empty
75+
with(answer \in { resp \in ledger_to_governance : resp.caller = self }) {
76+
ledger_to_governance := ledger_to_governance \ {answer};
77+
if(answer.response /= Variant("Fail", UNIT)) {
78+
with (b = VariantGetOrElse("BalanceQueryOk", answer.response, 0)) {
79+
if(b >= MIN_STAKE) {
80+
neuron := [neuron EXCEPT ![neuron_id].cached_stake = b ]
81+
};
82+
};
83+
};
84+
locks := locks \ {neuron_id};
85+
};
86+
refresh_neuron_reset_local_vars();
87+
};
88+
89+
}
90+
*)
91+
\* BEGIN TRANSLATION (chksum(pcal) = "e3951dde" /\ chksum(tla) = "3003d9f1")
92+
VARIABLES neuron, neuron_id_by_account, locks, governance_to_ledger,
93+
ledger_to_governance, spawning_neurons, pc, neuron_id
94+
95+
vars == << neuron, neuron_id_by_account, locks, governance_to_ledger,
96+
ledger_to_governance, spawning_neurons, pc, neuron_id >>
97+
98+
ProcSet == (Refresh_Neuron_Process_Ids)
99+
100+
Init == (* Global variables *)
101+
/\ neuron \in [{} -> {}]
102+
/\ neuron_id_by_account \in [{} -> {}]
103+
/\ locks = {}
104+
/\ governance_to_ledger = <<>>
105+
/\ ledger_to_governance = {}
106+
/\ spawning_neurons = FALSE
107+
(* Process Refresh_Neuron *)
108+
/\ neuron_id = [self \in Refresh_Neuron_Process_Ids |-> 0]
109+
/\ pc = [self \in ProcSet |-> "RefreshNeuron1"]
110+
111+
RefreshNeuron1(self) == /\ pc[self] = "RefreshNeuron1"
112+
/\ \/ /\ pc' = [pc EXCEPT ![self] = "Done"]
113+
/\ UNCHANGED <<locks, governance_to_ledger, neuron_id>>
114+
\/ /\ \E nid \in DOMAIN(neuron) \ locks:
115+
/\ neuron_id' = [neuron_id EXCEPT ![self] = nid]
116+
/\ locks' = (locks \union {neuron_id'[self]})
117+
/\ governance_to_ledger' = Append(governance_to_ledger, request(self, account_balance(neuron[nid].account)))
118+
/\ pc' = [pc EXCEPT ![self] = "WaitForBalanceQuery"]
119+
/\ UNCHANGED << neuron, neuron_id_by_account,
120+
ledger_to_governance, spawning_neurons >>
121+
122+
WaitForBalanceQuery(self) == /\ pc[self] = "WaitForBalanceQuery"
123+
/\ \E answer \in { resp \in ledger_to_governance : resp.caller = self }:
124+
/\ ledger_to_governance' = ledger_to_governance \ {answer}
125+
/\ IF answer.response /= Variant("Fail", UNIT)
126+
THEN /\ LET b == VariantGetOrElse("BalanceQueryOk", answer.response, 0) IN
127+
IF b >= MIN_STAKE
128+
THEN /\ neuron' = [neuron EXCEPT ![neuron_id[self]].cached_stake = b ]
129+
ELSE /\ TRUE
130+
/\ UNCHANGED neuron
131+
ELSE /\ TRUE
132+
/\ UNCHANGED neuron
133+
/\ locks' = locks \ {neuron_id[self]}
134+
/\ neuron_id' = [neuron_id EXCEPT ![self] = 0]
135+
/\ pc' = [pc EXCEPT ![self] = "Done"]
136+
/\ UNCHANGED << neuron_id_by_account,
137+
governance_to_ledger,
138+
spawning_neurons >>
139+
140+
Refresh_Neuron(self) == RefreshNeuron1(self) \/ WaitForBalanceQuery(self)
141+
142+
(* Allow infinite stuttering to prevent deadlock on termination. *)
143+
Terminating == /\ \A self \in ProcSet: pc[self] = "Done"
144+
/\ UNCHANGED vars
145+
146+
Next == (\E self \in Refresh_Neuron_Process_Ids: Refresh_Neuron(self))
147+
\/ Terminating
148+
149+
Spec == Init /\ [][Next]_vars
150+
151+
Termination == <>(\A self \in ProcSet: pc[self] = "Done")
152+
153+
\* END TRANSLATION
154+
155+
====
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---- MODULE Refresh_Neuron_Apalache ----
2+
3+
4+
EXTENDS TLC, Variants
5+
6+
(*
7+
@typeAlias: proc = Str;
8+
@typeAlias: account = Str;
9+
@typeAlias: neuronId = Int;
10+
@typeAlias: methodCall = Transfer({ from: $account, to: $account, amount: Int, fee: Int}) | AccountBalance({ account: $account });
11+
@typeAlias: methodResponse = Fail(UNIT) | TransferOk(UNIT) | BalanceQueryOk(Int);
12+
*)
13+
_type_alias_dummy == TRUE
14+
15+
\* CODE_LINK_INSERT_CONSTANTS
16+
17+
(*
18+
CONSTANTS
19+
\* @type: Set($account);
20+
Account_Ids,
21+
\* @type: Set($account);
22+
Governance_Account_Ids,
23+
\* @type: Set($neuronId);
24+
Neuron_Ids
25+
26+
CONSTANTS
27+
\* @type: Set($proc);
28+
Refresh_Neuron_Process_Ids
29+
30+
CONSTANTS
31+
\* Minimum stake a neuron can have
32+
\* @type: Int;
33+
MIN_STAKE,
34+
\* The transfer fee charged by the ledger canister
35+
\* @type: Int;
36+
TRANSACTION_FEE
37+
*)
38+
39+
VARIABLES
40+
\* @type: $neuronId -> {cached_stake: Int, account: $account, maturity: Int, fees: Int};
41+
neuron,
42+
\* @type: $account -> $neuronId;
43+
neuron_id_by_account,
44+
\* @type: Set($neuronId);
45+
locks,
46+
\* @type: Seq({caller : $proc, method_and_args: $methodCall });
47+
governance_to_ledger,
48+
\* @type: Set({caller: $proc, response: $methodResponse });
49+
ledger_to_governance,
50+
\* @type: $proc -> Str;
51+
pc,
52+
\* @type: $proc -> Int;
53+
neuron_id,
54+
\* Not used by this model, but it's a global variable used by spawn_neurons, so
55+
\* it's the easiest to just add it to all the other models
56+
\* @type: Bool;
57+
spawning_neurons
58+
59+
\* Not used in this model. Consider removing (TODO).
60+
\* @type: Set($neuronId) => $neuronId;
61+
FRESH_NEURON_ID(existing_neurons) == CHOOSE nid \in (Neuron_Ids \ existing_neurons): TRUE
62+
63+
MOD == INSTANCE Refresh_Neuron
64+
65+
Next == [MOD!Next]_MOD!vars
66+
67+
====

0 commit comments

Comments
 (0)