You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
382 lines
13 KiB
382 lines
13 KiB
use hyperlane_core::{Announcement, H160};
|
|
|
|
use std::{str::FromStr, thread::sleep};
|
|
|
|
use account_utils::SizedData;
|
|
use borsh::BorshSerialize;
|
|
use solana_program::{
|
|
instruction::{AccountMeta, Instruction},
|
|
pubkey,
|
|
pubkey::Pubkey,
|
|
system_program,
|
|
};
|
|
use solana_program_test::*;
|
|
use solana_sdk::{
|
|
instruction::InstructionError, signature::Signer, signer::keypair::Keypair,
|
|
transaction::TransactionError,
|
|
};
|
|
|
|
use hyperlane_sealevel_validator_announce::{
|
|
accounts::{
|
|
ReplayProtection, ReplayProtectionAccount, ValidatorAnnounce, ValidatorAnnounceAccount,
|
|
ValidatorStorageLocations, ValidatorStorageLocationsAccount,
|
|
},
|
|
instruction::{
|
|
AnnounceInstruction, InitInstruction, Instruction as ValidatorAnnounceInstruction,
|
|
},
|
|
processor::process_instruction as validator_announce_process_instruction,
|
|
replay_protection_pda_seeds, validator_announce_pda_seeds,
|
|
validator_storage_locations_pda_seeds,
|
|
};
|
|
use hyperlane_test_utils::{assert_transaction_error, process_instruction};
|
|
|
|
// The Ethereum mailbox & domain chosen for easy testing
|
|
const TEST_MAILBOX: &str = "00000000000000000000000035231d4c2d8b8adcb5617a638a0c4548684c7c70";
|
|
const TEST_DOMAIN: u32 = 1;
|
|
|
|
fn validator_announce_id() -> Pubkey {
|
|
pubkey!("DH43ae1LwemXAboWwSh8zc9pG8j72gKUEXNi57w8fEnn")
|
|
}
|
|
|
|
fn get_test_mailbox() -> Pubkey {
|
|
let mailbox_bytes = hex::decode(TEST_MAILBOX).unwrap();
|
|
Pubkey::new(&mailbox_bytes[..])
|
|
}
|
|
|
|
fn get_test_announcements() -> Vec<(Announcement, Vec<u8>)> {
|
|
// Signed by the following validator:
|
|
//
|
|
// Address: 0x13DFDeB827D4D7fACE707fAdbfd4D651438B4aB3
|
|
// Private Key: 0x2053099fadf2520efd407cbf043f89fe10eaf91a356d585e9ad12a5eb5f771dd
|
|
|
|
let announcement0 = Announcement {
|
|
validator: H160::from_str("0x13DFDeB827D4D7fACE707fAdbfd4D651438B4aB3").unwrap(),
|
|
mailbox_address: get_test_mailbox().to_bytes().into(),
|
|
mailbox_domain: TEST_DOMAIN,
|
|
storage_location: "s3://test-storage-location-foo/us-east-1".to_string(),
|
|
};
|
|
// Got using ethers.js to sign `announcement0.signing_hash()`, which is
|
|
// 0x6a4f7bcbbcf3f700c4f4da16d3d14ae907ced31d79779e196f4f40af710cfa85
|
|
//
|
|
// > await (new ethers.Wallet('0x2053099fadf2520efd407cbf043f89fe10eaf91a356d585e9ad12a5eb5f771dd')).signMessage(ethers.utils.arrayify('0x6a4f7bcbbcf3f700c4f4da16d3d14ae907ced31d79779e196f4f40af710cfa85'))
|
|
let signature0 = hex::decode("fa0d375457d9a98b3cd6c6ee308464ea23abc2f2368e80d942dacf0b2e3cc4d66ac51efabe169b7cb29170894c588221c91807e500a7a9f9648a8b1c47eceecc1c").unwrap();
|
|
|
|
// UTF-8 characters in the storage location
|
|
let announcement1 = Announcement {
|
|
validator: H160::from_str("0x13DFDeB827D4D7fACE707fAdbfd4D651438B4aB3").unwrap(),
|
|
mailbox_address: get_test_mailbox().to_bytes().into(),
|
|
mailbox_domain: TEST_DOMAIN,
|
|
storage_location: "s3://test-storage-location-Здравствуйте/us-east-1".to_string(),
|
|
};
|
|
// Got using ethers.js to sign `announcement1.signing_hash()`, which is
|
|
// 0xb647a8e18b8152d7cc122ef3e88b643a0dcd2b702dded70ac2d1c94477ca3090
|
|
//
|
|
// > await (new ethers.Wallet('0x2053099fadf2520efd407cbf043f89fe10eaf91a356d585e9ad12a5eb5f771dd')).signMessage(ethers.utils.arrayify('0xb647a8e18b8152d7cc122ef3e88b643a0dcd2b702dded70ac2d1c94477ca3090'))
|
|
let signature1 = hex::decode("983b941ae9bf939bf59abcf81e7d2e66735da5e2726f955b915aea247ae16afa3a03b69fe7d7c83154caca29d8ad62f2a1ccbbb5c56f67dab10c98a2d4aac3b01c").unwrap();
|
|
|
|
vec![(announcement0, signature0), (announcement1, signature1)]
|
|
}
|
|
|
|
async fn initialize(
|
|
banks_client: &mut BanksClient,
|
|
payer: &Keypair,
|
|
mailbox: Pubkey,
|
|
) -> Result<(Pubkey, u8), BanksClientError> {
|
|
let program_id = validator_announce_id();
|
|
|
|
let (validator_announce_key, validator_announce_bump_seed) =
|
|
Pubkey::find_program_address(validator_announce_pda_seeds!(), &program_id);
|
|
|
|
// Accounts:
|
|
// 0. `[signer]` The payer.
|
|
// 1. `[executable]` The system program.
|
|
// 2. `[writable]` The ValidatorAnnounce PDA account.
|
|
let init_instruction = Instruction::new_with_borsh(
|
|
program_id,
|
|
&ValidatorAnnounceInstruction::Init(InitInstruction {
|
|
mailbox,
|
|
local_domain: TEST_DOMAIN,
|
|
}),
|
|
vec![
|
|
AccountMeta::new_readonly(payer.pubkey(), true),
|
|
AccountMeta::new_readonly(system_program::id(), false),
|
|
AccountMeta::new(validator_announce_key, false),
|
|
],
|
|
);
|
|
|
|
process_instruction(banks_client, init_instruction, payer, &[payer]).await?;
|
|
|
|
Ok((validator_announce_key, validator_announce_bump_seed))
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_initialize() {
|
|
let program_id = validator_announce_id();
|
|
let (mut banks_client, payer, _recent_blockhash) = ProgramTest::new(
|
|
"hyperlane_sealevel_validator_announce",
|
|
program_id,
|
|
processor!(validator_announce_process_instruction),
|
|
)
|
|
.start()
|
|
.await;
|
|
|
|
let mailbox = get_test_mailbox();
|
|
let (validator_announce_key, validator_announce_bump_seed) =
|
|
initialize(&mut banks_client, &payer, mailbox)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Expect the validator announce account to be initialized.
|
|
let validator_announce_account = banks_client
|
|
.get_account(validator_announce_key)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(validator_announce_account.owner, program_id);
|
|
|
|
let validator_announce =
|
|
ValidatorAnnounceAccount::fetch(&mut &validator_announce_account.data[..])
|
|
.unwrap()
|
|
.into_inner();
|
|
assert_eq!(
|
|
validator_announce,
|
|
Box::new(ValidatorAnnounce {
|
|
bump_seed: validator_announce_bump_seed,
|
|
mailbox,
|
|
local_domain: TEST_DOMAIN,
|
|
}),
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_initialize_errors_if_called_twice() {
|
|
let program_id = validator_announce_id();
|
|
let (mut banks_client, payer, _recent_blockhash) = ProgramTest::new(
|
|
"hyperlane_sealevel_validator_announce",
|
|
program_id,
|
|
processor!(validator_announce_process_instruction),
|
|
)
|
|
.start()
|
|
.await;
|
|
|
|
let mailbox = get_test_mailbox();
|
|
initialize(&mut banks_client, &payer, mailbox)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Using the same mailbox / payer in the new initialize will result in the same
|
|
// tx hash because a new blockhash isn't used for the new transaction.
|
|
// As a workaround, use a different mailbox
|
|
let init_result = initialize(&mut banks_client, &payer, Pubkey::new_unique()).await;
|
|
|
|
assert_transaction_error(
|
|
init_result,
|
|
TransactionError::InstructionError(0, InstructionError::AccountAlreadyInitialized),
|
|
);
|
|
}
|
|
|
|
async fn announce(
|
|
banks_client: &mut BanksClient,
|
|
payer: &Keypair,
|
|
program_id: Pubkey,
|
|
validator_announce_key: Pubkey,
|
|
announce_instruction: AnnounceInstruction,
|
|
) -> Result<(Pubkey, u8, Pubkey, u8), BanksClientError> {
|
|
let (validator_storage_locations_key, validator_storage_locations_bump_seed) =
|
|
Pubkey::find_program_address(
|
|
validator_storage_locations_pda_seeds!(announce_instruction.validator),
|
|
&program_id,
|
|
);
|
|
|
|
let replay_id = announce_instruction.replay_id();
|
|
|
|
let (replay_protection_key, replay_protection_bump_seed) =
|
|
Pubkey::find_program_address(replay_protection_pda_seeds!(replay_id), &program_id);
|
|
|
|
// Accounts:
|
|
// 0. `[signer]` The payer.
|
|
// 1. `[executable]` The system program.
|
|
// 2. `[]` The ValidatorAnnounce PDA account.
|
|
// 3. `[writeable]` The validator-specific ValidatorStorageLocationsAccount PDA account.
|
|
// 4. `[writeable]` The ReplayProtection PDA account specific to the announcement being made.
|
|
let announce_instruction = Instruction::new_with_borsh(
|
|
program_id,
|
|
&ValidatorAnnounceInstruction::Announce(announce_instruction),
|
|
vec![
|
|
AccountMeta::new_readonly(payer.pubkey(), true),
|
|
AccountMeta::new_readonly(system_program::id(), false),
|
|
AccountMeta::new_readonly(validator_announce_key, false),
|
|
AccountMeta::new(validator_storage_locations_key, false),
|
|
AccountMeta::new(replay_protection_key, false),
|
|
],
|
|
);
|
|
|
|
process_instruction(banks_client, announce_instruction, payer, &[payer]).await?;
|
|
|
|
Ok((
|
|
validator_storage_locations_key,
|
|
validator_storage_locations_bump_seed,
|
|
replay_protection_key,
|
|
replay_protection_bump_seed,
|
|
))
|
|
}
|
|
|
|
async fn assert_successful_announcement(
|
|
banks_client: &mut BanksClient,
|
|
program_id: Pubkey,
|
|
validator_storage_locations_key: Pubkey,
|
|
replay_protection_key: Pubkey,
|
|
expected_validator_storage_locations: ValidatorStorageLocations,
|
|
) {
|
|
// Expect the validator storage locations account to be created & with the new announcement.
|
|
let validator_storage_locations_account = banks_client
|
|
.get_account(validator_storage_locations_key)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(validator_storage_locations_account.owner, program_id);
|
|
|
|
let validator_storage_locations =
|
|
ValidatorStorageLocationsAccount::fetch(&mut &validator_storage_locations_account.data[..])
|
|
.unwrap()
|
|
.into_inner();
|
|
assert_eq!(
|
|
validator_storage_locations,
|
|
Box::new(expected_validator_storage_locations.clone()),
|
|
);
|
|
// Also sanity check that the sizing logic is correct!
|
|
assert_eq!(
|
|
validator_storage_locations_account.data.len(),
|
|
// Plus 1 for the initialized byte
|
|
expected_validator_storage_locations
|
|
.try_to_vec()
|
|
.unwrap()
|
|
.len()
|
|
+ 1,
|
|
);
|
|
assert_eq!(
|
|
validator_storage_locations_account.data.len(),
|
|
ValidatorStorageLocationsAccount::from(expected_validator_storage_locations).size(),
|
|
);
|
|
|
|
// Expect the replay protection account to be created
|
|
let replay_protection_account = banks_client
|
|
.get_account(replay_protection_key)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(replay_protection_account.owner, program_id);
|
|
|
|
assert!(!validator_storage_locations_account.data.is_empty());
|
|
let replay_protection =
|
|
ReplayProtectionAccount::fetch_data(&mut &validator_storage_locations_account.data[..])
|
|
.unwrap();
|
|
assert_eq!(replay_protection, Some(Box::new(ReplayProtection(()))),);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_announce() {
|
|
let program_id = validator_announce_id();
|
|
let (mut banks_client, payer, _recent_blockhash) = ProgramTest::new(
|
|
"hyperlane_sealevel_validator_announce",
|
|
program_id,
|
|
processor!(validator_announce_process_instruction),
|
|
)
|
|
.start()
|
|
.await;
|
|
|
|
let mailbox = get_test_mailbox();
|
|
let (validator_announce_key, _validator_announce_bump_seed) =
|
|
initialize(&mut banks_client, &payer, mailbox)
|
|
.await
|
|
.unwrap();
|
|
|
|
let test_announcements = get_test_announcements();
|
|
|
|
// Make the first announcement
|
|
let (announcement, signature) = test_announcements[0].clone();
|
|
let announce_instruction = AnnounceInstruction {
|
|
validator: announcement.validator,
|
|
storage_location: announcement.storage_location,
|
|
signature,
|
|
};
|
|
let announcement_res = announce(
|
|
&mut banks_client,
|
|
&payer,
|
|
program_id,
|
|
validator_announce_key,
|
|
announce_instruction.clone(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
// there's a race condition that isn't fixed by setting `CommitmentLevel::Confirmed`
|
|
// just wait a bit to ensure the account is created
|
|
sleep(std::time::Duration::from_secs(1));
|
|
|
|
let (
|
|
validator_storage_locations_key,
|
|
validator_storage_locations_bump_seed,
|
|
replay_protection_key,
|
|
_replay_protection_bump_seed,
|
|
) = announcement_res;
|
|
|
|
assert_successful_announcement(
|
|
&mut banks_client,
|
|
program_id,
|
|
validator_storage_locations_key,
|
|
replay_protection_key,
|
|
ValidatorStorageLocations {
|
|
bump_seed: validator_storage_locations_bump_seed,
|
|
storage_locations: vec![announce_instruction.storage_location.clone()],
|
|
},
|
|
)
|
|
.await;
|
|
|
|
// And ensure that the announcement can't be made again!
|
|
let announce_result = announce(
|
|
&mut banks_client,
|
|
&payer,
|
|
program_id,
|
|
validator_announce_key,
|
|
announce_instruction.clone(),
|
|
)
|
|
.await;
|
|
assert_transaction_error(
|
|
announce_result,
|
|
TransactionError::InstructionError(0, InstructionError::AccountAlreadyInitialized),
|
|
);
|
|
|
|
// And then announce the second storage location, which we expect to be successful
|
|
let (announcement, signature) = test_announcements[1].clone();
|
|
let announce_instruction1 = AnnounceInstruction {
|
|
validator: announcement.validator,
|
|
storage_location: announcement.storage_location,
|
|
signature,
|
|
};
|
|
|
|
let (_, _, replay_protection_key, _replay_protection_bump_seed) = announce(
|
|
&mut banks_client,
|
|
&payer,
|
|
program_id,
|
|
validator_announce_key,
|
|
announce_instruction1.clone(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_successful_announcement(
|
|
&mut banks_client,
|
|
program_id,
|
|
validator_storage_locations_key,
|
|
replay_protection_key,
|
|
ValidatorStorageLocations {
|
|
bump_seed: validator_storage_locations_bump_seed,
|
|
storage_locations: vec![
|
|
announce_instruction.storage_location.clone(),
|
|
announce_instruction1.storage_location.clone(),
|
|
],
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|