From 20714236eb5420b67181f5c76920da5743357821 Mon Sep 17 00:00:00 2001 From: Evan Gray Date: Thu, 18 Jul 2024 09:38:19 -0400 Subject: [PATCH] tidying --- README.md | 37 +- .../example-queries-solana-verify/Cargo.toml | 10 +- .../src/error.rs | 196 +---- .../close_signatures.rs | 0 .../src/{processor => instructions}/mod.rs | 0 .../post_signatures.rs | 13 +- .../verify_query.rs | 21 +- .../example-queries-solana-verify/src/lib.rs | 10 +- .../src/state/guardian_signatures.rs | 5 +- tests/README.md | 27 + tests/example-queries-solana-verify.ts | 811 ++++++++++++------ .../helpers/utils/signaturesToSolanaArray.ts | 10 + 12 files changed, 660 insertions(+), 480 deletions(-) rename programs/example-queries-solana-verify/src/{processor => instructions}/close_signatures.rs (100%) rename programs/example-queries-solana-verify/src/{processor => instructions}/mod.rs (100%) rename programs/example-queries-solana-verify/src/{processor => instructions}/post_signatures.rs (57%) rename programs/example-queries-solana-verify/src/{processor => instructions}/verify_query.rs (94%) create mode 100644 tests/README.md create mode 100644 tests/helpers/utils/signaturesToSolanaArray.ts diff --git a/README.md b/README.md index 3c4b707..1d66336 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -## Queries Verification on Solana PoC +# Queries Verification on Solana PoC This is a demo of verifying and parsing [Wormhole Queries](https://wormhole.com/queries/) on Solana. @@ -6,20 +6,35 @@ This project was made with [Anchor](https://www.anchor-lang.com/). Learn more about developing with Queries in [the docs](https://docs.wormhole.com/wormhole/queries/getting-started). -> N.B. This is a work-in-progress provided for example purposes only. +## Accounts -- [x] Verify mainnet queries using an active mainnet core bridge guardian set account -- [x] Verify mocked queries using a mock core bridge guardian set account on a mainnet address -- [x] Validate a query result passed via instruction data -- [x] Rust parsing for all query requests and responses -- [x] Allow for cleanup of signature set accounts +- [GuardianSignatures](programs/solana-world-id-program/src/state/guardian_signatures.rs) stores unverified guardian signatures for subsequent verification. These are created with `post_signatures` in service of verifying a root via Queries and closed when that root is verified with `verify_query` or can be explicitly closed with `close_signatures` by the initial payer. -- [ ] Verify testnet queries using an active testnet core bridge guardian set account -- [ ] Validate a query result passed via account +## Instructions -## Tests +- [post_signatures](programs/example-queries-solana-verify/src/instructions/post_signatures.rs) posts unverified guardian signatures for verification during `update_root_with_query`. +- [verify_query](programs/example-queries-solana-verify/src/instructions/verify_query.rs) with a Query response and `GuardianSignatures` account, verifies the signatures against an active guardian set and logs the Query response. This is where you would add additional verification relevant to your use case and process the result. +- [close_signatures](programs/example-queries-solana-verify/src/instructions/close_signatures.rs) allows the initial payer to close a `GuardianSignatures` account in case the query was invalid. -To run the tests, `anchor test`. +## Testing + +```bash +anchor test +``` + +## Building + +### Wormhole Testnet / Solana Devnet + +```bash +anchor build -- --no-default-features --features testnet +``` + +### Mainnet + +```bash +anchor build +``` --- diff --git a/programs/example-queries-solana-verify/Cargo.toml b/programs/example-queries-solana-verify/Cargo.toml index 8e092c6..a4bb7f0 100644 --- a/programs/example-queries-solana-verify/Cargo.toml +++ b/programs/example-queries-solana-verify/Cargo.toml @@ -9,16 +9,18 @@ crate-type = ["cdylib", "lib"] name = "example_queries_solana_verify" [features] -idl-build = ["anchor-lang/idl-build"] +default = ["mainnet"] +cpi = ["no-entrypoint"] no-entrypoint = [] no-idl = [] no-log-ix-name = [] -cpi = ["no-entrypoint"] -default = [] +mainnet = ["wormhole-solana-consts/mainnet"] +testnet = ["wormhole-solana-consts/testnet"] +idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { version = "0.30.1", features = ["init-if-needed"] } wormhole-raw-vaas = {version = "0.3.0-alpha.1"} -wormhole-solana-consts = {version = "0.3.0-alpha.1", features = ["mainnet"] } +wormhole-solana-consts = {version = "0.3.0-alpha.1" } wormhole-solana-utils = {version = "0.3.0-alpha.1"} wormhole-query-sdk = { git = "https://github.com/wormholelabs-xyz/wormhole-query-sdk-rust", version = "0.0.1", rev = "0f34cb470f4e3137b53aa91adcbb0c7def280925" } diff --git a/programs/example-queries-solana-verify/src/error.rs b/programs/example-queries-solana-verify/src/error.rs index bcd07d9..7e9f496 100644 --- a/programs/example-queries-solana-verify/src/error.rs +++ b/programs/example-queries-solana-verify/src/error.rs @@ -1,199 +1,37 @@ -//! Errors that may arise when interacting with the Core Bridge Program. +//! Errors that may arise when interacting with the Example Queries Solana Verify Program. use anchor_lang::prelude::error_code; -/// Errors relevant to Core Bridge's malfunction. -/// -/// * \>= 0x0 -- General program related. -/// * \>= 0x10 -- General Core Bridge. -/// * \>= 0x20 -- General Core Bridge Governance. -/// * \>= 0x100 -- Legacy Post Message. -/// * \>= 0x200 -- Legacy Post VAA. -/// * \>= 0x300 -- Legacy Set Message Fee. -/// * \>= 0x400 -- Legacy Transfer Fees. -/// * \>= 0x500 -- Legacy Upgrade Contract. -/// * \>= 0x600 -- Legacy Guardian Set Update. -/// * \>= 0x700 -- Legacy Verify Signatures. -/// * \>= 0x800 -- Legacy Post Message Unreliable. -/// * \>= 0x1000 -- Core Bridge Anchor Instruction. -/// * \>= 0x2000 -- Core Bridge SDK. -/// * \>= 0x3000 -- Query Response SDK. +/// * \>= 0x100 -- Query Verification. /// /// NOTE: All of these error codes when triggered are offset by `ERROR_CODE_OFFSET` (6000). So for -/// example, `U64Overflow` will return as 6006. +/// example, `InvalidMessageHash` will return as 6256. #[error_code] pub enum ExampleQueriesSolanaVerifyError { - #[msg("InvalidInstructionArgument")] - InvalidInstructionArgument = 0x2, - - #[msg("AccountNotZeroed")] - AccountNotZeroed = 0x3, - - #[msg("InvalidDataConversion")] - InvalidDataConversion = 0x4, - - #[msg("U64Overflow")] - U64Overflow = 0x6, - - #[msg("InvalidComputeSize")] - InvalidComputeSize = 0x8, - - #[msg("InvalidChain")] - InvalidChain = 0x10, - - #[msg("InvalidGovernanceEmitter")] - InvalidGovernanceEmitter = 0x20, - - #[msg("InvalidGovernanceAction")] - InvalidGovernanceAction = 0x22, - - #[msg("LatestGuardianSetRequired")] - LatestGuardianSetRequired = 0x24, - - #[msg("GovernanceForAnotherChain")] - GovernanceForAnotherChain = 0x26, - - #[msg("InvalidGovernanceVaa")] - InvalidGovernanceVaa = 0x28, - - #[msg("InsufficientFees")] - InsufficientFees = 0x100, - - #[msg("EmitterMismatch")] - EmitterMismatch = 0x102, - - #[msg("NotReadyForPublishing")] - NotReadyForPublishing = 0x104, - - #[msg("InvalidPreparedMessage")] - InvalidPreparedMessage = 0x106, - - #[msg("ExecutableEmitter")] - ExecutableEmitter = 0x108, - - #[msg("LegacyEmitter")] - LegacyEmitter = 0x10a, + #[msg("WriteAuthorityMismatch")] + WriteAuthorityMismatch = 0x100, - #[msg("InvalidSignatureSet")] - InvalidSignatureSet = 0x200, + #[msg("GuardianSetExpired")] + GuardianSetExpired = 0x101, #[msg("InvalidMessageHash")] - InvalidMessageHash = 0x202, + InvalidMessageHash = 0x102, #[msg("NoQuorum")] - NoQuorum, - - #[msg("MessageMismatch")] - MessageMismatch = 0x204, - - #[msg("NotEnoughLamports")] - NotEnoughLamports = 0x400, - - #[msg("InvalidFeeRecipient")] - InvalidFeeRecipient = 0x402, - - #[msg("ImplementationMismatch")] - ImplementationMismatch = 0x500, - - #[msg("InvalidGuardianSetIndex")] - InvalidGuardianSetIndex = 0x600, - - #[msg("GuardianSetMismatch")] - GuardianSetMismatch = 0x700, - - #[msg("InstructionAtWrongIndex")] - InstructionAtWrongIndex = 0x702, - - #[msg("EmptySigVerifyInstruction")] - EmptySigVerifyInstruction = 0x703, - - #[msg("InvalidSigVerifyInstruction")] - InvalidSigVerifyInstruction = 0x704, + NoQuorum = 0x103, - #[msg("GuardianSetExpired")] - GuardianSetExpired = 0x706, - - #[msg("InvalidGuardianKeyRecovery")] - InvalidGuardianKeyRecovery = 0x708, - - #[msg("SignerIndicesMismatch")] - SignerIndicesMismatch = 0x70a, - - #[msg("PayloadSizeMismatch")] - PayloadSizeMismatch = 0x800, - - #[msg("ZeroGuardians")] - ZeroGuardians = 0x1010, - - #[msg("GuardianZeroAddress")] - GuardianZeroAddress = 0x1020, + #[msg("InvalidGuardianIndexNonIncreasing")] + InvalidGuardianIndexNonIncreasing = 0x104, - #[msg("DuplicateGuardianAddress")] - DuplicateGuardianAddress = 0x1030, - - #[msg("MessageAlreadyPublished")] - MessageAlreadyPublished = 0x1040, - - #[msg("VaaWritingDisallowed")] - VaaWritingDisallowed = 0x1050, - - #[msg("VaaAlreadyVerified")] - VaaAlreadyVerified = 0x1060, - - #[msg("InvalidGuardianIndex")] - InvalidGuardianIndex = 0x1070, + #[msg("InvalidGuardianIndexOutOfRange")] + InvalidGuardianIndexOutOfRange = 0x105, #[msg("InvalidSignature")] - InvalidSignature = 0x1080, - - #[msg("UnverifiedVaa")] - UnverifiedVaa = 0x10a0, - - #[msg("VaaStillProcessing")] - VaaStillProcessing = 0x10a2, - - #[msg("InWritingStatus")] - InWritingStatus = 0x10a4, + InvalidSignature = 0x106, - #[msg("NotInWritingStatus")] - NotInWritingStatus = 0x10a6, - - #[msg("InvalidMessageStatus")] - InvalidMessageStatus = 0x10a8, - - #[msg("HashNotComputed")] - HashNotComputed = 0x10aa, - - #[msg("InvalidVaaVersion")] - InvalidVaaVersion = 0x10ac, - - #[msg("InvalidCreatedAccountSize")] - InvalidCreatedAccountSize = 0x10ae, - - #[msg("DataOverflow")] - DataOverflow = 0x10b0, - - #[msg("ExceedsMaxPayloadSize (30KB)")] - ExceedsMaxPayloadSize = 0x10b2, - - #[msg("CannotParseVaa")] - CannotParseVaa = 0x10b4, - - #[msg("EmitterAuthorityMismatch")] - EmitterAuthorityMismatch = 0x10b6, - - #[msg("InvalidProgramEmitter")] - InvalidProgramEmitter = 0x10b8, - - #[msg("WriteAuthorityMismatch")] - WriteAuthorityMismatch = 0x10ba, - - #[msg("PostedVaaPayloadTooLarge")] - PostedVaaPayloadTooLarge = 0x10bc, - - #[msg("ExecutableDisallowed")] - ExecutableDisallowed = 0x10be, + #[msg("InvalidGuardianKeyRecovery")] + InvalidGuardianKeyRecovery = 0x107, #[msg("FailedToParseResponse")] - FailedToParseResponse = 0x3000, + FailedToParseResponse = 0x110, } diff --git a/programs/example-queries-solana-verify/src/processor/close_signatures.rs b/programs/example-queries-solana-verify/src/instructions/close_signatures.rs similarity index 100% rename from programs/example-queries-solana-verify/src/processor/close_signatures.rs rename to programs/example-queries-solana-verify/src/instructions/close_signatures.rs diff --git a/programs/example-queries-solana-verify/src/processor/mod.rs b/programs/example-queries-solana-verify/src/instructions/mod.rs similarity index 100% rename from programs/example-queries-solana-verify/src/processor/mod.rs rename to programs/example-queries-solana-verify/src/instructions/mod.rs diff --git a/programs/example-queries-solana-verify/src/processor/post_signatures.rs b/programs/example-queries-solana-verify/src/instructions/post_signatures.rs similarity index 57% rename from programs/example-queries-solana-verify/src/processor/post_signatures.rs rename to programs/example-queries-solana-verify/src/instructions/post_signatures.rs index a81d9f2..f4a6368 100644 --- a/programs/example-queries-solana-verify/src/processor/post_signatures.rs +++ b/programs/example-queries-solana-verify/src/instructions/post_signatures.rs @@ -8,17 +8,26 @@ pub struct PostSignatures<'info> { #[account(mut)] payer: Signer<'info>, - /// Stores signatures for later use by verify_query #[account( init_if_needed, payer = payer, - space = GuardianSignatures::compute_size(usize::from(total_signatures)) + space = 8 + GuardianSignatures::compute_size(usize::from(total_signatures)) )] guardian_signatures: Account<'info, GuardianSignatures>, system_program: Program<'info, System>, } +/// Creates or appends to a GuardianSignatures account for subsequent use by verify_query. +/// This is necessary as the Wormhole query response (220 bytes) +/// and 13 guardian signatures (a quorum of the current 19 mainnet guardians, 66 bytes each) +/// alongside the required accounts is larger than the transaction size limit on Solana (1232 bytes). +/// +/// This instruction allows for the initial payer to append additional signatures to the account by calling the instruction again. +/// This may be necessary if a quorum of signatures from the current guardian set grows larger than can fit into a single transaction. +/// +/// The GuardianSignatures account can be closed by anyone with a successful update_root_with_query instruction +/// or by the initial payer via close_signatures, either of which will refund the initial payer. pub fn post_signatures( ctx: Context, mut guardian_signatures: Vec<[u8; 66]>, diff --git a/programs/example-queries-solana-verify/src/processor/verify_query.rs b/programs/example-queries-solana-verify/src/instructions/verify_query.rs similarity index 94% rename from programs/example-queries-solana-verify/src/processor/verify_query.rs rename to programs/example-queries-solana-verify/src/instructions/verify_query.rs index 0bd0bdd..87b8dfe 100644 --- a/programs/example-queries-solana-verify/src/processor/verify_query.rs +++ b/programs/example-queries-solana-verify/src/instructions/verify_query.rs @@ -18,16 +18,15 @@ use wormhole_solana_consts::CORE_BRIDGE_PROGRAM_ID; #[derive(Accounts)] #[instruction(_bytes: Vec, guardian_set_index: u32)] pub struct VerifyQuery<'info> { - /// Guardian set used for signature verification (whose index should agree with the signature - /// set account's guardian set index). + /// Guardian set used for signature verification. #[account( seeds = [ WormholeGuardianSet::SEED_PREFIX, guardian_set_index.to_be_bytes().as_ref() - ], - bump, - seeds::program = CORE_BRIDGE_PROGRAM_ID - )] + ], + bump, + seeds::program = CORE_BRIDGE_PROGRAM_ID + )] guardian_set: Account<'info, WormholeGuardianSet>, /// Stores unverified guardian signatures as they are too large to fit in the instruction data. @@ -41,7 +40,7 @@ pub struct VerifyQuery<'info> { impl<'info> VerifyQuery<'info> { pub fn constraints(ctx: &Context, bytes: &Vec) -> Result<()> { - let guardian_set = ctx.accounts.guardian_set.clone().into_inner(); + let guardian_set = &ctx.accounts.guardian_set; // Check that the guardian set is still active. let timestamp = Clock::get()? @@ -91,14 +90,14 @@ impl<'info> VerifyQuery<'info> { if let Some(last_index) = last_guardian_index { require!( index > last_index, - ExampleQueriesSolanaVerifyError::InvalidGuardianIndex + ExampleQueriesSolanaVerifyError::InvalidGuardianIndexNonIncreasing ); } // Does this guardian index exist in this guardian set? - let guardian_pubkey = guardian_keys - .get(index) - .ok_or_else(|| error!(ExampleQueriesSolanaVerifyError::InvalidGuardianIndex))?; + let guardian_pubkey = guardian_keys.get(index).ok_or_else(|| { + error!(ExampleQueriesSolanaVerifyError::InvalidGuardianIndexOutOfRange) + })?; // Now verify that the signature agrees with the expected Guardian's pubkey. verify_guardian_signature(&sig, guardian_pubkey, digest.as_ref())?; diff --git a/programs/example-queries-solana-verify/src/lib.rs b/programs/example-queries-solana-verify/src/lib.rs index 907ca6b..c3a5a4c 100644 --- a/programs/example-queries-solana-verify/src/lib.rs +++ b/programs/example-queries-solana-verify/src/lib.rs @@ -4,8 +4,8 @@ declare_id!("HkDXBFRS9Tv9295d9wEVRL61c1pUXj3WZHiaTNZ9Q7TQ"); pub mod error; -mod processor; -pub(crate) use processor::*; +mod instructions; +pub(crate) use instructions::*; pub mod state; @@ -18,7 +18,7 @@ pub mod example_queries_solana_verify { } pub fn close_signatures(ctx: Context) -> Result<()> { - processor::close_signatures(ctx) + instructions::close_signatures(ctx) } pub fn post_signatures( @@ -26,7 +26,7 @@ pub mod example_queries_solana_verify { guardian_signatures: Vec<[u8; 66]>, total_signatures: u8, ) -> Result<()> { - processor::post_signatures(ctx, guardian_signatures, total_signatures) + instructions::post_signatures(ctx, guardian_signatures, total_signatures) } pub fn verify_query( @@ -34,7 +34,7 @@ pub mod example_queries_solana_verify { bytes: Vec, guardian_set_index: u32, ) -> Result<()> { - processor::verify_query(ctx, bytes, guardian_set_index) + instructions::verify_query(ctx, bytes, guardian_set_index) } } diff --git a/programs/example-queries-solana-verify/src/state/guardian_signatures.rs b/programs/example-queries-solana-verify/src/state/guardian_signatures.rs index 955e265..89002c3 100644 --- a/programs/example-queries-solana-verify/src/state/guardian_signatures.rs +++ b/programs/example-queries-solana-verify/src/state/guardian_signatures.rs @@ -8,14 +8,13 @@ pub struct GuardianSignatures { /// Used for reimbursements upon cleanup. pub refund_recipient: Pubkey, - /// Verification success per guardian. + /// Unverified guardian signatures. pub guardian_signatures: Vec<[u8; 66]>, } impl GuardianSignatures { pub(crate) fn compute_size(num_guardians: usize) -> usize { - 8 // discriminator - + 32 // write_authority + 32 // refund_recipient + 4 + num_guardians * 66 // signatures } diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..eb5e27c --- /dev/null +++ b/tests/README.md @@ -0,0 +1,27 @@ +# TypeScript Anchor Tests + +The goal of these tests is to provide positive and negative cases for account annotations and custom errors in the Example Queries Solana Verify program. + +- [x] [post_signatures](/programs/example-queries-solana-verify/src/instructions/post_signatures.rs) + - [x] Successfully posts signatures + - [x] Successfully appends signatures + - [x] Rejects append by non-initial payer +- [x] [verify_query](/programs/example-queries-solana-verify/src/instructions/verify_query.rs) + - [x] Successfully verifies mainnet queries + - [x] Successfully verifies mock queries + - [x] Successfully closed the signature set + - [x] Rejects guardian set account not owned by the core bridge + - [x] Rejects guardian set account mismatch + - [x] Rejects refund recipient account mismatch + - [x] Rejects expired guardian set + - [x] Rejects no quorum + - [x] Rejects out of order guardian signatures + - [x] Rejects duplicate guardian signatures + - [x] Rejects guardian index out of bounds + - [x] Rejects invalid signature + - [x] Rejects invalid message hash + - [x] Rejects un-parse-able response +- [x] [close_signatures](/programs/example-queries-solana-verify/src/instructions/close_signatures.rs) + - [x] Successfully closes signature accounts + - [x] Rejects refund recipient account mismatch + - [x] Rejects without refund recipient as signer diff --git a/tests/example-queries-solana-verify.ts b/tests/example-queries-solana-verify.ts index d74618a..a6c3d3e 100644 --- a/tests/example-queries-solana-verify.ts +++ b/tests/example-queries-solana-verify.ts @@ -1,21 +1,17 @@ import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import { QueryProxyMock } from "@wormhole-foundation/wormhole-query-sdk"; -import { assert, expect, use } from "chai"; +import { expect, use } from "chai"; import chaiAsPromised from "chai-as-promised"; import { ExampleQueriesSolanaVerify } from "../target/types/example_queries_solana_verify"; import { getWormholeBridgeData } from "./helpers/config"; import { deriveGuardianSetKey } from "./helpers/guardianSet"; +import { signaturesToSolanaArray } from "./helpers/utils/signaturesToSolanaArray"; use(chaiAsPromised); -// TODO: PR to @wormhole-foundation/wormhole-query-sdk -export function signaturesToSolanaArray(signatures: string[]) { - return signatures.map((s) => [ - ...Buffer.from(s.substring(130, 132), "hex"), - ...Buffer.from(s.substring(0, 130), "hex"), - ]); -} +const fmtTest = (instruction: string, name: string) => + `${instruction.padEnd(30)} ${name}`; describe("example-queries-solana-verify", () => { // Configure the client to use the local cluster. @@ -24,105 +20,62 @@ describe("example-queries-solana-verify", () => { const program = anchor.workspace .ExampleQueriesSolanaVerify as Program; + const programPaidBy = ( + payer: anchor.web3.Keypair + ): Program => { + const newProvider = new anchor.AnchorProvider( + anchor.getProvider().connection, + new anchor.Wallet(payer), + {} + ); + return new anchor.Program( + program.idl, + newProvider + ); + }; + + const devnetCoreBridgeAddress = new anchor.web3.PublicKey( + "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o" + ); const coreBridgeAddress = new anchor.web3.PublicKey( "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth" ); + const expiredGuardianSetIndex = 2; + let guardianSetIndex = 0; // updated to the current index in initialize const mockGuardianSetIndex = 5; - it("Core exists!", async () => { - const p = anchor.getProvider(); - { - // devnet - const coreBridge = await p.connection.getAccountInfo( - new anchor.web3.PublicKey("Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o") - ); - assert(coreBridge !== null, "devnet core bridge program does not exist"); - const bridgeConfig = await p.connection.getAccountInfo( - new anchor.web3.PublicKey( - "FKoMTctsC7vJbEqyRiiPskPnuQx2tX1kurmvWByq5uZP" - ) - ); - assert( - bridgeConfig !== null, - "devnet bridge config account does not exist" - ); - const guardianSet = await p.connection.getAccountInfo( - new anchor.web3.PublicKey( - "6MxkvoEwgB9EqQRLNhvYaPGhfcLtBtpBqdQugr3AZUgD" - ) - ); - assert( - guardianSet !== null, - "devnet guardian set account does not exist" - ); - } - { - // testnet - const coreBridge = await p.connection.getAccountInfo( - new anchor.web3.PublicKey( - "3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5" - ) - ); - assert(coreBridge !== null, "testnet core bridge program does not exist"); - const bridgeConfig = await p.connection.getAccountInfo( - new anchor.web3.PublicKey( - "6bi4JGDoRwUs9TYBuvoA7dUVyikTJDrJsJU1ew6KVLiu" - ) - ); - assert( - bridgeConfig !== null, - "testnet bridge config account does not exist" - ); - const guardianSet = await p.connection.getAccountInfo( - new anchor.web3.PublicKey("dxZtypiKT5D9LYzdPxjvSZER9MgYfeRVU5qpMTMTRs4") - ); - assert( - guardianSet !== null, - "testnet guardian set account does not exist" - ); - } - { - // mainnet - const coreBridge = await p.connection.getAccountInfo(coreBridgeAddress); - assert(coreBridge !== null, "mainnet core bridge program does not exist"); - const bridgeConfig = await p.connection.getAccountInfo( - new anchor.web3.PublicKey( - "2yVjuQwpsvdsrywzsJJVs9Ueh4zayyo5DYJbBNc3DDpn" - ) - ); - assert( - bridgeConfig !== null, - "mainnet bridge config account does not exist" - ); - const info = await getWormholeBridgeData(p.connection, coreBridgeAddress); - const currentGuardianSetIndex = info.guardianSetIndex; - assert( - mockGuardianSetIndex === currentGuardianSetIndex + 1, - "mockGuardianSetIndex is not set to following index" - ); - for ( - let guardianSetIndex = 0; - guardianSetIndex <= mockGuardianSetIndex; - guardianSetIndex++ - ) { - const gsAddr = deriveGuardianSetKey( - coreBridgeAddress, - guardianSetIndex - ); - const guardianSet = await p.connection.getAccountInfo( - new anchor.web3.PublicKey(gsAddr) - ); - assert( - guardianSet !== null, - `mainnet guardian set ${guardianSetIndex} account (${gsAddr}) does not exist` - ); - } - } - }); + const secondAccount = anchor.web3.Keypair.generate(); + + async function postQuerySigs( + querySignatures: string[], + signaturesKeypair: anchor.web3.Keypair, + totalSignatures: number = 0, + p: Program = program + ) { + const signatureData = signaturesToSolanaArray(querySignatures); + await p.methods + .postSignatures(signatureData, totalSignatures || signatureData.length) + .accounts({ guardianSignatures: signaturesKeypair.publicKey }) + .signers([signaturesKeypair]) + .rpc(); + } - it("Is initialized!", async () => { + it(fmtTest("initialize", "Successfully initializes"), async () => { await program.methods.initialize().rpc(); + // fetch current guardian set + const p = anchor.getProvider(); + const info = await getWormholeBridgeData(p.connection, coreBridgeAddress); + guardianSetIndex = info.guardianSetIndex; + // other setup + const tx = await p.connection.requestAirdrop( + secondAccount.publicKey, + 10000000000 + ); + await p.connection.confirmTransaction({ + ...(await p.connection.getLatestBlockhash()), + signature: tx, + }); }); const wethNameResponse = { @@ -164,131 +117,253 @@ describe("example-queries-solana-verify", () => { ], }; - it("Rejects an invalid guardian set!", async () => { - const signatureData = signaturesToSolanaArray(wethNameResponse.signatures); - const signatureKeypair = anchor.web3.Keypair.generate(); - await program.methods - .postSignatures(signatureData, signatureData.length) - .accounts({ guardianSignatures: signatureKeypair.publicKey }) - .signers([signatureKeypair]) - .rpc(); + it(fmtTest("post_signatures", "Successfully posts signatures"), async () => { + const signaturesKeypair = anchor.web3.Keypair.generate(); + await postQuerySigs(wethNameResponse.signatures, signaturesKeypair); + // this will fail if the account does not exist, match discriminator, and parse await expect( - program.methods - .verifyQuery(Buffer.from(wethNameResponse.bytes, "hex"), 5) - .accountsPartial({ - guardianSet: deriveGuardianSetKey(coreBridgeAddress, 2), - guardianSignatures: signatureKeypair.publicKey, - }) - .rpc() - ).to.be.rejectedWith( - "AnchorError caused by account: guardian_set. Error Code: ConstraintSeeds. Error Number: 2006. Error Message: A seeds constraint was violated." - ); + program.account.guardianSignatures.fetch(signaturesKeypair.publicKey) + ).to.be.fulfilled; }); - it("Verifies mainnet queries!", async () => { - const p = anchor.getProvider(); - const info = await getWormholeBridgeData(p.connection, coreBridgeAddress); - const guardianSetIndex = info.guardianSetIndex; - const signatureData = signaturesToSolanaArray(wethNameResponse.signatures); - const signatureKeypair = anchor.web3.Keypair.generate(); - await program.methods - .postSignatures(signatureData, signatureData.length) - .accounts({ guardianSignatures: signatureKeypair.publicKey }) - .signers([signatureKeypair]) - .rpc(); - await expect( - program.methods - .verifyQuery( - Buffer.from(wethNameResponse.bytes, "hex"), - guardianSetIndex + + it( + fmtTest("post_signatures", "Successfully appends signatures"), + async () => { + const signaturesKeypair = anchor.web3.Keypair.generate(); + const expectedSigs1 = signaturesToSolanaArray( + wethNameResponse.signatures.slice(0, 5) + ); + await postQuerySigs( + wethNameResponse.signatures.slice(0, 5), + signaturesKeypair, + wethNameResponse.signatures.length + ); + expect( + ( + await program.account.guardianSignatures.fetch( + signaturesKeypair.publicKey + ) + ).guardianSignatures + ).to.deep.equal(expectedSigs1); + + const expectedSigs2 = signaturesToSolanaArray( + wethNameResponse.signatures + ); + await postQuerySigs( + wethNameResponse.signatures.slice(5), + signaturesKeypair, + wethNameResponse.signatures.length + ); + expect( + ( + await program.account.guardianSignatures.fetch( + signaturesKeypair.publicKey + ) + ).guardianSignatures + ).to.deep.equal(expectedSigs2); + } + ); + + it( + fmtTest("post_signatures", "Rejects append by non-initial payer"), + async () => { + const signaturesKeypair = anchor.web3.Keypair.generate(); + await postQuerySigs( + wethNameResponse.signatures.slice(0, 1), + signaturesKeypair, + wethNameResponse.signatures.length + ); + const otherPayersProgram = programPaidBy(secondAccount); + await expect( + postQuerySigs( + wethNameResponse.signatures.slice(1), + signaturesKeypair, + wethNameResponse.signatures.length, + otherPayersProgram ) - .accountsPartial({ - guardianSignatures: signatureKeypair.publicKey, - guardianSet: deriveGuardianSetKey( - coreBridgeAddress, + ).to.be.rejectedWith("WriteAuthorityMismatch."); + } + ); + + it( + fmtTest("verify_query", "Successfully verifies mainnet queries"), + async () => { + const signaturesKeypair = anchor.web3.Keypair.generate(); + await postQuerySigs(wethNameResponse.signatures, signaturesKeypair); + await expect( + program.methods + .verifyQuery( + Buffer.from(wethNameResponse.bytes, "hex"), guardianSetIndex - ), - }) - .preInstructions([ - anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ - units: 420_000, - }), - ]) - .rpc() - ).to.be.fulfilled; - await expect( - program.account.guardianSignatures.fetch(signatureKeypair.publicKey) - ).to.be.rejectedWith("Account does not exist or has no data"); - }); - it("Verifies mock queries!", async () => { - const mock = new QueryProxyMock({}); - const mockSignatures = mock.sign( - Buffer.from(wethNameResponse.bytes, "hex") - ); - const signatureData = signaturesToSolanaArray(mockSignatures); - const signatureKeypair = anchor.web3.Keypair.generate(); - await program.methods - .postSignatures(signatureData, signatureData.length) - .accounts({ guardianSignatures: signatureKeypair.publicKey }) - .signers([signatureKeypair]) - .rpc(); - await expect( - program.methods - .verifyQuery( - Buffer.from(wethNameResponse.bytes, "hex"), - mockGuardianSetIndex + ) + .accountsPartial({ + guardianSignatures: signaturesKeypair.publicKey, + guardianSet: deriveGuardianSetKey( + coreBridgeAddress, + guardianSetIndex + ), + }) + .preInstructions([ + anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ + units: 420_000, + }), + ]) + .rpc() + ).to.be.fulfilled; + await expect( + program.account.guardianSignatures.fetch(signaturesKeypair.publicKey) + ).to.be.rejectedWith("Account does not exist or has no data"); + } + ); + + const successfulSignaturesKeypair = anchor.web3.Keypair.generate(); + + it( + fmtTest("verify_query", "Successfully verifies mock queries"), + async () => { + const mock = new QueryProxyMock({}); + const mockSignatures = mock.sign( + Buffer.from(wethNameResponse.bytes, "hex") + ); + await postQuerySigs(mockSignatures, successfulSignaturesKeypair); + await expect( + program.methods + .verifyQuery( + Buffer.from(wethNameResponse.bytes, "hex"), + mockGuardianSetIndex + ) + .accountsPartial({ + guardianSignatures: successfulSignaturesKeypair.publicKey, + guardianSet: deriveGuardianSetKey( + coreBridgeAddress, + mockGuardianSetIndex + ), + }) + .rpc() + ).to.be.fulfilled; + } + ); + + it( + fmtTest("verify_query", "Successfully closed the signature set"), + async () => { + await expect( + program.account.guardianSignatures.fetch( + successfulSignaturesKeypair.publicKey ) - .accountsPartial({ - guardianSignatures: signatureKeypair.publicKey, - guardianSet: deriveGuardianSetKey( - coreBridgeAddress, + ).to.be.rejectedWith("Account does not exist or has no data"); + } + ); + + const validSignaturesKeypair = anchor.web3.Keypair.generate(); + + it( + fmtTest( + "verify_query", + "Rejects guardian set account not owned by the core bridge" + ), + async () => { + await postQuerySigs(wethNameResponse.signatures, validSignaturesKeypair); + await expect( + program.methods + .verifyQuery(Buffer.from(wethNameResponse.bytes, "hex"), 0) + .accountsPartial({ + guardianSet: deriveGuardianSetKey(devnetCoreBridgeAddress, 0), + guardianSignatures: validSignaturesKeypair.publicKey, + }) + .rpc() + ).to.be.rejectedWith( + "AnchorError caused by account: guardian_set. Error Code: AccountOwnedByWrongProgram." + ); + } + ); + + it( + fmtTest("verify_query", "Rejects guardian set account mismatch"), + async () => { + await expect( + program.methods + .verifyQuery( + Buffer.from(wethNameResponse.bytes, "hex"), mockGuardianSetIndex - ), - }) - .rpc() - ).to.be.fulfilled; - }); - it("Rejects an expired guardian set!", async () => { + ) + .accountsPartial({ + guardianSet: deriveGuardianSetKey( + coreBridgeAddress, + guardianSetIndex + ), + guardianSignatures: validSignaturesKeypair.publicKey, + }) + .rpc() + ).to.be.rejectedWith( + "AnchorError caused by account: guardian_set. Error Code: ConstraintSeeds." + ); + } + ); + + it( + fmtTest("verify_query", "Rejects refund recipient account mismatch"), + async () => { + await expect( + program.methods + .verifyQuery( + Buffer.from(wethNameResponse.bytes, "hex"), + mockGuardianSetIndex + ) + .accountsPartial({ + guardianSet: deriveGuardianSetKey( + coreBridgeAddress, + mockGuardianSetIndex + ), + guardianSignatures: validSignaturesKeypair.publicKey, + refundRecipient: secondAccount.publicKey, + }) + .rpc() + ).to.be.rejectedWith( + "AnchorError caused by account: guardian_signatures. Error Code: ConstraintHasOne." + ); + } + ); + + it(fmtTest("verify_query", "Rejects expired guardian set"), async () => { // notably, `opWethNameResponse` does not have guardian index 7 - xLabs, which is not in guardian set 2 - const guardianSetIndex = 2; // this one is expired const signatureData = signaturesToSolanaArray( opWethNameResponse.signatures ); - const signatureKeypair = anchor.web3.Keypair.generate(); + const signaturesKeypair = anchor.web3.Keypair.generate(); await program.methods .postSignatures(signatureData, signatureData.length) - .accounts({ guardianSignatures: signatureKeypair.publicKey }) - .signers([signatureKeypair]) + .accounts({ guardianSignatures: signaturesKeypair.publicKey }) + .signers([signaturesKeypair]) .rpc(); await expect( program.methods .verifyQuery( Buffer.from(opWethNameResponse.bytes, "hex"), - guardianSetIndex + expiredGuardianSetIndex ) .accountsPartial({ guardianSet: deriveGuardianSetKey( coreBridgeAddress, - guardianSetIndex + expiredGuardianSetIndex ), - guardianSignatures: signatureKeypair.publicKey, + guardianSignatures: signaturesKeypair.publicKey, }) .rpc() ).to.be.rejectedWith( - "Error Code: GuardianSetExpired. Error Number: 7798. Error Message: GuardianSetExpired." + "Error Code: GuardianSetExpired. Error Number: 6257. Error Message: GuardianSetExpired." ); }); - it("Rejects a no quorum signature set!", async () => { - const p = anchor.getProvider(); - const info = await getWormholeBridgeData(p.connection, coreBridgeAddress); - const guardianSetIndex = info.guardianSetIndex; + + it(fmtTest("verify_query", "Rejects no quorum"), async () => { const signatureData = signaturesToSolanaArray( wethNameResponse.signatures.slice(1) ); - const signatureKeypair = anchor.web3.Keypair.generate(); + const signaturesKeypair = anchor.web3.Keypair.generate(); await program.methods .postSignatures(signatureData, signatureData.length) - .accounts({ guardianSignatures: signatureKeypair.publicKey }) - .signers([signatureKeypair]) + .accounts({ guardianSignatures: signaturesKeypair.publicKey }) + .signers([signaturesKeypair]) .rpc(); await expect( program.methods @@ -301,27 +376,121 @@ describe("example-queries-solana-verify", () => { coreBridgeAddress, guardianSetIndex ), - guardianSignatures: signatureKeypair.publicKey, + guardianSignatures: signaturesKeypair.publicKey, }) .rpc() ).to.be.rejectedWith( - "Error Code: NoQuorum. Error Number: 6515. Error Message: NoQuorum." + "Error Code: NoQuorum. Error Number: 6259. Error Message: NoQuorum." ); }); - it("Rejects a valid signature on the wrong guardian index!", async () => { - const p = anchor.getProvider(); - const info = await getWormholeBridgeData(p.connection, coreBridgeAddress); - const guardianSetIndex = info.guardianSetIndex; - const badSignatures = [...wethNameResponse.signatures]; - badSignatures[0] = - "f122af3db0ae62af57bc16f0b3e79c86cbfc860a5994ca65928c06a739a2f4ca0496c7c1de38350e7b7cdc573fa0b7af981f3ac3d60298d67c76ca99d3bcf1040001"; - const signatureData = signaturesToSolanaArray(badSignatures); - const signatureKeypair = anchor.web3.Keypair.generate(); - await program.methods - .postSignatures(signatureData, signatureData.length) - .accounts({ guardianSignatures: signatureKeypair.publicKey }) - .signers([signatureKeypair]) - .rpc(); + + it( + fmtTest("verify_query", "Rejects out of order guardian signatures"), + async () => { + const signatureSet = anchor.web3.Keypair.generate(); + await postQuerySigs( + [ + ...wethNameResponse.signatures.slice(1), + ...wethNameResponse.signatures.slice(0, 1), + ], + signatureSet + ); + await expect( + program.methods + .verifyQuery( + Buffer.from(wethNameResponse.bytes, "hex"), + guardianSetIndex + ) + .accountsPartial({ + guardianSet: deriveGuardianSetKey( + coreBridgeAddress, + guardianSetIndex + ), + guardianSignatures: signatureSet.publicKey, + }) + .preInstructions([ + anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ + units: 420_000, + }), + ]) + .rpc() + ).to.be.rejectedWith("InvalidGuardianIndexNonIncreasing."); + } + ); + + it( + fmtTest("verify_query", "Rejects duplicate guardian signatures"), + async () => { + const signatureSet = anchor.web3.Keypair.generate(); + await postQuerySigs( + new Array(13).fill(wethNameResponse.signatures[0]), + signatureSet + ); + await expect( + program.methods + .verifyQuery( + Buffer.from(wethNameResponse.bytes, "hex"), + guardianSetIndex + ) + .accountsPartial({ + guardianSet: deriveGuardianSetKey( + coreBridgeAddress, + guardianSetIndex + ), + guardianSignatures: signatureSet.publicKey, + }) + .preInstructions([ + anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ + units: 420_000, + }), + ]) + .rpc() + ).to.be.rejectedWith("InvalidGuardianIndexNonIncreasing."); + } + ); + + it( + fmtTest("verify_query", "Rejects guardian index out of bounds"), + async () => { + const signatureSet = anchor.web3.Keypair.generate(); + await postQuerySigs( + [ + wethNameResponse.signatures[0].substring(0, 130) + "20", + ...wethNameResponse.signatures.slice(1), + ], + signatureSet + ); + await expect( + program.methods + .verifyQuery( + Buffer.from(wethNameResponse.bytes, "hex"), + guardianSetIndex + ) + .accountsPartial({ + guardianSet: deriveGuardianSetKey( + coreBridgeAddress, + guardianSetIndex + ), + guardianSignatures: signatureSet.publicKey, + }) + .rpc() + ).to.be.rejectedWith("InvalidGuardianIndexOutOfRange."); + } + ); + + it(fmtTest("verify_query", "Rejects invalid signature"), async () => { + const signatureSet = anchor.web3.Keypair.generate(); + const badRecovery = "02"; + await postQuerySigs( + [ + `${wethNameResponse.signatures[0].substring( + 0, + 128 + )}${badRecovery}${wethNameResponse.signatures[0].substring(130)}`, + ...wethNameResponse.signatures.slice(1), + ], + signatureSet + ); await expect( program.methods .verifyQuery( @@ -333,31 +502,17 @@ describe("example-queries-solana-verify", () => { coreBridgeAddress, guardianSetIndex ), - guardianSignatures: signatureKeypair.publicKey, + guardianSignatures: signatureSet.publicKey, }) .rpc() - ).to.be.rejectedWith( - "Error Code: InvalidGuardianKeyRecovery. Error Number: 7800. Error Message: InvalidGuardianKeyRecovery." - ); + ).to.be.rejectedWith("InvalidSignature."); }); - it("Rejects an invalid signature!", async () => { - const p = anchor.getProvider(); - const info = await getWormholeBridgeData(p.connection, coreBridgeAddress); - const guardianSetIndex = info.guardianSetIndex; - const badSignatures = [...wethNameResponse.signatures]; - badSignatures[0] = - "f122af3db0ae62af57bc16f0b3e79c86cbfc860a5994ca65928c06a739a2f4ca0496c7c1de38350e7b7cdc573fa0b7af981f3ac3d60298d67c76ca99d3bcf1040102"; - const signatureData = signaturesToSolanaArray(badSignatures); - const signatureKeypair = anchor.web3.Keypair.generate(); - await program.methods - .postSignatures(signatureData, signatureData.length) - .accounts({ guardianSignatures: signatureKeypair.publicKey }) - .signers([signatureKeypair]) - .rpc(); + + it(fmtTest("verify_query", "Rejects invalid message hash"), async () => { await expect( program.methods .verifyQuery( - Buffer.from(wethNameResponse.bytes, "hex"), + Buffer.from(wethNameResponse.bytes + "00", "hex"), guardianSetIndex ) .accountsPartial({ @@ -365,28 +520,82 @@ describe("example-queries-solana-verify", () => { coreBridgeAddress, guardianSetIndex ), - guardianSignatures: signatureKeypair.publicKey, + guardianSignatures: validSignaturesKeypair.publicKey, }) .rpc() - ).to.be.rejectedWith( - "Error Code: InvalidGuardianKeyRecovery. Error Number: 7800. Error Message: InvalidGuardianKeyRecovery." - ); + ).to.be.rejectedWith("InvalidGuardianKeyRecovery."); }); - it("Rejects a valid signature for the wrong message!", async () => { - const p = anchor.getProvider(); - const info = await getWormholeBridgeData(p.connection, coreBridgeAddress); - const guardianSetIndex = info.guardianSetIndex; - const signatureData = signaturesToSolanaArray(wethNameResponse.signatures); - const signatureKeypair = anchor.web3.Keypair.generate(); + + it(fmtTest("verify_query", "Rejects un-parse-able response"), async () => { + const badBytes = Buffer.from("00" + wethNameResponse.bytes, "hex"); + const badBytesSigs = new QueryProxyMock({}).sign(badBytes); + const signatureSet = anchor.web3.Keypair.generate(); + await postQuerySigs(badBytesSigs, signatureSet); + await expect( + program.methods + .verifyQuery(badBytes, mockGuardianSetIndex) + .accountsPartial({ + guardianSet: deriveGuardianSetKey( + coreBridgeAddress, + mockGuardianSetIndex + ), + guardianSignatures: signatureSet.publicKey, + }) + .rpc() + ).to.be.rejectedWith("FailedToParseResponse."); + }); + + it( + fmtTest( + "verify_query", + "Rejects a valid signature on the wrong guardian index" + ), + async () => { + const badSignatures = [...wethNameResponse.signatures]; + badSignatures[0] = + "f122af3db0ae62af57bc16f0b3e79c86cbfc860a5994ca65928c06a739a2f4ca0496c7c1de38350e7b7cdc573fa0b7af981f3ac3d60298d67c76ca99d3bcf1040001"; + const signatureData = signaturesToSolanaArray(badSignatures); + const signaturesKeypair = anchor.web3.Keypair.generate(); + await program.methods + .postSignatures(signatureData, signatureData.length) + .accounts({ guardianSignatures: signaturesKeypair.publicKey }) + .signers([signaturesKeypair]) + .rpc(); + await expect( + program.methods + .verifyQuery( + Buffer.from(wethNameResponse.bytes, "hex"), + guardianSetIndex + ) + .accountsPartial({ + guardianSet: deriveGuardianSetKey( + coreBridgeAddress, + guardianSetIndex + ), + guardianSignatures: signaturesKeypair.publicKey, + }) + .rpc() + ).to.be.rejectedWith( + "Error Code: InvalidGuardianKeyRecovery. Error Number: 6263. Error Message: InvalidGuardianKeyRecovery." + ); + } + ); + + it(fmtTest("verify_query", "Rejects an invalid signature"), async () => { + const badSignatures = [...wethNameResponse.signatures]; + badSignatures[0] = + "f122af3db0ae62af57bc16f0b3e79c86cbfc860a5994ca65928c06a739a2f4ca0496c7c1de38350e7b7cdc573fa0b7af981f3ac3d60298d67c76ca99d3bcf1040102"; + const signatureData = signaturesToSolanaArray(badSignatures); + const signaturesKeypair = anchor.web3.Keypair.generate(); await program.methods .postSignatures(signatureData, signatureData.length) - .accounts({ guardianSignatures: signatureKeypair.publicKey }) - .signers([signatureKeypair]) + .accounts({ guardianSignatures: signaturesKeypair.publicKey }) + .signers([signaturesKeypair]) .rpc(); await expect( program.methods .verifyQuery( - Buffer.from(opWethNameResponse.bytes, "hex"), + Buffer.from(wethNameResponse.bytes, "hex"), guardianSetIndex ) .accountsPartial({ @@ -394,34 +603,106 @@ describe("example-queries-solana-verify", () => { coreBridgeAddress, guardianSetIndex ), - guardianSignatures: signatureKeypair.publicKey, + guardianSignatures: signaturesKeypair.publicKey, }) .rpc() ).to.be.rejectedWith( - "Error Code: InvalidGuardianKeyRecovery. Error Number: 7800. Error Message: InvalidGuardianKeyRecovery." + "Error Code: InvalidGuardianKeyRecovery. Error Number: 6263. Error Message: InvalidGuardianKeyRecovery." ); }); - it("Closes signaure accounts!", async () => { - const signatureData = signaturesToSolanaArray(wethNameResponse.signatures); - const signatureKeypair = anchor.web3.Keypair.generate(); - await program.methods - .postSignatures(signatureData, signatureData.length) - .accounts({ guardianSignatures: signatureKeypair.publicKey }) - .signers([signatureKeypair]) - .rpc(); - await expect( - program.account.guardianSignatures.fetch(signatureKeypair.publicKey) - ).to.be.fulfilled; - await expect( - program.methods - .closeSignatures() - .accounts({ - guardianSignatures: signatureKeypair.publicKey, - }) - .rpc() - ).to.be.fulfilled; - await expect( - program.account.guardianSignatures.fetch(signatureKeypair.publicKey) - ).to.be.rejectedWith("Account does not exist or has no data"); - }); + + it( + fmtTest("verify_query", "Rejects a valid signature for the wrong message"), + async () => { + const signatureData = signaturesToSolanaArray( + wethNameResponse.signatures + ); + const signaturesKeypair = anchor.web3.Keypair.generate(); + await program.methods + .postSignatures(signatureData, signatureData.length) + .accounts({ guardianSignatures: signaturesKeypair.publicKey }) + .signers([signaturesKeypair]) + .rpc(); + await expect( + program.methods + .verifyQuery( + Buffer.from(opWethNameResponse.bytes, "hex"), + guardianSetIndex + ) + .accountsPartial({ + guardianSet: deriveGuardianSetKey( + coreBridgeAddress, + guardianSetIndex + ), + guardianSignatures: signaturesKeypair.publicKey, + }) + .rpc() + ).to.be.rejectedWith( + "Error Code: InvalidGuardianKeyRecovery. Error Number: 6263. Error Message: InvalidGuardianKeyRecovery." + ); + } + ); + + it( + fmtTest("close_signatures", "Successfully closes signature accounts"), + async () => { + const signaturesKeypair = anchor.web3.Keypair.generate(); + await postQuerySigs(wethNameResponse.signatures, signaturesKeypair); + await expect( + program.account.guardianSignatures.fetch(signaturesKeypair.publicKey) + ).to.be.fulfilled; + await expect( + program.methods + .closeSignatures() + .accounts({ + guardianSignatures: signaturesKeypair.publicKey, + }) + .rpc() + ).to.be.fulfilled; + await expect( + program.account.guardianSignatures.fetch(signaturesKeypair.publicKey) + ).to.be.rejectedWith("Account does not exist or has no data"); + } + ); + + it( + fmtTest("close_signatures", "Rejects refund recipient account mismatch"), + async () => { + const signaturesKeypair = anchor.web3.Keypair.generate(); + await postQuerySigs(wethNameResponse.signatures, signaturesKeypair); + const otherPayersProgram = programPaidBy(secondAccount); + await expect( + otherPayersProgram.methods + .closeSignatures() + .accounts({ + guardianSignatures: signaturesKeypair.publicKey, + }) + .rpc() + ).to.be.rejectedWith( + "AnchorError caused by account: guardian_signatures. Error Code: ConstraintHasOne." + ); + } + ); + + it( + fmtTest("close_signatures", "Rejects without refund recipient as signer"), + async () => { + const signaturesKeypair = anchor.web3.Keypair.generate(); + await postQuerySigs(wethNameResponse.signatures, signaturesKeypair); + const otherPayersProgram = programPaidBy(secondAccount); + await expect( + otherPayersProgram.methods + .closeSignatures() + .accountsPartial({ + guardianSignatures: signaturesKeypair.publicKey, + refundRecipient: anchor.getProvider().publicKey, + }) + .rpc() + ).to.be.rejectedWith( + `Missing signature for public key [\`${anchor + .getProvider() + .publicKey.toString()}\`].` + ); + } + ); }); diff --git a/tests/helpers/utils/signaturesToSolanaArray.ts b/tests/helpers/utils/signaturesToSolanaArray.ts new file mode 100644 index 0000000..543fcde --- /dev/null +++ b/tests/helpers/utils/signaturesToSolanaArray.ts @@ -0,0 +1,10 @@ +// TODO: PR to @wormhole-foundation/wormhole-query-sdk +// GuardianSetSig expects the guardian index before the signature, the inverse of what the Query Proxy returns. +// https://docs.rs/wormhole-raw-vaas/0.3.0-alpha.1/src/wormhole_raw_vaas/protocol.rs.html#220 +// https://github.com/wormhole-foundation/wormhole/blob/31b01629087c610c12fa8e84069786139dc0b6bd/node/cmd/ccq/http.go#L191 +export function signaturesToSolanaArray(signatures: string[]) { + return signatures.map((s) => [ + ...Buffer.from(s.substring(130, 132), "hex"), + ...Buffer.from(s.substring(0, 130), "hex"), + ]); +}