1use std::{collections::HashMap, fs::File, path::PathBuf};
2
3use {
4 anyhow::Result,
5 serde::{Deserialize, Serialize},
6 serde_with::{serde_as, DisplayFromStr},
7 solana_account_decoder::UiAccountEncoding,
8 solana_client::{
9 rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
10 rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType},
11 },
12 solana_program::{pubkey, pubkey::Pubkey},
13 solana_sdk::{account::Account, commitment_config::CommitmentConfig},
14 tensor_whitelist::{
15 accounts::{Whitelist, WhitelistV2},
16 programs::TENSOR_WHITELIST_ID,
17 types::{Condition, Mode},
18 },
19};
20
21use crate::{
22 discriminators::{deserialize_account, Discriminator},
23 formatting::CustomFormat,
24 setup::CliConfig,
25 spinner::create_spinner,
26};
27
28pub const WHITELIST_SIGNER_PUBKEY: Pubkey = pubkey!("DD92UoQnVAaNgRnhvPQhxR7GJkQ9EXhHYq2TEpN8mn1J");
29
30const DEVNET_GENESIS_HASH: &str = "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG";
31const MAINNET_GENESIS_HASH: &str = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d";
32
33const DEFAULT_ROOT_HASH: [u8; 32] = [0; 32];
34
35#[serde_as]
36#[derive(Clone, Debug, Serialize, Deserialize)]
37pub struct WhitelistPair {
38 #[serde_as(as = "DisplayFromStr")]
39 pub v1_pubkey: Pubkey,
40 pub v1_data: Whitelist,
41 #[serde_as(as = "DisplayFromStr")]
42 pub v2_pubkey: Pubkey,
43 pub v2_data: Option<WhitelistV2>,
44}
45
46#[serde_as]
47#[derive(Debug, Clone, Deserialize, Serialize)]
48pub struct MissingWhitelistPair {
49 #[serde_as(as = "DisplayFromStr")]
50 pub v1_pubkey: Pubkey,
51 #[serde_as(as = "DisplayFromStr")]
52 pub v2_pubkey: Pubkey,
53}
54
55#[serde_as]
56#[derive(Debug, Clone, Deserialize, Serialize)]
57pub struct ComparisonResult {
58 #[serde_as(as = "DisplayFromStr")]
59 pub whitelist_v1: Pubkey,
60 #[serde_as(as = "DisplayFromStr")]
61 pub whitelist_v2: Pubkey,
62 pub mismatch: Option<Mismatch>,
63}
64
65#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
66pub enum Mismatch {
67 Uuid,
68 MerkleRoot,
69 Voc,
70 Fvc,
71 V2Missing,
72 V2ConditionsLength,
73 UnexpectedV2Conditions,
74}
75
76pub struct CompareParams {
77 pub keypair_path: Option<PathBuf>,
78 pub rpc_url: Option<String>,
79 pub list: Option<PathBuf>,
80 pub namespace: Option<Pubkey>,
81 pub verbose: bool,
82}
83
84pub fn handle_compare(args: CompareParams) -> Result<()> {
85 let cli_config = CliConfig::new(args.keypair_path, args.rpc_url)?;
86
87 let genesis_hash = cli_config.client.get_genesis_hash()?.to_string();
88
89 let namespace = args.namespace.unwrap_or(WHITELIST_SIGNER_PUBKEY);
90
91 let cluster = if genesis_hash == MAINNET_GENESIS_HASH {
92 "mainnet"
93 } else if genesis_hash == DEVNET_GENESIS_HASH {
94 "devnet"
95 } else {
96 "unknown"
97 };
98
99 println!("Fetching whitelists from: {}", cluster);
100
101 let spinner = create_spinner("")?;
103
104 let whitelists: Vec<(Pubkey, Account)> = if let Some(list) = args.list {
106 spinner.set_message("Opening specified file...");
107
108 let list: Vec<Pubkey> = serde_json::from_reader(File::open(&list)?)?;
109
110 let pubkeys: Vec<_> = list
111 .iter()
112 .map(|p| Whitelist::find_pda(p.to_bytes()).0)
113 .collect();
114
115 cli_config
116 .client
117 .get_multiple_accounts(&pubkeys)?
118 .into_iter()
119 .flatten()
120 .map(|account| {
121 (
122 Whitelist::find_pda(account.data[8..40].try_into().unwrap()).0,
123 account,
124 )
125 })
126 .collect()
127 } else {
128 spinner.set_message("Running gPA call to get all whitelist v1s...");
129
130 let mut disc = Vec::with_capacity(8);
132 disc.extend(Whitelist::discriminator());
133
134 let filter = RpcFilterType::Memcmp(Memcmp::new(0, MemcmpEncodedBytes::Bytes(disc)));
135 let filters = vec![filter];
136
137 let config = RpcProgramAccountsConfig {
138 filters: Some(filters),
139 account_config: RpcAccountInfoConfig {
140 data_slice: None,
141 encoding: Some(UiAccountEncoding::Base64),
142 commitment: Some(CommitmentConfig::confirmed()),
143 min_context_slot: None,
144 },
145 with_context: None,
146 };
147
148 cli_config
149 .client
150 .get_program_accounts_with_config(&TENSOR_WHITELIST_ID, config)?
151 };
152 spinner.finish_and_clear();
153
154 println!("Found {} v1 whitelists on-chain", whitelists.len());
155
156 let mut disc = Vec::with_capacity(8);
158 disc.extend(WhitelistV2::discriminator());
159
160 let filter = RpcFilterType::Memcmp(Memcmp::new(0, MemcmpEncodedBytes::Bytes(disc)));
161 let filters = vec![filter];
162
163 let config = RpcProgramAccountsConfig {
164 filters: Some(filters),
165 account_config: RpcAccountInfoConfig {
166 data_slice: None,
167 encoding: Some(UiAccountEncoding::Base64),
168 commitment: Some(CommitmentConfig::confirmed()),
169 min_context_slot: None,
170 },
171 with_context: None,
172 };
173
174 let spinner = create_spinner("Running gPA call to get all whitelist v2s...")?;
175
176 let on_chain_whitelist_v2s: HashMap<Pubkey, Account> = cli_config
177 .client
178 .get_program_accounts_with_config(&TENSOR_WHITELIST_ID, config)?
179 .into_iter()
180 .collect();
181
182 spinner.finish_and_clear();
183
184 println!(
185 "Found {} v2 whitelists on-chain",
186 on_chain_whitelist_v2s.len()
187 );
188
189 let whitelist_pairs: Vec<WhitelistPair> = whitelists
190 .into_iter()
191 .map(|(pubkey, account)| {
192 (
193 pubkey,
194 deserialize_account::<Whitelist>(&account.data).unwrap(),
195 )
196 })
197 .map(|(v1_pubkey, v1_data)| {
198 let v2_pubkey = WhitelistV2::find_pda(&namespace, v1_data.uuid).0;
199 let v2_data = on_chain_whitelist_v2s
200 .get(&v2_pubkey)
201 .and_then(|account| deserialize_account::<WhitelistV2>(&account.data).ok());
202
203 WhitelistPair {
204 v1_pubkey,
205 v1_data,
206 v2_pubkey,
207 v2_data,
208 }
209 })
210 .collect();
211
212 println!("Built pairs");
213
214 let (missing_v2s, existing_v2s): (Vec<WhitelistPair>, Vec<WhitelistPair>) = whitelist_pairs
216 .into_iter()
217 .partition(|pair| pair.v2_data.is_none());
218
219 println!("{} whitelists have no v2 on chain", missing_v2s.len());
220
221 let no_missing_v2s = missing_v2s.is_empty();
222 let number_of_missing_v2s = missing_v2s.len();
223
224 let missing_pairs: Vec<MissingWhitelistPair> = missing_v2s
225 .into_iter()
226 .map(|pair| MissingWhitelistPair {
227 v1_pubkey: pair.v1_pubkey,
228 v2_pubkey: pair.v2_pubkey,
229 })
230 .collect();
231
232 let spinner = create_spinner("Writing missing v2s to file...")?;
233 let file = File::create(format!("{}_v2_missing.json", cluster))?;
235 serde_json::to_writer_pretty(file, &missing_pairs)?;
236 spinner.finish_and_clear();
237
238 let comparison_results = compare_whitelists(&existing_v2s);
240
241 let mismatches = comparison_results
243 .iter()
244 .filter(|result| result.mismatch.is_some())
245 .collect::<Vec<_>>();
246
247 println!(
248 "Of the {} whitelist v1s with a v2 on chain, {} have a mismatch",
249 existing_v2s.len(),
250 mismatches.len()
251 );
252
253 let spinner = create_spinner("Writing mismatches to file...")?;
254
255 let file = File::create(format!("{}_mismatches.json", cluster))?;
256 serde_json::to_writer_pretty(file, &mismatches)?;
257
258 spinner.finish_and_clear();
259
260 if args.verbose {
261 for result in comparison_results.iter() {
262 println!("{}", result.custom_format());
263 println!(); }
265 }
266
267 if mismatches.is_empty() && no_missing_v2s {
268 println!("All good! ✅ 😎");
269 } else {
270 println!(
271 "There are {} mismatches and {} missing v2s",
272 mismatches.len(),
273 number_of_missing_v2s
274 );
275 }
276
277 Ok(())
278}
279
280fn has_matching_condition(conditions: &[Condition], mode: Mode, value: &Pubkey) -> bool {
281 conditions
282 .iter()
283 .any(|condition| condition.mode == mode && condition.value == *value)
284}
285
286pub fn compare_whitelists(whitelist_pairs: &[WhitelistPair]) -> Vec<ComparisonResult> {
287 whitelist_pairs
288 .iter()
289 .map(|pair| {
290 let mut result = ComparisonResult {
291 whitelist_v1: pair.v1_pubkey,
292 whitelist_v2: pair.v2_pubkey,
293 mismatch: None,
294 };
295
296 let v1 = &pair.v1_data;
297 let v2 = match &pair.v2_data {
298 Some(v2) => v2,
299 None => {
300 result.mismatch = Some(Mismatch::V2Missing);
301 return result;
302 }
303 };
304
305 if v1.uuid != v2.uuid {
307 result.mismatch = Some(Mismatch::Uuid);
308 return result;
309 }
310
311 if v2.conditions.len() != 1 {
313 result.mismatch = Some(Mismatch::V2ConditionsLength);
314 return result;
315 }
316
317 if v1.root_hash != DEFAULT_ROOT_HASH {
319 if has_matching_condition(
320 &v2.conditions,
321 Mode::MerkleTree,
322 &Pubkey::new_from_array(v1.root_hash),
323 ) {
324 return result;
325 } else {
326 result.mismatch = Some(Mismatch::MerkleRoot);
327 return result;
328 }
329 }
330
331 if let Some(voc) = v1.voc {
332 if has_matching_condition(&v2.conditions, Mode::VOC, &voc) {
333 return result;
334 } else {
335 result.mismatch = Some(Mismatch::Voc);
336 return result;
337 }
338 }
339
340 if let Some(fvc) = v1.fvc {
341 if has_matching_condition(&v2.conditions, Mode::FVC, &fvc) {
342 return result;
343 } else {
344 result.mismatch = Some(Mismatch::Fvc);
345 return result;
346 }
347 }
348
349 if !has_matching_condition(
350 &v2.conditions,
351 Mode::MerkleTree,
352 &pubkey!("1thX6LZfHDZZKUs92febYZhYRcXddmzfzF2NvTkPNE"),
353 ) {
354 result.mismatch = Some(Mismatch::UnexpectedV2Conditions);
355 }
356
357 result
358 })
359 .collect()
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365 use solana_program::pubkey::Pubkey;
366 use tensor_whitelist::types::{Condition, Mode, State};
367
368 fn create_whitelist(
369 uuid: [u8; 32],
370 root_hash: [u8; 32],
371 voc: Option<Pubkey>,
372 fvc: Option<Pubkey>,
373 ) -> Whitelist {
374 Whitelist {
375 discriminator: [0; 8],
376 version: 0,
377 bump: 0,
378 verified: false,
379 root_hash,
380 uuid,
381 name: [0; 32],
382 frozen: false,
383 voc,
384 fvc,
385 reserved: [0; 64],
386 }
387 }
388
389 fn create_whitelist_v2(
390 uuid: [u8; 32],
391 conditions: Vec<Condition>,
392 namespace: Pubkey,
393 ) -> WhitelistV2 {
394 WhitelistV2 {
395 discriminator: [0; 8],
396 version: 0,
397 bump: 0,
398 state: State::Unfrozen,
399 update_authority: Pubkey::new_unique(),
400 namespace,
401 freeze_authority: Pubkey::new_unique(),
402 conditions,
403 uuid,
404 }
405 }
406
407 #[test]
408 fn test_merkle_whitelist() {
409 let uuid = [1u8; 32];
412
413 let root_hash: [u8; 32] = [1; 32];
415
416 let namespace = Pubkey::new_unique();
417
418 let v1_data = create_whitelist(uuid, root_hash, None, None);
419
420 let condition = Condition {
421 mode: Mode::MerkleTree,
422 value: Pubkey::new_from_array(root_hash),
423 };
424 let v2_data = create_whitelist_v2(uuid, vec![condition], namespace);
425
426 let pair = WhitelistPair {
427 v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
428 v1_data,
429 v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data.uuid).0,
430 v2_data: Some(v2_data),
431 };
432
433 let results = compare_whitelists(&[pair]);
434
435 assert_eq!(results.len(), 1);
436 assert!(results[0].mismatch.is_none());
437 }
438
439 #[test]
440 fn test_voc_whitelist() {
441 let uuid = [2u8; 32];
444 let namespace = Pubkey::new_unique();
445
446 let voc = Pubkey::new_unique();
447
448 let v1_data = create_whitelist(uuid, DEFAULT_ROOT_HASH, Some(voc), None);
449
450 let condition = Condition {
451 mode: Mode::VOC,
452 value: voc,
453 };
454 let v2_data = create_whitelist_v2(uuid, vec![condition], namespace);
455
456 let pair = WhitelistPair {
457 v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
458 v1_data,
459 v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data.uuid).0,
460 v2_data: Some(v2_data),
461 };
462
463 let results = compare_whitelists(&[pair]);
464
465 assert_eq!(results.len(), 1);
466 assert!(results[0].mismatch.is_none());
467 }
468
469 #[test]
470 fn test_fvc_whitelist() {
471 let uuid = [3u8; 32];
474 let namespace = Pubkey::new_unique();
475
476 let fvc = Pubkey::new_unique();
477
478 let v1_data = create_whitelist(uuid, DEFAULT_ROOT_HASH, None, Some(fvc));
479
480 let condition = Condition {
481 mode: Mode::FVC,
482 value: fvc,
483 };
484 let v2_data = create_whitelist_v2(uuid, vec![condition], namespace);
485
486 let pair = WhitelistPair {
487 v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
488 v1_data,
489 v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data.uuid).0,
490 v2_data: Some(v2_data),
491 };
492
493 let results = compare_whitelists(&[pair]);
494
495 assert_eq!(results.len(), 1);
496 assert!(results[0].mismatch.is_none());
497 }
498
499 #[test]
500 fn test_voc_and_fvc_set() {
501 let uuid = [4u8; 32];
504 let namespace = Pubkey::new_unique();
505
506 let voc = Pubkey::new_unique();
507 let fvc = Pubkey::new_unique();
508
509 let v1_data = create_whitelist(uuid, DEFAULT_ROOT_HASH, Some(voc), Some(fvc));
510
511 let condition_voc = Condition {
513 mode: Mode::VOC,
514 value: voc,
515 };
516 let v2_data_voc = create_whitelist_v2(uuid, vec![condition_voc], namespace);
517
518 let pair_voc = WhitelistPair {
519 v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
520 v1_data: v1_data.clone(),
521 v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data_voc.uuid).0,
522 v2_data: Some(v2_data_voc),
523 };
524
525 let results_voc = compare_whitelists(&[pair_voc]);
526
527 assert_eq!(results_voc.len(), 1);
528 assert!(results_voc[0].mismatch.is_none());
529
530 let condition_fvc = Condition {
532 mode: Mode::FVC,
533 value: fvc,
534 };
535 let v2_data_fvc = create_whitelist_v2(uuid, vec![condition_fvc], namespace);
536
537 let pair_fvc = WhitelistPair {
538 v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
539 v1_data: v1_data.clone(),
540 v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data_fvc.uuid).0,
541 v2_data: Some(v2_data_fvc),
542 };
543
544 let results_fvc = compare_whitelists(&[pair_fvc]);
545
546 assert_eq!(results_fvc.len(), 1);
547 assert_eq!(results_fvc[0].mismatch, Some(Mismatch::Voc));
550 }
551
552 #[test]
553 fn test_merkle_and_fvc_set() {
554 let uuid = [5u8; 32];
557 let namespace = Pubkey::new_unique();
558
559 let root_hash: [u8; 32] = [2; 32];
560 let fvc = Pubkey::new_unique();
561
562 let v1_data = create_whitelist(uuid, root_hash, None, Some(fvc));
563
564 let condition_merkle = Condition {
566 mode: Mode::MerkleTree,
567 value: Pubkey::new_from_array(root_hash),
568 };
569 let v2_data_merkle = create_whitelist_v2(uuid, vec![condition_merkle], namespace);
570
571 let pair_merkle = WhitelistPair {
572 v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
573 v1_data: v1_data.clone(),
574 v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data_merkle.uuid).0,
575 v2_data: Some(v2_data_merkle),
576 };
577
578 let results_merkle = compare_whitelists(&[pair_merkle]);
579
580 assert_eq!(results_merkle.len(), 1);
581 assert!(results_merkle[0].mismatch.is_none());
582
583 let condition_fvc: Condition = Condition {
585 mode: Mode::FVC,
586 value: fvc,
587 };
588 let v2_data_fvc = create_whitelist_v2(uuid, vec![condition_fvc], namespace);
589
590 let pair_fvc = WhitelistPair {
591 v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
592 v1_data,
593 v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data_fvc.uuid).0,
594 v2_data: Some(v2_data_fvc),
595 };
596
597 let results_fvc = compare_whitelists(&[pair_fvc]);
598
599 assert_eq!(results_fvc.len(), 1);
600 assert_eq!(results_fvc[0].mismatch, Some(Mismatch::MerkleRoot));
603 }
604
605 #[test]
606 fn test_voc_with_multiple_v2_conditions() {
607 let uuid = [6u8; 32];
611 let namespace = Pubkey::new_unique();
612
613 let voc = Pubkey::new_unique();
614 let fvc = Pubkey::new_unique();
615
616 let v1_data = create_whitelist(uuid, DEFAULT_ROOT_HASH, Some(voc), None);
617
618 let conditions = vec![
619 Condition {
620 mode: Mode::VOC,
621 value: voc,
622 },
623 Condition {
624 mode: Mode::FVC,
625 value: fvc,
626 },
627 ];
628
629 let v2_data = create_whitelist_v2(uuid, conditions, namespace);
630
631 let pair = WhitelistPair {
632 v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
633 v1_data,
634 v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data.uuid).0,
635 v2_data: Some(v2_data),
636 };
637
638 let results = compare_whitelists(&[pair]);
639
640 assert_eq!(results.len(), 1);
641 assert_eq!(results[0].mismatch, Some(Mismatch::V2ConditionsLength));
643 }
644
645 #[test]
646 fn test_voc_and_fvc_v1_v2_has_voc() {
647 let uuid = [7u8; 32];
651 let namespace = Pubkey::new_unique();
652
653 let voc = Pubkey::new_unique();
654 let fvc = Pubkey::new_unique();
655
656 let v1_data = create_whitelist(uuid, DEFAULT_ROOT_HASH, Some(voc), Some(fvc));
657
658 let condition = Condition {
659 mode: Mode::VOC,
660 value: voc,
661 };
662 let v2_data = create_whitelist_v2(uuid, vec![condition], namespace);
663
664 let pair = WhitelistPair {
665 v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
666 v1_data,
667 v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data.uuid).0,
668 v2_data: Some(v2_data),
669 };
670
671 let results = compare_whitelists(&[pair]);
672
673 assert_eq!(results.len(), 1);
674 assert!(results[0].mismatch.is_none());
676 }
677
678 #[test]
679 fn test_merkle_with_multiple_v2_conditions() {
680 let uuid = [8u8; 32];
684 let namespace = Pubkey::new_unique();
685
686 let root_hash: [u8; 32] = [3; 32];
687 let fvc = Pubkey::new_unique();
688
689 let v1_data = create_whitelist(uuid, root_hash, None, None);
690
691 let conditions = vec![
692 Condition {
693 mode: Mode::MerkleTree,
694 value: Pubkey::new_from_array(root_hash),
695 },
696 Condition {
697 mode: Mode::FVC,
698 value: fvc,
699 },
700 ];
701
702 let v2_data = create_whitelist_v2(uuid, conditions, namespace);
703
704 let pair = WhitelistPair {
705 v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
706 v1_data,
707 v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data.uuid).0,
708 v2_data: Some(v2_data),
709 };
710
711 let results = compare_whitelists(&[pair]);
712
713 assert_eq!(results.len(), 1);
714 assert_eq!(results[0].mismatch, Some(Mismatch::V2ConditionsLength));
716 }
717
718 #[test]
719 fn test_v1_no_conditions_v2_has_fvc() {
720 let uuid = [9u8; 32];
724 let namespace = Pubkey::new_unique();
725
726 let fvc = Pubkey::new_unique();
727
728 let v1_data = create_whitelist(uuid, DEFAULT_ROOT_HASH, None, None);
729
730 let condition = Condition {
731 mode: Mode::FVC,
732 value: fvc,
733 };
734 let v2_data = create_whitelist_v2(uuid, vec![condition], namespace);
735
736 let pair = WhitelistPair {
737 v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
738 v1_data,
739 v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data.uuid).0,
740 v2_data: Some(v2_data),
741 };
742
743 let results = compare_whitelists(&[pair]);
744
745 assert_eq!(results.len(), 1);
746 assert_eq!(results[0].mismatch, Some(Mismatch::UnexpectedV2Conditions));
747 }
748}