Skip to content

Commit

Permalink
pmonitor: create client activity monitor (#4844)
Browse files Browse the repository at this point in the history
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
redshiftzero and conorsch authored Oct 17, 2024
1 parent 783db09 commit 954e777
Show file tree
Hide file tree
Showing 18 changed files with 1,768 additions and 27 deletions.
34 changes: 31 additions & 3 deletions .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
38 changes: 38 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ members = [
"crates/bin/pclientd",
"crates/bin/pd",
"crates/bin/pindexer",
"crates/bin/pmonitor",
"crates/cnidarium",
"crates/cnidarium-component",
"crates/core/app",
Expand Down
2 changes: 1 addition & 1 deletion crates/bin/pcli/src/opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ impl Opt {
tracing::info!(%path, "using local view service");

let registry_path = self.home.join("registry.json");
// Check if the path exists or set it to nojne
// Check if the path exists or set it to none
let registry_path = if registry_path.exists() {
Some(registry_path)
} else {
Expand Down
46 changes: 46 additions & 0 deletions crates/bin/pmonitor/Cargo.toml
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}
118 changes: 118 additions & 0 deletions crates/bin/pmonitor/src/config.rs
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"))
}
Loading

0 comments on commit 954e777

Please sign in to comment.