Skip to content

Commit 82771e0

Browse files
authored
feat: audit extra checks | NPG-000 (#592)
Additional features for external auditing
1 parent 6ac3a00 commit 82771e0

File tree

5 files changed

+268
-34
lines changed

5 files changed

+268
-34
lines changed

src/audit/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ tracing.workspace = true
3636
tracing-subscriber.workspace = true
3737
rand = "0.8.3"
3838

39-
4039
[dev-dependencies]
4140
rand_chacha = "0.3"
4241
smoke = "^0.2.1"

src/audit/src/find/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,35 @@ FRAGMENTS_STORAGE=/tmp/fund9-leader-1/persist/leader-1
2020

2121
```
2222

23+
### Aggregrate all voter keys and write to file
24+
```bash
25+
26+
FRAGMENTS_STORAGE=/tmp/fund9-leader-1/persist/leader-1
27+
./target/release/find --fragments $FRAGMENTS_STORAGE --aggregate true
28+
29+
```
30+
31+
### Convert key formats
32+
```bash
33+
34+
VOTING_KEY='e5b0a5c250f78b574b8b17283bcc6c7692f72fc58090f4a0a2362497d28d1a85'
35+
36+
./target/release/find --key-to-convert $VOTING_KEY
37+
38+
39+
VOTING_KEY='ca1q0uftf4873xazhmhqrrqg4kfx7fmzfqlm5w80wake5lu3fxjfjxpk6wv3f7'
40+
41+
./target/release/find --key-to-convert $VOTING_KEY
42+
43+
```
44+
45+
### Check a batch of keys presented in a file format and write key metadata to a file.
46+
```bash
47+
48+
KEY_FILE='/tmp/keyfile.txt'
49+
FRAGMENTS_STORAGE=/tmp/fund9-leader-1/persist/leader-1
50+
51+
./target/release/find --fragments $FRAGMENTS_STORAGE --key-file $KEY_FILE
52+
53+
```
54+

src/audit/src/find/bin/main.rs

Lines changed: 93 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//!
44
55
use clap::Parser;
6-
use lib::find::find_vote;
6+
use lib::find::{all_voters, batch_key_check, convert_key_formats, find_vote};
77
use tracing::{info, Level};
88

99
use color_eyre::Result;
@@ -20,10 +20,19 @@ use std::{error::Error, path::PathBuf};
2020
pub struct Args {
2121
/// Obtain fragments by providing path to historical fund data.
2222
#[clap(short, long)]
23-
pub fragments: String,
23+
pub fragments: Option<String>,
2424
/// voting key
25+
#[clap(short, long, requires = "fragments")]
26+
voting_key: Option<String>,
27+
/// aggregate voting keys
28+
#[clap(short, long, requires = "fragments")]
29+
aggregate: Option<bool>,
30+
///convert key formats
2531
#[clap(short, long)]
26-
voting_key: String,
32+
key_to_convert: Option<String>,
33+
/// check batch of keys and write history to file
34+
#[clap(short, long, requires = "fragments")]
35+
key_file: Option<String>,
2736
}
2837

2938
fn main() -> Result<(), Box<dyn Error>> {
@@ -49,31 +58,87 @@ fn main() -> Result<(), Box<dyn Error>> {
4958
info!("Audit Tool.");
5059
info!("Find my vote");
5160

52-
// Load and replay fund fragments from storage
53-
let storage_path = PathBuf::from(args.fragments);
54-
55-
// all fragments including tally fragments
56-
info!("finding vote history of voter {:?}", args.voting_key);
57-
58-
let matched_votes = find_vote(&storage_path, args.voting_key.clone())?;
59-
60-
// record of casters votes
61-
let matched_votes_path = PathBuf::from("/tmp/offline")
62-
.with_extension(format!("voting_history_of_{}.json", args.voting_key));
63-
64-
let file = File::options()
65-
.write(true)
66-
.create(true)
67-
.truncate(true)
68-
.open(matched_votes_path.clone())?;
69-
let writer = BufWriter::new(file);
70-
71-
info!(
72-
"writing voting history of voter {:?} to {:?}",
73-
args.voting_key, matched_votes_path
74-
);
75-
76-
serde_json::to_writer_pretty(writer, &matched_votes)?;
61+
if let Some(voting_key) = args.voting_key {
62+
// Load and replay fund fragments from storage
63+
let storage_path = PathBuf::from(
64+
args.fragments
65+
.clone()
66+
.expect("enforced by clap: infallible"),
67+
);
68+
69+
// all fragments including tally fragments
70+
info!("finding vote history of voter {:?}", voting_key);
71+
72+
let matched_votes = find_vote(&storage_path, voting_key.clone())?;
73+
74+
// record of casters votes
75+
let matched_votes_path = PathBuf::from("/tmp/offline")
76+
.with_extension(format!("voting_history_of_{}.json", voting_key));
77+
78+
let file = File::options()
79+
.write(true)
80+
.create(true)
81+
.truncate(true)
82+
.open(matched_votes_path.clone())?;
83+
let writer = BufWriter::new(file);
84+
85+
info!(
86+
"writing voting history of voter {:?} to {:?}",
87+
voting_key, matched_votes_path
88+
);
89+
90+
serde_json::to_writer_pretty(writer, &matched_votes)?;
91+
}
92+
93+
if let Some(_aggregate) = args.aggregate {
94+
// Load and replay fund fragments from storage
95+
let storage_path = PathBuf::from(
96+
args.fragments
97+
.clone()
98+
.expect("enforced by clap: infallible"),
99+
);
100+
101+
info!("collecting all voting keys in ca and 0x format");
102+
103+
let (unique_voters_ca, unique_voters_0x) = all_voters(&storage_path)?;
104+
105+
let voters_file_0x =
106+
PathBuf::from("/tmp/inspect").with_extension("validated_voters_0x.json");
107+
let voters_file_ca =
108+
PathBuf::from("/tmp/inspect").with_extension("validated_voters_ca.json");
109+
110+
let file = File::options()
111+
.write(true)
112+
.create(true)
113+
.truncate(true)
114+
.open(voters_file_ca)
115+
.unwrap();
116+
let writer = BufWriter::new(file);
117+
118+
serde_json::to_writer_pretty(writer, &unique_voters_ca)?;
119+
120+
let file = File::options()
121+
.write(true)
122+
.create(true)
123+
.truncate(true)
124+
.open(voters_file_0x)
125+
.unwrap();
126+
let writer = BufWriter::new(file);
127+
128+
serde_json::to_writer_pretty(writer, &unique_voters_0x)?;
129+
130+
info!("keys written to /tmp/inspect/validated_voters_*.json");
131+
}
132+
133+
if let Some(keyfile) = args.key_file {
134+
let storage_path = PathBuf::from(args.fragments.expect("enforced by clap: infallible"));
135+
batch_key_check(&storage_path, keyfile)?;
136+
}
137+
138+
if let Some(voting_key) = args.key_to_convert {
139+
let converted_key = convert_key_formats(voting_key)?;
140+
info!("Converted key: {}", converted_key);
141+
}
77142

78143
Ok(())
79144
}

src/audit/src/lib/find.rs

Lines changed: 142 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use bech32::{self, FromBase32};
2-
use chain_addr::{Address, Kind};
2+
use chain_addr::{Address, AddressReadable, Kind};
33
use chain_crypto::{Ed25519, PublicKey};
44
use chain_impl_mockchain::account;
55

@@ -10,15 +10,21 @@ use chain_impl_mockchain::{
1010
block::Block, chaintypes::HeaderId, fragment::Fragment, transaction::InputEnum,
1111
};
1212

13-
use tracing::error;
13+
use tracing::{error, info};
1414

1515
use jormungandr_lib::interfaces::AccountIdentifier;
1616

1717
const MAIN_TAG: &str = "HEAD";
1818

19-
use std::path::Path;
19+
use std::{
20+
collections::{HashMap, HashSet},
21+
error,
22+
fs::{read_to_string, File},
23+
io::BufWriter,
24+
path::{Path, PathBuf},
25+
};
2026

21-
use crate::offline::Vote;
27+
use crate::offline::{extract_fragments_from_storage, Vote};
2228

2329
#[derive(Debug, thiserror::Error)]
2430
pub enum Error {
@@ -177,15 +183,147 @@ pub fn find_vote(jormungandr_database: &Path, voting_key: String) -> Result<Vec<
177183
Ok(votes)
178184
}
179185

186+
/// Collect all voting keys in ca and 0x format and write to files
187+
pub fn all_voters(
188+
jormungandr_database: &Path,
189+
) -> Result<(HashSet<std::string::String>, HashSet<std::string::String>), Box<dyn error::Error>> {
190+
let fragments = extract_fragments_from_storage(jormungandr_database)?;
191+
192+
let mut unique_voters_ca = HashSet::new();
193+
let mut unique_voters_0x = HashSet::new();
194+
195+
for fragment in fragments {
196+
if let Fragment::VoteCast(tx) = fragment.clone() {
197+
let input = tx.as_slice().inputs().iter().next().unwrap().to_enum();
198+
let caster = if let InputEnum::AccountInput(account_id, _value) = input {
199+
AccountIdentifier::from(account_id).into_address(Discrimination::Production, "ca")
200+
} else {
201+
error!("Corrupted fragment {:?}", fragment);
202+
continue;
203+
};
204+
205+
unique_voters_ca.insert(caster.to_string().clone());
206+
207+
let voting_key_61824_format = AddressReadable::from_string("ca", &caster.to_string())
208+
.expect("infallible")
209+
.to_address();
210+
211+
let voting_key = voting_key_61824_format
212+
.public_key()
213+
.expect("infallible")
214+
.to_string();
215+
unique_voters_0x.insert(voting_key);
216+
}
217+
}
218+
219+
info!("unique voters ca {:?}", unique_voters_ca.len());
220+
info!("unique voters 0x {:?}", unique_voters_0x.len());
221+
222+
Ok((unique_voters_ca, unique_voters_0x))
223+
}
224+
225+
/// convert keys from ca to 0x and vice versa
226+
pub fn convert_key_formats(voting_key: String) -> Result<String, Box<dyn error::Error>> {
227+
if voting_key.starts_with("ca") {
228+
let voting_key_61824_format = AddressReadable::from_string("ca", &voting_key)?.to_address();
229+
230+
let voting_key = voting_key_61824_format
231+
.public_key()
232+
.expect("addr to pub key is infallible")
233+
.to_string();
234+
235+
Ok(voting_key)
236+
} else {
237+
// we need to convert this to our internal key representation
238+
let decoded_voting_key = hex::decode(voting_key)?;
239+
let voting_key: PublicKey<Ed25519> = PublicKey::from_binary(&decoded_voting_key)?;
240+
let addr = Address(Discrimination::Production, Kind::Single(voting_key.clone()));
241+
let addr_readable = AddressReadable::from_address("ca", &addr);
242+
243+
Ok(addr_readable.to_string())
244+
}
245+
}
246+
247+
/// read voter keys from file
248+
pub fn read_lines(filename: &str) -> Result<Vec<String>, Box<dyn error::Error>> {
249+
let mut result = Vec::new();
250+
251+
for line in read_to_string(filename)?.lines() {
252+
result.push(line.to_string())
253+
}
254+
255+
Ok(result)
256+
}
257+
258+
/// check key history of multiple keys and write metadata to file
259+
pub fn batch_key_check(
260+
jormungandr_database: &Path,
261+
key_file: String,
262+
) -> Result<(), Box<dyn error::Error>> {
263+
let mut flagged_keys = HashMap::new();
264+
265+
let keys = read_lines(&key_file)?;
266+
267+
for key in keys {
268+
let voting_key_61824_format = AddressReadable::from_string("ca", &key)
269+
.expect("infallible")
270+
.to_address();
271+
272+
let voting_key = voting_key_61824_format
273+
.public_key()
274+
.expect("infallible")
275+
.to_string();
276+
277+
let votes = find_vote(jormungandr_database, voting_key)?;
278+
279+
flagged_keys.insert(key.clone(), votes.clone());
280+
281+
info!("Inserted: key: {} vote: {:?}", key, votes);
282+
}
283+
284+
let flagged_file = PathBuf::from("/tmp/inspect").with_extension("flag_keys.json");
285+
286+
let file = File::options()
287+
.write(true)
288+
.create(true)
289+
.truncate(true)
290+
.open(flagged_file.clone())?;
291+
let writer = BufWriter::new(file);
292+
293+
serde_json::to_writer_pretty(writer, &flagged_keys)?;
294+
295+
info!("flagged keys and metadata saved here {:?}", flagged_file);
296+
297+
Ok(())
298+
}
299+
180300
#[cfg(test)]
181301
mod tests {
302+
182303
use std::path::PathBuf;
183304

184305
use chain_addr::{Address, AddressReadable, Discrimination, Kind};
185306
use chain_crypto::{Ed25519, PublicKey};
186307

187308
use crate::find::find_vote;
188309

310+
use super::convert_key_formats;
311+
312+
#[test]
313+
fn test_key_conversion() {
314+
let voting_key_0x =
315+
"f895a6a7f44dd15f7700c60456c93793b1241fdd1c77bbb6cd3fc8a4d24c8c1b".to_string();
316+
317+
let converted_key = convert_key_formats(voting_key_0x.clone()).unwrap();
318+
319+
let voting_key_ca =
320+
"ca1q0uftf4873xazhmhqrrqg4kfx7fmzfqlm5w80wake5lu3fxjfjxpk6wv3f7".to_string();
321+
322+
assert_eq!(converted_key, voting_key_ca,);
323+
324+
assert_eq!(convert_key_formats(voting_key_ca).unwrap(), voting_key_0x);
325+
}
326+
189327
#[test]
190328
#[ignore]
191329
fn test_account_parser() {

src/audit/src/lib/offline.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub enum Error {
3838
CorruptedFragments,
3939
}
4040

41-
#[derive(Serialize, Debug)]
41+
#[derive(Serialize, Debug, Clone)]
4242
pub struct Vote {
4343
pub fragment_id: String,
4444
pub caster: Address,

0 commit comments

Comments
 (0)