Skip to content

Commit aabb1d9

Browse files
committed
feat: add lamport snapshot helpers
1 parent 5de7967 commit aabb1d9

File tree

8 files changed

+81
-32
lines changed

8 files changed

+81
-32
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
target/

library/kani_solana_agent/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,7 @@ publish = false
1111
[dependencies]
1212
solana-program = "=3.0.0"
1313

14+
[lints.rust]
15+
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(kani)"] }
16+
1417
[workspace]

library/kani_solana_agent/README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,25 @@ Non-goals:
77
- Anchor-specific modeling
88
- Token program semantics
99

10-
The primary entry point is `any_account_info::<DATA_LEN>(...)`, which constructs an `AccountInfo<'static>` backed by leaked allocations so it can be passed into program logic during verification.
10+
The primary entry point is `any_account_info::<DATA_LEN>(...)` (alias: `any_agent_account::<DATA_LEN>(...)`), which constructs an `AccountInfo<'static>` backed by leaked allocations so it can be passed into program logic during verification.
1111

1212
## Example
1313

1414
```rust
1515
#[cfg(kani)]
1616
mod proofs {
17-
use kani_solana_agent::{AccountConfig, any_account_info};
17+
use kani_solana_agent::{AccountConfig, LamportSnapshot, any_agent_account};
1818

1919
#[kani::proof]
2020
fn can_build_accounts() {
21-
let payer = any_account_info::<0>(AccountConfig::new().payer());
22-
let escrow = any_account_info::<128>(AccountConfig::new().writable());
21+
let payer = any_agent_account::<0>(AccountConfig::new().payer());
22+
let escrow = any_agent_account::<128>(AccountConfig::new().writable());
2323

2424
kani::assert(payer.is_signer, "payer must be a signer");
2525
kani::assert(escrow.is_writable, "escrow must be writable");
26+
27+
let before = LamportSnapshot::new(&[&payer, &escrow]);
28+
before.assert_unchanged(&[&payer, &escrow], "snapshot must be stable without mutation");
2629
}
2730
}
2831
```

library/kani_solana_agent/src/account.rs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33

44
use solana_program::account_info::AccountInfo;
55
use solana_program::pubkey::Pubkey;
6+
use std::ops::RangeInclusive;
7+
8+
#[cfg(kani)]
69
use solana_program::rent::Rent;
10+
#[cfg(kani)]
711
use std::cell::RefCell;
8-
use std::ops::RangeInclusive;
12+
#[cfg(kani)]
913
use std::rc::Rc;
1014

1115
#[derive(Clone, Debug)]
@@ -100,14 +104,13 @@ impl AccountConfig {
100104
}
101105
}
102106

103-
#[cfg(kani)]
104-
fn any_pubkey() -> Pubkey {
105-
Pubkey::new_from_array(kani::any())
107+
pub fn any_agent_account<const DATA_LEN: usize>(cfg: AccountConfig) -> AccountInfo<'static> {
108+
any_account_info::<DATA_LEN>(cfg)
106109
}
107110

108-
#[cfg(not(kani))]
111+
#[cfg(kani)]
109112
fn any_pubkey() -> Pubkey {
110-
Pubkey::default()
113+
Pubkey::new_from_array(kani::any())
111114
}
112115

113116
#[cfg(kani)]
@@ -129,12 +132,6 @@ fn pick_lamports<const DATA_LEN: usize>(cfg: &AccountConfig) -> u64 {
129132
lamports
130133
}
131134

132-
#[cfg(not(kani))]
133-
fn pick_lamports<const DATA_LEN: usize>(_cfg: &AccountConfig) -> u64 {
134-
let _ = DATA_LEN;
135-
0
136-
}
137-
138135
#[cfg(kani)]
139136
pub fn any_account_info<const DATA_LEN: usize>(cfg: AccountConfig) -> AccountInfo<'static> {
140137
let key = cfg.key.unwrap_or_else(any_pubkey);
@@ -161,7 +158,16 @@ pub fn any_account_info<const DATA_LEN: usize>(cfg: AccountConfig) -> AccountInf
161158
}
162159

163160
#[cfg(not(kani))]
164-
pub fn any_account_info<const DATA_LEN: usize>(_cfg: AccountConfig) -> AccountInfo<'static> {
161+
pub fn any_account_info<const DATA_LEN: usize>(cfg: AccountConfig) -> AccountInfo<'static> {
165162
let _ = DATA_LEN;
163+
match cfg.lamports {
164+
Lamports::Any => {}
165+
Lamports::Exact(v) => {
166+
let _ = v;
167+
}
168+
Lamports::Range(r) => {
169+
let _ = r;
170+
}
171+
}
166172
panic!("any_account_info is only available under `cargo kani` (cfg(kani))");
167173
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
use solana_program::account_info::AccountInfo;
5+
6+
pub fn lamports(account: &AccountInfo<'_>) -> u64 {
7+
**account.lamports.borrow()
8+
}
9+
10+
pub fn sum_lamports(accounts: &[&AccountInfo<'_>]) -> u128 {
11+
accounts.iter().map(|a| lamports(a) as u128).sum()
12+
}
13+
14+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15+
pub struct LamportSnapshot(pub u128);
16+
17+
impl LamportSnapshot {
18+
pub fn new(accounts: &[&AccountInfo<'_>]) -> Self {
19+
Self(sum_lamports(accounts))
20+
}
21+
22+
pub fn assert_unchanged(self, accounts: &[&AccountInfo<'_>], msg: &'static str) {
23+
let after = sum_lamports(accounts);
24+
25+
#[cfg(kani)]
26+
kani::assert(after == self.0, msg);
27+
28+
#[cfg(not(kani))]
29+
assert!(after == self.0, "{msg}");
30+
}
31+
}

library/kani_solana_agent/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
33

44
pub mod account;
5+
pub mod invariants;
56

6-
pub use account::{AccountConfig, any_account_info};
7+
pub use account::{AccountConfig, any_account_info, any_agent_account};
8+
pub use invariants::{LamportSnapshot, lamports, sum_lamports};
79
pub use solana_program::account_info::AccountInfo;
810
pub use solana_program::pubkey::Pubkey;

tests/cargo-kani/solana-accountinfo/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ edition = "2018"
1010
kani-solana-agent = { path = "../../../library/kani_solana_agent" }
1111
solana-program = "=3.0.0"
1212

13+
[lints.rust]
14+
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(kani)"] }
15+
1316
[workspace]

tests/cargo-kani/solana-accountinfo/src/lib.rs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
// Copyright Kani Contributors
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
33

4-
use kani_solana_agent::{AccountConfig, AccountInfo, any_account_info};
4+
#[cfg(kani)]
5+
use kani_solana_agent::AccountInfo;
56

7+
#[cfg(kani)]
68
fn authorized(now: i64, expires_at: i64, agent: &AccountInfo<'_>, api: &AccountInfo<'_>) -> bool {
79
agent.is_signer || (api.is_signer && now >= expires_at)
810
}
911

10-
fn lamports(account: &AccountInfo<'_>) -> u64 {
11-
**account.lamports.borrow()
12-
}
13-
12+
#[cfg(kani)]
1413
fn transfer(from: &AccountInfo<'_>, to: &AccountInfo<'_>, amount: u64) -> Result<(), ()> {
1514
let mut from_lamports = from.lamports.borrow_mut();
1615
let Some(new_from) = (**from_lamports).checked_sub(amount) else {
@@ -27,6 +26,7 @@ fn transfer(from: &AccountInfo<'_>, to: &AccountInfo<'_>, amount: u64) -> Result
2726
Ok(())
2827
}
2928

29+
#[cfg(kani)]
3030
fn release_funds(
3131
now: i64,
3232
expires_at: i64,
@@ -46,14 +46,15 @@ fn release_funds(
4646
#[cfg(kani)]
4747
mod proofs {
4848
use super::*;
49+
use kani_solana_agent::{AccountConfig, LamportSnapshot, any_agent_account, lamports};
4950

5051
#[kani::proof]
5152
fn timelock_policy_matches_release_rule() {
5253
let now: i64 = kani::any();
5354
let expires_at: i64 = kani::any();
5455

55-
let mut agent = any_account_info::<0>(AccountConfig::new());
56-
let mut api = any_account_info::<0>(AccountConfig::new());
56+
let mut agent = any_agent_account::<0>(AccountConfig::new());
57+
let mut api = any_agent_account::<0>(AccountConfig::new());
5758
agent.is_signer = kani::any();
5859
api.is_signer = kani::any();
5960

@@ -75,31 +76,30 @@ mod proofs {
7576
let expires_at: i64 = kani::any();
7677
let amount: u64 = kani::any::<u32>() as u64;
7778

78-
let mut agent = any_account_info::<0>(AccountConfig::new());
79-
let mut api = any_account_info::<0>(AccountConfig::new());
79+
let mut agent = any_agent_account::<0>(AccountConfig::new());
80+
let mut api = any_agent_account::<0>(AccountConfig::new());
8081
agent.is_signer = kani::any();
8182
api.is_signer = kani::any();
8283

83-
let escrow = any_account_info::<0>(
84+
let escrow = any_agent_account::<0>(
8485
AccountConfig::new().writable().lamports_range(0..=(u32::MAX as u64)),
8586
);
86-
let payee = any_account_info::<0>(
87+
let payee = any_agent_account::<0>(
8788
AccountConfig::new().writable().lamports_range(0..=(u32::MAX as u64)),
8889
);
8990

9091
let escrow_before = lamports(&escrow);
9192
let payee_before = lamports(&payee);
9293
kani::assume(escrow_before >= amount);
9394

94-
let total_before = escrow_before as u128 + payee_before as u128;
95+
let total_before = LamportSnapshot::new(&[&escrow, &payee]);
9596

9697
let res = release_funds(now, expires_at, &agent, &api, &escrow, &payee, amount);
9798
let escrow_after = lamports(&escrow);
9899
let payee_after = lamports(&payee);
99100

100101
if res.is_ok() {
101-
let total_after = escrow_after as u128 + payee_after as u128;
102-
kani::assert(total_after == total_before, "release must conserve lamports");
102+
total_before.assert_unchanged(&[&escrow, &payee], "release must conserve lamports");
103103
kani::assert(escrow_after + amount == escrow_before, "escrow must decrease by amount");
104104
kani::assert(payee_before + amount == payee_after, "payee must increase by amount");
105105
} else {

0 commit comments

Comments
 (0)