diff --git a/packages/svm/programs/settler/src/instructions/add_axia_sig.rs b/packages/svm/programs/settler/src/instructions/add_axia_sig.rs new file mode 100644 index 0000000..64bc3d1 --- /dev/null +++ b/packages/svm/programs/settler/src/instructions/add_axia_sig.rs @@ -0,0 +1,77 @@ +use anchor_lang::{ + prelude::{instruction::Instruction, sysvar::instructions::get_instruction_relative, *}, + solana_program::sysvar::instructions::ID as IX_ID, +}; + +use crate::{ + controller::{accounts::EntityRegistry, types::EntityType}, + errors::SettlerError, + state::Proposal, + utils::{check_ed25519_ix, get_args_from_ed25519_ix_data, Ed25519Args}, +}; + +#[derive(Accounts)] +pub struct AddAxiaSig<'info> { + #[account(mut)] + pub solver: Signer<'info>, + + #[account( + seeds = [b"entity-registry", &[EntityType::Solver as u8 + 1], solver.key().as_ref()], + bump = solver_registry.bump, + seeds::program = crate::controller::ID, + )] + pub solver_registry: Box>, + + #[account( + seeds = [b"entity-registry", &[EntityType::Axia as u8 + 1], axia_registry.entity_pubkey.as_ref()], + bump = axia_registry.bump, + seeds::program = crate::controller::ID, + )] + pub axia_registry: Box>, + + /// CHECK: Any proposal + #[account( + mut, + constraint = proposal.deadline > Clock::get()?.unix_timestamp as u64 @ SettlerError::ProposalIsExpired, + constraint = proposal.is_final @ SettlerError::ProposalIsNotFinal, + )] + pub proposal: Box>, + + /// CHECK: The address check is needed because otherwise + /// the supplied Sysvar could be anything else. + #[account(address = IX_ID)] + pub ix_sysvar: AccountInfo<'info>, +} + +pub fn add_axia_sig(ctx: Context) -> Result<()> { + let proposal = &mut ctx.accounts.proposal; + + // NOP if already signed + if proposal.is_signed { + return Ok(()); + } + + // Get Ed25519 instruction + let ed25519_ix: Instruction = get_instruction_relative(-1, &ctx.accounts.ix_sysvar)?; + let ed25519_ix_args: Ed25519Args = get_args_from_ed25519_ix_data(&ed25519_ix.data)?; + + // Verify correct program and accounts + check_ed25519_ix(&ed25519_ix)?; + + // Verify correct message was signed + require!( + ed25519_ix_args.msg == proposal.key().as_array(), + SettlerError::SigVerificationFailed + ); + + // Verify pubkey is whitelisted Axia + require!( + ed25519_ix_args.pubkey == &ctx.accounts.axia_registry.entity_pubkey.to_bytes(), + SettlerError::AxiaNotAllowlisted + ); + + // Updates proposal as signed + proposal.is_signed = true; + + Ok(()) +} diff --git a/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs b/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs index 9ba9ed3..39533ef 100644 --- a/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/add_instructions_to_proposal.rs @@ -18,7 +18,12 @@ pub struct AddInstructionsToProposal<'info> { realloc::zero = true, has_one = creator @ SettlerError::IncorrectProposalCreator )] - // Any proposal + + /// Any proposal + #[account( + constraint = proposal.deadline > Clock::get()?.unix_timestamp as u64 @ SettlerError::ProposalIsExpired, + constraint = !proposal.is_final @ SettlerError::ProposalIsFinal + )] pub proposal: Box>, pub system_program: Program<'info, System>, @@ -29,12 +34,8 @@ pub fn add_instructions_to_proposal( more_instructions: Vec, finalize: bool, ) -> Result<()> { - let now = Clock::get()?.unix_timestamp as u64; let proposal = &mut ctx.accounts.proposal; - require!(proposal.deadline > now, SettlerError::ProposalIsExpired); - require!(!proposal.is_final, SettlerError::ProposalIsFinal); - proposal.instructions.extend_from_slice(&more_instructions); if finalize { diff --git a/packages/svm/programs/settler/src/instructions/add_validator_sig.rs b/packages/svm/programs/settler/src/instructions/add_validator_sig.rs new file mode 100644 index 0000000..79fbef1 --- /dev/null +++ b/packages/svm/programs/settler/src/instructions/add_validator_sig.rs @@ -0,0 +1,96 @@ +use anchor_lang::{ + prelude::{instruction::Instruction, sysvar::instructions::get_instruction_relative, *}, + solana_program::sysvar::instructions::ID as IX_ID, +}; + +use crate::{ + controller::{accounts::EntityRegistry, types::EntityType}, + errors::SettlerError, + state::Intent, + utils::{check_ed25519_ix, get_args_from_ed25519_ix_data, Ed25519Args}, +}; + +#[derive(Accounts)] +pub struct AddValidatorSig<'info> { + #[account(mut)] + pub solver: Signer<'info>, + + #[account( + seeds = [b"entity-registry", &[EntityType::Solver as u8 + 1], solver.key().as_ref()], + bump = solver_registry.bump, + seeds::program = crate::controller::ID, + )] + pub solver_registry: Box>, + + // Any Intent + #[account( + mut, + constraint = intent.deadline > Clock::get()?.unix_timestamp as u64 @ SettlerError::IntentIsExpired, + constraint = intent.is_final @ SettlerError::IntentIsNotFinal + )] + pub intent: Box>, + + #[account( + seeds = [b"fulfilled-intent", intent.hash.as_ref()], + bump + )] + /// This PDA must be uninitialized + pub fulfilled_intent: SystemAccount<'info>, + + /// CHECK: other checks in ix body + pub validator_registry: Box>, + + /// CHECK: The address check is needed because otherwise + /// the supplied Sysvar could be anything else. + #[account(address = IX_ID)] + pub ix_sysvar: AccountInfo<'info>, +} + +pub fn add_validator_sig(ctx: Context) -> Result<()> { + let intent = &mut ctx.accounts.intent; + + // Get Ed25519 instruction + let ed25519_ix: Instruction = get_instruction_relative(-1, &ctx.accounts.ix_sysvar)?; + let ed25519_ix_args: Ed25519Args = get_args_from_ed25519_ix_data(&ed25519_ix.data)?; + + // Verify correct program and accounts + check_ed25519_ix(&ed25519_ix)?; + + // Verify correct message was signed + require!( + ed25519_ix_args.msg == intent.hash, + SettlerError::SigVerificationFailed + ); + + // Verify pubkey is a whitelisted Validator + require_keys_eq!( + ctx.accounts.validator_registry.key(), + Pubkey::create_program_address( + &[ + b"entity-registry", + &[EntityType::Validator as u8 + 1], + ed25519_ix_args.pubkey, + &[ctx.accounts.validator_registry.bump] + ], + &crate::controller::ID, + ) + .map_err(|_| SettlerError::ValidatorNotAllowlisted)?, + SettlerError::ValidatorNotAllowlisted, + ); + + // Updates intent PDA if signature not present and min_validations not met + + if intent.validators.len() == intent.min_validations as usize { + return Ok(()); + } + + let ed25519_pubkey = Pubkey::try_from_slice(ed25519_ix_args.pubkey)?; + + if intent.validators.contains(&ed25519_pubkey) { + return Ok(()); + } + + intent.validators.push(ed25519_pubkey); + + Ok(()) +} diff --git a/packages/svm/programs/settler/src/instructions/change_controller_program.rs b/packages/svm/programs/settler/src/instructions/change_controller_program.rs new file mode 100644 index 0000000..62fca3b --- /dev/null +++ b/packages/svm/programs/settler/src/instructions/change_controller_program.rs @@ -0,0 +1,9 @@ +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct ChangeControllerProgram {} + +pub fn change_controller_program(ctx: Context) -> Result<()> { + // TODO: check against crate::controller::ID + Ok(()) +} diff --git a/packages/svm/programs/settler/src/instructions/claim_stale_intent.rs b/packages/svm/programs/settler/src/instructions/claim_stale_intent.rs index 9a3f6f1..b3df8ef 100644 --- a/packages/svm/programs/settler/src/instructions/claim_stale_intent.rs +++ b/packages/svm/programs/settler/src/instructions/claim_stale_intent.rs @@ -11,7 +11,7 @@ pub struct ClaimStaleIntent<'info> { mut, close = creator, has_one = creator @ SettlerError::IncorrectIntentCreator, - constraint = Clock::get()?.unix_timestamp as u64 > intent.deadline @ SettlerError::IntentNotYetExpired + constraint = intent.deadline < Clock::get()?.unix_timestamp as u64 @ SettlerError::IntentNotYetExpired )] pub intent: Box>, } diff --git a/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs b/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs index 3ec7674..e1178e8 100644 --- a/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/claim_stale_proposal.rs @@ -11,7 +11,7 @@ pub struct ClaimStaleProposal<'info> { mut, close = creator, has_one = creator @ SettlerError::IncorrectProposalCreator, - constraint = Clock::get()?.unix_timestamp as u64 > proposal.deadline @ SettlerError::ProposalNotYetExpired + constraint = proposal.deadline < Clock::get()?.unix_timestamp as u64 @ SettlerError::ProposalNotYetExpired )] pub proposal: Box>, } diff --git a/packages/svm/programs/settler/src/instructions/execute_proposal.rs b/packages/svm/programs/settler/src/instructions/execute_proposal.rs index ac7ab12..8f292ff 100644 --- a/packages/svm/programs/settler/src/instructions/execute_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/execute_proposal.rs @@ -28,6 +28,7 @@ pub struct ExecuteProposal<'info> { has_one = intent @ SettlerError::IncorrectIntentForProposal, constraint = proposal.creator == proposal_creator.key() @ SettlerError::IncorrectProposalCreator, constraint = proposal.is_signed @ SettlerError::ProposalIsNotSigned, + constraint = proposal.deadline > Clock::get()?.unix_timestamp as u64 @ SettlerError::ProposalIsExpired, close = proposal_creator )] pub proposal: Box>, @@ -56,12 +57,8 @@ pub struct ExecuteProposal<'info> { } pub fn execute_proposal(ctx: Context) -> Result<()> { - let now = Clock::get()?.unix_timestamp as u64; - let proposal = &ctx.accounts.proposal; let intent = &ctx.accounts.intent; - require!(proposal.deadline > now, SettlerError::ProposalIsExpired); - // TODO: Execute proposal // TODO: Validate execution diff --git a/packages/svm/programs/settler/src/instructions/extend_intent.rs b/packages/svm/programs/settler/src/instructions/extend_intent.rs index 46b8f05..ac56959 100644 --- a/packages/svm/programs/settler/src/instructions/extend_intent.rs +++ b/packages/svm/programs/settler/src/instructions/extend_intent.rs @@ -16,6 +16,7 @@ pub struct ExtendIntent<'info> { mut, has_one = creator @ SettlerError::IncorrectIntentCreator, constraint = !intent.is_final @ SettlerError::IntentIsFinal, + constraint = intent.deadline > Clock::get()?.unix_timestamp as u64 @ SettlerError::IntentIsExpired, realloc = Intent::extended_size(intent.to_account_info().data_len(), &more_data, &more_max_fees, &more_events)?, realloc::payer = creator, diff --git a/packages/svm/programs/settler/src/instructions/mod.rs b/packages/svm/programs/settler/src/instructions/mod.rs index 2a7938f..d7dd5e2 100644 --- a/packages/svm/programs/settler/src/instructions/mod.rs +++ b/packages/svm/programs/settler/src/instructions/mod.rs @@ -1,4 +1,7 @@ +pub mod add_axia_sig; pub mod add_instructions_to_proposal; +pub mod add_validator_sig; +pub mod change_controller_program; pub mod claim_stale_intent; pub mod claim_stale_proposal; pub mod create_intent; @@ -7,7 +10,10 @@ pub mod execute_proposal; pub mod extend_intent; pub mod initialize; +pub use add_axia_sig::*; pub use add_instructions_to_proposal::*; +pub use add_validator_sig::*; +pub use change_controller_program::*; pub use claim_stale_intent::*; pub use claim_stale_proposal::*; pub use create_intent::*; diff --git a/packages/svm/programs/settler/src/lib.rs b/packages/svm/programs/settler/src/lib.rs index b1ca22a..98104dd 100644 --- a/packages/svm/programs/settler/src/lib.rs +++ b/packages/svm/programs/settler/src/lib.rs @@ -16,6 +16,10 @@ use crate::{instructions::*, state::*, types::*}; pub mod settler { use super::*; + pub fn add_axia_sig(ctx: Context) -> Result<()> { + instructions::add_axia_sig(ctx) + } + pub fn add_instructions_to_proposal( ctx: Context, more_instructions: Vec, @@ -24,6 +28,14 @@ pub mod settler { instructions::add_instructions_to_proposal(ctx, more_instructions, finalize) } + pub fn add_validator_sig(ctx: Context) -> Result<()> { + instructions::add_validator_sig(ctx) + } + + pub fn change_controller_program(ctx: Context) -> Result<()> { + instructions::change_controller_program(ctx) + } + pub fn claim_stale_intent(ctx: Context) -> Result<()> { instructions::claim_stale_intent(ctx) } diff --git a/packages/svm/programs/settler/src/utils/mod.rs b/packages/svm/programs/settler/src/utils/mod.rs index ed1f5ec..bd9ec41 100644 --- a/packages/svm/programs/settler/src/utils/mod.rs +++ b/packages/svm/programs/settler/src/utils/mod.rs @@ -1,3 +1,5 @@ pub mod math; +pub mod sigs; pub use math::*; +pub use sigs::*; diff --git a/packages/svm/programs/settler/src/utils/sigs.rs b/packages/svm/programs/settler/src/utils/sigs.rs new file mode 100644 index 0000000..a5ce59b --- /dev/null +++ b/packages/svm/programs/settler/src/utils/sigs.rs @@ -0,0 +1,75 @@ +use anchor_lang::prelude::{instruction::Instruction, *}; + +use crate::errors::SettlerError; + +pub fn check_ed25519_ix(ix: &Instruction) -> Result<()> { + if ix.program_id.to_string() != "Ed25519SigVerify111111111111111111111111111" + || ix.accounts.len() != 0 + { + return err!(SettlerError::SigVerificationFailed); + } + + Ok(()) +} + +pub struct Ed25519Args<'a> { + pub pubkey: &'a [u8; 32], + pub sig: &'a [u8; 64], + pub msg: &'a [u8], +} + +pub fn get_args_from_ed25519_ix_data(data: &[u8]) -> Result> { + if data.len() < 112 { + return err!(SettlerError::SigVerificationFailed); + } + + // Header + let num_signatures = &[data[0]]; + let padding = &[data[1]]; + let signature_offset = &data[2..=3]; + let signature_instruction_index = &data[4..=5]; + let public_key_offset = &data[6..=7]; + let public_key_instruction_index = &data[8..=9]; + let message_data_offset = &data[10..=11]; + let message_data_size = &data[12..=13]; + let message_instruction_index = &data[14..=15]; + + // Data + let pubkey = &data[16..16 + 32]; + let sig = &data[48..48 + 64]; + let msg = &data[112..]; + + // Expected values + let exp_public_key_offset: u16 = 16; // 2*u8 + 7*u16 + let exp_signature_offset: u16 = exp_public_key_offset + 32_u16; + let exp_message_data_offset: u16 = exp_signature_offset + 64_u16; + let exp_num_signatures: u8 = 1; + let exp_message_data_size: u16 = msg + .len() + .try_into() + .map_err(|_| SettlerError::SigVerificationFailed)?; + + // Header + if num_signatures != &exp_num_signatures.to_le_bytes() + || padding != &[0] + || signature_offset != &exp_signature_offset.to_le_bytes() + || signature_instruction_index != &u16::MAX.to_le_bytes() + || public_key_offset != &exp_public_key_offset.to_le_bytes() + || public_key_instruction_index != &u16::MAX.to_le_bytes() + || message_data_offset != &exp_message_data_offset.to_le_bytes() + || message_data_size != &exp_message_data_size.to_le_bytes() + || message_instruction_index != &u16::MAX.to_le_bytes() + { + return err!(SettlerError::SigVerificationFailed); + } + + Ok(Ed25519Args { + pubkey: pubkey + .try_into() + .map_err(|_| SettlerError::SigVerificationFailed)?, + sig: sig + .try_into() + .map_err(|_| SettlerError::SigVerificationFailed)?, + msg, + }) +} diff --git a/packages/svm/sdks/settler/Settler.ts b/packages/svm/sdks/settler/Settler.ts index 48d07ee..6df6fd2 100644 --- a/packages/svm/sdks/settler/Settler.ts +++ b/packages/svm/sdks/settler/Settler.ts @@ -169,6 +169,57 @@ export default class SettlerSDK { return ix } + async addValidatorSigIxs( + intent: web3.PublicKey, + intentHash: Buffer, + validator: web3.PublicKey, + signature: number[] + ): Promise { + const ed25519Ix = web3.Ed25519Program.createInstructionWithPublicKey({ + message: intentHash, + publicKey: validator.toBuffer(), + signature: Buffer.from(signature), + }) + + const ix = await this.program.methods + .addValidatorSig() + .accountsPartial({ + solver: this.getSignerKey(), + solverRegistry: this.getEntityRegistryPubkey(EntityType.Solver, this.getSignerKey()), + intent, + validatorRegistry: this.getEntityRegistryPubkey(EntityType.Validator, validator), + ixSysvar: web3.SYSVAR_INSTRUCTIONS_PUBKEY, + }) + .instruction() + + return [ed25519Ix, ix] + } + + async addAxiaSigIxs( + proposal: web3.PublicKey, + axia: web3.PublicKey, + signature: number[] + ): Promise { + const ed25519Ix = web3.Ed25519Program.createInstructionWithPublicKey({ + message: proposal.toBuffer(), + publicKey: axia.toBuffer(), + signature: Buffer.from(signature), + }) + + const ix = await this.program.methods + .addAxiaSig() + .accountsPartial({ + solver: this.getSignerKey(), + solverRegistry: this.getEntityRegistryPubkey(EntityType.Solver, this.getSignerKey()), + proposal, + axiaRegistry: this.getEntityRegistryPubkey(EntityType.Axia, axia), + ixSysvar: web3.SYSVAR_INSTRUCTIONS_PUBKEY, + }) + .instruction() + + return [ed25519Ix, ix] + } + getSettlerSettingsPubkey(): web3.PublicKey { return web3.PublicKey.findProgramAddressSync([Buffer.from('settler-settings')], this.program.programId)[0] } diff --git a/packages/svm/tests/helpers/helpers.ts b/packages/svm/tests/helpers/helpers.ts index 81d3ff4..f36b3f0 100644 --- a/packages/svm/tests/helpers/helpers.ts +++ b/packages/svm/tests/helpers/helpers.ts @@ -1,5 +1,6 @@ import { Program, web3 } from '@coral-xyz/anchor' import { randomHex } from '@mimicprotocol/sdk' +import { signAsync } from '@noble/ed25519' import { Keypair, PublicKey } from '@solana/web3.js' import { LiteSVMProvider } from 'anchor-litesvm' import { expect } from 'chai' @@ -229,6 +230,22 @@ export async function createAllowlistedEntity( return entity } +/** + * Create an Ed25519 signature for a validator (signs intent hash) + */ +export async function createValidatorSignature(intentHash: string, validator: Keypair): Promise { + const signature = await signAsync(Buffer.from(intentHash, 'hex'), validator.secretKey.slice(0, 32)) + return Array.from(new Uint8Array(signature)) +} + +/** + * Create an Ed25519 signature for an axia (signs proposal key) + */ +export async function createAxiaSignature(proposalKey: PublicKey, axia: Keypair): Promise { + const signature = await signAsync(proposalKey.toBuffer(), axia.secretKey.slice(0, 32)) + return Array.from(new Uint8Array(signature)) +} + /** * Helper to expect transaction errors consistently */ diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index ad86680..8189f31 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -2,7 +2,8 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { Program, Wallet } from '@coral-xyz/anchor' -import { Keypair } from '@solana/web3.js' +import { signAsync } from '@noble/ed25519' +import { Ed25519Program, Keypair, SYSVAR_INSTRUCTIONS_PUBKEY, TransactionInstruction } from '@solana/web3.js' import { fromWorkspace, LiteSVMProvider } from 'anchor-litesvm' import { expect } from 'chai' import fs from 'fs' @@ -41,13 +42,18 @@ import { TEST_DATA_HEX_1, TEST_DATA_HEX_2, TEST_DATA_HEX_3, + VERY_SHORT_DEADLINE, WARP_TIME_LONG, WARP_TIME_SHORT, } from './helpers/constants' import { addValidatorsToIntent, + createAllowlistedEntity, + createAxiaSignature, + createFinalizedProposal, createTestIntent, createValidatedIntent, + createValidatorSignature, expectTransactionError, generateIntentHash, generateNonce, @@ -1817,5 +1823,726 @@ describe('Settler Program', () => { expect(errorMsg.includes(`AccountNotInitialized`)).to.be.true }) }) + + describe('add_validator_sigs', () => { + let whitelistedValidator: Keypair + + before(async () => { + whitelistedValidator = Keypair.generate() + const whitelistValidatorIx = await controllerSdk.setAllowedEntityIx( + EntityType.Validator, + whitelistedValidator.publicKey + ) + await makeTxSignAndSend(provider, whitelistValidatorIx) + }) + + it('should add validator signature successfully', async () => { + const intentHash = await createTestIntent(solverSdk, solverProvider, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: true, + }) + const intentKey = sdk.getIntentKey(intentHash) + + const signature = await createValidatorSignature(intentHash, whitelistedValidator) + + const intentBefore = await program.account.intent.fetch(intentKey) + expect(intentBefore.validators.length).to.be.eq(0) + + const ixs = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + whitelistedValidator.publicKey, + signature + ) + await makeTxSignAndSend(solverProvider, ...ixs) + + const intentAfter = await program.account.intent.fetch(intentKey) + expect(intentAfter.validators.length).to.be.eq(1) + expect(intentAfter.validators[0].toString()).to.be.eq(whitelistedValidator.publicKey.toString()) + }) + + it('should add multiple validator signatures', async () => { + const intentHash = await createTestIntent(solverSdk, solverProvider, { + minValidations: MULTIPLE_MIN_VALIDATIONS, + isFinal: true, + }) + const intentKey = sdk.getIntentKey(intentHash) + + const validator1 = await createAllowlistedEntity(controllerSdk, provider, EntityType.Validator) + const validator2 = await createAllowlistedEntity(controllerSdk, provider, EntityType.Validator) + const validator3 = await createAllowlistedEntity(controllerSdk, provider, EntityType.Validator) + + const signature1 = await createValidatorSignature(intentHash, validator1) + const ixs1 = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + validator1.publicKey, + signature1 + ) + await makeTxSignAndSend(solverProvider, ...ixs1) + + const intentAfter1 = await program.account.intent.fetch(intentKey) + expect(intentAfter1.validators.length).to.be.eq(1) + + const signature2 = await createValidatorSignature(intentHash, validator2) + const ixs2 = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + validator2.publicKey, + signature2 + ) + await makeTxSignAndSend(solverProvider, ...ixs2) + + const intentAfter2 = await program.account.intent.fetch(intentKey) + expect(intentAfter2.validators.length).to.be.eq(2) + + const signature3 = await createValidatorSignature(intentHash, validator3) + const ixs3 = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + validator3.publicKey, + signature3 + ) + await makeTxSignAndSend(solverProvider, ...ixs3) + + const intentAfter3 = await program.account.intent.fetch(intentKey) + expect(intentAfter3.validators.length).to.be.eq(3) + expect(intentAfter3.validators.map((v) => v.toString())).to.include(validator1.publicKey.toString()) + expect(intentAfter3.validators.map((v) => v.toString())).to.include(validator2.publicKey.toString()) + expect(intentAfter3.validators.map((v) => v.toString())).to.include(validator3.publicKey.toString()) + }) + + it('should handle duplicate validator signature gracefully', async () => { + const intentHash = await createTestIntent(solverSdk, solverProvider, { minValidations: 2, isFinal: true }) + const intentKey = sdk.getIntentKey(intentHash) + + const signature = await createValidatorSignature(intentHash, whitelistedValidator) + + const ixs1 = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + whitelistedValidator.publicKey, + signature + ) + await makeTxSignAndSend(solverProvider, ...ixs1) + + const intentAfter1 = await program.account.intent.fetch(intentKey) + expect(intentAfter1.validators.length).to.be.eq(1) + + client.expireBlockhash() + const ixs2 = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + whitelistedValidator.publicKey, + signature + ) + await makeTxSignAndSend(solverProvider, ...ixs2) + + const intentAfter2 = await program.account.intent.fetch(intentKey) + expect(intentAfter2.validators.length).to.be.eq(1) + }) + + it('should handle when min_validations already met', async () => { + const intentHash = await createTestIntent(solverSdk, solverProvider, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: true, + }) + const intentKey = sdk.getIntentKey(intentHash) + + const validator1 = await createAllowlistedEntity(controllerSdk, provider, EntityType.Validator) + const validator2 = await createAllowlistedEntity(controllerSdk, provider, EntityType.Validator) + + const signature1 = await createValidatorSignature(intentHash, validator1) + const ixs1 = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + validator1.publicKey, + signature1 + ) + await makeTxSignAndSend(solverProvider, ...ixs1) + + const intentAfter1 = await program.account.intent.fetch(intentKey) + expect(intentAfter1.validators.length).to.be.eq(1) + expect(intentAfter1.minValidations).to.be.eq(1) + + client.expireBlockhash() + const signature2 = await createValidatorSignature(intentHash, validator2) + const ixs2 = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + validator2.publicKey, + signature2 + ) + await makeTxSignAndSend(solverProvider, ...ixs2) + + const intentAfter2 = await program.account.intent.fetch(intentKey) + expect(intentAfter2.validators.length).to.be.eq(1) + expect(intentAfter2.validators[0].toString()).to.be.eq(validator1.publicKey.toString()) + }) + + it('cannot add signature if intent is not final', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: false, + }) + const intentKey = sdk.getIntentKey(intentHash) + + const signature = await createValidatorSignature(intentHash, whitelistedValidator) + + const ixs = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + whitelistedValidator.publicKey, + signature + ) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expectTransactionError(res, `Intent is not final`) + }) + + it('cannot add signature if intent has expired', async () => { + const intentHash = generateIntentHash() + const nonce = generateNonce() + const user = Keypair.generate().publicKey + const now = Number(client.getClock().unixTimestamp) + const deadline = now + SHORT_DEADLINE + + const params = { + op: OpType.Transfer, + user, + nonceHex: nonce, + deadline, + minValidations: DEFAULT_MIN_VALIDATIONS, + dataHex: DEFAULT_DATA_HEX, + maxFees: [ + { + mint: Keypair.generate().publicKey, + amount: DEFAULT_MAX_FEE, + }, + ], + eventsHex: [], + } + + const ix = await solverSdk.createIntentIx(intentHash, params, true) + await makeTxSignAndSend(solverProvider, ix) + + const intentKey = sdk.getIntentKey(intentHash) + + warpSeconds(provider, 101) + + const signature = await createValidatorSignature(intentHash, whitelistedValidator) + + const ixs = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + whitelistedValidator.publicKey, + signature + ) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expectTransactionError(res, `Intent has already expired`) + }) + + it('cannot add signature if validator is not whitelisted', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: true, + }) + const intentKey = sdk.getIntentKey(intentHash) + + const validator = Keypair.generate() + + const signature = await createValidatorSignature(intentHash, validator) + + const ixs = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + validator.publicKey, + signature + ) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expectTransactionError( + res, + `AnchorError caused by account: validator_registry. Error Code: AccountNotInitialized.` + ) + }) + + it('cannot add signature if solver is not whitelisted', async () => { + const intentHash = await createTestIntent(solverSdk, solverProvider, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: true, + }) + const intentKey = sdk.getIntentKey(intentHash) + + const signature = await createValidatorSignature(intentHash, whitelistedValidator) + + const ixs = await maliciousSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + whitelistedValidator.publicKey, + signature + ) + const res = await makeTxSignAndSend(maliciousProvider, ...ixs) + + expectTransactionError( + res, + `AnchorError caused by account: solver_registry. Error Code: AccountNotInitialized.` + ) + }) + + it('cannot add signature for non-existent intent', async () => { + const intentHash = generateIntentHash() + const intentKey = sdk.getIntentKey(intentHash) + + const signature = await createValidatorSignature(intentHash, whitelistedValidator) + + const ed25519Ix = Ed25519Program.createInstructionWithPublicKey({ + message: Buffer.from(intentHash, 'hex'), + publicKey: whitelistedValidator.publicKey.toBuffer(), + signature: Buffer.from(signature), + }) + + const ix = await program.methods + .addValidatorSig() + .accountsPartial({ + solver: solverSdk.getSignerKey(), + solverRegistry: solverSdk.getEntityRegistryPubkey(EntityType.Solver, solverSdk.getSignerKey()), + intent: intentKey, + fulfilledIntent: solverSdk.getFulfilledIntentKey(intentHash), + validatorRegistry: solverSdk.getEntityRegistryPubkey(EntityType.Validator, whitelistedValidator.publicKey), + ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, + }) + .instruction() + + const res = await makeTxSignAndSend(solverProvider, ed25519Ix, ix) + + expectTransactionError(res, `AccountNotInitialized`) + }) + + it('cannot add signature if fulfilled_intent PDA already exists', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: true, + }) + const intentKey = sdk.getIntentKey(intentHash) + + const fulfilledIntent = sdk.getFulfilledIntentKey(intentHash) + client.setAccount(fulfilledIntent, { + executable: false, + lamports: 1002240, + owner: program.programId, + data: Buffer.from('595168911b9267f7' + '010000000000000000', 'hex'), + }) + + const signature = await createValidatorSignature(intentHash, whitelistedValidator) + + const ixs = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + whitelistedValidator.publicKey, + signature + ) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expectTransactionError( + res, + `AnchorError caused by account: fulfilled_intent. Error Code: AccountNotSystemOwned. Error Number: 3011. Error Message: The given account is not owned by the system program` + ) + }) + + it('cannot add signature with wrong intent hash', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: true, + }) + const intentKey = sdk.getIntentKey(intentHash) + + const wrongIntentHash = generateIntentHash() + const signature = await createValidatorSignature(wrongIntentHash, whitelistedValidator) + + const ixs = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(wrongIntentHash, 'hex'), + whitelistedValidator.publicKey, + signature + ) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expectTransactionError(res, `Signature verification failed`) + }) + + it('cannot add signature with invalid signature', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: true, + }) + const intentKey = sdk.getIntentKey(intentHash) + + const invalidSignature: number[] = Array.from({ length: 64 }, () => Math.floor(Math.random() * 256)) + + const ixs = await solverSdk.addValidatorSigIxs( + intentKey, + Buffer.from(intentHash, 'hex'), + whitelistedValidator.publicKey, + invalidSignature + ) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expect(res.toString()).to.be.eq( + `FailedTransactionMetadata(FailedTransactionMetadata { err: InstructionError(0, Custom(2)), meta: TransactionMetadata { signature: 1111111111111111111111111111111111111111111111111111111111111111, logs: [], inner_instructions: [], compute_units_consumed: 0, return_data: TransactionReturnData { program_id: 11111111111111111111111111111111, data: [] } } })` + ) + }) + + it('cannot add valid signature but for another intent', async () => { + const intentHash1 = await createTestIntent(solverSdk, solverProvider, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: true, + }) + const intentKey1 = sdk.getIntentKey(intentHash1) + + const intentHash2 = await createTestIntent(solverSdk, solverProvider, { + minValidations: DEFAULT_MIN_VALIDATIONS, + isFinal: true, + }) + + const signature = await createValidatorSignature(intentHash2, whitelistedValidator) + + const ixs = await solverSdk.addValidatorSigIxs( + intentKey1, + Buffer.from(intentHash2, 'hex'), + whitelistedValidator.publicKey, + signature + ) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expectTransactionError(res, `Signature verification failed`) + }) + }) + + describe('add_axia_sig', () => { + let whitelistedAxia: Keypair + + before(async () => { + whitelistedAxia = Keypair.generate() + const whitelistAxiaIx = await controllerSdk.setAllowedEntityIx(EntityType.Axia, whitelistedAxia.publicKey) + await makeTxSignAndSend(provider, whitelistAxiaIx) + }) + + it('should add axia signature successfully', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) + + const signature = await createAxiaSignature(proposalKey, whitelistedAxia) + + const proposalBefore = await program.account.proposal.fetch(proposalKey) + expect(proposalBefore.isSigned).to.be.false + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + await makeTxSignAndSend(solverProvider, ...ixs) + + const proposalAfter = await program.account.proposal.fetch(proposalKey) + expect(proposalAfter.isSigned).to.be.true + }) + + it('should handle duplicate signature gracefully', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) + + const signature = await createAxiaSignature(proposalKey, whitelistedAxia) + + const ixs1 = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + await makeTxSignAndSend(solverProvider, ...ixs1) + + const proposalAfter1 = await program.account.proposal.fetch(proposalKey) + expect(proposalAfter1.isSigned).to.be.true + + client.expireBlockhash() + const ixs2 = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + await makeTxSignAndSend(solverProvider, ...ixs2) + + const proposalAfter2 = await program.account.proposal.fetch(proposalKey) + expect(proposalAfter2.isSigned).to.be.true + }) + + it('should add signature multiple times safely', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) + + const signature = await createAxiaSignature(proposalKey, whitelistedAxia) + + const ixs1 = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + await makeTxSignAndSend(solverProvider, ...ixs1) + + client.expireBlockhash() + const ixs2 = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + await makeTxSignAndSend(solverProvider, ...ixs2) + + client.expireBlockhash() + const ixs3 = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + await makeTxSignAndSend(solverProvider, ...ixs3) + + const proposalAfter = await program.account.proposal.fetch(proposalKey) + expect(proposalAfter.isSigned).to.be.true + }) + + it('cannot add signature if proposal is not final', async () => { + const intentHash = await createValidatedIntent(solverSdk, solverProvider, client, { isFinal: true }) + const intent = await program.account.intent.fetch(sdk.getIntentKey(intentHash)) + const now = Number(client.getClock().unixTimestamp) + const deadline = now + PROPOSAL_DEADLINE_OFFSET + + const instructions = [ + { + programId: Keypair.generate().publicKey, + accounts: [], + data: TEST_DATA_HEX_3, + }, + ] + + const fees = intent.maxFees.map((maxFee) => ({ + mint: maxFee.mint, + amount: maxFee.amount.toNumber(), + })) + + const ix = await solverSdk.createProposalIx(intentHash, instructions, fees, deadline, false) + await makeTxSignAndSend(solverProvider, ix) + + const proposalKey = sdk.getProposalKey(intentHash, solver.publicKey) + const signature = await createAxiaSignature(proposalKey, whitelistedAxia) + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expectTransactionError(res, 'Proposal is not final') + }) + + it('cannot add signature if proposal has expired', async () => { + const now = Number(client.getClock().unixTimestamp) + const deadline = now + STALE_CLAIM_DELAY + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program, { deadline }) + + warpSeconds(provider, STALE_CLAIM_DELAY_PLUS_ONE) + + const signature = await createAxiaSignature(proposalKey, whitelistedAxia) + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expectTransactionError(res, 'Proposal has already expired') + }) + + it('cannot add signature if axia is not whitelisted', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) + + const axia = Keypair.generate() + const signature = await createAxiaSignature(proposalKey, axia) + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, axia.publicKey, signature) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expectTransactionError(res, `AnchorError caused by account: axia_registry. Error Code: AccountNotInitialized.`) + }) + + it('cannot add signature if solver is not whitelisted', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) + + const signature = await createAxiaSignature(proposalKey, whitelistedAxia) + + const ixs = await maliciousSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + const res = await makeTxSignAndSend(maliciousProvider, ...ixs) + + expectTransactionError( + res, + `AnchorError caused by account: solver_registry. Error Code: AccountNotInitialized.` + ) + }) + + it('cannot add signature for non-existent proposal', async () => { + const proposalKey = Keypair.generate().publicKey + + const signature = await createAxiaSignature(proposalKey, whitelistedAxia) + + const ed25519Ix = Ed25519Program.createInstructionWithPublicKey({ + message: proposalKey.toBuffer(), + publicKey: whitelistedAxia.publicKey.toBuffer(), + signature: Buffer.from(signature), + }) + + const ix = await program.methods + .addAxiaSig() + .accountsPartial({ + solver: solverSdk.getSignerKey(), + solverRegistry: solverSdk.getEntityRegistryPubkey(EntityType.Solver, solverSdk.getSignerKey()), + proposal: proposalKey, + axiaRegistry: solverSdk.getEntityRegistryPubkey(EntityType.Axia, whitelistedAxia.publicKey), + ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, + }) + .instruction() + + const res = await makeTxSignAndSend(solverProvider, ed25519Ix, ix) + + expectTransactionError( + res, + `Program log: AnchorError caused by account: proposal. Error Code: AccountNotInitialized` + ) + }) + + it('cannot add signature if proposal deadline equals now', async () => { + const now = Number(client.getClock().unixTimestamp) + const deadline = now + SHORT_DEADLINE + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program, { deadline }) + + warpSeconds(provider, WARP_TIME_SHORT) + + const signature = await createAxiaSignature(proposalKey, whitelistedAxia) + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expectTransactionError(res, 'Proposal has already expired') + }) + + it('cannot add signature with wrong proposal key', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) + const wrongProposalKey = Keypair.generate().publicKey + + const signature = await createAxiaSignature(wrongProposalKey, whitelistedAxia) + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expect(res.toString()).to.be.eq( + `FailedTransactionMetadata(FailedTransactionMetadata { err: InstructionError(0, Custom(2)), meta: TransactionMetadata { signature: 1111111111111111111111111111111111111111111111111111111111111111, logs: [], inner_instructions: [], compute_units_consumed: 0, return_data: TransactionReturnData { program_id: 11111111111111111111111111111111, data: [] } } })` + ) + }) + + it('cannot add signature with invalid signature', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) + + const invalidSignature: number[] = Array.from({ length: 64 }, () => Math.floor(Math.random() * 256)) + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, invalidSignature) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expect(res.toString()).to.be.eq( + `FailedTransactionMetadata(FailedTransactionMetadata { err: InstructionError(0, Custom(2)), meta: TransactionMetadata { signature: 1111111111111111111111111111111111111111111111111111111111111111, logs: [], inner_instructions: [], compute_units_consumed: 0, return_data: TransactionReturnData { program_id: 11111111111111111111111111111111, data: [] } } })` + ) + }) + + it('cannot add signature from wrong axia pubkey', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) + + const wrongAxia = Keypair.generate() + const signature = await createAxiaSignature(proposalKey, wrongAxia) + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, wrongAxia.publicKey, signature) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expectTransactionError(res, `AnchorError caused by account: axia_registry. Error Code: AccountNotInitialized.`) + }) + + it('cannot add signature with signature from different axia', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) + + const axia2 = await createAllowlistedEntity(controllerSdk, provider, EntityType.Axia) + const signature = await createAxiaSignature(proposalKey, axia2) + + // Try to use axia2's signature but claim it's from whitelistedAxia + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expect(res.toString()).to.be.eq( + `FailedTransactionMetadata(FailedTransactionMetadata { err: InstructionError(0, Custom(2)), meta: TransactionMetadata { signature: 1111111111111111111111111111111111111111111111111111111111111111, logs: [], inner_instructions: [], compute_units_consumed: 0, return_data: TransactionReturnData { program_id: 11111111111111111111111111111111, data: [] } } })` + ) + }) + + it('cannot add signature with signature from validator instead of axia', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) + + const validator = await createAllowlistedEntity(controllerSdk, provider, EntityType.Validator) + const signature = await createAxiaSignature(proposalKey, validator) + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, validator.publicKey, signature) + const res = await makeTxSignAndSend(solverProvider, ...ixs) + + expectTransactionError(res, `AnchorError caused by account: axia_registry. Error Code: AccountNotInitialized.`) + }) + + it('cannot add signature if signed message is wrong', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) + + // Sign a different message (e.g., intent hash instead of proposal key) + const intentHash = generateIntentHash() + const signature = await signAsync(Buffer.from(intentHash, 'hex'), whitelistedAxia.secretKey.slice(0, 32)) + const signatureArray = Array.from(new Uint8Array(signature)) + + const ed25519Ix = Ed25519Program.createInstructionWithPublicKey({ + message: Buffer.from(intentHash, 'hex'), + publicKey: whitelistedAxia.publicKey.toBuffer(), + signature: Buffer.from(signatureArray), + }) + + const ix = await program.methods + .addAxiaSig() + .accountsPartial({ + solver: solverSdk.getSignerKey(), + solverRegistry: solverSdk.getEntityRegistryPubkey(EntityType.Solver, solverSdk.getSignerKey()), + proposal: proposalKey, + axiaRegistry: solverSdk.getEntityRegistryPubkey(EntityType.Axia, whitelistedAxia.publicKey), + ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, + }) + .instruction() + + const res = await makeTxSignAndSend(solverProvider, ed25519Ix, ix) + + expectTransactionError(res, `Signature verification failed`) + }) + + it('cannot add signature with corrupted ed25519 instruction', async () => { + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program) + + // Create corrupted Ed25519 instruction with wrong program ID + const corruptedEd25519Ix = new TransactionInstruction({ + programId: Keypair.generate().publicKey, + keys: [], + data: Buffer.from([ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ]), + }) + + const ix = await program.methods + .addAxiaSig() + .accountsPartial({ + solver: solverSdk.getSignerKey(), + solverRegistry: solverSdk.getEntityRegistryPubkey(EntityType.Solver, solverSdk.getSignerKey()), + proposal: proposalKey, + axiaRegistry: solverSdk.getEntityRegistryPubkey(EntityType.Axia, whitelistedAxia.publicKey), + ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY, + }) + .instruction() + + const res = await makeTxSignAndSend(solverProvider, corruptedEd25519Ix, ix) + + expect(res.toString()).to.be.eq( + // eslint-disable-next-line no-secrets/no-secrets + `FailedTransactionMetadata(FailedTransactionMetadata { err: InvalidProgramForExecution, meta: TransactionMetadata { signature: 1111111111111111111111111111111111111111111111111111111111111111, logs: [], inner_instructions: [], compute_units_consumed: 0, return_data: TransactionReturnData { program_id: 11111111111111111111111111111111, data: [] } } })` + ) + }) + + it('should add signature when proposal deadline is close', async () => { + const now = Number(client.getClock().unixTimestamp) + const deadline = now + VERY_SHORT_DEADLINE + const { proposalKey } = await createFinalizedProposal(solverSdk, solverProvider, client, program, { deadline }) + + const signature = await createAxiaSignature(proposalKey, whitelistedAxia) + + const ixs = await solverSdk.addAxiaSigIxs(proposalKey, whitelistedAxia.publicKey, signature) + await makeTxSignAndSend(solverProvider, ...ixs) + + const proposalAfter = await program.account.proposal.fetch(proposalKey) + expect(proposalAfter.isSigned).to.be.true + }) + }) }) })