-
Notifications
You must be signed in to change notification settings - Fork 305
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
pmonitor: create client activity monitor (#4844)
Creates a new `pmonitor` tool that allows for tracking balances across multiple accounts over time, given an input list of FVKs, specified in JSON. Separate directories are maintained for each wallet's view database, so that network syncs are stored locally and faster on subsequent updates. The tool will exit non-zero if non-compliance was detected. Includes integration tests for common use cases, to guard against regressions. Closes #4832. Co-authored-by: Conor Schaefer <[email protected]>
- Loading branch information
1 parent
783db09
commit 954e777
Showing
18 changed files
with
1,768 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,12 +5,13 @@ on: | |
paths-ignore: | ||
- 'docs/**' | ||
|
||
concurrency: | ||
group: ${{ github.workflow }}-${{ github.ref }} | ||
cancel-in-progress: true | ||
|
||
jobs: | ||
smoke_test: | ||
runs-on: buildjet-16vcpu-ubuntu-2204 | ||
concurrency: | ||
group: ${{ github.workflow }}-${{ github.ref }} | ||
cancel-in-progress: true | ||
environment: smoke-test | ||
steps: | ||
- uses: actions/checkout@v4 | ||
|
@@ -39,3 +40,30 @@ jobs: | |
- name: Display smoke-test logs | ||
if: always() | ||
run: cat deployments/logs/smoke-*.log | ||
|
||
pmonitor-integration: | ||
runs-on: buildjet-16vcpu-ubuntu-2204 | ||
steps: | ||
- uses: actions/checkout@v4 | ||
with: | ||
lfs: true | ||
|
||
- name: install nix | ||
uses: nixbuild/nix-quick-install-action@v28 | ||
|
||
- name: setup nix cache | ||
uses: nix-community/cache-nix-action@v5 | ||
with: | ||
primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix') }} | ||
restore-prefixes-first-match: nix-${{ runner.os }}- | ||
backend: buildjet | ||
|
||
- name: Load rust cache | ||
uses: astriaorg/[email protected] | ||
|
||
# Confirm that the nix devshell is buildable and runs at all. | ||
- name: validate nix env | ||
run: nix develop --command echo hello | ||
|
||
- name: run the pmonitor integration tests | ||
run: nix develop --command just test-pmonitor |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
[package] | ||
name = "pmonitor" | ||
version = { workspace = true } | ||
authors = { workspace = true } | ||
edition = { workspace = true } | ||
repository = { workspace = true } | ||
homepage = { workspace = true } | ||
license = { workspace = true } | ||
publish = false | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[dependencies] | ||
anyhow = {workspace = true} | ||
camino = {workspace = true} | ||
clap = {workspace = true, features = ["derive", "env"]} | ||
colored = "2.1.0" | ||
directories = {workspace = true} | ||
futures = {workspace = true} | ||
indicatif = {workspace = true} | ||
pcli = {path = "../pcli", default-features = true} | ||
penumbra-app = {workspace = true} | ||
penumbra-asset = {workspace = true, default-features = false} | ||
penumbra-compact-block = {workspace = true, default-features = false} | ||
penumbra-keys = {workspace = true, default-features = false} | ||
penumbra-num = {workspace = true, default-features = false} | ||
penumbra-proto = {workspace = true} | ||
penumbra-shielded-pool = {workspace = true, default-features = false} | ||
penumbra-stake = {workspace = true, default-features = false} | ||
penumbra-tct = {workspace = true, default-features = false} | ||
penumbra-view = {workspace = true} | ||
regex = {workspace = true} | ||
serde = {workspace = true, features = ["derive"]} | ||
serde_json = {workspace = true} | ||
tokio = {workspace = true, features = ["full"]} | ||
toml = {workspace = true} | ||
tonic = {workspace = true, features = ["tls-webpki-roots", "tls"]} | ||
tracing = {workspace = true} | ||
tracing-subscriber = { workspace = true, features = ["env-filter", "ansi"] } | ||
url = {workspace = true, features = ["serde"]} | ||
uuid = { version = "1.3", features = ["v4", "serde"] } | ||
|
||
[dev-dependencies] | ||
assert_cmd = {workspace = true} | ||
once_cell = {workspace = true} | ||
tempfile = {workspace = true} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
//! Logic for reading and writing config files for `pmonitor`, in the TOML format. | ||
use anyhow::Result; | ||
use regex::Regex; | ||
use serde::{Deserialize, Serialize}; | ||
use url::Url; | ||
use uuid::Uuid; | ||
|
||
use penumbra_keys::FullViewingKey; | ||
use penumbra_num::Amount; | ||
|
||
#[derive(Clone, Debug, Serialize, Deserialize)] | ||
pub struct FvkEntry { | ||
pub fvk: FullViewingKey, | ||
pub wallet_id: Uuid, | ||
} | ||
|
||
#[derive(Clone, Debug, Serialize, Deserialize)] | ||
/// Representation of a single Penumbra wallet to track. | ||
pub struct AccountConfig { | ||
/// The initial [FullViewingKey] has specified during `pmonitor init`. | ||
/// | ||
/// Distinct because the tool understands account migrations. | ||
original: FvkEntry, | ||
/// The amount held by the account at the time of genesis. | ||
genesis_balance: Amount, | ||
/// List of account migrations, performed via `pcli migrate balance`, if any. | ||
migrations: Vec<FvkEntry>, | ||
} | ||
|
||
impl AccountConfig { | ||
pub fn new(original: FvkEntry, genesis_balance: Amount) -> Self { | ||
Self { | ||
original, | ||
genesis_balance, | ||
migrations: vec![], | ||
} | ||
} | ||
|
||
/// Get original/genesis FVK. | ||
pub fn original_fvk(&self) -> FullViewingKey { | ||
self.original.fvk.clone() | ||
} | ||
|
||
/// Get genesis balance. | ||
pub fn genesis_balance(&self) -> Amount { | ||
self.genesis_balance | ||
} | ||
|
||
/// Add migration to the account config. | ||
pub fn add_migration(&mut self, fvk_entry: FvkEntry) { | ||
self.migrations.push(fvk_entry); | ||
} | ||
|
||
/// Get the active wallet, which is the last migration or the original FVK if no migrations have occurred. | ||
pub fn active_wallet(&self) -> FvkEntry { | ||
if self.migrations.is_empty() { | ||
self.original.clone() | ||
} else { | ||
self.migrations | ||
.last() | ||
.expect("migrations must not be empty") | ||
.clone() | ||
} | ||
} | ||
|
||
pub fn active_fvk(&self) -> FullViewingKey { | ||
self.active_wallet().fvk | ||
} | ||
|
||
pub fn active_uuid(&self) -> Uuid { | ||
self.active_wallet().wallet_id | ||
} | ||
} | ||
|
||
#[derive(Clone, Debug, Serialize, Deserialize)] | ||
/// The primary TOML file for configuring `pmonitor`, containing all its account info. | ||
/// | ||
/// During `pmonitor audit` runs, the config will be automatically updated | ||
/// if tracked FVKs were detected to migrate, via `pcli migrate balance`, to save time | ||
/// on future syncs. | ||
pub struct PmonitorConfig { | ||
/// The gRPC URL for a Penumbra node's `pd` endpoint, used for retrieving account activity. | ||
grpc_url: Url, | ||
/// The list of Penumbra wallets to track. | ||
accounts: Vec<AccountConfig>, | ||
} | ||
|
||
impl PmonitorConfig { | ||
pub fn new(grpc_url: Url, accounts: Vec<AccountConfig>) -> Self { | ||
Self { grpc_url, accounts } | ||
} | ||
|
||
pub fn grpc_url(&self) -> Url { | ||
self.grpc_url.clone() | ||
} | ||
|
||
pub fn accounts(&self) -> &Vec<AccountConfig> { | ||
&self.accounts | ||
} | ||
|
||
pub fn set_account(&mut self, index: usize, account: AccountConfig) { | ||
self.accounts[index] = account; | ||
} | ||
} | ||
|
||
/// Get the destination FVK from a migration memo. | ||
pub fn parse_dest_fvk_from_memo(memo: &str) -> Result<FullViewingKey> { | ||
let re = Regex::new(r"Migrating balance from .+ to (.+)")?; | ||
if let Some(captures) = re.captures(memo) { | ||
if let Some(dest_fvk_str) = captures.get(1) { | ||
return dest_fvk_str | ||
.as_str() | ||
.parse::<FullViewingKey>() | ||
.map_err(|_| anyhow::anyhow!("Invalid destination FVK in memo")); | ||
} | ||
} | ||
Err(anyhow::anyhow!("Could not parse destination FVK from memo")) | ||
} |
Oops, something went wrong.