diff --git a/.github/workflows/solana.yml b/.github/workflows/solana.yml index 61f4c6d03..9935b53c7 100644 --- a/.github/workflows/solana.yml +++ b/.github/workflows/solana.yml @@ -53,6 +53,12 @@ jobs: - name: Install just uses: extractions/setup-just@v2 + - name: Install protobuf compiler + run: | + sudo apt-get update + sudo apt-get install -y protobuf-compiler + protoc --version + - name: Build Solana programs run: just build-solana diff --git a/.gitignore b/.gitignore index 5306b6737..d6bec0a8c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ cache coverage node_modules out +tmp # files *.env diff --git a/Cargo.lock b/Cargo.lock index bf9646049..7c16eed18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5093,6 +5093,7 @@ dependencies = [ "ibc-proto 0.51.1 (registry+https://github.com/rust-lang/crates.io-index)", "ibc-proto 0.51.1 (git+https://github.com/srdtrk/ibc-proto-rs?rev=7074fb20c3a654e945e5ca46627c5a971d785e04)", "prost", + "prost-build", "serde", "serde_json", "sha2 0.10.9", diff --git a/Cargo.toml b/Cargo.toml index db5d08169..d4b58370b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ serde_with = { version = "3.11", default-features = false } hex = { version = "0.4", default-features = false } base64 = { version = "0.22", default-features = false } prost = { version = "0.13", default-features = false } +prost-build = { version = "0.13", default-features = false } subtle-encoding = { version = "0.5", default-features = false } schemars = { version = "0.8", default-features = false } diff --git a/docs/adr/solana-ics27-gmp-architecture.md b/docs/adr/solana-ics27-gmp-architecture.md new file mode 100644 index 000000000..0782a4b8c --- /dev/null +++ b/docs/adr/solana-ics27-gmp-architecture.md @@ -0,0 +1,456 @@ +# ADR: ICS27 General Message Passing (GMP) Architecture for Solana + +**Status**: Implemented +**Date**: 2025-09-18 +**Last Updated**: 2025-10-10 + +## Executive Summary + +We implement ICS27 GMP for Solana using a **flattened single-program architecture** that preserves CPI depth for target applications while maintaining cross-chain account determinism through Program Derived Addresses (PDAs). + +## Problem + +The ICS27 General Message Passing protocol enables cross-chain smart contract execution. The Ethereum reference implementation uses a two-contract architecture: + +``` +User → GMP Contract → Account Contract → Target Contract → Unlimited subcalls +``` + +However, Solana's constraints make this architecture impractical: + +| Constraint | Ethereum | Solana | Impact | +| ----------------------- | -------------------- | ---------------- | ------------------------------------------------ | +| **Call Depth** | Unlimited | 4 CPI levels max | Would consume 3/4 levels before target execution | +| **Account Model** | Deployable contracts | PDAs only | Cannot deploy per-user contracts | +| **Account Declaration** | Dynamic | All upfront | Must know all accounts before execution | +| **Address Format** | String (0x...) | 32-byte Pubkey | Type conversion required | + +## Solution + +### Architectural Innovation: Flattened Single-Program Design + +Instead of separate GMP and Account contracts, we merge them into a single program that preserves precious CPI depth: + +``` +Ethereum: Router → GMP → Account → Target → ... (3 levels used) +Solana: Router → GMP+Account → Target → ... (2 levels used) +``` + +This gives target programs 2 additional CPI levels to work with - critical for complex DeFi operations. + +### Key Design Decisions + +1. **PDA as Cross-Chain Identity** + + - Each Cosmos user gets a deterministic PDA: `hash(client_id + sender + salt)` + - Acts as signing authority via `invoke_signed` + - Can own SPL tokens and other assets + - No deployment cost - address exists deterministically + +2. **Relayer-Computed Accounts** + + - Relayer derives `account_state_pda` and `target_program` + - Sender only provides target-specific accounts + - Simplifies sender complexity while maintaining security + +3. **Protobuf Payload Format** + - Cross-chain compatible encoding + - Contains target program ID, accounts, and instruction data + - Relayer parses to extract required accounts + +## Implementation Details + +### PDA Derivation + +Each Cosmos user gets a deterministic Solana address: + +```rust +// PDA seeds: [b"gmp_account", client_id, hash(sender), salt] +let sender_hash = hash(sender.as_bytes()).to_bytes(); // Hash for >32 byte addresses +let (account_pda, bump) = Pubkey::find_program_address(&[ + b"gmp_account", // Constant seed + client_id.as_bytes(), // e.g., "07-tendermint-0" + &sender_hash, // Hashed Cosmos address + salt, // User-provided uniqueness +], &gmp_program_id); +``` + +### Account Layout and Execution Flow + +The relayer constructs the transaction with carefully ordered accounts: + +```rust +// Account ordering (critical for proper execution): +// [0] account_state_pda - Relayer computes from seeds +// [1] target_program - Relayer extracts from GMPPacketData.receiver +// [2+] target accounts - Sender provides in SolanaInstruction.accounts + +// The GMP program executes target with PDA as signer: +invoke_signed( + &target_instruction, + &target_accounts, + &[&[b"gmp_account", client_id, &sender_hash, salt, &[bump]]] +)?; +``` + +**Critical Design: Conditional Fee Payer Injection** + +Solana PDAs with data cannot pay for account creation. We solve this with a **configurable payer injection** mechanism controlled by the optional `payer_position` field in `SolanaInstruction`: + +- **`payer_position` not set**: No injection (for programs that don't create accounts, e.g., SPL Token Transfer) +- **`payer_position = N`**: Inject at index N (0-indexed array position) + +This allows: +- GMP PDA to sign for operations via `invoke_signed` +- Relayer to pay for new account rent when needed +- Target programs to create accounts as needed +- Preserves exact account layouts for programs with fixed schemas +- Full flexibility for sender to control account ordering + +### Payload Structure + +The payload uses Protobuf for cross-chain compatibility: + +```proto +message SolanaAccountMeta { + bytes pubkey = 1; // Account public key (32 bytes) + bool is_signer = 2; // Should this account sign at CPI instruction level? + bool is_writable = 3; // Will this account be modified? +} + +message SolanaInstruction { + bytes program_id = 1; // Target program to execute + repeated SolanaAccountMeta accounts = 2; // Accounts needed by target + bytes data = 3; // Instruction data + optional uint32 payer_position = 4; // Position to inject relayer as payer +} + +message GMPPacketData { + string client_id = 1; // Source chain identifier + string sender = 2; // Original sender address + string receiver = 3; // Target program ID + bytes salt = 4; // Account uniqueness + bytes payload = 5; // SolanaInstruction (protobuf) +} +``` + +**Key Design**: Sender provides only target-specific accounts. Relayer adds protocol accounts (account_state_pda, target_program) automatically. + +### Signing Architecture: Two-Level Model + +Solana has two distinct levels of signing that are critical to understand: + +1. **Transaction Level**: Requires private key signatures before transaction submission +2. **Instruction Level**: PDAs sign via `invoke_signed` using seed-based authority during CPI + +The `is_signer` field in `SolanaAccountMeta` indicates whether an account should be a signer **at the CPI instruction level** (not at the transaction level). This simplified design works because: + +**For cross-chain calls from Cosmos**: +- Cosmos users don't have Solana private keys, so transaction-level signing is not applicable +- The ICS27 account_state PDA represents the user and signs via `invoke_signed` +- All payload accounts are marked `is_signer: false` at transaction level +- The GMP program marks the account_state PDA as a signer when making the CPI call + +**Account Signing Behavior**: +- `is_signer: false` → Account does not sign (most accounts: data accounts, programs, system accounts) +- `is_signer: true` → PDA signs via `invoke_signed` during CPI (ICS27 account_state PDA) + +This keeps the architecture simple while correctly modeling how accounts sign in cross-chain scenarios. + +## Example 1: Counter Application + +Our e2e tests demonstrate the architecture with a counter application where Cosmos users can increment counters on Solana: + +### Counter App Design + +```rust +// Solana counter program maintains per-user counters +pub struct UserCounter { + pub user: Pubkey, // User identifier + pub count: u64, // Current counter value + pub increments: u64, // Total increment operations + pub last_updated: i64, // Timestamp of last update +} +``` + +### Cross-Chain Flow + +```go +// 1. Cosmos user constructs increment instruction +// Note: Only the amount is in instruction data +// The user authority (ICS27 account_state PDA) is passed as an account, not in data +incrementData := []byte{ + INSTRUCTION_INCREMENT, // Discriminator (8 bytes) + amount, // Increment amount (8 bytes, little-endian u64) +} + +// 2. User provides only target-specific accounts +// Note: payer_position = 3 tells GMP program to inject relayer at index 3 +payerPosition := uint32(3) +solanaInstruction := &SolanaInstruction{ + ProgramId: counterProgramID, + Data: incrementData, + Accounts: []*SolanaAccountMeta{ + {counterAppState, false, true}, // [0] app_state (not a signer, writable) + {userCounterPDA, false, true}, // [1] user_counter (not a signer, writable) + {ics27AccountPDA, true, false}, // [2] user_authority (PDA signer, read-only) + // [3] payer will be injected here by GMP program + {systemProgram, false, false}, // [4] system_program (not a signer, read-only) + }, + PayerPosition: &payerPosition, // Inject relayer at index 3 +} + +// 3. Send via IBC as GMPPacketData +msg := &MsgSendCall{ + Sender: cosmosUser, + Receiver: counterProgramID.String(), + Payload: proto.Marshal(solanaInstruction), + Salt: []byte{}, // Optional uniqueness +} +``` + +### Relayer Processing + +The relayer automatically adds protocol accounts and handles payer injection: + +```rust +// Relayer adds protocol accounts at the beginning: +// [0] account_state_pda - Derived: hash(client_id + cosmosUser + salt) +// [1] target_program - From GMPPacketData.receiver +// [2+] user accounts - From SolanaInstruction.accounts +// [N] payer (injected) - Injected at payer_position if specified + +let account_state_pda = derive_gmp_pda(client_id, sender, salt); +accounts.insert(0, AccountMeta { + pubkey: account_state_pda, + is_signer: false, // No keypair at transaction level + is_writable: true +}); +accounts.insert(1, AccountMeta { + pubkey: counter_program_id, + is_signer: false, + is_writable: false +}); + +// Parse payload to extract user's accounts +let solana_instruction = SolanaInstruction::decode(gmp_packet.payload)?; +for account in solana_instruction.accounts { + accounts.push(AccountMeta { + pubkey: Pubkey::try_from(account.pubkey)?, + is_signer: false, // All payload accounts are non-signers at transaction level + is_writable: account.is_writable, + }); +} + +// Inject payer at specified position if payer_position is set +if let Some(position) = solana_instruction.payer_position { + accounts.insert(position, AccountMeta { + pubkey: relayer_keypair.pubkey(), + is_signer: true, // Relayer signs to pay for rent + is_writable: true, + }); +} +``` + +The GMP program then marks the account_state PDA as a signer at CPI instruction level via `invoke_signed`. + +### Result + +- Each Cosmos user gets their own counter via deterministic user counter PDA (derived from ICS27 account_state PDA) +- Multiple users can have independent counters +- **Security**: Only the ICS27 account_state PDA can increment its own counter (enforced by `user_authority: Signer` constraint) +- GMP program signs as the account_state PDA via `invoke_signed` during CPI +- Counter increments atomically with proper access control + +This demonstrates how complex cross-chain operations work with minimal sender complexity while maintaining strong security guarantees. + +## Example 2: SPL Token Transfer + +A more complex example showing cross-chain token transfers: + +### SPL Transfer Flow + +```go +// 1. Cosmos user wants to transfer USDC owned by their ICS27 Account PDA +// The ICS27 PDA was previously funded and owns SPL tokens + +// 2. Build SPL transfer instruction +transferInstruction := token.NewTransferInstruction( + 1_000_000, // 1 USDC (6 decimals) + sourceTokenAccount, // Token account owned by ICS27 PDA + destTokenAccount, // Recipient's token account + ics27AccountPDA, // Authority (will be signed by GMP) +).Build() + +// 3. Create SolanaInstruction with required accounts +// Note: PayerPosition is NOT set because SPL Transfer doesn't create accounts +solanaInstruction := &SolanaInstruction{ + ProgramId: SPL_TOKEN_PROGRAM_ID, + Data: transferInstruction.Data(), + Accounts: []*SolanaAccountMeta{ + {sourceTokenAccount, false, true}, // Source (not a signer, writable) + {destTokenAccount, false, true}, // Destination (not a signer, writable) + {ics27AccountPDA, true, false}, // Authority (PDA signer, read-only) + }, + // PayerPosition: nil - no payer injection needed for SPL transfers +} + +// 4. Send as GMP packet +msg := &MsgSendCall{ + Sender: cosmosUser, + Receiver: SPL_TOKEN_PROGRAM_ID.String(), + Payload: proto.Marshal(solanaInstruction), + Salt: userSalt, // Same salt to get same ICS27 PDA +} +``` + +### Key Points + +1. **PDA as Token Owner**: The ICS27 Account PDA can own SPL token accounts +2. **Authority Signing**: GMP program uses `invoke_signed` to sign as the PDA +3. **Deterministic Addressing**: Same user + salt always gets same PDA +4. **Composability**: Works with any SPL token + +### Relayer Handling + +```rust +// Relayer adds the same protocol accounts as before: +// [0] account_state_pda - Derived from (client_id, cosmosUser, salt) +// [1] spl_token_program - From GMPPacketData.receiver +// [2+] token accounts - From SolanaInstruction.accounts + +// The ICS27 PDA signs for the transfer +invoke_signed( + &spl_transfer_instruction, + &[source_account, dest_account, authority], + &[&[b"gmp_account", client_id, &sender_hash, salt, &[bump]]] +)?; +``` + +This example shows how ICS27 enables complex DeFi operations across chains while maintaining the same simple interface for users. + +## Address Lookup Tables (ALT) Optimization + +### The Transaction Size Challenge + +Solana imposes a strict 1232-byte limit on transaction size. A typical GMP transaction requires: + +- 10 infrastructure accounts (router, GMP program, light client, state PDAs, etc.) +- Target-specific accounts (varies by application) +- IBC proof data +- Instruction data + +Without optimization, each account consumes 32 bytes in the transaction. This leaves limited room for IBC proofs. + +### ALT Solution + +Address Lookup Tables allow referencing accounts by 1-byte indices instead of 32-byte pubkeys: + +```rust +// ALT Creation (one-time setup) +// These are common accounts that appear in every IBC GMP transaction +let alt_accounts = vec![ + system_program_id, // Index 0 + ics26_router_program, // Index 1 + ics07_tendermint_program, // Index 2 + ics27_gmp_program, // Index 3 (ibc_app_program) + router_state_pda, // Index 4 + relayer_fee_payer, // Index 5 + ibc_app_pda, // Index 6 (port-specific: "gmp" port) + gmp_app_state_pda, // Index 7 + client_pda, // Index 8 (e.g., "solclient-0") + client_state_pda, // Index 9 (ICS07 client state for source chain) +]; + +// Transaction uses 1-byte indices instead of 32-byte pubkeys +let v0_message = v0::Message::try_compile( + &fee_payer, + &[instruction], + &[address_lookup_table], // Pass ALT + blockhash, +)?; +``` + +### Impact + +Address Lookup Tables significantly reduce transaction size by replacing 32-byte pubkeys with 1-byte indices for infrastructure accounts. This optimization is critical for fitting IBC proofs within Solana's 1232-byte transaction limit. + +## Relayer Architecture + +The relayer acts as a smart intermediary that: + +1. **Observes** IBC packets from Cosmos chains +2. **Derives** protocol accounts (`account_state_pda` from packet data) +3. **Extracts** target accounts from protobuf payload +4. **Constructs** complete Solana transaction with all accounts +5. **Submits** to Solana using ALT for size optimization + +This design keeps sender complexity minimal while maintaining security - senders don't need to understand Solana PDAs or account derivation. + +### Port-Specific Logic in Relayer + +Currently, the relayer contains conditional logic for the GMP port: + +```rust +fn extract_payload_accounts( + payload: &Payload, + port_id: &str, + source_client: &str, + existing_accounts: &[AccountMeta], +) -> Result> { + // Conditional logic for GMP port + if port_id == GMP_PORT_ID && payload.encoding == PROTOBUF_ENCODING { + // Parse GMPPacketData + let gmp_packet = GmpPacketData::decode(payload.value)?; + + // Derive account_state_pda (GMP-specific) + let account_state_pda = derive_gmp_account(...); + accounts.push(account_state_pda); + + // Extract target program and accounts + let solana_instruction = SolanaInstruction::decode(gmp_packet.payload)?; + // ... add accounts + } else { + // Other ports would need their own logic + return Err("Unsupported port"); + } +} +``` + +## Security Model + +- **Account Control**: Only GMP program can sign via `invoke_signed` - users cannot directly control PDAs +- **Replay Protection**: Per-account nonces prevent replay attacks +- **Deterministic Addressing**: Same seeds always produce same PDA - no address spoofing +- **Equivalent to Ethereum**: Same security properties as `onlyICS27` modifier + +## Performance + +- **Transaction Size**: Stays within 1232-byte limit using ALT optimization +- **Compute Units**: Well below 1.4M limit +- **CPI Depth**: 2 levels available for target programs + +## Trade-offs + +### Accepted Limitations + +- **CPI Depth**: 2 levels for target programs (vs unlimited on Ethereum) +- **Account Pre-declaration**: All accounts must be known upfront +- **Transaction Size**: 1232-byte limit constrains complexity + +### Mitigation Strategies + +- **Address Lookup Tables**: Reduce account size from 32 bytes to 1 byte +- **Relayer Intelligence**: Relayer derives protocol accounts, sender only provides target accounts +- **Protobuf Encoding**: Efficient cross-chain serialization + +## Conclusion + +This architecture successfully adapts ICS27 GMP to Solana's constraints by flattening the contract hierarchy and leveraging PDAs for deterministic addressing. The design preserves maximum CPI depth for target applications while maintaining security equivalence with Ethereum. + +The key innovation is the split of responsibilities: senders provide minimal information (just target accounts), while the relayer handles all protocol complexity. This makes cross-chain calls from Cosmos to Solana as simple as possible for end users. + +## References + +- [Solana Program Derived Addresses](https://docs.solana.com/developing/programming-model/calling-between-programs#program-derived-addresses) +- [ICS27 Interchain Accounts Specification](https://github.com/cosmos/ibc/tree/main/spec/app/ics-027-interchain-accounts) diff --git a/e2e/interchaintestv8/chainconfig/chain_config.go b/e2e/interchaintestv8/chainconfig/chain_config.go index 0b81baa0a..a90127fde 100644 --- a/e2e/interchaintestv8/chainconfig/chain_config.go +++ b/e2e/interchaintestv8/chainconfig/chain_config.go @@ -18,9 +18,10 @@ func IbcGoChainSpec(name, chainId string) *interchaintest.ChainSpec { ChainID: chainId, Images: []ibc.DockerImage{ { - Repository: "ghcr.io/cosmos/ibc-go-wasm-simd", // FOR LOCAL IMAGE USE: Docker Image Name - Version: "modules-light-clients-08-wasm-v10.3.0", // FOR LOCAL IMAGE USE: Docker Image Tag - UIDGID: "1025:1025", + Repository: "ghcr.io/cosmos/ibc-go-wasm-simd", + // Version with GMP enabled + Version: "serdar-xxx-contract-calls", + UIDGID: "1025:1025", }, }, Bin: "simd", diff --git a/e2e/interchaintestv8/chainconfig/encoding.go b/e2e/interchaintestv8/chainconfig/encoding.go index 0ed0b0f6d..0e9db37e3 100644 --- a/e2e/interchaintestv8/chainconfig/encoding.go +++ b/e2e/interchaintestv8/chainconfig/encoding.go @@ -16,6 +16,7 @@ import ( proposaltypes "github.com/cosmos/cosmos-sdk/x/params/types/proposal" ibcwasmtypes "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10/types" + gmptypes "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" icacontrollertypes "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/controller/types" icahosttypes "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/host/types" transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" @@ -52,6 +53,7 @@ func codecAndEncodingConfig() (*codec.ProtoCodec, sdktestutil.TestEncodingConfig cfg := sdktestutil.MakeTestEncodingConfig() // ibc types + gmptypes.RegisterInterfaces(cfg.InterfaceRegistry) icacontrollertypes.RegisterInterfaces(cfg.InterfaceRegistry) icahosttypes.RegisterInterfaces(cfg.InterfaceRegistry) solomachine.RegisterInterfaces(cfg.InterfaceRegistry) diff --git a/e2e/interchaintestv8/e2esuite/utils.go b/e2e/interchaintestv8/e2esuite/utils.go index 95dfbeaa7..8a90c2ba0 100644 --- a/e2e/interchaintestv8/e2esuite/utils.go +++ b/e2e/interchaintestv8/e2esuite/utils.go @@ -90,7 +90,12 @@ func (s *TestSuite) BroadcastMessages(ctx context.Context, chain *cosmos.CosmosC // CreateAndFundCosmosUser returns a new cosmos user with the given initial balance and funds it with the native chain denom. func (s *TestSuite) CreateAndFundCosmosUser(ctx context.Context, chain *cosmos.CosmosChain) ibc.Wallet { - cosmosUserFunds := sdkmath.NewInt(testvalues.InitialBalance) + return s.CreateAndFundCosmosUserWithBalance(ctx, chain, testvalues.InitialBalance) +} + +// CreateAndFundCosmosUserWithBalance returns a new cosmos user with the given balance and funds it with the native chain denom. +func (s *TestSuite) CreateAndFundCosmosUserWithBalance(ctx context.Context, chain *cosmos.CosmosChain, balance int64) ibc.Wallet { + cosmosUserFunds := sdkmath.NewInt(balance) cosmosUsers := interchaintest.GetAndFundTestUsers(s.T(), ctx, s.T().Name(), cosmosUserFunds, chain) return cosmosUsers[0] diff --git a/e2e/interchaintestv8/go.mod b/e2e/interchaintestv8/go.mod index bb7b9c5af..05bf037ae 100644 --- a/e2e/interchaintestv8/go.mod +++ b/e2e/interchaintestv8/go.mod @@ -109,7 +109,7 @@ require ( github.com/dgraph-io/badger/v4 v4.2.0 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/docker v27.5.1+incompatible // indirect + github.com/docker/docker v28.0.0+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect @@ -324,4 +324,10 @@ replace github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alp replace github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 +// GMP Support: Using remote ibc-go PR that includes the 27-gmp module for General Message Passing +// Reference: https://github.com/cosmos/ibc-go/pull/8660 +replace github.com/cosmos/ibc-go/v10 => github.com/cosmos/ibc-go/v10 v10.0.0-beta.0.0.20251009124337-8b13a7f221a3 + +replace github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10 => github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10 v10.0.0-20251009124337-8b13a7f221a3 + // this line is used by go-codegen # suite/module diff --git a/e2e/interchaintestv8/go.sum b/e2e/interchaintestv8/go.sum index 052c46f52..98d82e69f 100644 --- a/e2e/interchaintestv8/go.sum +++ b/e2e/interchaintestv8/go.sum @@ -763,8 +763,8 @@ github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/btcsuite/btcd v0.22.1 h1:CnwP9LM/M9xuRrGSCGeMVs9iv09uMqwsVX7EeIpgV2c= github.com/btcsuite/btcd v0.22.1/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y= -github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= -github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c= github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= @@ -886,10 +886,10 @@ github.com/cosmos/gogoproto v1.7.0 h1:79USr0oyXAbxg3rspGh/m4SWNyoz/GLaAh0QlCe2fr github.com/cosmos/gogoproto v1.7.0/go.mod h1:yWChEv5IUEYURQasfyBW5ffkMHR/90hiHgbNgrtp4j0= github.com/cosmos/iavl v1.2.4 h1:IHUrG8dkyueKEY72y92jajrizbkZKPZbMmG14QzsEkw= github.com/cosmos/iavl v1.2.4/go.mod h1:GiM43q0pB+uG53mLxLDzimxM9l/5N9UuSY3/D0huuVw= -github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10 v10.3.0 h1:m7ngaGY7/eeBC7Ly+/sGrUKCp/HNMeKWao8ksWlxH8Q= -github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10 v10.3.0/go.mod h1:T4hi2bTWs+37XOhXyKNNL6knOVDvmb4TtHxSmhXsyfM= -github.com/cosmos/ibc-go/v10 v10.3.0 h1:w5DkHih8qn15deAeFoTk778WJU+xC1krJ5kDnicfUBc= -github.com/cosmos/ibc-go/v10 v10.3.0/go.mod h1:CthaR7n4d23PJJ7wZHegmNgbVcLXCQql7EwHrAXnMtw= +github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10 v10.0.0-20251009124337-8b13a7f221a3 h1:c1Il6y3myDGAn60TfcRdfLdBX+a6dM0Qp/HsYvj+1yA= +github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10 v10.0.0-20251009124337-8b13a7f221a3/go.mod h1:azb42j5WjM4uWmDyfVKKa1HiPb+msuf6fwdZCZdHrZA= +github.com/cosmos/ibc-go/v10 v10.0.0-beta.0.0.20251009124337-8b13a7f221a3 h1:BdgFV1Khbr5LecQmJHaiHuIkv8bERpPfsFihqZxZ+i4= +github.com/cosmos/ibc-go/v10 v10.0.0-beta.0.0.20251009124337-8b13a7f221a3/go.mod h1:sne37ZZ+haNZ2KhaND0MYqRGzIutw1lS+x0B9qHNEsI= github.com/cosmos/ics23/go v0.11.0 h1:jk5skjT0TqX5e5QJbEnwXIS2yI2vnmLOgpQPeM5RtnU= github.com/cosmos/ics23/go v0.11.0/go.mod h1:A8OjxPE67hHST4Icw94hOxxFEJMBG031xIGF/JHNIY0= github.com/cosmos/interchain-security/v7 v7.0.0-20250408210344-06e0dc6bf6d6 h1:SzJ/+uqrTsJmI+f/GqPdC4lGxgDQKYvtRCMXFdJljNM= @@ -944,8 +944,8 @@ github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUn github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v27.5.1+incompatible h1:4PYU5dnBYqRQi0294d1FBECqT9ECWeQAIfE8q4YnPY8= -github.com/docker/docker v27.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.0.0+incompatible h1:Olh0KS820sJ7nPsBKChVhk5pzqcwDR15fumfAd/p9hM= +github.com/docker/docker v28.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -1316,8 +1316,8 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= -github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= @@ -1588,13 +1588,13 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= +github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= diff --git a/e2e/interchaintestv8/relayer/solana_and_cosmos_config.go b/e2e/interchaintestv8/relayer/solana_and_cosmos_config.go index f3b2035f7..30e3779e9 100644 --- a/e2e/interchaintestv8/relayer/solana_and_cosmos_config.go +++ b/e2e/interchaintestv8/relayer/solana_and_cosmos_config.go @@ -20,8 +20,10 @@ type SolanaCosmosConfigInfo struct { CosmosSignerAddress string // Solana fee payer address (for cosmos-to-solana) SolanaFeePayer string - // Whether we use the mock client in Cosmos - Mock bool + // Address Lookup Table address for reducing transaction size (optional) + SolanaAltAddress string + // Whether we use the mock WASM client in Cosmos (for Solana->Cosmos) + MockWasmClient bool } type SolanaToCosmosModuleConfig struct { @@ -35,8 +37,8 @@ type SolanaToCosmosModuleConfig struct { SignerAddress string `json:"signer_address"` // Solana ICS26 router program ID (must be "solana_ics26_program_id") SolanaIcs26ProgramId string `json:"solana_ics26_program_id"` - // Whether to use mock proofs for testing - Mock bool `json:"mock"` + // Whether to use mock WASM client on Cosmos for testing + MockWasmClient bool `json:"mock_wasm_client"` } type CosmosToSolanaModuleConfig struct { @@ -48,13 +50,21 @@ type CosmosToSolanaModuleConfig struct { SolanaIcs26ProgramId string `json:"solana_ics26_program_id"` // Solana ICS07 Tendermint light client program ID (must be "solana_ics07_program_id") SolanaIcs07ProgramId string `json:"solana_ics07_program_id"` - // Solana IBC app program ID (must be "solana_ibc_app_program_id") - SolanaIbcAppProgramId string `json:"solana_ibc_app_program_id"` // Solana fee payer address for unsigned transactions SolanaFeePayer string `json:"solana_fee_payer"` + // Address Lookup Table address for reducing transaction size (optional) + SolanaAltAddress *string `json:"solana_alt_address,omitempty"` + // Whether to use mock WASM client on Cosmos for testing + MockWasmClient bool `json:"mock_wasm_client"` } func CreateSolanaCosmosModules(configInfo SolanaCosmosConfigInfo) []ModuleConfig { + // Prepare ALT address pointer (only if non-empty) + var altAddress *string + if configInfo.SolanaAltAddress != "" { + altAddress = &configInfo.SolanaAltAddress + } + return []ModuleConfig{ { Name: ModuleSolanaToCosmos, @@ -66,7 +76,7 @@ func CreateSolanaCosmosModules(configInfo SolanaCosmosConfigInfo) []ModuleConfig TargetRpcUrl: configInfo.TmRPC, SignerAddress: configInfo.CosmosSignerAddress, SolanaIcs26ProgramId: configInfo.ICS26RouterProgramID, - Mock: configInfo.Mock, + MockWasmClient: configInfo.MockWasmClient, }, }, { @@ -74,12 +84,13 @@ func CreateSolanaCosmosModules(configInfo SolanaCosmosConfigInfo) []ModuleConfig SrcChain: configInfo.CosmosChainID, DstChain: configInfo.SolanaChainID, Config: CosmosToSolanaModuleConfig{ - SourceRpcUrl: configInfo.TmRPC, - TargetRpcUrl: configInfo.SolanaRPC, - SolanaIcs26ProgramId: configInfo.ICS26RouterProgramID, - SolanaIcs07ProgramId: configInfo.ICS07ProgramID, - SolanaIbcAppProgramId: configInfo.IBCAppProgramID, - SolanaFeePayer: configInfo.SolanaFeePayer, + SourceRpcUrl: configInfo.TmRPC, + TargetRpcUrl: configInfo.SolanaRPC, + SolanaIcs26ProgramId: configInfo.ICS26RouterProgramID, + SolanaIcs07ProgramId: configInfo.ICS07ProgramID, + SolanaFeePayer: configInfo.SolanaFeePayer, + SolanaAltAddress: altAddress, + MockWasmClient: configInfo.MockWasmClient, }, }, } diff --git a/e2e/interchaintestv8/solana/gmp_counter_app-keypair.json b/e2e/interchaintestv8/solana/gmp_counter_app-keypair.json new file mode 100644 index 000000000..29fd99b43 --- /dev/null +++ b/e2e/interchaintestv8/solana/gmp_counter_app-keypair.json @@ -0,0 +1 @@ +[157,187,152,254,11,76,71,158,181,120,32,92,49,238,138,254,49,34,172,238,195,176,199,253,205,224,251,242,34,112,116,141,232,39,115,242,196,255,157,103,211,100,217,184,217,94,217,87,217,175,138,195,207,232,212,222,208,25,23,43,90,14,241,86] \ No newline at end of file diff --git a/e2e/interchaintestv8/solana/go-anchor/gmpcounter/accounts.go b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/accounts.go new file mode 100644 index 000000000..2a9099f4f --- /dev/null +++ b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/accounts.go @@ -0,0 +1,69 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains parsers for the accounts defined in the IDL. + +package gmp_counter_app + +import ( + "fmt" + binary "github.com/gagliardetto/binary" +) + +func ParseAnyAccount(accountData []byte) (any, error) { + decoder := binary.NewBorshDecoder(accountData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek account discriminator: %w", err) + } + switch discriminator { + case Account_CounterAppState: + value := new(CounterAppState) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal account as CounterAppState: %w", err) + } + return value, nil + case Account_UserCounter: + value := new(UserCounter) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal account as UserCounter: %w", err) + } + return value, nil + default: + return nil, fmt.Errorf("unknown discriminator: %s", binary.FormatDiscriminator(discriminator)) + } +} + +func ParseAccount_CounterAppState(accountData []byte) (*CounterAppState, error) { + decoder := binary.NewBorshDecoder(accountData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Account_CounterAppState { + return nil, fmt.Errorf("expected discriminator %v, got %s", Account_CounterAppState, binary.FormatDiscriminator(discriminator)) + } + acc := new(CounterAppState) + err = acc.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal account of type CounterAppState: %w", err) + } + return acc, nil +} + +func ParseAccount_UserCounter(accountData []byte) (*UserCounter, error) { + decoder := binary.NewBorshDecoder(accountData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Account_UserCounter { + return nil, fmt.Errorf("expected discriminator %v, got %s", Account_UserCounter, binary.FormatDiscriminator(discriminator)) + } + acc := new(UserCounter) + err = acc.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal account of type UserCounter: %w", err) + } + return acc, nil +} diff --git a/e2e/interchaintestv8/solana/go-anchor/gmpcounter/constants.go b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/constants.go new file mode 100644 index 000000000..70848bb54 --- /dev/null +++ b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/constants.go @@ -0,0 +1,4 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains constants. + +package gmp_counter_app diff --git a/e2e/interchaintestv8/solana/go-anchor/gmpcounter/discriminators.go b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/discriminators.go new file mode 100644 index 000000000..1dfc91d9c --- /dev/null +++ b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/discriminators.go @@ -0,0 +1,21 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains the discriminators for accounts and events defined in the IDL. + +package gmp_counter_app + +// Account discriminators +var ( + Account_CounterAppState = [8]byte{53, 163, 115, 233, 187, 151, 163, 139} + Account_UserCounter = [8]byte{154, 114, 103, 93, 77, 57, 80, 227} +) + +// Event discriminators +var () + +// Instruction discriminators +var ( + Instruction_Decrement = [8]byte{106, 227, 168, 59, 248, 27, 150, 101} + Instruction_GetCounter = [8]byte{178, 42, 93, 7, 140, 213, 93, 150} + Instruction_Increment = [8]byte{11, 18, 104, 9, 104, 174, 59, 33} + Instruction_Initialize = [8]byte{175, 175, 109, 31, 13, 152, 155, 237} +) diff --git a/e2e/interchaintestv8/solana/go-anchor/gmpcounter/doc.go b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/doc.go new file mode 100644 index 000000000..2e5d28a3c --- /dev/null +++ b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/doc.go @@ -0,0 +1,17 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains documentation and example usage for the generated code. + +package gmp_counter_app + +// Documentation from the IDL: +// GMP Counter App Program +// +// This program demonstrates a simple counter application that can be called +// via the ICS27 GMP program through cross-chain IBC messages. +// +// It provides: +// - `initialize`: Initialize the counter app +// - `increment`: Increment a user's counter (called by GMP) +// - `decrement`: Decrement a user's counter (called by GMP) +// - `get_counter`: Get a user's current counter value +// diff --git a/e2e/interchaintestv8/solana/go-anchor/gmpcounter/errors.go b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/errors.go new file mode 100644 index 000000000..1eca93918 --- /dev/null +++ b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/errors.go @@ -0,0 +1,4 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains errors. + +package gmp_counter_app diff --git a/e2e/interchaintestv8/solana/go-anchor/gmpcounter/fetchers.go b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/fetchers.go new file mode 100644 index 000000000..86253022d --- /dev/null +++ b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/fetchers.go @@ -0,0 +1,4 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains fetcher functions. + +package gmp_counter_app diff --git a/e2e/interchaintestv8/solana/go-anchor/gmpcounter/instructions.go b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/instructions.go new file mode 100644 index 000000000..f8b58ff6b --- /dev/null +++ b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/instructions.go @@ -0,0 +1,201 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains instructions. + +package gmp_counter_app + +import ( + "bytes" + "fmt" + errors "github.com/gagliardetto/anchor-go/errors" + binary "github.com/gagliardetto/binary" + solanago "github.com/gagliardetto/solana-go" +) + +// Builds a "decrement" instruction. +// Decrement a user's counter (typically called by GMP program) +func NewDecrementInstruction( + // Params: + userParam solanago.PublicKey, + amountParam uint64, + + // Accounts: + appStateAccount solanago.PublicKey, + userCounterAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_Decrement[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `userParam`: + err = enc__.Encode(userParam) + if err != nil { + return nil, errors.NewField("userParam", err) + } + // Serialize `amountParam`: + err = enc__.Encode(amountParam) + if err != nil { + return nil, errors.NewField("amountParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "app_state": Writable, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(appStateAccount, true, false)) + // Account 1 "user_counter": Writable, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(userCounterAccount, true, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + +// Builds a "get_counter" instruction. +// Get a user's counter value +func NewGetCounterInstruction( + // Params: + userParam solanago.PublicKey, + + // Accounts: + userCounterAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_GetCounter[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `userParam`: + err = enc__.Encode(userParam) + if err != nil { + return nil, errors.NewField("userParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "user_counter": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(userCounterAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + +// Builds a "increment" instruction. +// Increment a user's counter (typically called by GMP program) // The user is identified by the `user_authority` signer (ICS27 `account_state` PDA) +func NewIncrementInstruction( + // Params: + amountParam uint64, + + // Accounts: + appStateAccount solanago.PublicKey, + userCounterAccount solanago.PublicKey, + userAuthorityAccount solanago.PublicKey, + payerAccount solanago.PublicKey, + systemProgramAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_Increment[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `amountParam`: + err = enc__.Encode(amountParam) + if err != nil { + return nil, errors.NewField("amountParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "app_state": Writable, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(appStateAccount, true, false)) + // Account 1 "user_counter": Writable, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(userCounterAccount, true, false)) + // Account 2 "user_authority": Read-only, Signer, Required + // The user authority (`account_state` PDA for ICS27) + // MUST be a signer to authorize operations on this user's counter + accounts__.Append(solanago.NewAccountMeta(userAuthorityAccount, false, true)) + // Account 3 "payer": Writable, Signer, Required + accounts__.Append(solanago.NewAccountMeta(payerAccount, true, true)) + // Account 4 "system_program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + +// Builds a "initialize" instruction. +// Initialize the counter app +func NewInitializeInstruction( + // Params: + authorityParam solanago.PublicKey, + + // Accounts: + appStateAccount solanago.PublicKey, + payerAccount solanago.PublicKey, + systemProgramAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_Initialize[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `authorityParam`: + err = enc__.Encode(authorityParam) + if err != nil { + return nil, errors.NewField("authorityParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "app_state": Writable, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(appStateAccount, true, false)) + // Account 1 "payer": Writable, Signer, Required + accounts__.Append(solanago.NewAccountMeta(payerAccount, true, true)) + // Account 2 "system_program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} diff --git a/e2e/interchaintestv8/solana/go-anchor/gmpcounter/program-id.go b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/program-id.go new file mode 100644 index 000000000..e635a4e7a --- /dev/null +++ b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/program-id.go @@ -0,0 +1,8 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains the program ID. + +package gmp_counter_app + +import solanago "github.com/gagliardetto/solana-go" + +var ProgramID = solanago.MustPublicKeyFromBase58("GdEUjpVtKvHKStM3Hph6PnLSUMsJXvcVqugubhtQ5QUD") diff --git a/e2e/interchaintestv8/solana/go-anchor/gmpcounter/tests_test.go b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/tests_test.go new file mode 100644 index 000000000..b3f20dbec --- /dev/null +++ b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/tests_test.go @@ -0,0 +1,4 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains tests. + +package gmp_counter_app diff --git a/e2e/interchaintestv8/solana/go-anchor/gmpcounter/types.go b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/types.go new file mode 100644 index 000000000..647ac9485 --- /dev/null +++ b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/types.go @@ -0,0 +1,218 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains parsers for the types defined in the IDL. + +package gmp_counter_app + +import ( + "bytes" + "fmt" + errors "github.com/gagliardetto/anchor-go/errors" + binary "github.com/gagliardetto/binary" + solanago "github.com/gagliardetto/solana-go" +) + +// Global counter app state +type CounterAppState struct { + // Authority that can manage the app + Authority solanago.PublicKey `json:"authority"` + + // Total number of user counters created + TotalCounters uint64 `json:"totalCounters"` + + // Total number of GMP calls processed + TotalGmpCalls uint64 `json:"totalGmpCalls"` + + // Program bump seed + Bump uint8 `json:"bump"` +} + +func (obj CounterAppState) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `Authority`: + err = encoder.Encode(obj.Authority) + if err != nil { + return errors.NewField("Authority", err) + } + // Serialize `TotalCounters`: + err = encoder.Encode(obj.TotalCounters) + if err != nil { + return errors.NewField("TotalCounters", err) + } + // Serialize `TotalGmpCalls`: + err = encoder.Encode(obj.TotalGmpCalls) + if err != nil { + return errors.NewField("TotalGmpCalls", err) + } + // Serialize `Bump`: + err = encoder.Encode(obj.Bump) + if err != nil { + return errors.NewField("Bump", err) + } + return nil +} + +func (obj CounterAppState) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding CounterAppState: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *CounterAppState) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `Authority`: + err = decoder.Decode(&obj.Authority) + if err != nil { + return errors.NewField("Authority", err) + } + // Deserialize `TotalCounters`: + err = decoder.Decode(&obj.TotalCounters) + if err != nil { + return errors.NewField("TotalCounters", err) + } + // Deserialize `TotalGmpCalls`: + err = decoder.Decode(&obj.TotalGmpCalls) + if err != nil { + return errors.NewField("TotalGmpCalls", err) + } + // Deserialize `Bump`: + err = decoder.Decode(&obj.Bump) + if err != nil { + return errors.NewField("Bump", err) + } + return nil +} + +func (obj *CounterAppState) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling CounterAppState: %w", err) + } + return nil +} + +func UnmarshalCounterAppState(buf []byte) (*CounterAppState, error) { + obj := new(CounterAppState) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Per-user counter state +type UserCounter struct { + // User's public key + User solanago.PublicKey `json:"user"` + + // Current counter value + Count uint64 `json:"count"` + + // Number of increments + Increments uint64 `json:"increments"` + + // Number of decrements + Decrements uint64 `json:"decrements"` + + // Last updated timestamp + LastUpdated int64 `json:"lastUpdated"` + + // PDA bump seed + Bump uint8 `json:"bump"` +} + +func (obj UserCounter) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `User`: + err = encoder.Encode(obj.User) + if err != nil { + return errors.NewField("User", err) + } + // Serialize `Count`: + err = encoder.Encode(obj.Count) + if err != nil { + return errors.NewField("Count", err) + } + // Serialize `Increments`: + err = encoder.Encode(obj.Increments) + if err != nil { + return errors.NewField("Increments", err) + } + // Serialize `Decrements`: + err = encoder.Encode(obj.Decrements) + if err != nil { + return errors.NewField("Decrements", err) + } + // Serialize `LastUpdated`: + err = encoder.Encode(obj.LastUpdated) + if err != nil { + return errors.NewField("LastUpdated", err) + } + // Serialize `Bump`: + err = encoder.Encode(obj.Bump) + if err != nil { + return errors.NewField("Bump", err) + } + return nil +} + +func (obj UserCounter) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding UserCounter: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *UserCounter) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `User`: + err = decoder.Decode(&obj.User) + if err != nil { + return errors.NewField("User", err) + } + // Deserialize `Count`: + err = decoder.Decode(&obj.Count) + if err != nil { + return errors.NewField("Count", err) + } + // Deserialize `Increments`: + err = decoder.Decode(&obj.Increments) + if err != nil { + return errors.NewField("Increments", err) + } + // Deserialize `Decrements`: + err = decoder.Decode(&obj.Decrements) + if err != nil { + return errors.NewField("Decrements", err) + } + // Deserialize `LastUpdated`: + err = decoder.Decode(&obj.LastUpdated) + if err != nil { + return errors.NewField("LastUpdated", err) + } + // Deserialize `Bump`: + err = decoder.Decode(&obj.Bump) + if err != nil { + return errors.NewField("Bump", err) + } + return nil +} + +func (obj *UserCounter) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling UserCounter: %w", err) + } + return nil +} + +func UnmarshalUserCounter(buf []byte) (*UserCounter, error) { + obj := new(UserCounter) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} diff --git a/e2e/interchaintestv8/solana/ics27_gmp-keypair.json b/e2e/interchaintestv8/solana/ics27_gmp-keypair.json new file mode 100644 index 000000000..28de81bab --- /dev/null +++ b/e2e/interchaintestv8/solana/ics27_gmp-keypair.json @@ -0,0 +1 @@ +[21,206,32,94,107,253,169,195,243,64,188,125,118,232,156,108,123,151,54,74,70,229,15,33,159,201,42,84,220,232,17,72,37,40,66,123,139,0,91,33,44,55,180,176,253,94,71,113,48,226,97,219,212,239,34,219,50,240,72,193,101,64,229,25] \ No newline at end of file diff --git a/e2e/interchaintestv8/solana/solana.go b/e2e/interchaintestv8/solana/solana.go index cfcc3fbe7..d2eacee4d 100644 --- a/e2e/interchaintestv8/solana/solana.go +++ b/e2e/interchaintestv8/solana/solana.go @@ -1,6 +1,7 @@ package solana import ( + "bytes" "context" "encoding/binary" "fmt" @@ -8,6 +9,7 @@ import ( "time" "github.com/cosmos/solidity-ibc-eureka/e2e/v8/testvalues" + bin "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/programs/system" @@ -117,13 +119,22 @@ func (s *Solana) WaitForTxStatus(txSig solana.Signature, status rpc.Confirmation return false, err } + // // Check if transaction status exists + // if len(out.Value) == 0 || out.Value[0] == nil { + // // Transaction not yet processed + // return false, nil + // } + if out.Value[0].Err != nil { return false, fmt.Errorf("transaction %s failed with error: %s", txSig, out.Value[0].Err) } + // Check if transaction has reached the desired status using level-based comparison + // This allows accepting higher confirmation levels (e.g., finalized when waiting for confirmed) if confirmationStatusLevel(out.Value[0].ConfirmationStatus) >= confirmationStatusLevel(status) { return true, nil } + return false, nil }) } @@ -177,14 +188,26 @@ func (s *Solana) WaitForProgramAvailabilityWithTimeout(ctx context.Context, prog return false } -func (s *Solana) SignAndBroadcastTxWithRetry(ctx context.Context, tx *solana.Transaction, wallet *solana.Wallet) (solana.Signature, error) { - return s.SignAndBroadcastTxWithRetryTimeout(ctx, tx, wallet, 30) +// SignAndBroadcastTxWithRetry retries transaction broadcasting with default timeout +func (s *Solana) SignAndBroadcastTxWithRetry(ctx context.Context, tx *solana.Transaction, signers ...*solana.Wallet) (solana.Signature, error) { + return s.SignAndBroadcastTxWithRetryTimeout(ctx, tx, 30, signers...) } -func (s *Solana) SignAndBroadcastTxWithRetryTimeout(ctx context.Context, tx *solana.Transaction, wallet *solana.Wallet, timeoutSeconds int) (solana.Signature, error) { +// SignAndBroadcastTxWithRetryTimeout retries transaction broadcasting with specified timeout +// It refreshes the blockhash on each attempt to handle expired blockhashes +func (s *Solana) SignAndBroadcastTxWithRetryTimeout(ctx context.Context, tx *solana.Transaction, timeoutSeconds int, signers ...*solana.Wallet) (solana.Signature, error) { var lastErr error for range timeoutSeconds { - sig, err := s.SignAndBroadcastTx(ctx, tx, wallet) + // Refresh blockhash on each retry attempt (blockhashes expire after ~60 seconds) + recent, err := s.RPCClient.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) + if err != nil { + lastErr = fmt.Errorf("failed to get latest blockhash: %w", err) + time.Sleep(1 * time.Second) + continue + } + tx.Message.RecentBlockhash = recent.Value.Blockhash + + sig, err := s.SignAndBroadcastTx(ctx, tx, signers...) if err == nil { return sig, nil } @@ -243,17 +266,123 @@ func (s *Solana) WaitForBalanceChangeWithTimeout(ctx context.Context, account so return initialBalance, false } +// ComputeBudgetProgramID returns the Solana Compute Budget program ID +func ComputeBudgetProgramID() solana.PublicKey { + return solana.MustPublicKeyFromBase58("ComputeBudget111111111111111111111111111111") +} + // NewComputeBudgetInstruction creates a SetComputeUnitLimit instruction to increase available compute units func NewComputeBudgetInstruction(computeUnits uint32) solana.Instruction { - // Compute Budget Program ID - computeBudgetProgramID := solana.MustPublicKeyFromBase58("ComputeBudget111111111111111111111111111111") data := make([]byte, 5) data[0] = 0x02 // SetComputeUnitLimit instruction discriminator binary.LittleEndian.PutUint32(data[1:], computeUnits) return solana.NewInstruction( - computeBudgetProgramID, + ComputeBudgetProgramID(), solana.AccountMetaSlice{}, data, ) } + +// CreateAddressLookupTable creates an Address Lookup Table and extends it with the given accounts. +// Returns the ALT address. Requires at least one account. +func (s *Solana) CreateAddressLookupTable(ctx context.Context, authority *solana.Wallet, accounts []solana.PublicKey) (solana.PublicKey, error) { + if len(accounts) == 0 { + return solana.PublicKey{}, fmt.Errorf("at least one account is required for ALT") + } + + // Get recent slot for ALT creation + slot, err := s.RPCClient.GetSlot(ctx, "confirmed") + if err != nil { + return solana.PublicKey{}, fmt.Errorf("failed to get slot: %w", err) + } + + // Derive ALT address with bump seed + // The derivation uses: [authority, recent_slot] seeds + altAddress, bumpSeed, err := solana.FindProgramAddress( + [][]byte{authority.PublicKey().Bytes(), Uint64ToLeBytes(slot)}, + solana.AddressLookupTableProgramID, + ) + if err != nil { + return solana.PublicKey{}, fmt.Errorf("failed to derive ALT address: %w", err) + } + + // Create ALT instruction data + // ProgramInstruction enum: CreateLookupTable { recent_slot: u64, bump_seed: u8 } + var createBuf bytes.Buffer + encoder := bin.NewBinEncoder(&createBuf) + mustWrite(encoder.WriteUint32(0, bin.LE)) + mustWrite(encoder.WriteUint64(slot, bin.LE)) + mustWrite(encoder.WriteUint8(bumpSeed)) + createInstructionData := createBuf.Bytes() + + createAltIx := solana.NewInstruction( + solana.AddressLookupTableProgramID, + solana.AccountMetaSlice{ + solana.Meta(altAddress).WRITE(), // lookup_table (to be created) + solana.Meta(authority.PublicKey()).WRITE().SIGNER(), // authority + solana.Meta(authority.PublicKey()).WRITE().SIGNER(), // payer + solana.Meta(solana.SystemProgramID), // system_program + }, + createInstructionData, + ) + + // Create ALT + createTx, err := s.NewTransactionFromInstructions(authority.PublicKey(), createAltIx) + if err != nil { + return solana.PublicKey{}, fmt.Errorf("failed to create ALT transaction: %w", err) + } + + _, err = s.SignAndBroadcastTxWithRetry(ctx, createTx, authority) + if err != nil { + return solana.PublicKey{}, fmt.Errorf("failed to create ALT: %w", err) + } + + // Extend ALT with accounts instruction data + // ProgramInstruction::ExtendLookupTable { new_addresses: Vec } + var extendBuf bytes.Buffer + extendEncoder := bin.NewBinEncoder(&extendBuf) + mustWrite(extendEncoder.WriteUint32(2, bin.LE)) + mustWrite(extendEncoder.WriteUint64(uint64(len(accounts)), bin.LE)) + for _, acc := range accounts { + mustWrite(extendEncoder.WriteBytes(acc.Bytes(), false)) + } + extendInstructionData := extendBuf.Bytes() + + extendAltIx := solana.NewInstruction( + solana.AddressLookupTableProgramID, + solana.AccountMetaSlice{ + solana.Meta(altAddress).WRITE(), // lookup_table + solana.Meta(authority.PublicKey()).WRITE().SIGNER(), // authority + solana.Meta(authority.PublicKey()).WRITE().SIGNER(), // payer (for reallocation) + solana.Meta(solana.SystemProgramID), // system_program + }, + extendInstructionData, + ) + + extendTx, err := s.NewTransactionFromInstructions(authority.PublicKey(), extendAltIx) + if err != nil { + return solana.PublicKey{}, fmt.Errorf("failed to create extend ALT transaction: %w", err) + } + + _, err = s.SignAndBroadcastTxWithRetry(ctx, extendTx, authority) + if err != nil { + return solana.PublicKey{}, fmt.Errorf("failed to extend ALT: %w", err) + } + + return altAddress, nil +} + +// Uint64ToLeBytes converts a uint64 to little-endian byte slice +func Uint64ToLeBytes(n uint64) []byte { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, n) + return b +} + +// mustWrite wraps encoder write calls and panics on error (should never happen with bytes.Buffer) +func mustWrite(err error) { + if err != nil { + panic(fmt.Sprintf("unexpected encoding error: %v", err)) + } +} diff --git a/e2e/interchaintestv8/solana_gmp_test.go b/e2e/interchaintestv8/solana_gmp_test.go new file mode 100644 index 000000000..c976b8ce6 --- /dev/null +++ b/e2e/interchaintestv8/solana_gmp_test.go @@ -0,0 +1,1444 @@ +package main + +import ( + "context" + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "fmt" + "time" + + "github.com/cosmos/gogoproto/proto" + gmp_counter_app "github.com/cosmos/solidity-ibc-eureka/e2e/interchaintestv8/solana/go-anchor/gmpcounter" + "github.com/cosmos/solidity-ibc-eureka/e2e/v8/e2esuite" + "github.com/cosmos/solidity-ibc-eureka/e2e/v8/solana" + "github.com/cosmos/solidity-ibc-eureka/e2e/v8/testvalues" + "github.com/cosmos/solidity-ibc-eureka/e2e/v8/types/gmphelpers" + relayertypes "github.com/cosmos/solidity-ibc-eureka/e2e/v8/types/relayer" + solanatypes "github.com/cosmos/solidity-ibc-eureka/e2e/v8/types/solana" + + solanago "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/programs/system" + "github.com/gagliardetto/solana-go/programs/token" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + gmptypes "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" + + "github.com/cosmos/interchaintest/v10/ibc" + + ics07_tendermint "github.com/cosmos/solidity-ibc-eureka/packages/go-anchor/ics07tendermint" + ics26_router "github.com/cosmos/solidity-ibc-eureka/packages/go-anchor/ics26router" + ics27_gmp "github.com/cosmos/solidity-ibc-eureka/packages/go-anchor/ics27gmp" +) + +const ( + // GMP App + DefaultIncrementAmount = uint64(5) + DefaultDecrementAmount = uint64(2) + GMPPortID = testvalues.SolanaGMPPortID + // SPL Token amounts (with 6 decimals) + SPLTokenDecimals = uint8(6) + SPLTokenMintAmount = uint64(10_000_000) // 10 tokens + SPLTokenTransferAmount = uint64(1_000_000) // 1 token + // Test amounts + CosmosTestAmount = int64(1000) // stake denom +) + +func (s *IbcEurekaSolanaTestSuite) deployAndInitializeGMPCounterApp(ctx context.Context) solanago.PublicKey { + var gmpCounterProgramID solanago.PublicKey + + s.Require().True(s.Run("Deploy and Initialize GMP Counter App", func() { + gmpCounterProgramID = s.deploySolanaProgram(ctx, "gmp_counter_app") + + gmp_counter_app.ProgramID = gmpCounterProgramID + + programAvailable := s.SolanaChain.WaitForProgramAvailabilityWithTimeout(ctx, gmpCounterProgramID, 120) + s.Require().True(programAvailable, "GMP Counter program failed to become available within timeout") + + // Initialize GMP counter app state + counterAppStatePDA, _, err := solanago.FindProgramAddress([][]byte{[]byte("counter_app_state")}, gmpCounterProgramID) + s.Require().NoError(err) + + initInstruction, err := gmp_counter_app.NewInitializeInstruction( + s.SolanaUser.PublicKey(), // authority + counterAppStatePDA, + s.SolanaUser.PublicKey(), // payer + solanago.SystemProgramID, + ) + s.Require().NoError(err) + + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), initInstruction) + s.Require().NoError(err) + + _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, s.SolanaUser) + s.Require().NoError(err) + s.T().Logf("GMP Counter app initialized") + })) + + return gmpCounterProgramID +} + +// createAddressLookupTable creates an Address Lookup Table with common IBC accounts +// to reduce transaction size. Returns the ALT address. +func (s *IbcEurekaSolanaTestSuite) createAddressLookupTable(ctx context.Context) solanago.PublicKey { + // Define common accounts to add to ALT + // These are accounts that appear in every IBC packet transaction + // Derive router_state PDA (same as relayer uses) + routerStatePDA, _, err := solanago.FindProgramAddress( + [][]byte{[]byte("router_state")}, + ics26_router.ProgramID, + ) + s.Require().NoError(err) + + // Get Cosmos chain ID for deriving ICS07 accounts + simd := s.CosmosChains[0] + cosmosChainID := simd.Config().ChainID + + // Derive IBC app PDA (port-specific, constant for all GMP packets) + ibcAppPDA, _, err := solanago.FindProgramAddress([][]byte{[]byte("ibc_app"), []byte(GMPPortID)}, ics26_router.ProgramID) + s.Require().NoError(err) + + // Derive ICS27 GMP app state PDA (port-specific, constant for all GMP packets) + gmpAppStatePDA, _, err := solanago.FindProgramAddress([][]byte{[]byte("app_state"), []byte(GMPPortID)}, ics27_gmp.ProgramID) + s.Require().NoError(err) + + // Derive client PDA (client-specific, constant if using same destination client) + // Assuming destination client is "solclient-0" + clientPDA, _, err := solanago.FindProgramAddress([][]byte{[]byte("clients"), []byte("solclient-0")}, ics26_router.ProgramID) + s.Require().NoError(err) + + // Derive ICS07 client state PDA (source chain specific, constant for all packets from Cosmos) + clientStatePDA, _, err := solanago.FindProgramAddress([][]byte{[]byte("client"), []byte(cosmosChainID)}, ics07_tendermint.ProgramID) + s.Require().NoError(err) + + // Derive router caller PDA (GMP's CPI signer, constant for all GMP packets) + routerCallerPDA, _, err := solanago.FindProgramAddress([][]byte{[]byte("router_caller")}, ics27_gmp.ProgramID) + s.Require().NoError(err) + + // Derive client sequence PDA (tracks packet sequence for destination client) + clientSequencePDA, _, err := solanago.FindProgramAddress([][]byte{[]byte("client_sequence"), []byte(SolanaClientID)}, ics26_router.ProgramID) + s.Require().NoError(err) + + // NOTE: We do NOT include target app-specific accounts (like gmp_counter_app.ProgramID or its state) + // because those vary per application. ALT should only contain universal GMP infrastructure accounts. + commonAccounts := []solanago.PublicKey{ + solanago.SystemProgramID, + solana.ComputeBudgetProgramID(), // Compute Budget program (used by update_client) + ics26_router.ProgramID, // Router program + ics07_tendermint.ProgramID, // Light client program + ics27_gmp.ProgramID, // GMP program (ibc_app_program) + routerStatePDA, // Router state PDA + s.SolanaUser.PublicKey(), // Fee payer / relayer + ibcAppPDA, // IBC app PDA for GMP port + gmpAppStatePDA, // GMP app state PDA + clientPDA, // Client PDA + clientStatePDA, // ICS07 client state PDA + routerCallerPDA, // GMP router caller PDA (CPI signer) + clientSequencePDA, // Client sequence PDA (tracks packet sequence) + } + + // Create ALT with common accounts + altAddress, err := s.SolanaChain.CreateAddressLookupTable(ctx, s.SolanaUser, commonAccounts) + s.Require().NoError(err) + s.T().Logf("Created and extended ALT %s with %d common accounts", altAddress, len(commonAccounts)) + + return altAddress +} + +func (s *IbcEurekaSolanaTestSuite) deployAndInitializeICS27GMP(ctx context.Context) solanago.PublicKey { + var ics27GMPProgramID solanago.PublicKey + + s.Require().True(s.Run("Deploy and Initialize ICS27 GMP Program", func() { + ics27GMPProgramID = s.deploySolanaProgram(ctx, "ics27_gmp") + + // Set the program ID in the bindings + ics27_gmp.ProgramID = ics27GMPProgramID + + programAvailable := s.SolanaChain.WaitForProgramAvailabilityWithTimeout(ctx, ics27GMPProgramID, 120) + s.Require().True(programAvailable, "ICS27 GMP program failed to become available within timeout") + + // Find GMP app state PDA (using standard pattern with port_id) + gmpAppStatePDA, _, err := solanago.FindProgramAddress([][]byte{[]byte("app_state"), []byte(GMPPortID)}, ics27GMPProgramID) + s.Require().NoError(err) + + // Find router caller PDA + routerCallerPDA, _, err := solanago.FindProgramAddress([][]byte{[]byte("router_caller")}, ics27GMPProgramID) + s.Require().NoError(err) + + // Initialize ICS27 GMP app using the actual generated bindings + // Using GMP port for proper GMP functionality + initInstruction, err := ics27_gmp.NewInitializeInstruction( + ics26_router.ProgramID, // router program + gmpAppStatePDA, // app state account + routerCallerPDA, // router caller account + s.SolanaUser.PublicKey(), // payer + s.SolanaUser.PublicKey(), // authority + solanago.SystemProgramID, // system program + ) + s.Require().NoError(err) + + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), initInstruction) + s.Require().NoError(err) + + _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, s.SolanaUser) + s.Require().NoError(err) + + s.T().Logf("ICS27 GMP program initialized at: %s", ics27GMPProgramID) + s.T().Logf("GMP app state PDA: %s", gmpAppStatePDA) + s.T().Logf("GMP port ID: %s (using proper GMP port)", GMPPortID) + })) + + // Register GMP app with ICS26 router + s.Require().True(s.Run("Register ICS27 GMP with Router", func() { + routerStateAccount, _, err := solanago.FindProgramAddress([][]byte{[]byte("router_state")}, ics26_router.ProgramID) + s.Require().NoError(err) + + ibcAppAccount, _, err := solanago.FindProgramAddress([][]byte{[]byte("ibc_app"), []byte(GMPPortID)}, ics26_router.ProgramID) + s.Require().NoError(err) + + registerInstruction, err := ics26_router.NewAddIbcAppInstruction( + GMPPortID, + routerStateAccount, + ibcAppAccount, + ics27GMPProgramID, + s.SolanaUser.PublicKey(), + s.SolanaUser.PublicKey(), + solanago.SystemProgramID, + ) + s.Require().NoError(err) + + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), registerInstruction) + s.Require().NoError(err) + + _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, s.SolanaUser) + s.Require().NoError(err) + s.T().Logf("ICS27 GMP registered with router on port: %s (using proper GMP port)", GMPPortID) + })) + + return ics27GMPProgramID +} + +func (s *IbcEurekaSolanaTestSuite) registerGMPCounterAppWithRouter(_ context.Context, gmpCounterProgramID solanago.PublicKey) { + s.Require().True(s.Run("Setup GMP Counter App as Target", func() { + // The counter app is now ready to be called via GMP + // ICS27 GMP will route execution calls to this program based on the receiver field in packets + s.T().Logf("GMP Counter app %s is ready for GMP execution", gmpCounterProgramID) + s.T().Logf("Counter app will be callable via GMP packets with receiver = %s", gmpCounterProgramID) + s.T().Logf("GMP flow: IBC Packet → Router → ICS27 GMP → Counter App") + })) +} + +// Test_GMPCounterFromCosmos tests sending a counter increment call from Cosmos to Solana +// This mirrors the Ethereum GMP test pattern but for Solana +func (s *IbcEurekaSolanaTestSuite) Test_GMPCounterFromCosmos() { + ctx := context.Background() + + s.UseMockWasmClient = true + s.SetupGMP = true + + s.SetupSuite(ctx) + + simd := s.CosmosChains[0] + + // Create a second Cosmos user for multi-user testing + var cosmosUser1 ibc.Wallet + s.Require().True(s.Run("Create Second Cosmos User", func() { + cosmosUser1 = s.CreateAndFundCosmosUser(ctx, simd) + s.CosmosUsers = append(s.CosmosUsers, cosmosUser1) + s.T().Logf("Created second Cosmos user: %s", cosmosUser1.FormattedAddress()) + })) + + // ICS27 GMP program is already deployed and initialized in SetupSuite + ics27GMPProgramID := ics27_gmp.ProgramID + s.Require().True(s.Run("Verify ICS27 GMP Program", func() { + })) + + // Deploy and initialize GMP counter app, then register it with router + var gmpCounterProgramID solanago.PublicKey + s.Require().True(s.Run("Deploy and Initialize GMP Counter App", func() { + gmpCounterProgramID = s.deployAndInitializeGMPCounterApp(ctx) + })) + + s.Require().True(s.Run("Register GMP Counter App with Router", func() { + s.registerGMPCounterAppWithRouter(ctx, gmpCounterProgramID) + })) + + _ = ics27GMPProgramID // Use the GMP program ID for future packet flow + + // Setup user identities and helper functions + var getCounterValue func(cosmosUserAddress string) uint64 + var sendGMPIncrement func(cosmosUser ibc.Wallet, amount uint64) []byte + var relayGMPPacket func(cosmosGMPTxHash []byte, userLabel string) solanago.Signature + + s.Require().True(s.Run("Setup User Identities and Helpers", func() { + // We don't need separate Solana user keys - the ICS27 account_state PDAs are the identities + // The user counter PDAs are derived from the ICS27 account_state PDAs + + // Helper to get counter value for a Cosmos user + // This derives the ICS27 account_state PDA, then the user counter PDA from that + getCounterValue = func(cosmosUserAddress string) uint64 { + // Derive ICS27 account_state PDA for this Cosmos user + salt := []byte{} // Empty salt for this test + hasher := sha256.New() + hasher.Write([]byte(cosmosUserAddress)) + senderHash := hasher.Sum(nil) + + ics27AccountPDA, _, err := solanago.FindProgramAddress([][]byte{ + []byte("gmp_account"), + []byte(CosmosClientID), + senderHash, + salt, + }, ics27GMPProgramID) + s.Require().NoError(err) + + // Derive user counter PDA from ICS27 account_state PDA + userCounterPDA, _, err := solanago.FindProgramAddress( + [][]byte{[]byte("user_counter"), ics27AccountPDA.Bytes()}, + gmpCounterProgramID, + ) + s.Require().NoError(err) + + account, err := s.SolanaChain.RPCClient.GetAccountInfo(ctx, userCounterPDA) + if err != nil || account.Value == nil { + return 0 // Account doesn't exist yet + } + + data := account.Value.Data.GetBinary() + if len(data) >= 48 { + return binary.LittleEndian.Uint64(data[40:48]) + } + return 0 + } + + // Helper to send GMP increment from a Cosmos user + sendGMPIncrement = func(cosmosUser ibc.Wallet, amount uint64) []byte { + timeout := uint64(time.Now().Add(30 * time.Minute).Unix()) + simd := s.CosmosChains[0] + + // Derive the ICS27 account_state PDA for this Cosmos user + // This PDA is the authority that signs for the counter operations + cosmosAddress := cosmosUser.FormattedAddress() + salt := []byte{} // Empty salt for this test + hasher := sha256.New() + hasher.Write([]byte(cosmosAddress)) + senderHash := hasher.Sum(nil) + + ics27AccountPDA, _, err := solanago.FindProgramAddress([][]byte{ + []byte("gmp_account"), + []byte(CosmosClientID), + senderHash, + salt, + }, ics27GMPProgramID) + if err != nil { + return nil + } + + // Create the raw instruction data (just discriminator + amount, no user pubkey) + incrementInstructionData := []byte{} + incrementInstructionData = append(incrementInstructionData, gmp_counter_app.Instruction_Increment[:]...) + amountBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(amountBytes, amount) + incrementInstructionData = append(incrementInstructionData, amountBytes...) + + // Derive required account addresses + // 1. Counter app_state PDA + counterAppStateAddress, _, err := solanago.FindProgramAddress([][]byte{[]byte("counter_app_state")}, gmpCounterProgramID) + if err != nil { + return nil + } + + // 2. User counter PDA - derived from the ICS27 account_state PDA (not userKey) + userCounterAddress, _, err := solanago.FindProgramAddress([][]byte{[]byte("user_counter"), ics27AccountPDA.Bytes()}, gmpCounterProgramID) + if err != nil { + return nil + } + + // Create SolanaInstruction protobuf message + // Note: PayerPosition = 3 means inject at index 3 (0-indexed) + // The payer (relayer) is injected by GMP program since Cosmos doesn't know relayer's address + payerPosition := uint32(3) + solanaInstruction := &solanatypes.SolanaInstruction{ + ProgramId: gmpCounterProgramID.Bytes(), + Data: incrementInstructionData, + Accounts: []*solanatypes.SolanaAccountMeta{ + // Required accounts for increment instruction (matches IncrementCounter struct order) + {Pubkey: counterAppStateAddress.Bytes(), IsSigner: false, IsWritable: true}, // [0] counter app_state + {Pubkey: userCounterAddress.Bytes(), IsSigner: false, IsWritable: true}, // [1] user_counter + {Pubkey: ics27AccountPDA.Bytes(), IsSigner: true, IsWritable: false}, // [2] user_authority (ICS27 account_state PDA signs via invoke_signed) + // [3] payer will be injected at index 3 by GMP program + {Pubkey: solanago.SystemProgramID.Bytes(), IsSigner: false, IsWritable: false}, // [4] system_program (shifts to index 4) + }, + PayerPosition: &payerPosition, // Inject at index 3 (between user_authority and system_program) + } + + // Marshal to protobuf bytes + payload, err := proto.Marshal(solanaInstruction) + if err != nil { + return nil + } + + // Send GMP call using proper gmptypes.MsgSendCall + resp, err := s.BroadcastMessages(ctx, simd, cosmosUser, 2_000_000, &gmptypes.MsgSendCall{ + SourceClient: CosmosClientID, + Sender: cosmosUser.FormattedAddress(), + Receiver: gmpCounterProgramID.String(), + Salt: []byte{}, + Payload: payload, + TimeoutTimestamp: timeout, + Memo: "increment counter via GMP", + Encoding: testvalues.Ics27ProtobufEncoding, + }) + if err != nil { + return nil + } + + cosmosGMPTxHashBytes, err := hex.DecodeString(resp.TxHash) + if err != nil { + return nil + } + + s.T().Logf("GMP packet sent from %s: %s (increment by %d)", cosmosUser.FormattedAddress(), resp.TxHash, amount) + return cosmosGMPTxHashBytes + } + + // Helper to relay and execute a GMP packet + relayGMPPacket = func(cosmosGMPTxHash []byte, userLabel string) solanago.Signature { + var solanaRelayTxSig solanago.Signature + + simd := s.CosmosChains[0] + + // First, update the Solana client to the latest height + updateResp, err := s.RelayerClient.UpdateClient(context.Background(), &relayertypes.UpdateClientRequest{ + SrcChain: simd.Config().ChainID, + DstChain: testvalues.SolanaChainID, + DstClientId: SolanaClientID, + }) + s.Require().NoError(err, "Relayer Update Client failed") + s.Require().NotEmpty(updateResp.Txs, "Relayer Update client should return chunked transactions") + + s.submitChunkedUpdateClient(ctx, updateResp, s.SolanaUser) + s.T().Logf("%s: Updated Tendermint client on Solana using %d chunked transactions", userLabel, len(updateResp.Txs)) + + // Now retrieve and relay the GMP packet + resp, err := s.RelayerClient.RelayByTx(context.Background(), &relayertypes.RelayByTxRequest{ + SrcChain: simd.Config().ChainID, + DstChain: testvalues.SolanaChainID, + SourceTxIds: [][]byte{cosmosGMPTxHash}, + SrcClientId: CosmosClientID, + DstClientId: SolanaClientID, + }) + s.Require().NoError(err) + s.Require().NotEmpty(resp.Txs, "Relay should return chunked transactions") + s.T().Logf("%s: Retrieved %d relay transactions (chunks + final instructions)", userLabel, len(resp.Txs)) + + // Execute on Solana using chunked submission + solanaRelayTxSig = s.submitChunkedRelayPackets(ctx, resp, s.SolanaUser) + s.T().Logf("%s: GMP execution completed on Solana", userLabel) + + return solanaRelayTxSig + } + + s.T().Logf("Setup complete - User0 key: %s, User1 key: %s", s.CosmosUsers[0].FormattedAddress(), s.CosmosUsers[1].FormattedAddress()) + })) + + // Check initial counter states + var initialCounterUser0, initialCounterUser1 uint64 + s.Require().True(s.Run("Check Initial Counter States", func() { + initialCounterUser0 = getCounterValue(s.CosmosUsers[0].FormattedAddress()) + initialCounterUser1 = getCounterValue(s.CosmosUsers[1].FormattedAddress()) + s.T().Logf("Initial counter for user0: %d", initialCounterUser0) + s.T().Logf("Initial counter for user1: %d", initialCounterUser1) + })) + + // Send increment from User 0 + var cosmosGMPTxHashUser0 []byte + s.Require().True(s.Run("User0: Send GMP increment call from Cosmos", func() { + cosmosGMPTxHashUser0 = sendGMPIncrement(s.CosmosUsers[0], DefaultIncrementAmount) + s.Require().NotEmpty(cosmosGMPTxHashUser0) + })) + + // Relay User 0's increment + var solanaRelayTxSigUser0 solanago.Signature + s.Require().True(s.Run("User0: Relay and execute GMP packet on Solana", func() { + solanaRelayTxSigUser0 = relayGMPPacket(cosmosGMPTxHashUser0, "User0") + })) + + s.Require().True(s.Run("User0: Verify counter was incremented", func() { + newCounter := getCounterValue(s.CosmosUsers[0].FormattedAddress()) + expectedCounter := initialCounterUser0 + DefaultIncrementAmount + s.Require().Equal(expectedCounter, newCounter) + s.T().Logf("User0: Counter successfully incremented from %d to %d", initialCounterUser0, newCounter) + })) + + // Now send increment from User 1 + var cosmosGMPTxHashUser1 []byte + s.Require().True(s.Run("User1: Send GMP increment call from Cosmos", func() { + cosmosGMPTxHashUser1 = sendGMPIncrement(s.CosmosUsers[1], 3) // Increment by 3 for variety + s.Require().NotEmpty(cosmosGMPTxHashUser1) + })) + + // Relay User 1's increment + var solanaRelayTxSigUser1 solanago.Signature + s.Require().True(s.Run("User1: Relay and execute GMP packet on Solana", func() { + solanaRelayTxSigUser1 = relayGMPPacket(cosmosGMPTxHashUser1, "User1") + })) + + s.Require().True(s.Run("User1: Verify counter was incremented", func() { + newCounter := getCounterValue(s.CosmosUsers[1].FormattedAddress()) + expectedCounter := initialCounterUser1 + 3 // We incremented by 3 + s.Require().Equal(expectedCounter, newCounter) + s.T().Logf("User1: Counter successfully incremented from %d to %d", initialCounterUser1, newCounter) + })) + + s.Require().True(s.Run("Verify final counter states for both users", func() { + finalCounterUser0 := getCounterValue(s.CosmosUsers[0].FormattedAddress()) + finalCounterUser1 := getCounterValue(s.CosmosUsers[1].FormattedAddress()) + + // User 0 should have: initial + DefaultIncrementAmount (5) + expectedFinalUser0 := initialCounterUser0 + DefaultIncrementAmount + s.Require().Equal(expectedFinalUser0, finalCounterUser0) + + // User 1 should have: initial + 3 + expectedFinalUser1 := initialCounterUser1 + 3 + s.Require().Equal(expectedFinalUser1, finalCounterUser1) + + s.T().Logf("Final counter states - User0: %d (expected: %d), User1: %d (expected: %d)", + finalCounterUser0, expectedFinalUser0, finalCounterUser1, expectedFinalUser1) + })) + + s.Require().True(s.Run("Relay acknowledgments back to Cosmos", func() { + simd := s.CosmosChains[0] + + s.Require().True(s.Run("Relay User0 acknowledgment", func() { + var ackRelayTxBodyBz []byte + s.Require().True(s.Run("Retrieve acknowledgment relay tx", func() { + resp, err := s.RelayerClient.RelayByTx(context.Background(), &relayertypes.RelayByTxRequest{ + SrcChain: testvalues.SolanaChainID, + DstChain: simd.Config().ChainID, + SourceTxIds: [][]byte{[]byte(solanaRelayTxSigUser0.String())}, + SrcClientId: SolanaClientID, + DstClientId: CosmosClientID, + }) + s.Require().NoError(err) + s.Require().NotEmpty(resp.Tx) + s.T().Logf("Retrieved User0 GMP acknowledgment relay transaction") + + ackRelayTxBodyBz = resp.Tx + })) + + s.Require().True(s.Run("Broadcast acknowledgment on Cosmos", func() { + relayTxResult := s.MustBroadcastSdkTxBody(ctx, simd, s.CosmosUsers[0], CosmosDefaultGasLimit, ackRelayTxBodyBz) + s.T().Logf("User0 GMP acknowledgment relay transaction: %s (code: %d, gas: %d)", + relayTxResult.TxHash, relayTxResult.Code, relayTxResult.GasUsed) + })) + })) + + s.Require().True(s.Run("Relay User1 acknowledgment", func() { + var ackRelayTxBodyBz []byte + s.Require().True(s.Run("Retrieve acknowledgment relay tx", func() { + resp, err := s.RelayerClient.RelayByTx(context.Background(), &relayertypes.RelayByTxRequest{ + SrcChain: testvalues.SolanaChainID, + DstChain: simd.Config().ChainID, + SourceTxIds: [][]byte{[]byte(solanaRelayTxSigUser1.String())}, + SrcClientId: SolanaClientID, + DstClientId: CosmosClientID, + }) + s.Require().NoError(err) + s.Require().NotEmpty(resp.Tx) + s.T().Logf("Retrieved User1 GMP acknowledgment relay transaction") + + ackRelayTxBodyBz = resp.Tx + })) + + s.Require().True(s.Run("Broadcast acknowledgment on Cosmos", func() { + relayTxResult := s.MustBroadcastSdkTxBody(ctx, simd, s.CosmosUsers[0], CosmosDefaultGasLimit, ackRelayTxBodyBz) + s.T().Logf("User1 GMP acknowledgment relay transaction: %s (code: %d, gas: %d)", + relayTxResult.TxHash, relayTxResult.Code, relayTxResult.GasUsed) + })) + })) + + s.T().Logf("GMP calls from Cosmos successfully acknowledged") + })) +} + +// Test_GMPSPLTokenTransfer tests transferring SPL tokens via GMP from Cosmos to Solana +// This demonstrates the SPL token transfer example from the ADR where: +// 1. A Cosmos user controls an ICS27 Account PDA on Solana +// 2. The ICS27 PDA owns SPL token accounts +// 3. Through GMP, the Cosmos user sends cross-chain calls to transfer tokens +func (s *IbcEurekaSolanaTestSuite) Test_GMPSPLTokenTransferFromCosmos() { + ctx := context.Background() + + s.UseMockWasmClient = true + s.SetupGMP = true + + s.SetupSuite(ctx) + + simd := s.CosmosChains[0] + cosmosUser := s.CosmosUsers[0] + + // Setup SPL token infrastructure + var tokenMint solanago.PublicKey + var ics27AccountPDA solanago.PublicKey + var sourceTokenAccount solanago.PublicKey + var destTokenAccount solanago.PublicKey + var recipientWallet *solanago.Wallet + + s.Require().True(s.Run("Setup SPL Token Infrastructure", func() { + s.Require().True(s.Run("Create Test SPL Token Mint", func() { + var err error + tokenMint, err = s.createSPLTokenMint(ctx, 6) + s.Require().NoError(err) + s.T().Logf("Created test SPL token mint: %s (6 decimals)", tokenMint.String()) + })) + + s.Require().True(s.Run("Derive ICS27 Account PDA", func() { + var err error + ics27AccountPDA, err = s.deriveICS27AccountPDA(cosmosUser.FormattedAddress(), []byte{}) + s.Require().NoError(err) + s.T().Logf("ICS27 Account PDA for Cosmos user: %s", ics27AccountPDA.String()) + })) + + s.Require().True(s.Run("Create Token Accounts", func() { + var err error + + // Create source token account (owned by ICS27 PDA) + sourceTokenAccount, err = s.createTokenAccount(ctx, tokenMint, ics27AccountPDA) + s.Require().NoError(err) + s.T().Logf("Created source token account (owned by ICS27 PDA): %s", sourceTokenAccount.String()) + + // Create recipient wallet and destination token account + recipientWallet, err = s.SolanaChain.CreateAndFundWallet() + s.Require().NoError(err) + + destTokenAccount, err = s.createTokenAccount(ctx, tokenMint, recipientWallet.PublicKey()) + s.Require().NoError(err) + s.T().Logf("Created destination token account (owned by recipient): %s", destTokenAccount.String()) + })) + + s.Require().True(s.Run("Mint Tokens to ICS27 PDA", func() { + // Mint 10 tokens (10,000,000 with 6 decimals) + mintAmount := SPLTokenMintAmount + err := s.mintTokensTo(ctx, tokenMint, sourceTokenAccount, mintAmount) + s.Require().NoError(err) + + balance, err := s.getTokenBalance(ctx, sourceTokenAccount) + s.Require().NoError(err) + s.Require().Equal(mintAmount, balance) + s.T().Logf("Minted %d tokens to ICS27 PDA's token account", mintAmount) + })) + })) + + // Execute SPL token transfer via GMP + var cosmosGMPTxHash []byte + transferAmount := SPLTokenTransferAmount // 1 token (1,000,000 with 6 decimals) + + s.Require().True(s.Run("Send GMP SPL Token Transfer from Cosmos", func() { + timeout := uint64(time.Now().Add(30 * time.Minute).Unix()) + + // Build SPL transfer instruction + splTransferInstruction := token.NewTransferInstruction( + transferAmount, + sourceTokenAccount, + destTokenAccount, + ics27AccountPDA, // Authority - will be signed by GMP program via invoke_signed + []solanago.PublicKey{}, + ).Build() + + // Get instruction data + instructionData, err := splTransferInstruction.Data() + s.Require().NoError(err) + + // Create SolanaInstruction protobuf + // Note: PayerPosition is left unset (nil) - NO payer injection since SPL Transfer doesn't create accounts + // SPL Transfer requires exactly 3 accounts: source, destination, authority + // The authority (ICS27 PDA) must be marked as PDA_SIGNER so GMP program builds CPI with it as signer + solanaInstruction := &solanatypes.SolanaInstruction{ + ProgramId: token.ProgramID.Bytes(), + Data: instructionData, + Accounts: []*solanatypes.SolanaAccountMeta{ + {Pubkey: sourceTokenAccount.Bytes(), IsSigner: false, IsWritable: true}, // [0] source + {Pubkey: destTokenAccount.Bytes(), IsSigner: false, IsWritable: true}, // [1] destination + {Pubkey: ics27AccountPDA.Bytes(), IsSigner: true, IsWritable: false}, // [2] authority (GMP PDA signs via invoke_signed) + }, + // PayerPosition is nil - no payer injection needed + } + + payload, err := proto.Marshal(solanaInstruction) + s.Require().NoError(err) + + // Send GMP call + resp, err := s.BroadcastMessages(ctx, simd, cosmosUser, 2_000_000, &gmptypes.MsgSendCall{ + SourceClient: CosmosClientID, + Sender: cosmosUser.FormattedAddress(), + Receiver: token.ProgramID.String(), + Salt: []byte{}, + Payload: payload, + TimeoutTimestamp: timeout, + Memo: fmt.Sprintf("SPL token transfer: %d tokens", transferAmount), + Encoding: testvalues.Ics27ProtobufEncoding, + }) + s.Require().NoError(err) + + cosmosGMPTxHashBytes, err := hex.DecodeString(resp.TxHash) + s.Require().NoError(err) + cosmosGMPTxHash = cosmosGMPTxHashBytes + + s.T().Logf("GMP SPL transfer packet sent from Cosmos: %s", resp.TxHash) + })) + + // Record initial balances + var initialSourceBalance uint64 + var initialDestBalance uint64 + s.Require().True(s.Run("Record Initial Token Balances", func() { + var err error + initialSourceBalance, err = s.getTokenBalance(ctx, sourceTokenAccount) + s.Require().NoError(err) + + initialDestBalance, err = s.getTokenBalance(ctx, destTokenAccount) + s.Require().NoError(err) + + s.T().Logf("Initial balances - Source: %d, Dest: %d", initialSourceBalance, initialDestBalance) + })) + + // Relay and execute on Solana + var solanaRelayTxSig solanago.Signature + s.Require().True(s.Run("Relay and Execute SPL Transfer on Solana", func() { + s.Require().True(s.Run("Update Tendermint client on Solana", func() { + updateResp, err := s.RelayerClient.UpdateClient(context.Background(), &relayertypes.UpdateClientRequest{ + SrcChain: simd.Config().ChainID, + DstChain: testvalues.SolanaChainID, + DstClientId: SolanaClientID, + }) + s.Require().NoError(err, "Relayer Update Client failed") + s.Require().NotEmpty(updateResp.Txs, "Relayer Update client should return chunked transactions") + + s.submitChunkedUpdateClient(ctx, updateResp, s.SolanaUser) + s.T().Logf("Updated Tendermint client on Solana using %d chunked transactions", len(updateResp.Txs)) + })) + + s.Require().True(s.Run("Retrieve relay tx from relayer", func() { + resp, err := s.RelayerClient.RelayByTx(context.Background(), &relayertypes.RelayByTxRequest{ + SrcChain: simd.Config().ChainID, + DstChain: testvalues.SolanaChainID, + SourceTxIds: [][]byte{cosmosGMPTxHash}, + SrcClientId: CosmosClientID, + DstClientId: SolanaClientID, + }) + s.Require().NoError(err) + s.Require().NotEmpty(resp.Txs, "Relay should return chunked transactions") + s.T().Logf("Retrieved %d relay transactions (chunks + final instructions)", len(resp.Txs)) + + solanaRelayTxSig = s.submitChunkedRelayPackets(ctx, resp, s.SolanaUser) + s.T().Logf("SPL transfer executed on Solana: %s", solanaRelayTxSig) + })) + })) + + // Verify transfer completed + s.Require().True(s.Run("Verify SPL Token Transfer", func() { + finalSourceBalance, err := s.getTokenBalance(ctx, sourceTokenAccount) + s.Require().NoError(err) + + finalDestBalance, err := s.getTokenBalance(ctx, destTokenAccount) + s.Require().NoError(err) + + expectedSourceBalance := initialSourceBalance - transferAmount + expectedDestBalance := initialDestBalance + transferAmount + + s.Require().Equal(expectedSourceBalance, finalSourceBalance, + "Source balance should decrease by transfer amount") + s.Require().Equal(expectedDestBalance, finalDestBalance, + "Destination balance should increase by transfer amount") + + s.T().Logf("Transfer verified!") + s.T().Logf(" Source: %d → %d (-%d)", initialSourceBalance, finalSourceBalance, transferAmount) + s.T().Logf(" Dest: %d → %d (+%d)", initialDestBalance, finalDestBalance, transferAmount) + })) + + // Relay acknowledgment back to Cosmos + s.Require().True(s.Run("Relay Acknowledgment to Cosmos", func() { + var ackRelayTxBodyBz []byte + s.Require().True(s.Run("Retrieve acknowledgment relay tx", func() { + resp, err := s.RelayerClient.RelayByTx(context.Background(), &relayertypes.RelayByTxRequest{ + SrcChain: testvalues.SolanaChainID, + DstChain: simd.Config().ChainID, + SourceTxIds: [][]byte{[]byte(solanaRelayTxSig.String())}, + SrcClientId: SolanaClientID, + DstClientId: CosmosClientID, + }) + s.Require().NoError(err) + s.Require().NotEmpty(resp.Tx) + ackRelayTxBodyBz = resp.Tx + s.T().Logf("Retrieved acknowledgment relay transaction") + })) + + s.Require().True(s.Run("Broadcast acknowledgment on Cosmos", func() { + relayTxResult := s.MustBroadcastSdkTxBody(ctx, simd, cosmosUser, CosmosDefaultGasLimit, ackRelayTxBodyBz) + s.T().Logf("SPL transfer acknowledgment relay transaction: %s (code: %d, gas: %d)", + relayTxResult.TxHash, relayTxResult.Code, relayTxResult.GasUsed) + })) + + s.T().Logf("✓ SPL token transfer via GMP completed successfully") + s.T().Logf(" Cosmos user %s controlled Solana ICS27 PDA %s", + cosmosUser.FormattedAddress(), ics27AccountPDA.String()) + s.T().Logf(" Transferred %d tokens from ICS27 PDA to recipient", transferAmount) + })) +} + +// SPL Token Helper Functions + +// createSPLTokenMint creates a new SPL token mint with specified decimals +func (s *IbcEurekaSolanaTestSuite) createSPLTokenMint(ctx context.Context, decimals uint8) (solanago.PublicKey, error) { + mintAccount := solanago.NewWallet() + mintPubkey := mintAccount.PublicKey() + + // Get minimum balance for rent exemption (mint account is 82 bytes) + const mintAccountSize = uint64(82) + rentExemption, err := s.SolanaChain.RPCClient.GetMinimumBalanceForRentExemption(ctx, mintAccountSize, "confirmed") + if err != nil { + return solanago.PublicKey{}, err + } + + // Create mint account + createAccountIx := system.NewCreateAccountInstruction( + rentExemption, + mintAccountSize, + token.ProgramID, + s.SolanaUser.PublicKey(), + mintPubkey, + ).Build() + + // Initialize mint + initMintIx := token.NewInitializeMint2Instruction( + decimals, + s.SolanaUser.PublicKey(), // Mint authority + s.SolanaUser.PublicKey(), // Freeze authority + mintPubkey, + ).Build() + + // Build transaction using the chain helper + tx, err := s.SolanaChain.NewTransactionFromInstructions( + s.SolanaUser.PublicKey(), + createAccountIx, + initMintIx, + ) + if err != nil { + return solanago.PublicKey{}, err + } + + // Sign and broadcast with both payer and mint account (with retry) + _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, s.SolanaUser, mintAccount) + if err != nil { + return solanago.PublicKey{}, err + } + + return mintPubkey, nil +} + +// createTokenAccount creates a new SPL token account for the specified owner +func (s *IbcEurekaSolanaTestSuite) createTokenAccount(ctx context.Context, mint, owner solanago.PublicKey) (solanago.PublicKey, error) { + tokenAccount := solanago.NewWallet() + tokenAccountPubkey := tokenAccount.PublicKey() + + // Token account size is 165 bytes + const tokenAccountSize = uint64(165) + + // Calculate rent exemption + rentExemption, err := s.SolanaChain.RPCClient.GetMinimumBalanceForRentExemption(ctx, tokenAccountSize, "confirmed") + if err != nil { + return solanago.PublicKey{}, err + } + + // Create account instruction + createAccountIx := system.NewCreateAccountInstruction( + rentExemption, + tokenAccountSize, + token.ProgramID, + s.SolanaUser.PublicKey(), + tokenAccountPubkey, + ).Build() + + // Initialize token account (using InitializeAccount3 which doesn't require rent sysvar) + // Parameters: owner, account, mint + initAccountIx := token.NewInitializeAccount3Instruction( + owner, + tokenAccountPubkey, + mint, + ).Build() + + // Build transaction using the chain helper + tx, err := s.SolanaChain.NewTransactionFromInstructions( + s.SolanaUser.PublicKey(), + createAccountIx, + initAccountIx, + ) + if err != nil { + return solanago.PublicKey{}, err + } + + // Sign and broadcast with both payer and token account (with retry) + _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, s.SolanaUser, tokenAccount) + if err != nil { + return solanago.PublicKey{}, err + } + + return tokenAccountPubkey, nil +} + +// mintTokensTo mints tokens to a specified token account +func (s *IbcEurekaSolanaTestSuite) mintTokensTo(ctx context.Context, mint, destination solanago.PublicKey, amount uint64) error { + mintToIx := token.NewMintToInstruction( + amount, + mint, + destination, + s.SolanaUser.PublicKey(), // Mint authority + []solanago.PublicKey{}, + ).Build() + + tx, err := s.SolanaChain.NewTransactionFromInstructions( + s.SolanaUser.PublicKey(), + mintToIx, + ) + if err != nil { + return err + } + + _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, s.SolanaUser) + return err +} + +// getTokenBalance retrieves the token balance for a token account +func (s *IbcEurekaSolanaTestSuite) getTokenBalance(ctx context.Context, tokenAccount solanago.PublicKey) (uint64, error) { + accountInfo, err := s.SolanaChain.RPCClient.GetAccountInfo(ctx, tokenAccount) + if err != nil { + return 0, err + } + + if accountInfo.Value == nil { + return 0, fmt.Errorf("token account not found") + } + + data := accountInfo.Value.Data.GetBinary() + if len(data) < 72 { + return 0, fmt.Errorf("invalid token account data") + } + + // Token balance is at offset 64 (8 bytes, little endian) + balance := binary.LittleEndian.Uint64(data[64:72]) + return balance, nil +} + +// deriveICS27AccountPDA derives the ICS27 Account PDA for a Cosmos user +func (s *IbcEurekaSolanaTestSuite) deriveICS27AccountPDA(cosmosAddress string, salt []byte) (solanago.PublicKey, error) { + // Hash the sender address using SHA256 (matches the Rust implementation: hash(sender.as_bytes()).to_bytes()) + hasher := sha256.New() + hasher.Write([]byte(cosmosAddress)) + senderHash := hasher.Sum(nil) + + // Derive PDA: [b"gmp_account", client_id, hash(sender), salt] + seeds := [][]byte{ + []byte("gmp_account"), + []byte(CosmosClientID), + senderHash, + salt, + } + + pda, _, err := solanago.FindProgramAddress(seeds, ics27_gmp.ProgramID) + return pda, err +} + +func (s *IbcEurekaSolanaTestSuite) Test_GMPSendCallFromSolana() { + ctx := context.Background() + + s.UseMockWasmClient = true + + s.SetupSuite(ctx) + + simd := s.CosmosChains[0] + + var ics27GMPProgramID solanago.PublicKey + s.Require().True(s.Run("Deploy and Initialize ICS27 GMP Program", func() { + ics27GMPProgramID = s.deployAndInitializeICS27GMP(ctx) + })) + + testAmount := sdk.NewCoins(sdk.NewCoin(simd.Config().Denom, sdkmath.NewInt(CosmosTestAmount))) + testCosmosUser := s.CreateAndFundCosmosUserWithBalance(ctx, simd, testAmount[0].Amount.Int64()) + + var computedAddress sdk.AccAddress + s.Require().True(s.Run("Fund pre-computed ICS27 address on Cosmos", func() { + solanaUserAddress := s.SolanaUser.PublicKey().String() + + // Use CosmosClientID (08-wasm-0) - the dest_client on Cosmos + // The GMP keeper creates accounts using NewAccountIdentifier(destClient, sender, salt) + res, err := e2esuite.GRPCQuery[gmptypes.QueryAccountAddressResponse](ctx, simd, &gmptypes.QueryAccountAddressRequest{ + ClientId: CosmosClientID, + Sender: solanaUserAddress, + Salt: "", + }) + s.Require().NoError(err) + s.Require().NotEmpty(res.AccountAddress) + + computedAddress, err = sdk.AccAddressFromBech32(res.AccountAddress) + s.Require().NoError(err) + + s.T().Logf("ICS27 account on Cosmos: %s", computedAddress.String()) + + _, err = s.BroadcastMessages(ctx, simd, testCosmosUser, CosmosDefaultGasLimit, &banktypes.MsgSend{ + FromAddress: testCosmosUser.FormattedAddress(), + ToAddress: computedAddress.String(), + Amount: testAmount, + }) + s.Require().NoError(err) + })) + + s.Require().True(s.Run("Verify initial balance on Cosmos", func() { + resp, err := e2esuite.GRPCQuery[banktypes.QueryBalanceResponse](ctx, simd, &banktypes.QueryBalanceRequest{ + Address: computedAddress.String(), + Denom: simd.Config().Denom, + }) + s.Require().NoError(err) + s.Require().NotNil(resp.Balance) + s.Require().Equal(testAmount[0].Amount.Int64(), resp.Balance.Amount.Int64()) + })) + + var solanaPacketTxHash string + s.Require().True(s.Run("Send call from Solana", func() { + timeout := uint64(time.Now().Add(30 * time.Minute).Unix()) + + var payload []byte + s.Require().True(s.Run("Prepare GMP payload", func() { + msgSend := &banktypes.MsgSend{ + FromAddress: computedAddress.String(), + ToAddress: testCosmosUser.FormattedAddress(), + Amount: testAmount, + } + + var err error + payload, err = gmphelpers.NewPayload_FromProto([]proto.Message{msgSend}) + s.Require().NoError(err) + s.T().Logf("Encoded GMP payload (%d bytes)", len(payload)) + })) + + var gmpAppStatePDA, routerStatePDA, routerCallerPDA, clientPDA, ibcAppPDA, clientSequencePDA solanago.PublicKey + s.Require().True(s.Run("Derive required PDAs", func() { + var err error + + gmpAppStatePDA, _, err = solanago.FindProgramAddress( + [][]byte{[]byte("app_state"), []byte(GMPPortID)}, + ics27GMPProgramID, + ) + s.Require().NoError(err) + + routerStatePDA, _, err = solanago.FindProgramAddress( + [][]byte{[]byte("router_state")}, + ics26_router.ProgramID, + ) + s.Require().NoError(err) + + routerCallerPDA, _, err = solanago.FindProgramAddress( + [][]byte{[]byte("router_caller")}, + ics27GMPProgramID, + ) + s.Require().NoError(err) + + clientPDA, _, err = solanago.FindProgramAddress( + [][]byte{[]byte("client"), []byte(SolanaClientID)}, + ics26_router.ProgramID, + ) + s.Require().NoError(err) + + ibcAppPDA, _, err = solanago.FindProgramAddress( + [][]byte{[]byte("ibc_app"), []byte(GMPPortID)}, + ics26_router.ProgramID, + ) + s.Require().NoError(err) + + clientSequencePDA, _, err = solanago.FindProgramAddress( + [][]byte{[]byte("client_sequence"), []byte(SolanaClientID)}, + ics26_router.ProgramID, + ) + s.Require().NoError(err) + + s.T().Logf("Derived PDAs: gmpAppState=%s, routerState=%s, client=%s", + gmpAppStatePDA.String(), routerStatePDA.String(), clientPDA.String()) + })) + + var packetCommitmentPDA solanago.PublicKey + var nextSequence uint64 + s.Require().True(s.Run("Get next sequence number", func() { + nextSequence = 1 // Default if account doesn't exist yet + clientSequenceAccount, err := s.SolanaChain.RPCClient.GetAccountInfo(ctx, clientSequencePDA) + if err == nil && clientSequenceAccount.Value != nil { + data := clientSequenceAccount.Value.Data.GetBinary() + if len(data) >= 16 { + // Use the CURRENT value - router derives PDA before incrementing + nextSequence = binary.LittleEndian.Uint64(data[8:16]) + } + } + + sequenceBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(sequenceBytes, nextSequence) + packetCommitmentPDA, _, err = solanago.FindProgramAddress( + [][]byte{ + []byte("packet_commitment"), + []byte(SolanaClientID), + sequenceBytes, + }, + ics26_router.ProgramID, + ) + s.Require().NoError(err) + + s.T().Logf("Using sequence number: %d", nextSequence) + })) + + var sendCallInstruction solanago.Instruction + s.Require().True(s.Run("Build send_call instruction", func() { + var err error + sendCallInstruction, err = ics27_gmp.NewSendCallInstruction( + ics27_gmp.SendCallMsg{ + SourceClient: SolanaClientID, + TimeoutTimestamp: int64(timeout), + Receiver: solanago.PublicKey{}, + Salt: []byte{}, + Payload: payload, + Memo: "send from Solana to Cosmos", + }, + gmpAppStatePDA, + s.SolanaUser.PublicKey(), + s.SolanaUser.PublicKey(), + ics26_router.ProgramID, + routerStatePDA, + clientSequencePDA, + packetCommitmentPDA, + routerCallerPDA, + ibcAppPDA, + clientPDA, + solanago.SystemProgramID, + ) + s.Require().NoError(err) + s.T().Log("Built send_call instruction") + })) + + s.Require().True(s.Run("Broadcast transaction", func() { + tx, err := s.SolanaChain.NewTransactionFromInstructions( + s.SolanaUser.PublicKey(), + sendCallInstruction, + ) + s.Require().NoError(err) + + sig, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, s.SolanaUser) + s.Require().NoError(err) + s.Require().NotEmpty(sig) + + solanaPacketTxHash = sig.String() + s.T().Logf("Send call transaction: %s", solanaPacketTxHash) + })) + })) + + var ackTxHash []byte + s.Require().True(s.Run("Receive packet in Cosmos", func() { + var recvRelayTx []byte + s.Require().True(s.Run("Retrieve relay tx", func() { + txHashBytes := []byte(solanaPacketTxHash) + + resp, err := s.RelayerClient.RelayByTx(context.Background(), &relayertypes.RelayByTxRequest{ + SrcChain: testvalues.SolanaChainID, + DstChain: simd.Config().ChainID, + SourceTxIds: [][]byte{txHashBytes}, + SrcClientId: SolanaClientID, + DstClientId: CosmosClientID, + }) + s.Require().NoError(err) + s.Require().NotEmpty(resp.Tx) + s.Require().Empty(resp.Address) + + recvRelayTx = resp.Tx + })) + + s.Require().True(s.Run("Submit relay tx to Cosmos", func() { + receipt := s.MustBroadcastSdkTxBody(ctx, simd, s.CosmosUsers[0], 2_000_000, recvRelayTx) + s.T().Logf("Recv packet tx result: code=%d, log=%s, gas=%d", receipt.Code, receipt.RawLog, receipt.GasUsed) + + s.Require().Equal(uint32(0), receipt.Code, "Tx should succeed") + s.Require().NotEmpty(receipt.TxHash) + + var err error + ackTxHash, err = hex.DecodeString(receipt.TxHash) + s.Require().NoError(err) + })) + + s.Require().True(s.Run("Verify balance changed on Cosmos", func() { + resp, err := e2esuite.GRPCQuery[banktypes.QueryBalanceResponse](ctx, simd, &banktypes.QueryBalanceRequest{ + Address: computedAddress.String(), + Denom: simd.Config().Denom, + }) + s.Require().NoError(err) + s.Require().NotNil(resp.Balance) + s.Require().Zero(resp.Balance.Amount.Int64()) + + resp, err = e2esuite.GRPCQuery[banktypes.QueryBalanceResponse](ctx, simd, &banktypes.QueryBalanceRequest{ + Address: testCosmosUser.FormattedAddress(), + Denom: simd.Config().Denom, + }) + s.Require().NoError(err) + s.Require().NotNil(resp.Balance) + s.Require().Equal(testAmount[0].Amount.Int64(), resp.Balance.Amount.Int64()) + })) + })) + + s.Require().True(s.Run("Acknowledge packet in Solana", func() { + s.Require().True(s.Run("Update Tendermint client on Solana", func() { + resp, err := s.RelayerClient.UpdateClient(context.Background(), &relayertypes.UpdateClientRequest{ + SrcChain: simd.Config().ChainID, + DstChain: testvalues.SolanaChainID, + DstClientId: SolanaClientID, + }) + s.Require().NoError(err, "Relayer Update Client failed") + s.Require().NotEmpty(resp.Txs, "Relayer Update client should return transactions") + + s.submitChunkedUpdateClient(ctx, resp, s.SolanaUser) + s.T().Logf("Successfully updated Tendermint client on Solana using %d transaction(s)", len(resp.Txs)) + })) + + s.Require().True(s.Run("Relay acknowledgement", func() { + resp, err := s.RelayerClient.RelayByTx(context.Background(), &relayertypes.RelayByTxRequest{ + SrcChain: simd.Config().ChainID, + DstChain: testvalues.SolanaChainID, + SourceTxIds: [][]byte{ackTxHash}, + SrcClientId: CosmosClientID, + DstClientId: SolanaClientID, + }) + s.Require().NoError(err) + s.Require().NotEmpty(resp.Txs, "Relay should return chunked transactions") + s.T().Logf("Retrieved %d relay transactions (chunks + final instructions)", len(resp.Txs)) + + sig := s.submitChunkedRelayPackets(ctx, resp, s.SolanaUser) + s.T().Logf("Acknowledgement transaction broadcasted: %s", sig) + })) + + s.Require().True(s.Run("Verify acknowledgement was processed", func() { + s.verifyPacketCommitmentDeleted(ctx, SolanaClientID, 1) + })) + })) +} + +func (s *IbcEurekaSolanaTestSuite) Test_GMPTimeoutFromSolana() { + ctx := context.Background() + + s.UseMockWasmClient = true + + s.SetupSuite(ctx) + + simd := s.CosmosChains[0] + + var ics27GMPProgramID solanago.PublicKey + s.Require().True(s.Run("Deploy and Initialize ICS27 GMP Program", func() { + ics27GMPProgramID = s.deployAndInitializeICS27GMP(ctx) + })) + + testAmount := sdk.NewCoins(sdk.NewCoin(simd.Config().Denom, sdkmath.NewInt(CosmosTestAmount))) + testCosmosUser := s.CreateAndFundCosmosUserWithBalance(ctx, simd, testAmount[0].Amount.Int64()) + + var computedAddress sdk.AccAddress + s.Require().True(s.Run("Fund pre-computed ICS27 address on Cosmos", func() { + solanaUserAddress := s.SolanaUser.PublicKey().String() + + res, err := e2esuite.GRPCQuery[gmptypes.QueryAccountAddressResponse](ctx, simd, &gmptypes.QueryAccountAddressRequest{ + ClientId: CosmosClientID, + Sender: solanaUserAddress, + Salt: "", + }) + s.Require().NoError(err) + s.Require().NotEmpty(res.AccountAddress) + + computedAddress, err = sdk.AccAddressFromBech32(res.AccountAddress) + s.Require().NoError(err) + + s.T().Logf("ICS27 account on Cosmos: %s", computedAddress.String()) + + _, err = s.BroadcastMessages(ctx, simd, testCosmosUser, CosmosDefaultGasLimit, &banktypes.MsgSend{ + FromAddress: testCosmosUser.FormattedAddress(), + ToAddress: computedAddress.String(), + Amount: testAmount, + }) + s.Require().NoError(err) + })) + + var solanaPacketTxHash []byte + s.Require().True(s.Run("Send call from Solana with short timeout", func() { + // Use 61 seconds (just above MIN_TIMEOUT_DURATION of 60 seconds) + timeout := uint64(time.Now().Add(61 * time.Second).Unix()) + + var payload []byte + s.Require().True(s.Run("Prepare GMP payload", func() { + msgSend := &banktypes.MsgSend{ + FromAddress: computedAddress.String(), + ToAddress: testCosmosUser.FormattedAddress(), + Amount: testAmount, + } + + var err error + payload, err = gmphelpers.NewPayload_FromProto([]proto.Message{msgSend}) + s.Require().NoError(err) + s.T().Logf("Encoded GMP payload (%d bytes)", len(payload)) + })) + + var gmpAppStatePDA, routerStatePDA, routerCallerPDA, clientPDA, ibcAppPDA, clientSequencePDA solanago.PublicKey + s.Require().True(s.Run("Derive required PDAs", func() { + var err error + + gmpAppStatePDA, _, err = solanago.FindProgramAddress( + [][]byte{[]byte("app_state"), []byte(GMPPortID)}, + ics27GMPProgramID, + ) + s.Require().NoError(err) + + routerStatePDA, _, err = solanago.FindProgramAddress( + [][]byte{[]byte("router_state")}, + ics26_router.ProgramID, + ) + s.Require().NoError(err) + + routerCallerPDA, _, err = solanago.FindProgramAddress( + [][]byte{[]byte("router_caller")}, + ics27GMPProgramID, + ) + s.Require().NoError(err) + + clientPDA, _, err = solanago.FindProgramAddress( + [][]byte{[]byte("client"), []byte(SolanaClientID)}, + ics26_router.ProgramID, + ) + s.Require().NoError(err) + + ibcAppPDA, _, err = solanago.FindProgramAddress( + [][]byte{[]byte("ibc_app"), []byte(GMPPortID)}, + ics26_router.ProgramID, + ) + s.Require().NoError(err) + + clientSequencePDA, _, err = solanago.FindProgramAddress( + [][]byte{[]byte("client_sequence"), []byte(SolanaClientID)}, + ics26_router.ProgramID, + ) + s.Require().NoError(err) + + s.T().Logf("Derived PDAs: gmpAppState=%s, routerState=%s, client=%s", + gmpAppStatePDA.String(), routerStatePDA.String(), clientPDA.String()) + })) + + var packetCommitmentPDA solanago.PublicKey + var nextSequence uint64 + s.Require().True(s.Run("Get next sequence number", func() { + nextSequence = 1 + clientSequenceAccount, err := s.SolanaChain.RPCClient.GetAccountInfo(ctx, clientSequencePDA) + if err == nil && clientSequenceAccount.Value != nil { + data := clientSequenceAccount.Value.Data.GetBinary() + if len(data) >= 16 { + nextSequence = binary.LittleEndian.Uint64(data[8:16]) + } + } + + sequenceBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(sequenceBytes, nextSequence) + packetCommitmentPDA, _, err = solanago.FindProgramAddress( + [][]byte{ + []byte("packet_commitment"), + []byte(SolanaClientID), + sequenceBytes, + }, + ics26_router.ProgramID, + ) + s.Require().NoError(err) + + s.T().Logf("Using sequence number: %d (timeout test)", nextSequence) + })) + + var sendCallInstruction solanago.Instruction + s.Require().True(s.Run("Build send_call instruction", func() { + var err error + sendCallInstruction, err = ics27_gmp.NewSendCallInstruction( + ics27_gmp.SendCallMsg{ + SourceClient: SolanaClientID, + TimeoutTimestamp: int64(timeout), + Receiver: solanago.PublicKey{}, + Salt: []byte{}, + Payload: payload, + Memo: "timeout test from Solana", + }, + gmpAppStatePDA, + s.SolanaUser.PublicKey(), + s.SolanaUser.PublicKey(), + ics26_router.ProgramID, + routerStatePDA, + clientSequencePDA, + packetCommitmentPDA, + routerCallerPDA, + ibcAppPDA, + clientPDA, + solanago.SystemProgramID, + ) + s.Require().NoError(err) + s.T().Log("Built send_call instruction with short timeout") + })) + + s.Require().True(s.Run("Broadcast transaction", func() { + tx, err := s.SolanaChain.NewTransactionFromInstructions( + s.SolanaUser.PublicKey(), + sendCallInstruction, + ) + s.Require().NoError(err) + + sig, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, s.SolanaUser) + s.Require().NoError(err) + s.Require().NotEmpty(sig) + + solanaPacketTxHash = []byte(sig.String()) + s.T().Logf("Send call transaction (will timeout): %s", sig) + })) + })) + + // Sleep for 65 seconds to let the packet timeout (timeout is set to 61 seconds) + s.T().Log("Sleeping 65 seconds to let packet timeout...") + time.Sleep(65 * time.Second) + + s.Require().True(s.Run("Relay timeout back to Solana", func() { + // Update Tendermint client on Solana before relaying timeout + // The relayer now queries Cosmos for current height for timeout proofs, + // so we just need to ensure Solana has a recent consensus state + s.Require().True(s.Run("Update Tendermint client on Solana", func() { + resp, err := s.RelayerClient.UpdateClient(context.Background(), &relayertypes.UpdateClientRequest{ + SrcChain: simd.Config().ChainID, + DstChain: testvalues.SolanaChainID, + DstClientId: SolanaClientID, + }) + s.Require().NoError(err, "Relayer Update Client failed") + s.Require().NotEmpty(resp.Txs, "Relayer Update client should return transactions") + + s.submitChunkedUpdateClient(ctx, resp, s.SolanaUser) + s.T().Logf("Successfully updated Tendermint client on Solana using %d transaction(s)", len(resp.Txs)) + })) + + s.Require().True(s.Run("Relay timeout transaction", func() { + resp, err := s.RelayerClient.RelayByTx(context.Background(), &relayertypes.RelayByTxRequest{ + SrcChain: simd.Config().ChainID, + DstChain: testvalues.SolanaChainID, + TimeoutTxIds: [][]byte{solanaPacketTxHash}, + SrcClientId: CosmosClientID, + DstClientId: SolanaClientID, + }) + s.Require().NoError(err) + s.Require().NotEmpty(resp.Txs, "Relay should return chunked transactions") + s.T().Logf("Retrieved %d relay transactions (chunks + final instructions)", len(resp.Txs)) + + sig := s.submitChunkedRelayPackets(ctx, resp, s.SolanaUser) + s.T().Logf("Timeout transaction broadcasted: %s", sig) + + s.T().Log("Timeout successfully processed on Solana") + })) + })) +} diff --git a/e2e/interchaintestv8/solana_test.go b/e2e/interchaintestv8/solana_test.go index ea0268cfb..937e0ecde 100644 --- a/e2e/interchaintestv8/solana_test.go +++ b/e2e/interchaintestv8/solana_test.go @@ -36,11 +36,20 @@ import ( ) const ( - TestTransferAmount = 1000000 // 0.001 SOL in lamports + // General DefaultTimeoutSeconds = 30 - SolDenom = "sol" - CosmosClientID = testvalues.FirstWasmClientID SolanaClientID = testvalues.CustomClientID + CosmosClientID = testvalues.FirstWasmClientID + // Transfer App + OneSolInLamports = 1_000_000_000 // 1 SOL in lamports + TestTransferAmount = OneSolInLamports / 1_000 // 0.001 SOL in lamports + SolDenom = "sol" + TransferPortID = transfertypes.PortID + // Compute Units + DefaultComputeUnits = uint32(400_000) + // Cosmos Gas Limits + CosmosDefaultGasLimit = uint64(200_000) + CosmosCreateClientGasLimit = uint64(20_000_000) ) type IbcEurekaSolanaTestSuite struct { @@ -50,6 +59,19 @@ type IbcEurekaSolanaTestSuite struct { RelayerClient relayertypes.RelayerServiceClient DummyAppProgramID solanago.PublicKey + + // Mock configuration for tests + UseMockWasmClient bool + + // GMP setup - if true, deploys ICS27 GMP program and creates ALT during setup + SetupGMP bool + + // Dummy App setup - if true, deploys and registers dummy IBC app during setup + SetupDummyApp bool + + // ALT configuration - if set, will be used when starting relayer + SolanaAltAddress string + RelayerProcess *os.Process } func TestWithIbcEurekaSolanaTestSuite(t *testing.T) { @@ -66,8 +88,6 @@ func (s *IbcEurekaSolanaTestSuite) SetupSuite(ctx context.Context) { os.Setenv(testvalues.EnvKeySolanaTestnetType, testvalues.SolanaTestnetType_Localnet) s.TestSuite.SetupSuite(ctx) - simd := s.CosmosChains[0] - s.T().Log("Waiting for Solana cluster to be ready...") err = s.SolanaChain.WaitForClusterReady(ctx, 30*time.Second) s.Require().NoError(err, "Solana cluster failed to initialize") @@ -76,13 +96,14 @@ func (s *IbcEurekaSolanaTestSuite) SetupSuite(ctx context.Context) { s.SolanaUser, err = s.SolanaChain.CreateAndFundWalletWithRetry(ctx, 5) s.Require().NoError(err, "Solana create/fund wallet has failed") - s.Require().True(s.Run("Deploy contracts", func() { + simd := s.CosmosChains[0] + + s.Require().True(s.Run("Deploy IBC core contracts", func() { _, err := s.SolanaChain.FundUser(solana.DeployerPubkey, 20*testvalues.InitialSolBalance) s.Require().NoError(err, "FundUser user failed") ics07ProgramID := s.deploySolanaProgram(ctx, "ics07_tendermint") s.Require().Equal(ics07_tendermint.ProgramID, ics07ProgramID) - ics07_tendermint.ProgramID = ics07ProgramID ics26RouterProgramID := s.deploySolanaProgram(ctx, "ics26_router") @@ -95,37 +116,123 @@ func (s *IbcEurekaSolanaTestSuite) SetupSuite(ctx context.Context) { s.Require().True(ics26Available, "ICS26 router program failed to become available") })) - var relayerProcess *os.Process + // Initialize router first (required before GMP/Dummy App can register) + s.Require().True(s.Run("Initialize ICS26 Router", func() { + routerStateAccount, _, err := solanago.FindProgramAddress([][]byte{[]byte("router_state")}, ics26_router.ProgramID) + s.Require().NoError(err, "Could not find router_state") + initInstruction, err := ics26_router.NewInitializeInstruction(s.SolanaUser.PublicKey(), routerStateAccount, s.SolanaUser.PublicKey(), solanago.SystemProgramID) + s.Require().NoError(err) + + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), initInstruction) + s.Require().NoError(err) + _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, s.SolanaUser) + s.Require().NoError(err) + })) + + // Deploy and initialize ICS27 GMP program if SetupGMP is enabled (requires initialized router) + if s.SetupGMP { + s.deployAndInitializeICS27GMP(ctx) + + // Create Address Lookup Table after GMP deployment (if not already set) + if s.SolanaAltAddress == "" { + s.Require().True(s.Run("Create Address Lookup Table", func() { + altAddress := s.createAddressLookupTable(ctx) + s.SolanaAltAddress = altAddress.String() + s.T().Logf("Created Address Lookup Table: %s", s.SolanaAltAddress) + })) + } + } + + // Deploy and register Dummy App if SetupDummyApp is enabled (requires initialized router) + if s.SetupDummyApp { + s.Require().True(s.Run("Deploy and Register Dummy App", func() { + dummyAppProgramID := s.deploySolanaProgram(ctx, "dummy_ibc_app") + dummy_ibc_app.ProgramID = dummyAppProgramID + + programAvailable := s.SolanaChain.WaitForProgramAvailabilityWithTimeout(ctx, dummyAppProgramID, 120) + s.Require().True(programAvailable, "Program failed to become available within timeout") + + appStateAccount, _, err := solanago.FindProgramAddress([][]byte{[]byte("app_state"), []byte(transfertypes.PortID)}, dummyAppProgramID) + s.Require().NoError(err) + + initInstruction, err := dummy_ibc_app.NewInitializeInstruction( + s.SolanaUser.PublicKey(), + appStateAccount, + s.SolanaUser.PublicKey(), + solanago.SystemProgramID, + ) + s.Require().NoError(err) + + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), initInstruction) + s.Require().NoError(err) + + _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, s.SolanaUser) + s.Require().NoError(err) + s.T().Logf("Dummy app initialized") + + routerStateAccount, _, err := solanago.FindProgramAddress([][]byte{[]byte("router_state")}, ics26_router.ProgramID) + s.Require().NoError(err) + + ibcAppAccount, _, err := solanago.FindProgramAddress([][]byte{[]byte("ibc_app"), []byte(transfertypes.PortID)}, ics26_router.ProgramID) + s.Require().NoError(err) + + registerInstruction, err := ics26_router.NewAddIbcAppInstruction( + transfertypes.PortID, + routerStateAccount, + ibcAppAccount, + dummyAppProgramID, + s.SolanaUser.PublicKey(), + s.SolanaUser.PublicKey(), + solanago.SystemProgramID, + ) + s.Require().NoError(err) + + tx2, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), registerInstruction) + s.Require().NoError(err) + + _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx2, s.SolanaUser) + s.Require().NoError(err) + s.T().Logf("Registered for transfer port") + + s.DummyAppProgramID = dummyAppProgramID + })) + } + + // Start relayer after all infrastructure is set up (including ALT if needed) s.Require().True(s.Run("Start Relayer", func() { - config := relayer.NewConfig(relayer.CreateSolanaCosmosModules( - relayer.SolanaCosmosConfigInfo{ - SolanaChainID: testvalues.SolanaChainID, - CosmosChainID: simd.Config().ChainID, - SolanaRPC: testvalues.SolanaLocalnetRPC, - TmRPC: simd.GetHostRPCAddress(), - ICS07ProgramID: ics07_tendermint.ProgramID.String(), - ICS26RouterProgramID: ics26_router.ProgramID.String(), - IBCAppProgramID: dummy_ibc_app.ProgramID.String(), - CosmosSignerAddress: s.CosmosUsers[0].FormattedAddress(), - SolanaFeePayer: s.SolanaUser.PublicKey().String(), - Mock: true, - }), - ) + configInfo := relayer.SolanaCosmosConfigInfo{ + SolanaChainID: testvalues.SolanaChainID, + CosmosChainID: simd.Config().ChainID, + SolanaRPC: testvalues.SolanaLocalnetRPC, + TmRPC: simd.GetHostRPCAddress(), + ICS07ProgramID: ics07_tendermint.ProgramID.String(), + ICS26RouterProgramID: ics26_router.ProgramID.String(), + CosmosSignerAddress: s.CosmosUsers[0].FormattedAddress(), + SolanaFeePayer: s.SolanaUser.PublicKey().String(), + SolanaAltAddress: s.SolanaAltAddress, // Use ALT if set + MockWasmClient: s.UseMockWasmClient, + } + + config := relayer.NewConfig(relayer.CreateSolanaCosmosModules(configInfo)) err = config.GenerateConfigFile(testvalues.RelayerConfigFilePath) s.Require().NoError(err) - relayerProcess, err = relayer.StartRelayer(testvalues.RelayerConfigFilePath) + s.RelayerProcess, err = relayer.StartRelayer(testvalues.RelayerConfigFilePath) s.Require().NoError(err, "Relayer failed to start") + if s.SolanaAltAddress != "" { + s.T().Logf("Started relayer with ALT address: %s", s.SolanaAltAddress) + } + s.T().Cleanup(func() { os.Remove(testvalues.RelayerConfigFilePath) }) })) s.T().Cleanup(func() { - if relayerProcess != nil { - err := relayerProcess.Kill() + if s.RelayerProcess != nil { + err := s.RelayerProcess.Kill() if err != nil { s.T().Logf("Failed to kill the relayer process: %v", err) } @@ -139,20 +246,9 @@ func (s *IbcEurekaSolanaTestSuite) SetupSuite(ctx context.Context) { s.T().Log("Relayer client created successfully") })) - s.Require().True(s.Run("Initialize Contracts", func() { - s.Require().True(s.Run("Initialize ICS26 Router", func() { - routerStateAccount, _, err := solanago.FindProgramAddress([][]byte{[]byte("router_state")}, ics26_router.ProgramID) - s.Require().NoError(err, "Could not find router_state") - initInstruction, err := ics26_router.NewInitializeInstruction(s.SolanaUser.PublicKey(), routerStateAccount, s.SolanaUser.PublicKey(), solanago.SystemProgramID) - s.Require().NoError(err) - - tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), initInstruction) - s.Require().NoError(err) - _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, s.SolanaUser) - s.Require().NoError(err) - })) - - s.Require().True(s.Run("Create Relayer Client", func() { + // Create clients and setup IBC infrastructure + s.Require().True(s.Run("Setup IBC Clients", func() { + s.Require().True(s.Run("Create Tendermint Client on Solana", func() { var createClientTxBz []byte s.Require().True(s.Run("Retrieve create client tx from relayer", func() { resp, err := s.RelayerClient.CreateClient(context.Background(), &relayertypes.CreateClientRequest{ @@ -200,7 +296,7 @@ func (s *IbcEurekaSolanaTestSuite) SetupSuite(ctx context.Context) { })) s.Require().True(s.Run("Broadcast create client tx on Cosmos", func() { - resp := s.MustBroadcastSdkTxBody(ctx, simd, s.CosmosUsers[0], 20_000_000, createClientTxBodyBz) + resp := s.MustBroadcastSdkTxBody(ctx, simd, s.CosmosUsers[0], CosmosCreateClientGasLimit, createClientTxBodyBz) s.T().Logf("WASM client created on Cosmos: %s", resp.TxHash) })) })) @@ -208,7 +304,7 @@ func (s *IbcEurekaSolanaTestSuite) SetupSuite(ctx context.Context) { s.Require().True(s.Run("Register counterparty on Cosmos chain", func() { merklePathPrefix := [][]byte{[]byte("")} - _, err := s.BroadcastMessages(ctx, simd, s.CosmosUsers[0], 200_000, &clienttypesv2.MsgRegisterCounterparty{ + _, err := s.BroadcastMessages(ctx, simd, s.CosmosUsers[0], CosmosDefaultGasLimit, &clienttypesv2.MsgRegisterCounterparty{ ClientId: CosmosClientID, CounterpartyMerklePrefix: merklePathPrefix, CounterpartyClientId: SolanaClientID, @@ -252,64 +348,15 @@ func (s *IbcEurekaSolanaTestSuite) SetupSuite(ctx context.Context) { s.Require().NoError(err) s.T().Logf("Client added to router") })) - - s.Require().True(s.Run("Deploy and Register Dummy App", func() { - dummyAppProgramID := s.deploySolanaProgram(ctx, "dummy_ibc_app") - dummy_ibc_app.ProgramID = dummyAppProgramID - - programAvailable := s.SolanaChain.WaitForProgramAvailabilityWithTimeout(ctx, dummyAppProgramID, 120) - s.Require().True(programAvailable, "Program failed to become available within timeout") - - appStateAccount, _, err := solanago.FindProgramAddress([][]byte{[]byte("app_state"), []byte(transfertypes.PortID)}, dummyAppProgramID) - s.Require().NoError(err) - - initInstruction, err := dummy_ibc_app.NewInitializeInstruction( - s.SolanaUser.PublicKey(), - appStateAccount, - s.SolanaUser.PublicKey(), - solanago.SystemProgramID, - ) - s.Require().NoError(err) - - tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), initInstruction) - s.Require().NoError(err) - - _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, s.SolanaUser) - s.Require().NoError(err) - s.T().Logf("Dummy app initialized") - - routerStateAccount, _, err := solanago.FindProgramAddress([][]byte{[]byte("router_state")}, ics26_router.ProgramID) - s.Require().NoError(err) - - ibcAppAccount, _, err := solanago.FindProgramAddress([][]byte{[]byte("ibc_app"), []byte(transfertypes.PortID)}, ics26_router.ProgramID) - s.Require().NoError(err) - - registerInstruction, err := ics26_router.NewAddIbcAppInstruction( - transfertypes.PortID, - routerStateAccount, - ibcAppAccount, - dummyAppProgramID, - s.SolanaUser.PublicKey(), - s.SolanaUser.PublicKey(), - solanago.SystemProgramID, - ) - s.Require().NoError(err) - - tx2, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), registerInstruction) - s.Require().NoError(err) - - _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx2, s.SolanaUser) - s.Require().NoError(err) - s.T().Logf("Registered for transfer port") - - s.DummyAppProgramID = dummyAppProgramID - })) })) } // Tests func (s *IbcEurekaSolanaTestSuite) Test_Deploy() { ctx := context.Background() + + s.UseMockWasmClient = true + s.SetupSuite(ctx) simd := s.CosmosChains[0] @@ -362,6 +409,10 @@ func (s *IbcEurekaSolanaTestSuite) Test_Deploy() { func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendPacket() { ctx := context.Background() + + s.UseMockWasmClient = true + s.SetupDummyApp = true + s.SetupSuite(ctx) simd := s.CosmosChains[0] @@ -412,7 +463,6 @@ func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendPacket() { accounts.Client, ics26_router.ProgramID, solanago.SystemProgramID, - solanago.SysVarClockPubkey, accounts.RouterCaller, ) s.Require().NoError(err) @@ -531,6 +581,10 @@ func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendPacket() { func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendTransfer() { ctx := context.Background() + + s.UseMockWasmClient = true + s.SetupDummyApp = true + s.SetupSuite(ctx) simd := s.CosmosChains[0] @@ -577,7 +631,6 @@ func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendTransfer() { accounts.Client, ics26_router.ProgramID, solanago.SystemProgramID, - solanago.SysVarClockPubkey, accounts.RouterCaller, ) s.Require().NoError(err) @@ -697,6 +750,9 @@ func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendTransfer() { func (s *IbcEurekaSolanaTestSuite) Test_CosmosToSolanaTransfer() { ctx := context.Background() + s.UseMockWasmClient = true + s.SetupDummyApp = true + s.SetupSuite(ctx) simd := s.CosmosChains[0] diff --git a/e2e/interchaintestv8/testvalues/values.go b/e2e/interchaintestv8/testvalues/values.go index f75fa5436..24f35ada1 100644 --- a/e2e/interchaintestv8/testvalues/values.go +++ b/e2e/interchaintestv8/testvalues/values.go @@ -105,6 +105,13 @@ const ( SolanaChainID = "solana-localnet" // SolanaLocalnetRPC is the default RPC URL for Solana localnet. SolanaLocalnetRPC = "http://localhost:8899" + // SolanaGMPPortID is the port identifier for GMP (General Message Passing) application. + SolanaGMPPortID = "gmpport" + + // Ics27AbiEncoding is the solidity abi encoding type for the ICS27 packets. + Ics27AbiEncoding = "application/x-solidity-abi" + // Ics27ProtobufEncoding is the protobuf encoding type for ICS27 packets (used for Solana). + Ics27ProtobufEncoding = "application/x-protobuf" // Sp1GenesisFilePath is the path to the genesis file for the SP1 chain. // This file is generated and then deleted by the test. diff --git a/e2e/interchaintestv8/types/gmp/gmp.pb.go b/e2e/interchaintestv8/types/gmp/gmp.pb.go new file mode 100644 index 000000000..d39ec68d0 --- /dev/null +++ b/e2e/interchaintestv8/types/gmp/gmp.pb.go @@ -0,0 +1,236 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.7 +// protoc (unknown) +// source: gmp/gmp.proto + +package gmp + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// `GMPPacketData` is the packet data sent over IBC for General Message Passing +// This is the inner packet data that gets wrapped in the IBC packet payload +type GMPPacketData struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Source chain sender address (e.g., cosmos1...) + Sender string `protobuf:"bytes,1,opt,name=sender,proto3" json:"sender,omitempty"` + // Target program ID on destination chain (base58 Solana pubkey) + Receiver string `protobuf:"bytes,2,opt,name=receiver,proto3" json:"receiver,omitempty"` + // Salt for GMP account uniqueness (allows multiple accounts per sender) + Salt []byte `protobuf:"bytes,3,opt,name=salt,proto3" json:"salt,omitempty"` + // Protobuf-encoded execution payload + // For Solana: This contains a `SolanaInstruction` + Payload []byte `protobuf:"bytes,4,opt,name=payload,proto3" json:"payload,omitempty"` + // Optional memo field + Memo string `protobuf:"bytes,5,opt,name=memo,proto3" json:"memo,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GMPPacketData) Reset() { + *x = GMPPacketData{} + mi := &file_gmp_gmp_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GMPPacketData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GMPPacketData) ProtoMessage() {} + +func (x *GMPPacketData) ProtoReflect() protoreflect.Message { + mi := &file_gmp_gmp_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GMPPacketData.ProtoReflect.Descriptor instead. +func (*GMPPacketData) Descriptor() ([]byte, []int) { + return file_gmp_gmp_proto_rawDescGZIP(), []int{0} +} + +func (x *GMPPacketData) GetSender() string { + if x != nil { + return x.Sender + } + return "" +} + +func (x *GMPPacketData) GetReceiver() string { + if x != nil { + return x.Receiver + } + return "" +} + +func (x *GMPPacketData) GetSalt() []byte { + if x != nil { + return x.Salt + } + return nil +} + +func (x *GMPPacketData) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + +func (x *GMPPacketData) GetMemo() string { + if x != nil { + return x.Memo + } + return "" +} + +// `GMPAcknowledgement` is returned after packet execution on the destination chain +type GMPAcknowledgement struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Whether execution succeeded + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + // Result data from execution + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + // Error message if failed + Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GMPAcknowledgement) Reset() { + *x = GMPAcknowledgement{} + mi := &file_gmp_gmp_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GMPAcknowledgement) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GMPAcknowledgement) ProtoMessage() {} + +func (x *GMPAcknowledgement) ProtoReflect() protoreflect.Message { + mi := &file_gmp_gmp_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GMPAcknowledgement.ProtoReflect.Descriptor instead. +func (*GMPAcknowledgement) Descriptor() ([]byte, []int) { + return file_gmp_gmp_proto_rawDescGZIP(), []int{1} +} + +func (x *GMPAcknowledgement) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *GMPAcknowledgement) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *GMPAcknowledgement) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +var File_gmp_gmp_proto protoreflect.FileDescriptor + +const file_gmp_gmp_proto_rawDesc = "" + + "\n" + + "\rgmp/gmp.proto\x12\x03gmp\"\x85\x01\n" + + "\rGMPPacketData\x12\x16\n" + + "\x06sender\x18\x01 \x01(\tR\x06sender\x12\x1a\n" + + "\breceiver\x18\x02 \x01(\tR\breceiver\x12\x12\n" + + "\x04salt\x18\x03 \x01(\fR\x04salt\x12\x18\n" + + "\apayload\x18\x04 \x01(\fR\apayload\x12\x12\n" + + "\x04memo\x18\x05 \x01(\tR\x04memo\"X\n" + + "\x12GMPAcknowledgement\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x12\n" + + "\x04data\x18\x02 \x01(\fR\x04data\x12\x14\n" + + "\x05error\x18\x03 \x01(\tR\x05errorBJ\n" + + "\acom.gmpB\bGmpProtoP\x01Z\ttypes/gmp\xa2\x02\x03GXX\xaa\x02\x03Gmp\xca\x02\x03Gmp\xe2\x02\x0fGmp\\GPBMetadata\xea\x02\x03Gmpb\x06proto3" + +var ( + file_gmp_gmp_proto_rawDescOnce sync.Once + file_gmp_gmp_proto_rawDescData []byte +) + +func file_gmp_gmp_proto_rawDescGZIP() []byte { + file_gmp_gmp_proto_rawDescOnce.Do(func() { + file_gmp_gmp_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_gmp_gmp_proto_rawDesc), len(file_gmp_gmp_proto_rawDesc))) + }) + return file_gmp_gmp_proto_rawDescData +} + +var file_gmp_gmp_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_gmp_gmp_proto_goTypes = []any{ + (*GMPPacketData)(nil), // 0: gmp.GMPPacketData + (*GMPAcknowledgement)(nil), // 1: gmp.GMPAcknowledgement +} +var file_gmp_gmp_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_gmp_gmp_proto_init() } +func file_gmp_gmp_proto_init() { + if File_gmp_gmp_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_gmp_gmp_proto_rawDesc), len(file_gmp_gmp_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_gmp_gmp_proto_goTypes, + DependencyIndexes: file_gmp_gmp_proto_depIdxs, + MessageInfos: file_gmp_gmp_proto_msgTypes, + }.Build() + File_gmp_gmp_proto = out.File + file_gmp_gmp_proto_goTypes = nil + file_gmp_gmp_proto_depIdxs = nil +} diff --git a/e2e/interchaintestv8/types/gmphelpers/helpers.go b/e2e/interchaintestv8/types/gmphelpers/helpers.go new file mode 100644 index 000000000..3cf545b94 --- /dev/null +++ b/e2e/interchaintestv8/types/gmphelpers/helpers.go @@ -0,0 +1,32 @@ +package gmphelpers + +import ( + "github.com/cosmos/gogoproto/proto" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + + gmptypes "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" +) + +// NewPayload_FromProto creates a new payload to be submitted to cosmos through gmp. +func NewPayload_FromProto(msgs []proto.Message) ([]byte, error) { + cosmosMsgs := make([]*codectypes.Any, len(msgs)) + for i, msg := range msgs { + protoAny, err := codectypes.NewAnyWithValue(msg) + if err != nil { + return nil, err + } + + cosmosMsgs[i] = protoAny + } + + cosmosTx := gmptypes.CosmosTx{ + Messages: cosmosMsgs, + } + cosmosTxBz, err := proto.Marshal(&cosmosTx) + if err != nil { + return nil, err + } + + return cosmosTxBz, nil +} diff --git a/e2e/interchaintestv8/types/solana/solana_instruction.pb.go b/e2e/interchaintestv8/types/solana/solana_instruction.pb.go new file mode 100644 index 000000000..42b676450 --- /dev/null +++ b/e2e/interchaintestv8/types/solana/solana_instruction.pb.go @@ -0,0 +1,242 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.7 +// protoc (unknown) +// source: solana/solana_instruction.proto + +package solana + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// `SolanaInstruction` represents a Solana instruction for cross-chain execution +type SolanaInstruction struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Target Solana program ID (32 bytes) + ProgramId []byte `protobuf:"bytes,1,opt,name=program_id,json=programId,proto3" json:"program_id,omitempty"` + // ALL accounts that will be accessed during execution + Accounts []*SolanaAccountMeta `protobuf:"bytes,2,rep,name=accounts,proto3" json:"accounts,omitempty"` + // Instruction data + Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` + // Optional: Position to inject relayer payer account for rent payment (0-indexed) + // + // - If not set: No payer injection (use for programs that don't create accounts) + // - If set to N: Inject payer at index N in the final account list + // + // Example: `payer_position=2` means payer will be at index 2 in the accounts array. + // The relayer's fee payer will be inserted at this position as a signer. + PayerPosition *uint32 `protobuf:"varint,4,opt,name=payer_position,json=payerPosition,proto3,oneof" json:"payer_position,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SolanaInstruction) Reset() { + *x = SolanaInstruction{} + mi := &file_solana_solana_instruction_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SolanaInstruction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SolanaInstruction) ProtoMessage() {} + +func (x *SolanaInstruction) ProtoReflect() protoreflect.Message { + mi := &file_solana_solana_instruction_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SolanaInstruction.ProtoReflect.Descriptor instead. +func (*SolanaInstruction) Descriptor() ([]byte, []int) { + return file_solana_solana_instruction_proto_rawDescGZIP(), []int{0} +} + +func (x *SolanaInstruction) GetProgramId() []byte { + if x != nil { + return x.ProgramId + } + return nil +} + +func (x *SolanaInstruction) GetAccounts() []*SolanaAccountMeta { + if x != nil { + return x.Accounts + } + return nil +} + +func (x *SolanaInstruction) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *SolanaInstruction) GetPayerPosition() uint32 { + if x != nil && x.PayerPosition != nil { + return *x.PayerPosition + } + return 0 +} + +// `SolanaAccountMeta` represents account metadata for Solana instructions. +// +// Note: `is_signer` indicates whether the account should be a signer at the CPI instruction level. +// PDAs are marked `is_signer=true` even though they don't sign at the transaction level. +type SolanaAccountMeta struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Account public key (32 bytes) + Pubkey []byte `protobuf:"bytes,1,opt,name=pubkey,proto3" json:"pubkey,omitempty"` + // Should this account be a signer in the instruction? + // + // For PDAs: true (signs via `invoke_signed` during CPI) + // For regular accounts: false (doesn't sign) + IsSigner bool `protobuf:"varint,2,opt,name=is_signer,json=isSigner,proto3" json:"is_signer,omitempty"` + // Will this account be modified? + IsWritable bool `protobuf:"varint,3,opt,name=is_writable,json=isWritable,proto3" json:"is_writable,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SolanaAccountMeta) Reset() { + *x = SolanaAccountMeta{} + mi := &file_solana_solana_instruction_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SolanaAccountMeta) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SolanaAccountMeta) ProtoMessage() {} + +func (x *SolanaAccountMeta) ProtoReflect() protoreflect.Message { + mi := &file_solana_solana_instruction_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SolanaAccountMeta.ProtoReflect.Descriptor instead. +func (*SolanaAccountMeta) Descriptor() ([]byte, []int) { + return file_solana_solana_instruction_proto_rawDescGZIP(), []int{1} +} + +func (x *SolanaAccountMeta) GetPubkey() []byte { + if x != nil { + return x.Pubkey + } + return nil +} + +func (x *SolanaAccountMeta) GetIsSigner() bool { + if x != nil { + return x.IsSigner + } + return false +} + +func (x *SolanaAccountMeta) GetIsWritable() bool { + if x != nil { + return x.IsWritable + } + return false +} + +var File_solana_solana_instruction_proto protoreflect.FileDescriptor + +const file_solana_solana_instruction_proto_rawDesc = "" + + "\n" + + "\x1fsolana/solana_instruction.proto\x12\x06solana\"\xbc\x01\n" + + "\x11SolanaInstruction\x12\x1d\n" + + "\n" + + "program_id\x18\x01 \x01(\fR\tprogramId\x125\n" + + "\baccounts\x18\x02 \x03(\v2\x19.solana.SolanaAccountMetaR\baccounts\x12\x12\n" + + "\x04data\x18\x03 \x01(\fR\x04data\x12*\n" + + "\x0epayer_position\x18\x04 \x01(\rH\x00R\rpayerPosition\x88\x01\x01B\x11\n" + + "\x0f_payer_position\"i\n" + + "\x11SolanaAccountMeta\x12\x16\n" + + "\x06pubkey\x18\x01 \x01(\fR\x06pubkey\x12\x1b\n" + + "\tis_signer\x18\x02 \x01(\bR\bisSigner\x12\x1f\n" + + "\vis_writable\x18\x03 \x01(\bR\n" + + "isWritableBj\n" + + "\n" + + "com.solanaB\x16SolanaInstructionProtoP\x01Z\ftypes/solana\xa2\x02\x03SXX\xaa\x02\x06Solana\xca\x02\x06Solana\xe2\x02\x12Solana\\GPBMetadata\xea\x02\x06Solanab\x06proto3" + +var ( + file_solana_solana_instruction_proto_rawDescOnce sync.Once + file_solana_solana_instruction_proto_rawDescData []byte +) + +func file_solana_solana_instruction_proto_rawDescGZIP() []byte { + file_solana_solana_instruction_proto_rawDescOnce.Do(func() { + file_solana_solana_instruction_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_solana_solana_instruction_proto_rawDesc), len(file_solana_solana_instruction_proto_rawDesc))) + }) + return file_solana_solana_instruction_proto_rawDescData +} + +var file_solana_solana_instruction_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_solana_solana_instruction_proto_goTypes = []any{ + (*SolanaInstruction)(nil), // 0: solana.SolanaInstruction + (*SolanaAccountMeta)(nil), // 1: solana.SolanaAccountMeta +} +var file_solana_solana_instruction_proto_depIdxs = []int32{ + 1, // 0: solana.SolanaInstruction.accounts:type_name -> solana.SolanaAccountMeta + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_solana_solana_instruction_proto_init() } +func file_solana_solana_instruction_proto_init() { + if File_solana_solana_instruction_proto != nil { + return + } + file_solana_solana_instruction_proto_msgTypes[0].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_solana_solana_instruction_proto_rawDesc), len(file_solana_solana_instruction_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_solana_solana_instruction_proto_goTypes, + DependencyIndexes: file_solana_solana_instruction_proto_depIdxs, + MessageInfos: file_solana_solana_instruction_proto_msgTypes, + }.Build() + File_solana_solana_instruction_proto = out.File + file_solana_solana_instruction_proto_goTypes = nil + file_solana_solana_instruction_proto_depIdxs = nil +} diff --git a/flake.nix b/flake.nix index 09188bd23..d30bd733c 100644 --- a/flake.nix +++ b/flake.nix @@ -80,6 +80,9 @@ solana-agave anchor-go protobuf + buf + protoc-gen-go + protoc-gen-go-grpc just rust golangci-lint diff --git a/justfile b/justfile index 4471fdc09..6b82a4189 100644 --- a/justfile +++ b/justfile @@ -139,8 +139,11 @@ generate-solana-types: build-solana @echo "Generating SVM types..." anchor-go --idl ./programs/solana/target/idl/ics07_tendermint.json --output packages/go-anchor/ics07tendermint --no-go-mod anchor-go --idl ./programs/solana/target/idl/ics26_router.json --output packages/go-anchor/ics26router --no-go-mod + anchor-go --idl ./programs/solana/target/idl/ics27_gmp.json --output packages/go-anchor/ics27gmp --no-go-mod anchor-go --idl ./programs/solana/target/idl/dummy_ibc_app.json --output packages/go-anchor/dummyibcapp --no-go-mod anchor-go --idl ./programs/solana/target/idl/mock_light_client.json --output e2e/interchaintestv8/solana/go-anchor/mocklightclient --no-go-mod + anchor-go --idl ./programs/solana/target/idl/gmp_counter_app.json --output e2e/interchaintestv8/solana/go-anchor/gmpcounter --no-go-mod + # Generate the fixtures for the wasm tests using the e2e tests [group('generate')] generate-fixtures-wasm: clean-foundry install-relayer diff --git a/packages/go-anchor/dummyibcapp/instructions.go b/packages/go-anchor/dummyibcapp/instructions.go index 17dc374a4..c4356ee4f 100644 --- a/packages/go-anchor/dummyibcapp/instructions.go +++ b/packages/go-anchor/dummyibcapp/instructions.go @@ -230,7 +230,6 @@ func NewSendPacketInstruction( clientAccount solanago.PublicKey, routerProgramAccount solanago.PublicKey, systemProgramAccount solanago.PublicKey, - clockAccount solanago.PublicKey, routerCallerAccount solanago.PublicKey, ) (solanago.Instruction, error) { buf__ := new(bytes.Buffer) @@ -273,10 +272,7 @@ func NewSendPacketInstruction( accounts__.Append(solanago.NewAccountMeta(routerProgramAccount, false, false)) // Account 8 "system_program": Read-only, Non-signer, Required accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) - // Account 9 "clock": Read-only, Non-signer, Required, Address: SysvarC1ock11111111111111111111111111111111 - // Clock sysvar for timeout validation - accounts__.Append(solanago.NewAccountMeta(clockAccount, false, false)) - // Account 10 "router_caller": Read-only, Non-signer, Required + // Account 9 "router_caller": Read-only, Non-signer, Required // PDA that acts as the router caller for CPI calls to the IBC router. accounts__.Append(solanago.NewAccountMeta(routerCallerAccount, false, false)) } @@ -307,7 +303,6 @@ func NewSendTransferInstruction( clientAccount solanago.PublicKey, routerProgramAccount solanago.PublicKey, systemProgramAccount solanago.PublicKey, - clockAccount solanago.PublicKey, routerCallerAccount solanago.PublicKey, ) (solanago.Instruction, error) { buf__ := new(bytes.Buffer) @@ -356,10 +351,7 @@ func NewSendTransferInstruction( accounts__.Append(solanago.NewAccountMeta(routerProgramAccount, false, false)) // Account 10 "system_program": Read-only, Non-signer, Required accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) - // Account 11 "clock": Read-only, Non-signer, Required, Address: SysvarC1ock11111111111111111111111111111111 - // Clock sysvar for timeout validation - accounts__.Append(solanago.NewAccountMeta(clockAccount, false, false)) - // Account 12 "router_caller": Read-only, Non-signer, Required + // Account 11 "router_caller": Read-only, Non-signer, Required // PDA that acts as the router caller for CPI calls to the IBC router. accounts__.Append(solanago.NewAccountMeta(routerCallerAccount, false, false)) } diff --git a/packages/go-anchor/ics07tendermint/accounts.go b/packages/go-anchor/ics07tendermint/accounts.go index e84efb11c..56a7e4885 100644 --- a/packages/go-anchor/ics07tendermint/accounts.go +++ b/packages/go-anchor/ics07tendermint/accounts.go @@ -29,6 +29,13 @@ func ParseAnyAccount(accountData []byte) (any, error) { return nil, fmt.Errorf("failed to unmarshal account as ConsensusStateStore: %w", err) } return value, nil + case Account_HeaderChunk: + value := new(HeaderChunk) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal account as HeaderChunk: %w", err) + } + return value, nil default: return nil, fmt.Errorf("unknown discriminator: %s", binary.FormatDiscriminator(discriminator)) } @@ -67,3 +74,20 @@ func ParseAccount_ConsensusStateStore(accountData []byte) (*ConsensusStateStore, } return acc, nil } + +func ParseAccount_HeaderChunk(accountData []byte) (*HeaderChunk, error) { + decoder := binary.NewBorshDecoder(accountData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Account_HeaderChunk { + return nil, fmt.Errorf("expected discriminator %v, got %s", Account_HeaderChunk, binary.FormatDiscriminator(discriminator)) + } + acc := new(HeaderChunk) + err = acc.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal account of type HeaderChunk: %w", err) + } + return acc, nil +} diff --git a/packages/go-anchor/ics07tendermint/discriminators.go b/packages/go-anchor/ics07tendermint/discriminators.go index ddd016e9e..f39121373 100644 --- a/packages/go-anchor/ics07tendermint/discriminators.go +++ b/packages/go-anchor/ics07tendermint/discriminators.go @@ -7,6 +7,7 @@ package ics07_tendermint var ( Account_ClientState = [8]byte{147, 10, 249, 80, 145, 124, 219, 60} Account_ConsensusStateStore = [8]byte{82, 126, 130, 187, 68, 64, 80, 32} + Account_HeaderChunk = [8]byte{236, 15, 220, 133, 128, 6, 145, 240} ) // Event discriminators @@ -14,9 +15,12 @@ var () // Instruction discriminators var ( - Instruction_Initialize = [8]byte{175, 175, 109, 31, 13, 152, 155, 237} - Instruction_SubmitMisbehaviour = [8]byte{52, 62, 181, 78, 95, 10, 180, 150} - Instruction_UpdateClient = [8]byte{184, 89, 17, 76, 97, 57, 165, 10} - Instruction_VerifyMembership = [8]byte{101, 53, 78, 0, 103, 151, 236, 209} - Instruction_VerifyNonMembership = [8]byte{231, 161, 86, 239, 111, 236, 14, 74} + Instruction_AssembleAndUpdateClient = [8]byte{86, 215, 199, 79, 131, 79, 180, 158} + Instruction_CleanupIncompleteUpload = [8]byte{53, 54, 142, 122, 91, 50, 60, 171} + Instruction_Initialize = [8]byte{175, 175, 109, 31, 13, 152, 155, 237} + Instruction_SubmitMisbehaviour = [8]byte{52, 62, 181, 78, 95, 10, 180, 150} + Instruction_UpdateClient = [8]byte{184, 89, 17, 76, 97, 57, 165, 10} + Instruction_UploadHeaderChunk = [8]byte{154, 38, 82, 143, 56, 2, 24, 33} + Instruction_VerifyMembership = [8]byte{101, 53, 78, 0, 103, 151, 236, 209} + Instruction_VerifyNonMembership = [8]byte{231, 161, 86, 239, 111, 236, 14, 74} ) diff --git a/packages/go-anchor/ics07tendermint/instructions.go b/packages/go-anchor/ics07tendermint/instructions.go index f2eaa0d44..df47c17ba 100644 --- a/packages/go-anchor/ics07tendermint/instructions.go +++ b/packages/go-anchor/ics07tendermint/instructions.go @@ -11,6 +11,128 @@ import ( solanago "github.com/gagliardetto/solana-go" ) +// Builds a "assemble_and_update_client" instruction. +// Assemble chunks and update the client // Automatically cleans up all chunks after successful update +func NewAssembleAndUpdateClientInstruction( + // Params: + chainIdParam string, + targetHeightParam uint64, + + // Accounts: + clientStateAccount solanago.PublicKey, + trustedConsensusStateAccount solanago.PublicKey, + newConsensusStateStoreAccount solanago.PublicKey, + submitterAccount solanago.PublicKey, + payerAccount solanago.PublicKey, + systemProgramAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_AssembleAndUpdateClient[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `chainIdParam`: + err = enc__.Encode(chainIdParam) + if err != nil { + return nil, errors.NewField("chainIdParam", err) + } + // Serialize `targetHeightParam`: + err = enc__.Encode(targetHeightParam) + if err != nil { + return nil, errors.NewField("targetHeightParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "client_state": Writable, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(clientStateAccount, true, false)) + // Account 1 "trusted_consensus_state": Read-only, Non-signer, Required + // Trusted consensus state (will be validated after header assembly) + accounts__.Append(solanago.NewAccountMeta(trustedConsensusStateAccount, false, false)) + // Account 2 "new_consensus_state_store": Read-only, Non-signer, Required + // New consensus state store + accounts__.Append(solanago.NewAccountMeta(newConsensusStateStoreAccount, false, false)) + // Account 3 "submitter": Writable, Non-signer, Required + // The original submitter who paid for the chunks (receives rent back) + accounts__.Append(solanago.NewAccountMeta(submitterAccount, true, false)) + // Account 4 "payer": Writable, Signer, Required + accounts__.Append(solanago.NewAccountMeta(payerAccount, true, true)) + // Account 5 "system_program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + +// Builds a "cleanup_incomplete_upload" instruction. +// Clean up incomplete header uploads at lower heights // This can be called to reclaim rent from failed or abandoned uploads +func NewCleanupIncompleteUploadInstruction( + // Params: + chainIdParam string, + cleanupHeightParam uint64, + submitterParam solanago.PublicKey, + + // Accounts: + clientStateAccount solanago.PublicKey, + submitterAccountAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_CleanupIncompleteUpload[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `chainIdParam`: + err = enc__.Encode(chainIdParam) + if err != nil { + return nil, errors.NewField("chainIdParam", err) + } + // Serialize `cleanupHeightParam`: + err = enc__.Encode(cleanupHeightParam) + if err != nil { + return nil, errors.NewField("cleanupHeightParam", err) + } + // Serialize `submitterParam`: + err = enc__.Encode(submitterParam) + if err != nil { + return nil, errors.NewField("submitterParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "client_state": Read-only, Non-signer, Required + // Client state to verify this is a valid client + accounts__.Append(solanago.NewAccountMeta(clientStateAccount, false, false)) + // Account 1 "submitter_account": Writable, Signer, Required + // The original submitter who gets their rent back + // Must be the signer to prove they own the upload + accounts__.Append(solanago.NewAccountMeta(submitterAccountAccount, true, true)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + // Builds a "initialize" instruction. func NewInitializeInstruction( // Params: @@ -184,6 +306,58 @@ func NewUpdateClientInstruction( ), nil } +// Builds a "upload_header_chunk" instruction. +// Upload a chunk of header data for multi-transaction updates // Fails if a chunk already exists at this position (no overwrites allowed) +func NewUploadHeaderChunkInstruction( + // Params: + paramsParam UploadChunkParams, + + // Accounts: + chunkAccount solanago.PublicKey, + clientStateAccount solanago.PublicKey, + submitterAccount solanago.PublicKey, + systemProgramAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_UploadHeaderChunk[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `paramsParam`: + err = enc__.Encode(paramsParam) + if err != nil { + return nil, errors.NewField("paramsParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "chunk": Writable, Non-signer, Required + // The header chunk account to create (fails if already exists) + accounts__.Append(solanago.NewAccountMeta(chunkAccount, true, false)) + // Account 1 "client_state": Read-only, Non-signer, Required + // Client state to verify this is a valid client + accounts__.Append(solanago.NewAccountMeta(clientStateAccount, false, false)) + // Account 2 "submitter": Writable, Signer, Required + // The submitter who pays for and owns these accounts + accounts__.Append(solanago.NewAccountMeta(submitterAccount, true, true)) + // Account 3 "system_program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + // Builds a "verify_membership" instruction. func NewVerifyMembershipInstruction( // Params: diff --git a/packages/go-anchor/ics07tendermint/types.go b/packages/go-anchor/ics07tendermint/types.go index 5a355946e..e967662b4 100644 --- a/packages/go-anchor/ics07tendermint/types.go +++ b/packages/go-anchor/ics07tendermint/types.go @@ -267,6 +267,57 @@ func UnmarshalConsensusStateStore(buf []byte) (*ConsensusStateStore, error) { return obj, nil } +// Storage for a single chunk of header data during multi-transaction upload +type HeaderChunk struct { + // The chunk data + ChunkData []byte `json:"chunkData"` +} + +func (obj HeaderChunk) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `ChunkData`: + err = encoder.Encode(obj.ChunkData) + if err != nil { + return errors.NewField("ChunkData", err) + } + return nil +} + +func (obj HeaderChunk) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding HeaderChunk: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *HeaderChunk) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `ChunkData`: + err = decoder.Decode(&obj.ChunkData) + if err != nil { + return errors.NewField("ChunkData", err) + } + return nil +} + +func (obj *HeaderChunk) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling HeaderChunk: %w", err) + } + return nil +} + +func UnmarshalHeaderChunk(buf []byte) (*HeaderChunk, error) { + obj := new(HeaderChunk) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + type IbcHeight struct { RevisionNumber uint64 `json:"revisionNumber"` RevisionHeight uint64 `json:"revisionHeight"` @@ -570,3 +621,86 @@ func (value UpdateResult) String() string { return "" } } + +// Parameters for uploading a header chunk +type UploadChunkParams struct { + ChainId string `json:"chainId"` + TargetHeight uint64 `json:"targetHeight"` + ChunkIndex uint8 `json:"chunkIndex"` + ChunkData []byte `json:"chunkData"` +} + +func (obj UploadChunkParams) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `ChainId`: + err = encoder.Encode(obj.ChainId) + if err != nil { + return errors.NewField("ChainId", err) + } + // Serialize `TargetHeight`: + err = encoder.Encode(obj.TargetHeight) + if err != nil { + return errors.NewField("TargetHeight", err) + } + // Serialize `ChunkIndex`: + err = encoder.Encode(obj.ChunkIndex) + if err != nil { + return errors.NewField("ChunkIndex", err) + } + // Serialize `ChunkData`: + err = encoder.Encode(obj.ChunkData) + if err != nil { + return errors.NewField("ChunkData", err) + } + return nil +} + +func (obj UploadChunkParams) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding UploadChunkParams: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *UploadChunkParams) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `ChainId`: + err = decoder.Decode(&obj.ChainId) + if err != nil { + return errors.NewField("ChainId", err) + } + // Deserialize `TargetHeight`: + err = decoder.Decode(&obj.TargetHeight) + if err != nil { + return errors.NewField("TargetHeight", err) + } + // Deserialize `ChunkIndex`: + err = decoder.Decode(&obj.ChunkIndex) + if err != nil { + return errors.NewField("ChunkIndex", err) + } + // Deserialize `ChunkData`: + err = decoder.Decode(&obj.ChunkData) + if err != nil { + return errors.NewField("ChunkData", err) + } + return nil +} + +func (obj *UploadChunkParams) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling UploadChunkParams: %w", err) + } + return nil +} + +func UnmarshalUploadChunkParams(buf []byte) (*UploadChunkParams, error) { + obj := new(UploadChunkParams) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} diff --git a/packages/go-anchor/ics26router/accounts.go b/packages/go-anchor/ics26router/accounts.go index 01a8abd55..98f8d3d8e 100644 --- a/packages/go-anchor/ics26router/accounts.go +++ b/packages/go-anchor/ics26router/accounts.go @@ -43,6 +43,20 @@ func ParseAnyAccount(accountData []byte) (any, error) { return nil, fmt.Errorf("failed to unmarshal account as IbcApp: %w", err) } return value, nil + case Account_PayloadChunk: + value := new(PayloadChunk) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal account as PayloadChunk: %w", err) + } + return value, nil + case Account_ProofChunk: + value := new(ProofChunk) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal account as ProofChunk: %w", err) + } + return value, nil case Account_RouterState: value := new(RouterState) err := value.UnmarshalWithDecoder(decoder) @@ -123,6 +137,40 @@ func ParseAccount_IbcApp(accountData []byte) (*IbcApp, error) { return acc, nil } +func ParseAccount_PayloadChunk(accountData []byte) (*PayloadChunk, error) { + decoder := binary.NewBorshDecoder(accountData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Account_PayloadChunk { + return nil, fmt.Errorf("expected discriminator %v, got %s", Account_PayloadChunk, binary.FormatDiscriminator(discriminator)) + } + acc := new(PayloadChunk) + err = acc.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal account of type PayloadChunk: %w", err) + } + return acc, nil +} + +func ParseAccount_ProofChunk(accountData []byte) (*ProofChunk, error) { + decoder := binary.NewBorshDecoder(accountData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Account_ProofChunk { + return nil, fmt.Errorf("expected discriminator %v, got %s", Account_ProofChunk, binary.FormatDiscriminator(discriminator)) + } + acc := new(ProofChunk) + err = acc.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal account of type ProofChunk: %w", err) + } + return acc, nil +} + func ParseAccount_RouterState(accountData []byte) (*RouterState, error) { decoder := binary.NewBorshDecoder(accountData) discriminator, err := decoder.ReadDiscriminator() diff --git a/packages/go-anchor/ics26router/discriminators.go b/packages/go-anchor/ics26router/discriminators.go index 0b6095a6e..6ac135b14 100644 --- a/packages/go-anchor/ics26router/discriminators.go +++ b/packages/go-anchor/ics26router/discriminators.go @@ -9,29 +9,25 @@ var ( Account_ClientSequence = [8]byte{18, 97, 143, 135, 107, 101, 53, 226} Account_Commitment = [8]byte{61, 112, 129, 128, 24, 147, 77, 87} Account_IbcApp = [8]byte{204, 125, 117, 159, 158, 163, 105, 66} + Account_PayloadChunk = [8]byte{82, 192, 37, 21, 4, 6, 71, 89} + Account_ProofChunk = [8]byte{44, 69, 61, 168, 202, 104, 103, 207} Account_RouterState = [8]byte{141, 157, 194, 155, 75, 208, 200, 27} ) // Event discriminators -var ( - Event_AckPacketEvent = [8]byte{77, 168, 233, 72, 104, 170, 223, 187} - Event_ClientAddedEvent = [8]byte{115, 228, 28, 166, 212, 126, 80, 103} - Event_ClientStatusUpdatedEvent = [8]byte{234, 161, 185, 104, 128, 87, 102, 179} - Event_IbcAppAdded = [8]byte{203, 71, 209, 220, 197, 188, 7, 160} - Event_NoopEvent = [8]byte{59, 182, 57, 141, 7, 255, 75, 55} - Event_SendPacketEvent = [8]byte{193, 230, 168, 142, 93, 141, 211, 151} - Event_TimeoutPacketEvent = [8]byte{175, 73, 51, 208, 241, 155, 242, 254} - Event_WriteAcknowledgementEvent = [8]byte{10, 54, 186, 209, 240, 25, 50, 0} -) +var () // Instruction discriminators var ( - Instruction_AckPacket = [8]byte{43, 194, 45, 54, 2, 40, 211, 228} - Instruction_AddClient = [8]byte{97, 103, 215, 121, 86, 53, 223, 241} - Instruction_AddIbcApp = [8]byte{233, 201, 201, 149, 2, 13, 134, 27} - Instruction_Initialize = [8]byte{175, 175, 109, 31, 13, 152, 155, 237} - Instruction_RecvPacket = [8]byte{130, 14, 240, 161, 35, 63, 45, 71} - Instruction_SendPacket = [8]byte{242, 7, 23, 143, 124, 157, 42, 102} - Instruction_TimeoutPacket = [8]byte{224, 56, 82, 83, 77, 13, 120, 103} - Instruction_UpdateClient = [8]byte{184, 89, 17, 76, 97, 57, 165, 10} + Instruction_AckPacket = [8]byte{43, 194, 45, 54, 2, 40, 211, 228} + Instruction_AddClient = [8]byte{97, 103, 215, 121, 86, 53, 223, 241} + Instruction_AddIbcApp = [8]byte{233, 201, 201, 149, 2, 13, 134, 27} + Instruction_CleanupChunks = [8]byte{161, 232, 178, 127, 188, 117, 9, 18} + Instruction_Initialize = [8]byte{175, 175, 109, 31, 13, 152, 155, 237} + Instruction_RecvPacket = [8]byte{130, 14, 240, 161, 35, 63, 45, 71} + Instruction_SendPacket = [8]byte{242, 7, 23, 143, 124, 157, 42, 102} + Instruction_TimeoutPacket = [8]byte{224, 56, 82, 83, 77, 13, 120, 103} + Instruction_UpdateClient = [8]byte{184, 89, 17, 76, 97, 57, 165, 10} + Instruction_UploadPayloadChunk = [8]byte{191, 138, 167, 248, 208, 192, 24, 82} + Instruction_UploadProofChunk = [8]byte{60, 215, 88, 47, 168, 107, 123, 150} ) diff --git a/packages/go-anchor/ics26router/events.go b/packages/go-anchor/ics26router/events.go deleted file mode 100644 index bfa2309a5..000000000 --- a/packages/go-anchor/ics26router/events.go +++ /dev/null @@ -1,213 +0,0 @@ -// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. -// This file contains parsers for the events defined in the IDL. - -package ics26_router - -import ( - "fmt" - binary "github.com/gagliardetto/binary" -) - -func ParseAnyEvent(eventData []byte) (any, error) { - decoder := binary.NewBorshDecoder(eventData) - discriminator, err := decoder.ReadDiscriminator() - if err != nil { - return nil, fmt.Errorf("failed to peek event discriminator: %w", err) - } - switch discriminator { - case Event_AckPacketEvent: - value := new(AckPacketEvent) - err := value.UnmarshalWithDecoder(decoder) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal event as AckPacketEvent: %w", err) - } - return value, nil - case Event_ClientAddedEvent: - value := new(ClientAddedEvent) - err := value.UnmarshalWithDecoder(decoder) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal event as ClientAddedEvent: %w", err) - } - return value, nil - case Event_ClientStatusUpdatedEvent: - value := new(ClientStatusUpdatedEvent) - err := value.UnmarshalWithDecoder(decoder) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal event as ClientStatusUpdatedEvent: %w", err) - } - return value, nil - case Event_IbcAppAdded: - value := new(IbcAppAdded) - err := value.UnmarshalWithDecoder(decoder) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal event as IbcAppAdded: %w", err) - } - return value, nil - case Event_NoopEvent: - value := new(NoopEvent) - err := value.UnmarshalWithDecoder(decoder) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal event as NoopEvent: %w", err) - } - return value, nil - case Event_SendPacketEvent: - value := new(SendPacketEvent) - err := value.UnmarshalWithDecoder(decoder) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal event as SendPacketEvent: %w", err) - } - return value, nil - case Event_TimeoutPacketEvent: - value := new(TimeoutPacketEvent) - err := value.UnmarshalWithDecoder(decoder) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal event as TimeoutPacketEvent: %w", err) - } - return value, nil - case Event_WriteAcknowledgementEvent: - value := new(WriteAcknowledgementEvent) - err := value.UnmarshalWithDecoder(decoder) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal event as WriteAcknowledgementEvent: %w", err) - } - return value, nil - default: - return nil, fmt.Errorf("unknown discriminator: %s", binary.FormatDiscriminator(discriminator)) - } -} - -func ParseEvent_AckPacketEvent(eventData []byte) (*AckPacketEvent, error) { - decoder := binary.NewBorshDecoder(eventData) - discriminator, err := decoder.ReadDiscriminator() - if err != nil { - return nil, fmt.Errorf("failed to peek discriminator: %w", err) - } - if discriminator != Event_AckPacketEvent { - return nil, fmt.Errorf("expected discriminator %v, got %s", Event_AckPacketEvent, binary.FormatDiscriminator(discriminator)) - } - event := new(AckPacketEvent) - err = event.UnmarshalWithDecoder(decoder) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal event of type AckPacketEvent: %w", err) - } - return event, nil -} - -func ParseEvent_ClientAddedEvent(eventData []byte) (*ClientAddedEvent, error) { - decoder := binary.NewBorshDecoder(eventData) - discriminator, err := decoder.ReadDiscriminator() - if err != nil { - return nil, fmt.Errorf("failed to peek discriminator: %w", err) - } - if discriminator != Event_ClientAddedEvent { - return nil, fmt.Errorf("expected discriminator %v, got %s", Event_ClientAddedEvent, binary.FormatDiscriminator(discriminator)) - } - event := new(ClientAddedEvent) - err = event.UnmarshalWithDecoder(decoder) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal event of type ClientAddedEvent: %w", err) - } - return event, nil -} - -func ParseEvent_ClientStatusUpdatedEvent(eventData []byte) (*ClientStatusUpdatedEvent, error) { - decoder := binary.NewBorshDecoder(eventData) - discriminator, err := decoder.ReadDiscriminator() - if err != nil { - return nil, fmt.Errorf("failed to peek discriminator: %w", err) - } - if discriminator != Event_ClientStatusUpdatedEvent { - return nil, fmt.Errorf("expected discriminator %v, got %s", Event_ClientStatusUpdatedEvent, binary.FormatDiscriminator(discriminator)) - } - event := new(ClientStatusUpdatedEvent) - err = event.UnmarshalWithDecoder(decoder) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal event of type ClientStatusUpdatedEvent: %w", err) - } - return event, nil -} - -func ParseEvent_IbcAppAdded(eventData []byte) (*IbcAppAdded, error) { - decoder := binary.NewBorshDecoder(eventData) - discriminator, err := decoder.ReadDiscriminator() - if err != nil { - return nil, fmt.Errorf("failed to peek discriminator: %w", err) - } - if discriminator != Event_IbcAppAdded { - return nil, fmt.Errorf("expected discriminator %v, got %s", Event_IbcAppAdded, binary.FormatDiscriminator(discriminator)) - } - event := new(IbcAppAdded) - err = event.UnmarshalWithDecoder(decoder) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal event of type IbcAppAdded: %w", err) - } - return event, nil -} - -func ParseEvent_NoopEvent(eventData []byte) (*NoopEvent, error) { - decoder := binary.NewBorshDecoder(eventData) - discriminator, err := decoder.ReadDiscriminator() - if err != nil { - return nil, fmt.Errorf("failed to peek discriminator: %w", err) - } - if discriminator != Event_NoopEvent { - return nil, fmt.Errorf("expected discriminator %v, got %s", Event_NoopEvent, binary.FormatDiscriminator(discriminator)) - } - event := new(NoopEvent) - err = event.UnmarshalWithDecoder(decoder) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal event of type NoopEvent: %w", err) - } - return event, nil -} - -func ParseEvent_SendPacketEvent(eventData []byte) (*SendPacketEvent, error) { - decoder := binary.NewBorshDecoder(eventData) - discriminator, err := decoder.ReadDiscriminator() - if err != nil { - return nil, fmt.Errorf("failed to peek discriminator: %w", err) - } - if discriminator != Event_SendPacketEvent { - return nil, fmt.Errorf("expected discriminator %v, got %s", Event_SendPacketEvent, binary.FormatDiscriminator(discriminator)) - } - event := new(SendPacketEvent) - err = event.UnmarshalWithDecoder(decoder) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal event of type SendPacketEvent: %w", err) - } - return event, nil -} - -func ParseEvent_TimeoutPacketEvent(eventData []byte) (*TimeoutPacketEvent, error) { - decoder := binary.NewBorshDecoder(eventData) - discriminator, err := decoder.ReadDiscriminator() - if err != nil { - return nil, fmt.Errorf("failed to peek discriminator: %w", err) - } - if discriminator != Event_TimeoutPacketEvent { - return nil, fmt.Errorf("expected discriminator %v, got %s", Event_TimeoutPacketEvent, binary.FormatDiscriminator(discriminator)) - } - event := new(TimeoutPacketEvent) - err = event.UnmarshalWithDecoder(decoder) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal event of type TimeoutPacketEvent: %w", err) - } - return event, nil -} - -func ParseEvent_WriteAcknowledgementEvent(eventData []byte) (*WriteAcknowledgementEvent, error) { - decoder := binary.NewBorshDecoder(eventData) - discriminator, err := decoder.ReadDiscriminator() - if err != nil { - return nil, fmt.Errorf("failed to peek discriminator: %w", err) - } - if discriminator != Event_WriteAcknowledgementEvent { - return nil, fmt.Errorf("expected discriminator %v, got %s", Event_WriteAcknowledgementEvent, binary.FormatDiscriminator(discriminator)) - } - event := new(WriteAcknowledgementEvent) - err = event.UnmarshalWithDecoder(decoder) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal event of type WriteAcknowledgementEvent: %w", err) - } - return event, nil -} diff --git a/packages/go-anchor/ics26router/instructions.go b/packages/go-anchor/ics26router/instructions.go index 4d70e5c7f..488c3b0cf 100644 --- a/packages/go-anchor/ics26router/instructions.go +++ b/packages/go-anchor/ics26router/instructions.go @@ -63,8 +63,8 @@ func NewAckPacketInstruction( // Account 5 "router_program": Read-only, Non-signer, Required, Address: FRGF7cthWUvDvAHMUARUHFycyUK2VDUtBchmkwrz7hgx // The router program account (this program) accounts__.Append(solanago.NewAccountMeta(routerProgramAccount, false, false)) - // Account 6 "relayer": Read-only, Signer, Required - accounts__.Append(solanago.NewAccountMeta(relayerAccount, false, true)) + // Account 6 "relayer": Writable, Signer, Required + accounts__.Append(solanago.NewAccountMeta(relayerAccount, true, true)) // Account 7 "payer": Writable, Signer, Required accounts__.Append(solanago.NewAccountMeta(payerAccount, true, true)) // Account 8 "system_program": Read-only, Non-signer, Required @@ -205,6 +205,46 @@ func NewAddIbcAppInstruction( ), nil } +// Builds a "cleanup_chunks" instruction. +func NewCleanupChunksInstruction( + // Params: + msgParam MsgCleanupChunks, + + // Accounts: + relayerAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_CleanupChunks[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `msgParam`: + err = enc__.Encode(msgParam) + if err != nil { + return nil, errors.NewField("msgParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "relayer": Writable, Signer, Required + // Relayer who created the chunks and can clean them up + accounts__.Append(solanago.NewAccountMeta(relayerAccount, true, true)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + // Builds a "initialize" instruction. func NewInitializeInstruction( // Params: @@ -267,7 +307,6 @@ func NewRecvPacketInstruction( relayerAccount solanago.PublicKey, payerAccount solanago.PublicKey, systemProgramAccount solanago.PublicKey, - clockAccount solanago.PublicKey, clientAccount solanago.PublicKey, lightClientProgramAccount solanago.PublicKey, clientStateAccount solanago.PublicKey, @@ -309,21 +348,19 @@ func NewRecvPacketInstruction( // Account 7 "router_program": Read-only, Non-signer, Required, Address: FRGF7cthWUvDvAHMUARUHFycyUK2VDUtBchmkwrz7hgx // The router program account (this program) accounts__.Append(solanago.NewAccountMeta(routerProgramAccount, false, false)) - // Account 8 "relayer": Read-only, Signer, Required - accounts__.Append(solanago.NewAccountMeta(relayerAccount, false, true)) + // Account 8 "relayer": Writable, Signer, Required + accounts__.Append(solanago.NewAccountMeta(relayerAccount, true, true)) // Account 9 "payer": Writable, Signer, Required accounts__.Append(solanago.NewAccountMeta(payerAccount, true, true)) // Account 10 "system_program": Read-only, Non-signer, Required accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) - // Account 11 "clock": Read-only, Non-signer, Required, Address: SysvarC1ock11111111111111111111111111111111 - accounts__.Append(solanago.NewAccountMeta(clockAccount, false, false)) - // Account 12 "client": Read-only, Non-signer, Required + // Account 11 "client": Read-only, Non-signer, Required accounts__.Append(solanago.NewAccountMeta(clientAccount, false, false)) - // Account 13 "light_client_program": Read-only, Non-signer, Required + // Account 12 "light_client_program": Read-only, Non-signer, Required accounts__.Append(solanago.NewAccountMeta(lightClientProgramAccount, false, false)) - // Account 14 "client_state": Read-only, Non-signer, Required + // Account 13 "client_state": Read-only, Non-signer, Required accounts__.Append(solanago.NewAccountMeta(clientStateAccount, false, false)) - // Account 15 "consensus_state": Read-only, Non-signer, Required + // Account 14 "consensus_state": Read-only, Non-signer, Required accounts__.Append(solanago.NewAccountMeta(consensusStateAccount, false, false)) } @@ -348,7 +385,6 @@ func NewSendPacketInstruction( appCallerAccount solanago.PublicKey, payerAccount solanago.PublicKey, systemProgramAccount solanago.PublicKey, - clockAccount solanago.PublicKey, clientAccount solanago.PublicKey, ) (solanago.Instruction, error) { buf__ := new(bytes.Buffer) @@ -385,9 +421,7 @@ func NewSendPacketInstruction( accounts__.Append(solanago.NewAccountMeta(payerAccount, true, true)) // Account 6 "system_program": Read-only, Non-signer, Required accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) - // Account 7 "clock": Read-only, Non-signer, Required, Address: SysvarC1ock11111111111111111111111111111111 - accounts__.Append(solanago.NewAccountMeta(clockAccount, false, false)) - // Account 8 "client": Read-only, Non-signer, Required + // Account 7 "client": Read-only, Non-signer, Required accounts__.Append(solanago.NewAccountMeta(clientAccount, false, false)) } @@ -451,8 +485,8 @@ func NewTimeoutPacketInstruction( // Account 5 "router_program": Read-only, Non-signer, Required, Address: FRGF7cthWUvDvAHMUARUHFycyUK2VDUtBchmkwrz7hgx // The router program account (this program) accounts__.Append(solanago.NewAccountMeta(routerProgramAccount, false, false)) - // Account 6 "relayer": Read-only, Signer, Required - accounts__.Append(solanago.NewAccountMeta(relayerAccount, false, true)) + // Account 6 "relayer": Writable, Signer, Required + accounts__.Append(solanago.NewAccountMeta(relayerAccount, true, true)) // Account 7 "payer": Writable, Signer, Required accounts__.Append(solanago.NewAccountMeta(payerAccount, true, true)) // Account 8 "system_program": Read-only, Non-signer, Required @@ -528,3 +562,93 @@ func NewUpdateClientInstruction( buf__.Bytes(), ), nil } + +// Builds a "upload_payload_chunk" instruction. +func NewUploadPayloadChunkInstruction( + // Params: + msgParam MsgUploadChunk, + + // Accounts: + chunkAccount solanago.PublicKey, + payerAccount solanago.PublicKey, + systemProgramAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_UploadPayloadChunk[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `msgParam`: + err = enc__.Encode(msgParam) + if err != nil { + return nil, errors.NewField("msgParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "chunk": Writable, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(chunkAccount, true, false)) + // Account 1 "payer": Writable, Signer, Required + accounts__.Append(solanago.NewAccountMeta(payerAccount, true, true)) + // Account 2 "system_program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + +// Builds a "upload_proof_chunk" instruction. +func NewUploadProofChunkInstruction( + // Params: + msgParam MsgUploadChunk, + + // Accounts: + chunkAccount solanago.PublicKey, + payerAccount solanago.PublicKey, + systemProgramAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_UploadProofChunk[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `msgParam`: + err = enc__.Encode(msgParam) + if err != nil { + return nil, errors.NewField("msgParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "chunk": Writable, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(chunkAccount, true, false)) + // Account 1 "payer": Writable, Signer, Required + accounts__.Append(solanago.NewAccountMeta(payerAccount, true, true)) + // Account 2 "system_program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} diff --git a/packages/go-anchor/ics26router/types.go b/packages/go-anchor/ics26router/types.go index dea0b7597..5667cb774 100644 --- a/packages/go-anchor/ics26router/types.go +++ b/packages/go-anchor/ics26router/types.go @@ -11,88 +11,6 @@ import ( solanago "github.com/gagliardetto/solana-go" ) -type AckPacketEvent struct { - ClientId string `json:"clientId"` - Sequence uint64 `json:"sequence"` - PacketData []byte `json:"packetData"` - Acknowledgement []byte `json:"acknowledgement"` -} - -func (obj AckPacketEvent) MarshalWithEncoder(encoder *binary.Encoder) (err error) { - // Serialize `ClientId`: - err = encoder.Encode(obj.ClientId) - if err != nil { - return errors.NewField("ClientId", err) - } - // Serialize `Sequence`: - err = encoder.Encode(obj.Sequence) - if err != nil { - return errors.NewField("Sequence", err) - } - // Serialize `PacketData`: - err = encoder.Encode(obj.PacketData) - if err != nil { - return errors.NewField("PacketData", err) - } - // Serialize `Acknowledgement`: - err = encoder.Encode(obj.Acknowledgement) - if err != nil { - return errors.NewField("Acknowledgement", err) - } - return nil -} - -func (obj AckPacketEvent) Marshal() ([]byte, error) { - buf := bytes.NewBuffer(nil) - encoder := binary.NewBorshEncoder(buf) - err := obj.MarshalWithEncoder(encoder) - if err != nil { - return nil, fmt.Errorf("error while encoding AckPacketEvent: %w", err) - } - return buf.Bytes(), nil -} - -func (obj *AckPacketEvent) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { - // Deserialize `ClientId`: - err = decoder.Decode(&obj.ClientId) - if err != nil { - return errors.NewField("ClientId", err) - } - // Deserialize `Sequence`: - err = decoder.Decode(&obj.Sequence) - if err != nil { - return errors.NewField("Sequence", err) - } - // Deserialize `PacketData`: - err = decoder.Decode(&obj.PacketData) - if err != nil { - return errors.NewField("PacketData", err) - } - // Deserialize `Acknowledgement`: - err = decoder.Decode(&obj.Acknowledgement) - if err != nil { - return errors.NewField("Acknowledgement", err) - } - return nil -} - -func (obj *AckPacketEvent) Unmarshal(buf []byte) error { - err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) - if err != nil { - return fmt.Errorf("error while unmarshaling AckPacketEvent: %w", err) - } - return nil -} - -func UnmarshalAckPacketEvent(buf []byte) (*AckPacketEvent, error) { - obj := new(AckPacketEvent) - err := obj.Unmarshal(buf) - if err != nil { - return nil, err - } - return obj, nil -} - // Client mapping client IDs to light client program IDs type Client struct { // The client identifier @@ -196,77 +114,6 @@ func UnmarshalClient(buf []byte) (*Client, error) { return obj, nil } -type ClientAddedEvent struct { - ClientId string `json:"clientId"` - ClientProgramId solanago.PublicKey `json:"clientProgramId"` - Authority solanago.PublicKey `json:"authority"` -} - -func (obj ClientAddedEvent) MarshalWithEncoder(encoder *binary.Encoder) (err error) { - // Serialize `ClientId`: - err = encoder.Encode(obj.ClientId) - if err != nil { - return errors.NewField("ClientId", err) - } - // Serialize `ClientProgramId`: - err = encoder.Encode(obj.ClientProgramId) - if err != nil { - return errors.NewField("ClientProgramId", err) - } - // Serialize `Authority`: - err = encoder.Encode(obj.Authority) - if err != nil { - return errors.NewField("Authority", err) - } - return nil -} - -func (obj ClientAddedEvent) Marshal() ([]byte, error) { - buf := bytes.NewBuffer(nil) - encoder := binary.NewBorshEncoder(buf) - err := obj.MarshalWithEncoder(encoder) - if err != nil { - return nil, fmt.Errorf("error while encoding ClientAddedEvent: %w", err) - } - return buf.Bytes(), nil -} - -func (obj *ClientAddedEvent) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { - // Deserialize `ClientId`: - err = decoder.Decode(&obj.ClientId) - if err != nil { - return errors.NewField("ClientId", err) - } - // Deserialize `ClientProgramId`: - err = decoder.Decode(&obj.ClientProgramId) - if err != nil { - return errors.NewField("ClientProgramId", err) - } - // Deserialize `Authority`: - err = decoder.Decode(&obj.Authority) - if err != nil { - return errors.NewField("Authority", err) - } - return nil -} - -func (obj *ClientAddedEvent) Unmarshal(buf []byte) error { - err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) - if err != nil { - return fmt.Errorf("error while unmarshaling ClientAddedEvent: %w", err) - } - return nil -} - -func UnmarshalClientAddedEvent(buf []byte) (*ClientAddedEvent, error) { - obj := new(ClientAddedEvent) - err := obj.Unmarshal(buf) - if err != nil { - return nil, err - } - return obj, nil -} - // Client sequence tracking type ClientSequence struct { // Next sequence number for sending packets @@ -318,66 +165,6 @@ func UnmarshalClientSequence(buf []byte) (*ClientSequence, error) { return obj, nil } -type ClientStatusUpdatedEvent struct { - ClientId string `json:"clientId"` - Active bool `json:"active"` -} - -func (obj ClientStatusUpdatedEvent) MarshalWithEncoder(encoder *binary.Encoder) (err error) { - // Serialize `ClientId`: - err = encoder.Encode(obj.ClientId) - if err != nil { - return errors.NewField("ClientId", err) - } - // Serialize `Active`: - err = encoder.Encode(obj.Active) - if err != nil { - return errors.NewField("Active", err) - } - return nil -} - -func (obj ClientStatusUpdatedEvent) Marshal() ([]byte, error) { - buf := bytes.NewBuffer(nil) - encoder := binary.NewBorshEncoder(buf) - err := obj.MarshalWithEncoder(encoder) - if err != nil { - return nil, fmt.Errorf("error while encoding ClientStatusUpdatedEvent: %w", err) - } - return buf.Bytes(), nil -} - -func (obj *ClientStatusUpdatedEvent) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { - // Deserialize `ClientId`: - err = decoder.Decode(&obj.ClientId) - if err != nil { - return errors.NewField("ClientId", err) - } - // Deserialize `Active`: - err = decoder.Decode(&obj.Active) - if err != nil { - return errors.NewField("Active", err) - } - return nil -} - -func (obj *ClientStatusUpdatedEvent) Unmarshal(buf []byte) error { - err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) - if err != nil { - return fmt.Errorf("error while unmarshaling ClientStatusUpdatedEvent: %w", err) - } - return nil -} - -func UnmarshalClientStatusUpdatedEvent(buf []byte) (*ClientStatusUpdatedEvent, error) { - obj := new(ClientStatusUpdatedEvent) - err := obj.Unmarshal(buf) - if err != nil { - return nil, err - } - return obj, nil -} - // Commitment storage (simple key-value) type Commitment struct { // The commitment value (sha256 hash) @@ -570,59 +357,82 @@ func UnmarshalIbcApp(buf []byte) (*IbcApp, error) { return obj, nil } -type IbcAppAdded struct { - PortId string `json:"portId"` - AppProgramId solanago.PublicKey `json:"appProgramId"` +// Message for acknowledging a packet +type MsgAckPacket struct { + Packet Packet `json:"packet"` + Payloads []PayloadMetadata `json:"payloads"` + Acknowledgement []byte `json:"acknowledgement"` + Proof ProofMetadata `json:"proof"` } -func (obj IbcAppAdded) MarshalWithEncoder(encoder *binary.Encoder) (err error) { - // Serialize `PortId`: - err = encoder.Encode(obj.PortId) +func (obj MsgAckPacket) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `Packet`: + err = encoder.Encode(obj.Packet) if err != nil { - return errors.NewField("PortId", err) + return errors.NewField("Packet", err) } - // Serialize `AppProgramId`: - err = encoder.Encode(obj.AppProgramId) + // Serialize `Payloads`: + err = encoder.Encode(obj.Payloads) if err != nil { - return errors.NewField("AppProgramId", err) + return errors.NewField("Payloads", err) + } + // Serialize `Acknowledgement`: + err = encoder.Encode(obj.Acknowledgement) + if err != nil { + return errors.NewField("Acknowledgement", err) + } + // Serialize `Proof`: + err = encoder.Encode(obj.Proof) + if err != nil { + return errors.NewField("Proof", err) } return nil } -func (obj IbcAppAdded) Marshal() ([]byte, error) { +func (obj MsgAckPacket) Marshal() ([]byte, error) { buf := bytes.NewBuffer(nil) encoder := binary.NewBorshEncoder(buf) err := obj.MarshalWithEncoder(encoder) if err != nil { - return nil, fmt.Errorf("error while encoding IbcAppAdded: %w", err) + return nil, fmt.Errorf("error while encoding MsgAckPacket: %w", err) } return buf.Bytes(), nil } -func (obj *IbcAppAdded) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { - // Deserialize `PortId`: - err = decoder.Decode(&obj.PortId) +func (obj *MsgAckPacket) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `Packet`: + err = decoder.Decode(&obj.Packet) if err != nil { - return errors.NewField("PortId", err) + return errors.NewField("Packet", err) } - // Deserialize `AppProgramId`: - err = decoder.Decode(&obj.AppProgramId) + // Deserialize `Payloads`: + err = decoder.Decode(&obj.Payloads) if err != nil { - return errors.NewField("AppProgramId", err) + return errors.NewField("Payloads", err) + } + // Deserialize `Acknowledgement`: + err = decoder.Decode(&obj.Acknowledgement) + if err != nil { + return errors.NewField("Acknowledgement", err) + } + // Deserialize `Proof`: + err = decoder.Decode(&obj.Proof) + if err != nil { + return errors.NewField("Proof", err) } return nil } -func (obj *IbcAppAdded) Unmarshal(buf []byte) error { +func (obj *MsgAckPacket) Unmarshal(buf []byte) error { err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) if err != nil { - return fmt.Errorf("error while unmarshaling IbcAppAdded: %w", err) + return fmt.Errorf("error while unmarshaling MsgAckPacket: %w", err) } return nil } -func UnmarshalIbcAppAdded(buf []byte) (*IbcAppAdded, error) { - obj := new(IbcAppAdded) +func UnmarshalMsgAckPacket(buf []byte) (*MsgAckPacket, error) { + obj := new(MsgAckPacket) err := obj.Unmarshal(buf) if err != nil { return nil, err @@ -630,82 +440,82 @@ func UnmarshalIbcAppAdded(buf []byte) (*IbcAppAdded, error) { return obj, nil } -// Message for acknowledging a packet -type MsgAckPacket struct { - Packet Packet `json:"packet"` - Acknowledgement []byte `json:"acknowledgement"` - ProofAcked []byte `json:"proofAcked"` - ProofHeight uint64 `json:"proofHeight"` +// Message for cleanup +type MsgCleanupChunks struct { + ClientId string `json:"clientId"` + Sequence uint64 `json:"sequence"` + PayloadChunks []byte `json:"payloadChunks"` + TotalProofChunks uint8 `json:"totalProofChunks"` } -func (obj MsgAckPacket) MarshalWithEncoder(encoder *binary.Encoder) (err error) { - // Serialize `Packet`: - err = encoder.Encode(obj.Packet) +func (obj MsgCleanupChunks) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `ClientId`: + err = encoder.Encode(obj.ClientId) if err != nil { - return errors.NewField("Packet", err) + return errors.NewField("ClientId", err) } - // Serialize `Acknowledgement`: - err = encoder.Encode(obj.Acknowledgement) + // Serialize `Sequence`: + err = encoder.Encode(obj.Sequence) if err != nil { - return errors.NewField("Acknowledgement", err) + return errors.NewField("Sequence", err) } - // Serialize `ProofAcked`: - err = encoder.Encode(obj.ProofAcked) + // Serialize `PayloadChunks`: + err = encoder.Encode(obj.PayloadChunks) if err != nil { - return errors.NewField("ProofAcked", err) + return errors.NewField("PayloadChunks", err) } - // Serialize `ProofHeight`: - err = encoder.Encode(obj.ProofHeight) + // Serialize `TotalProofChunks`: + err = encoder.Encode(obj.TotalProofChunks) if err != nil { - return errors.NewField("ProofHeight", err) + return errors.NewField("TotalProofChunks", err) } return nil } -func (obj MsgAckPacket) Marshal() ([]byte, error) { +func (obj MsgCleanupChunks) Marshal() ([]byte, error) { buf := bytes.NewBuffer(nil) encoder := binary.NewBorshEncoder(buf) err := obj.MarshalWithEncoder(encoder) if err != nil { - return nil, fmt.Errorf("error while encoding MsgAckPacket: %w", err) + return nil, fmt.Errorf("error while encoding MsgCleanupChunks: %w", err) } return buf.Bytes(), nil } -func (obj *MsgAckPacket) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { - // Deserialize `Packet`: - err = decoder.Decode(&obj.Packet) +func (obj *MsgCleanupChunks) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `ClientId`: + err = decoder.Decode(&obj.ClientId) if err != nil { - return errors.NewField("Packet", err) + return errors.NewField("ClientId", err) } - // Deserialize `Acknowledgement`: - err = decoder.Decode(&obj.Acknowledgement) + // Deserialize `Sequence`: + err = decoder.Decode(&obj.Sequence) if err != nil { - return errors.NewField("Acknowledgement", err) + return errors.NewField("Sequence", err) } - // Deserialize `ProofAcked`: - err = decoder.Decode(&obj.ProofAcked) + // Deserialize `PayloadChunks`: + err = decoder.Decode(&obj.PayloadChunks) if err != nil { - return errors.NewField("ProofAcked", err) + return errors.NewField("PayloadChunks", err) } - // Deserialize `ProofHeight`: - err = decoder.Decode(&obj.ProofHeight) + // Deserialize `TotalProofChunks`: + err = decoder.Decode(&obj.TotalProofChunks) if err != nil { - return errors.NewField("ProofHeight", err) + return errors.NewField("TotalProofChunks", err) } return nil } -func (obj *MsgAckPacket) Unmarshal(buf []byte) error { +func (obj *MsgCleanupChunks) Unmarshal(buf []byte) error { err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) if err != nil { - return fmt.Errorf("error while unmarshaling MsgAckPacket: %w", err) + return fmt.Errorf("error while unmarshaling MsgCleanupChunks: %w", err) } return nil } -func UnmarshalMsgAckPacket(buf []byte) (*MsgAckPacket, error) { - obj := new(MsgAckPacket) +func UnmarshalMsgCleanupChunks(buf []byte) (*MsgCleanupChunks, error) { + obj := new(MsgCleanupChunks) err := obj.Unmarshal(buf) if err != nil { return nil, err @@ -715,9 +525,9 @@ func UnmarshalMsgAckPacket(buf []byte) (*MsgAckPacket, error) { // Message for receiving a packet type MsgRecvPacket struct { - Packet Packet `json:"packet"` - ProofCommitment []byte `json:"proofCommitment"` - ProofHeight uint64 `json:"proofHeight"` + Packet Packet `json:"packet"` + Payloads []PayloadMetadata `json:"payloads"` + Proof ProofMetadata `json:"proof"` } func (obj MsgRecvPacket) MarshalWithEncoder(encoder *binary.Encoder) (err error) { @@ -726,15 +536,15 @@ func (obj MsgRecvPacket) MarshalWithEncoder(encoder *binary.Encoder) (err error) if err != nil { return errors.NewField("Packet", err) } - // Serialize `ProofCommitment`: - err = encoder.Encode(obj.ProofCommitment) + // Serialize `Payloads`: + err = encoder.Encode(obj.Payloads) if err != nil { - return errors.NewField("ProofCommitment", err) + return errors.NewField("Payloads", err) } - // Serialize `ProofHeight`: - err = encoder.Encode(obj.ProofHeight) + // Serialize `Proof`: + err = encoder.Encode(obj.Proof) if err != nil { - return errors.NewField("ProofHeight", err) + return errors.NewField("Proof", err) } return nil } @@ -755,15 +565,15 @@ func (obj *MsgRecvPacket) UnmarshalWithDecoder(decoder *binary.Decoder) (err err if err != nil { return errors.NewField("Packet", err) } - // Deserialize `ProofCommitment`: - err = decoder.Decode(&obj.ProofCommitment) + // Deserialize `Payloads`: + err = decoder.Decode(&obj.Payloads) if err != nil { - return errors.NewField("ProofCommitment", err) + return errors.NewField("Payloads", err) } - // Deserialize `ProofHeight`: - err = decoder.Decode(&obj.ProofHeight) + // Deserialize `Proof`: + err = decoder.Decode(&obj.Proof) if err != nil { - return errors.NewField("ProofHeight", err) + return errors.NewField("Proof", err) } return nil } @@ -859,9 +669,9 @@ func UnmarshalMsgSendPacket(buf []byte) (*MsgSendPacket, error) { // Message for timing out a packet type MsgTimeoutPacket struct { - Packet Packet `json:"packet"` - ProofTimeout []byte `json:"proofTimeout"` - ProofHeight uint64 `json:"proofHeight"` + Packet Packet `json:"packet"` + Payloads []PayloadMetadata `json:"payloads"` + Proof ProofMetadata `json:"proof"` } func (obj MsgTimeoutPacket) MarshalWithEncoder(encoder *binary.Encoder) (err error) { @@ -870,15 +680,15 @@ func (obj MsgTimeoutPacket) MarshalWithEncoder(encoder *binary.Encoder) (err err if err != nil { return errors.NewField("Packet", err) } - // Serialize `ProofTimeout`: - err = encoder.Encode(obj.ProofTimeout) + // Serialize `Payloads`: + err = encoder.Encode(obj.Payloads) if err != nil { - return errors.NewField("ProofTimeout", err) + return errors.NewField("Payloads", err) } - // Serialize `ProofHeight`: - err = encoder.Encode(obj.ProofHeight) + // Serialize `Proof`: + err = encoder.Encode(obj.Proof) if err != nil { - return errors.NewField("ProofHeight", err) + return errors.NewField("Proof", err) } return nil } @@ -899,15 +709,15 @@ func (obj *MsgTimeoutPacket) UnmarshalWithDecoder(decoder *binary.Decoder) (err if err != nil { return errors.NewField("Packet", err) } - // Deserialize `ProofTimeout`: - err = decoder.Decode(&obj.ProofTimeout) + // Deserialize `Payloads`: + err = decoder.Decode(&obj.Payloads) if err != nil { - return errors.NewField("ProofTimeout", err) + return errors.NewField("Payloads", err) } - // Deserialize `ProofHeight`: - err = decoder.Decode(&obj.ProofHeight) + // Deserialize `Proof`: + err = decoder.Decode(&obj.Proof) if err != nil { - return errors.NewField("ProofHeight", err) + return errors.NewField("Proof", err) } return nil } @@ -929,36 +739,93 @@ func UnmarshalMsgTimeoutPacket(buf []byte) (*MsgTimeoutPacket, error) { return obj, nil } -type NoopEvent struct{} +// Message for uploading chunks +type MsgUploadChunk struct { + ClientId string `json:"clientId"` + Sequence uint64 `json:"sequence"` + PayloadIndex uint8 `json:"payloadIndex"` + ChunkIndex uint8 `json:"chunkIndex"` + ChunkData []byte `json:"chunkData"` +} -func (obj NoopEvent) MarshalWithEncoder(encoder *binary.Encoder) (err error) { +func (obj MsgUploadChunk) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `ClientId`: + err = encoder.Encode(obj.ClientId) + if err != nil { + return errors.NewField("ClientId", err) + } + // Serialize `Sequence`: + err = encoder.Encode(obj.Sequence) + if err != nil { + return errors.NewField("Sequence", err) + } + // Serialize `PayloadIndex`: + err = encoder.Encode(obj.PayloadIndex) + if err != nil { + return errors.NewField("PayloadIndex", err) + } + // Serialize `ChunkIndex`: + err = encoder.Encode(obj.ChunkIndex) + if err != nil { + return errors.NewField("ChunkIndex", err) + } + // Serialize `ChunkData`: + err = encoder.Encode(obj.ChunkData) + if err != nil { + return errors.NewField("ChunkData", err) + } return nil } -func (obj NoopEvent) Marshal() ([]byte, error) { +func (obj MsgUploadChunk) Marshal() ([]byte, error) { buf := bytes.NewBuffer(nil) encoder := binary.NewBorshEncoder(buf) err := obj.MarshalWithEncoder(encoder) if err != nil { - return nil, fmt.Errorf("error while encoding NoopEvent: %w", err) + return nil, fmt.Errorf("error while encoding MsgUploadChunk: %w", err) } return buf.Bytes(), nil } -func (obj *NoopEvent) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { +func (obj *MsgUploadChunk) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `ClientId`: + err = decoder.Decode(&obj.ClientId) + if err != nil { + return errors.NewField("ClientId", err) + } + // Deserialize `Sequence`: + err = decoder.Decode(&obj.Sequence) + if err != nil { + return errors.NewField("Sequence", err) + } + // Deserialize `PayloadIndex`: + err = decoder.Decode(&obj.PayloadIndex) + if err != nil { + return errors.NewField("PayloadIndex", err) + } + // Deserialize `ChunkIndex`: + err = decoder.Decode(&obj.ChunkIndex) + if err != nil { + return errors.NewField("ChunkIndex", err) + } + // Deserialize `ChunkData`: + err = decoder.Decode(&obj.ChunkData) + if err != nil { + return errors.NewField("ChunkData", err) + } return nil } -func (obj *NoopEvent) Unmarshal(buf []byte) error { +func (obj *MsgUploadChunk) Unmarshal(buf []byte) error { err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) if err != nil { - return fmt.Errorf("error while unmarshaling NoopEvent: %w", err) + return fmt.Errorf("error while unmarshaling MsgUploadChunk: %w", err) } return nil } -func UnmarshalNoopEvent(buf []byte) (*NoopEvent, error) { - obj := new(NoopEvent) +func UnmarshalMsgUploadChunk(buf []byte) (*MsgUploadChunk, error) { + obj := new(MsgUploadChunk) err := obj.Unmarshal(buf) if err != nil { return nil, err @@ -1154,51 +1021,102 @@ func UnmarshalPayload(buf []byte) (*Payload, error) { return obj, nil } -// Router state account -// TODO: Implement multi-router ACL -type RouterState struct { - // Authority that can perform restricted operations - Authority solanago.PublicKey `json:"authority"` +// Storage for payload chunks during multi-transaction upload +type PayloadChunk struct { + // Client ID this chunk belongs to + ClientId string `json:"clientId"` + + // Packet sequence number + Sequence uint64 `json:"sequence"` + + // Index of the payload this chunk belongs to (for multi-payload packets) + PayloadIndex uint8 `json:"payloadIndex"` + + // Index of this chunk (0-based) + ChunkIndex uint8 `json:"chunkIndex"` + + // The chunk data + ChunkData []byte `json:"chunkData"` } -func (obj RouterState) MarshalWithEncoder(encoder *binary.Encoder) (err error) { - // Serialize `Authority`: - err = encoder.Encode(obj.Authority) +func (obj PayloadChunk) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `ClientId`: + err = encoder.Encode(obj.ClientId) if err != nil { - return errors.NewField("Authority", err) + return errors.NewField("ClientId", err) + } + // Serialize `Sequence`: + err = encoder.Encode(obj.Sequence) + if err != nil { + return errors.NewField("Sequence", err) + } + // Serialize `PayloadIndex`: + err = encoder.Encode(obj.PayloadIndex) + if err != nil { + return errors.NewField("PayloadIndex", err) + } + // Serialize `ChunkIndex`: + err = encoder.Encode(obj.ChunkIndex) + if err != nil { + return errors.NewField("ChunkIndex", err) + } + // Serialize `ChunkData`: + err = encoder.Encode(obj.ChunkData) + if err != nil { + return errors.NewField("ChunkData", err) } return nil } -func (obj RouterState) Marshal() ([]byte, error) { +func (obj PayloadChunk) Marshal() ([]byte, error) { buf := bytes.NewBuffer(nil) encoder := binary.NewBorshEncoder(buf) err := obj.MarshalWithEncoder(encoder) if err != nil { - return nil, fmt.Errorf("error while encoding RouterState: %w", err) + return nil, fmt.Errorf("error while encoding PayloadChunk: %w", err) } return buf.Bytes(), nil } -func (obj *RouterState) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { - // Deserialize `Authority`: - err = decoder.Decode(&obj.Authority) +func (obj *PayloadChunk) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `ClientId`: + err = decoder.Decode(&obj.ClientId) if err != nil { - return errors.NewField("Authority", err) + return errors.NewField("ClientId", err) + } + // Deserialize `Sequence`: + err = decoder.Decode(&obj.Sequence) + if err != nil { + return errors.NewField("Sequence", err) + } + // Deserialize `PayloadIndex`: + err = decoder.Decode(&obj.PayloadIndex) + if err != nil { + return errors.NewField("PayloadIndex", err) + } + // Deserialize `ChunkIndex`: + err = decoder.Decode(&obj.ChunkIndex) + if err != nil { + return errors.NewField("ChunkIndex", err) + } + // Deserialize `ChunkData`: + err = decoder.Decode(&obj.ChunkData) + if err != nil { + return errors.NewField("ChunkData", err) } return nil } -func (obj *RouterState) Unmarshal(buf []byte) error { +func (obj *PayloadChunk) Unmarshal(buf []byte) error { err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) if err != nil { - return fmt.Errorf("error while unmarshaling RouterState: %w", err) + return fmt.Errorf("error while unmarshaling PayloadChunk: %w", err) } return nil } -func UnmarshalRouterState(buf []byte) (*RouterState, error) { - obj := new(RouterState) +func UnmarshalPayloadChunk(buf []byte) (*PayloadChunk, error) { + obj := new(PayloadChunk) err := obj.Unmarshal(buf) if err != nil { return nil, err @@ -1206,70 +1124,93 @@ func UnmarshalRouterState(buf []byte) (*RouterState, error) { return obj, nil } -type SendPacketEvent struct { - ClientId string `json:"clientId"` - Sequence uint64 `json:"sequence"` - PacketData []byte `json:"packetData"` +// Payload metadata for chunked operations +type PayloadMetadata struct { + SourcePort string `json:"sourcePort"` + DestPort string `json:"destPort"` + Version string `json:"version"` + Encoding string `json:"encoding"` + TotalChunks uint8 `json:"totalChunks"` } -func (obj SendPacketEvent) MarshalWithEncoder(encoder *binary.Encoder) (err error) { - // Serialize `ClientId`: - err = encoder.Encode(obj.ClientId) +func (obj PayloadMetadata) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `SourcePort`: + err = encoder.Encode(obj.SourcePort) if err != nil { - return errors.NewField("ClientId", err) + return errors.NewField("SourcePort", err) } - // Serialize `Sequence`: - err = encoder.Encode(obj.Sequence) + // Serialize `DestPort`: + err = encoder.Encode(obj.DestPort) if err != nil { - return errors.NewField("Sequence", err) + return errors.NewField("DestPort", err) + } + // Serialize `Version`: + err = encoder.Encode(obj.Version) + if err != nil { + return errors.NewField("Version", err) + } + // Serialize `Encoding`: + err = encoder.Encode(obj.Encoding) + if err != nil { + return errors.NewField("Encoding", err) } - // Serialize `PacketData`: - err = encoder.Encode(obj.PacketData) + // Serialize `TotalChunks`: + err = encoder.Encode(obj.TotalChunks) if err != nil { - return errors.NewField("PacketData", err) + return errors.NewField("TotalChunks", err) } return nil } -func (obj SendPacketEvent) Marshal() ([]byte, error) { +func (obj PayloadMetadata) Marshal() ([]byte, error) { buf := bytes.NewBuffer(nil) encoder := binary.NewBorshEncoder(buf) err := obj.MarshalWithEncoder(encoder) if err != nil { - return nil, fmt.Errorf("error while encoding SendPacketEvent: %w", err) + return nil, fmt.Errorf("error while encoding PayloadMetadata: %w", err) } return buf.Bytes(), nil } -func (obj *SendPacketEvent) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { - // Deserialize `ClientId`: - err = decoder.Decode(&obj.ClientId) +func (obj *PayloadMetadata) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `SourcePort`: + err = decoder.Decode(&obj.SourcePort) if err != nil { - return errors.NewField("ClientId", err) + return errors.NewField("SourcePort", err) } - // Deserialize `Sequence`: - err = decoder.Decode(&obj.Sequence) + // Deserialize `DestPort`: + err = decoder.Decode(&obj.DestPort) if err != nil { - return errors.NewField("Sequence", err) + return errors.NewField("DestPort", err) + } + // Deserialize `Version`: + err = decoder.Decode(&obj.Version) + if err != nil { + return errors.NewField("Version", err) + } + // Deserialize `Encoding`: + err = decoder.Decode(&obj.Encoding) + if err != nil { + return errors.NewField("Encoding", err) } - // Deserialize `PacketData`: - err = decoder.Decode(&obj.PacketData) + // Deserialize `TotalChunks`: + err = decoder.Decode(&obj.TotalChunks) if err != nil { - return errors.NewField("PacketData", err) + return errors.NewField("TotalChunks", err) } return nil } -func (obj *SendPacketEvent) Unmarshal(buf []byte) error { +func (obj *PayloadMetadata) Unmarshal(buf []byte) error { err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) if err != nil { - return fmt.Errorf("error while unmarshaling SendPacketEvent: %w", err) + return fmt.Errorf("error while unmarshaling PayloadMetadata: %w", err) } return nil } -func UnmarshalSendPacketEvent(buf []byte) (*SendPacketEvent, error) { - obj := new(SendPacketEvent) +func UnmarshalPayloadMetadata(buf []byte) (*PayloadMetadata, error) { + obj := new(PayloadMetadata) err := obj.Unmarshal(buf) if err != nil { return nil, err @@ -1277,13 +1218,22 @@ func UnmarshalSendPacketEvent(buf []byte) (*SendPacketEvent, error) { return obj, nil } -type TimeoutPacketEvent struct { - ClientId string `json:"clientId"` - Sequence uint64 `json:"sequence"` - PacketData []byte `json:"packetData"` +// Storage for proof chunks during multi-transaction upload +type ProofChunk struct { + // Client ID this chunk belongs to + ClientId string `json:"clientId"` + + // Packet sequence number + Sequence uint64 `json:"sequence"` + + // Index of this chunk (0-based) + ChunkIndex uint8 `json:"chunkIndex"` + + // The chunk data + ChunkData []byte `json:"chunkData"` } -func (obj TimeoutPacketEvent) MarshalWithEncoder(encoder *binary.Encoder) (err error) { +func (obj ProofChunk) MarshalWithEncoder(encoder *binary.Encoder) (err error) { // Serialize `ClientId`: err = encoder.Encode(obj.ClientId) if err != nil { @@ -1294,25 +1244,30 @@ func (obj TimeoutPacketEvent) MarshalWithEncoder(encoder *binary.Encoder) (err e if err != nil { return errors.NewField("Sequence", err) } - // Serialize `PacketData`: - err = encoder.Encode(obj.PacketData) + // Serialize `ChunkIndex`: + err = encoder.Encode(obj.ChunkIndex) if err != nil { - return errors.NewField("PacketData", err) + return errors.NewField("ChunkIndex", err) + } + // Serialize `ChunkData`: + err = encoder.Encode(obj.ChunkData) + if err != nil { + return errors.NewField("ChunkData", err) } return nil } -func (obj TimeoutPacketEvent) Marshal() ([]byte, error) { +func (obj ProofChunk) Marshal() ([]byte, error) { buf := bytes.NewBuffer(nil) encoder := binary.NewBorshEncoder(buf) err := obj.MarshalWithEncoder(encoder) if err != nil { - return nil, fmt.Errorf("error while encoding TimeoutPacketEvent: %w", err) + return nil, fmt.Errorf("error while encoding ProofChunk: %w", err) } return buf.Bytes(), nil } -func (obj *TimeoutPacketEvent) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { +func (obj *ProofChunk) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { // Deserialize `ClientId`: err = decoder.Decode(&obj.ClientId) if err != nil { @@ -1323,24 +1278,29 @@ func (obj *TimeoutPacketEvent) UnmarshalWithDecoder(decoder *binary.Decoder) (er if err != nil { return errors.NewField("Sequence", err) } - // Deserialize `PacketData`: - err = decoder.Decode(&obj.PacketData) + // Deserialize `ChunkIndex`: + err = decoder.Decode(&obj.ChunkIndex) + if err != nil { + return errors.NewField("ChunkIndex", err) + } + // Deserialize `ChunkData`: + err = decoder.Decode(&obj.ChunkData) if err != nil { - return errors.NewField("PacketData", err) + return errors.NewField("ChunkData", err) } return nil } -func (obj *TimeoutPacketEvent) Unmarshal(buf []byte) error { +func (obj *ProofChunk) Unmarshal(buf []byte) error { err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) if err != nil { - return fmt.Errorf("error while unmarshaling TimeoutPacketEvent: %w", err) + return fmt.Errorf("error while unmarshaling ProofChunk: %w", err) } return nil } -func UnmarshalTimeoutPacketEvent(buf []byte) (*TimeoutPacketEvent, error) { - obj := new(TimeoutPacketEvent) +func UnmarshalProofChunk(buf []byte) (*ProofChunk, error) { + obj := new(ProofChunk) err := obj.Unmarshal(buf) if err != nil { return nil, err @@ -1348,81 +1308,112 @@ func UnmarshalTimeoutPacketEvent(buf []byte) (*TimeoutPacketEvent, error) { return obj, nil } -type WriteAcknowledgementEvent struct { - ClientId string `json:"clientId"` - Sequence uint64 `json:"sequence"` - PacketData []byte `json:"packetData"` - Acknowledgements [][]byte `json:"acknowledgements"` +// Proof metadata for chunked operations +type ProofMetadata struct { + Height uint64 `json:"height"` + TotalChunks uint8 `json:"totalChunks"` } -func (obj WriteAcknowledgementEvent) MarshalWithEncoder(encoder *binary.Encoder) (err error) { - // Serialize `ClientId`: - err = encoder.Encode(obj.ClientId) +func (obj ProofMetadata) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `Height`: + err = encoder.Encode(obj.Height) if err != nil { - return errors.NewField("ClientId", err) + return errors.NewField("Height", err) } - // Serialize `Sequence`: - err = encoder.Encode(obj.Sequence) + // Serialize `TotalChunks`: + err = encoder.Encode(obj.TotalChunks) if err != nil { - return errors.NewField("Sequence", err) - } - // Serialize `PacketData`: - err = encoder.Encode(obj.PacketData) - if err != nil { - return errors.NewField("PacketData", err) - } - // Serialize `Acknowledgements`: - err = encoder.Encode(obj.Acknowledgements) - if err != nil { - return errors.NewField("Acknowledgements", err) + return errors.NewField("TotalChunks", err) } return nil } -func (obj WriteAcknowledgementEvent) Marshal() ([]byte, error) { +func (obj ProofMetadata) Marshal() ([]byte, error) { buf := bytes.NewBuffer(nil) encoder := binary.NewBorshEncoder(buf) err := obj.MarshalWithEncoder(encoder) if err != nil { - return nil, fmt.Errorf("error while encoding WriteAcknowledgementEvent: %w", err) + return nil, fmt.Errorf("error while encoding ProofMetadata: %w", err) } return buf.Bytes(), nil } -func (obj *WriteAcknowledgementEvent) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { - // Deserialize `ClientId`: - err = decoder.Decode(&obj.ClientId) +func (obj *ProofMetadata) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `Height`: + err = decoder.Decode(&obj.Height) if err != nil { - return errors.NewField("ClientId", err) + return errors.NewField("Height", err) } - // Deserialize `Sequence`: - err = decoder.Decode(&obj.Sequence) + // Deserialize `TotalChunks`: + err = decoder.Decode(&obj.TotalChunks) if err != nil { - return errors.NewField("Sequence", err) + return errors.NewField("TotalChunks", err) + } + return nil +} + +func (obj *ProofMetadata) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling ProofMetadata: %w", err) + } + return nil +} + +func UnmarshalProofMetadata(buf []byte) (*ProofMetadata, error) { + obj := new(ProofMetadata) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Router state account +// TODO: Implement multi-router ACL +type RouterState struct { + // Authority that can perform restricted operations + Authority solanago.PublicKey `json:"authority"` +} + +func (obj RouterState) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `Authority`: + err = encoder.Encode(obj.Authority) + if err != nil { + return errors.NewField("Authority", err) } - // Deserialize `PacketData`: - err = decoder.Decode(&obj.PacketData) + return nil +} + +func (obj RouterState) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) if err != nil { - return errors.NewField("PacketData", err) + return nil, fmt.Errorf("error while encoding RouterState: %w", err) } - // Deserialize `Acknowledgements`: - err = decoder.Decode(&obj.Acknowledgements) + return buf.Bytes(), nil +} + +func (obj *RouterState) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `Authority`: + err = decoder.Decode(&obj.Authority) if err != nil { - return errors.NewField("Acknowledgements", err) + return errors.NewField("Authority", err) } return nil } -func (obj *WriteAcknowledgementEvent) Unmarshal(buf []byte) error { +func (obj *RouterState) Unmarshal(buf []byte) error { err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) if err != nil { - return fmt.Errorf("error while unmarshaling WriteAcknowledgementEvent: %w", err) + return fmt.Errorf("error while unmarshaling RouterState: %w", err) } return nil } -func UnmarshalWriteAcknowledgementEvent(buf []byte) (*WriteAcknowledgementEvent, error) { - obj := new(WriteAcknowledgementEvent) +func UnmarshalRouterState(buf []byte) (*RouterState, error) { + obj := new(RouterState) err := obj.Unmarshal(buf) if err != nil { return nil, err diff --git a/packages/go-anchor/ics27gmp/accounts.go b/packages/go-anchor/ics27gmp/accounts.go new file mode 100644 index 000000000..9f72f0c58 --- /dev/null +++ b/packages/go-anchor/ics27gmp/accounts.go @@ -0,0 +1,45 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains parsers for the accounts defined in the IDL. + +package ics27_gmp + +import ( + "fmt" + binary "github.com/gagliardetto/binary" +) + +func ParseAnyAccount(accountData []byte) (any, error) { + decoder := binary.NewBorshDecoder(accountData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek account discriminator: %w", err) + } + switch discriminator { + case Account_GmpAppState: + value := new(GmpAppState) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal account as GmpAppState: %w", err) + } + return value, nil + default: + return nil, fmt.Errorf("unknown discriminator: %s", binary.FormatDiscriminator(discriminator)) + } +} + +func ParseAccount_GmpAppState(accountData []byte) (*GmpAppState, error) { + decoder := binary.NewBorshDecoder(accountData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Account_GmpAppState { + return nil, fmt.Errorf("expected discriminator %v, got %s", Account_GmpAppState, binary.FormatDiscriminator(discriminator)) + } + acc := new(GmpAppState) + err = acc.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal account of type GmpAppState: %w", err) + } + return acc, nil +} diff --git a/packages/go-anchor/ics27gmp/constants.go b/packages/go-anchor/ics27gmp/constants.go new file mode 100644 index 000000000..cf00a4847 --- /dev/null +++ b/packages/go-anchor/ics27gmp/constants.go @@ -0,0 +1,4 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains constants. + +package ics27_gmp diff --git a/packages/go-anchor/ics27gmp/discriminators.go b/packages/go-anchor/ics27gmp/discriminators.go new file mode 100644 index 000000000..b9183c5c6 --- /dev/null +++ b/packages/go-anchor/ics27gmp/discriminators.go @@ -0,0 +1,35 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains the discriminators for accounts and events defined in the IDL. + +package ics27_gmp + +// Account discriminators +var ( + Account_GmpAppState = [8]byte{133, 28, 169, 125, 62, 118, 161, 140} +) + +// Event discriminators +var ( + Event_GmpAccountCreated = [8]byte{122, 53, 243, 174, 243, 78, 190, 171} + Event_GmpAcknowledgementProcessed = [8]byte{140, 142, 176, 159, 180, 217, 50, 73} + Event_GmpAppInitialized = [8]byte{212, 236, 132, 161, 142, 153, 248, 176} + Event_GmpAppPaused = [8]byte{85, 230, 80, 71, 172, 103, 11, 186} + Event_GmpAppUnpaused = [8]byte{154, 224, 206, 7, 164, 216, 81, 181} + Event_GmpCallSent = [8]byte{97, 205, 149, 75, 198, 28, 132, 141} + Event_GmpExecutionCompleted = [8]byte{102, 200, 105, 241, 0, 238, 198, 16} + Event_GmpExecutionFailed = [8]byte{78, 147, 179, 232, 5, 38, 127, 64} + Event_GmpTimeoutProcessed = [8]byte{92, 151, 67, 120, 184, 236, 227, 78} + Event_RouterCallerCreated = [8]byte{37, 85, 180, 19, 123, 213, 153, 73} +) + +// Instruction discriminators +var ( + Instruction_Initialize = [8]byte{175, 175, 109, 31, 13, 152, 155, 237} + Instruction_OnAcknowledgementPacket = [8]byte{1, 142, 48, 169, 216, 66, 198, 31} + Instruction_OnRecvPacket = [8]byte{153, 133, 78, 48, 156, 128, 229, 104} + Instruction_OnTimeoutPacket = [8]byte{152, 10, 26, 185, 36, 193, 95, 76} + Instruction_PauseApp = [8]byte{142, 191, 211, 112, 238, 129, 131, 66} + Instruction_SendCall = [8]byte{254, 95, 190, 68, 194, 140, 28, 103} + Instruction_UnpauseApp = [8]byte{73, 253, 89, 192, 87, 42, 245, 3} + Instruction_UpdateAuthority = [8]byte{32, 46, 64, 28, 149, 75, 243, 88} +) diff --git a/packages/go-anchor/ics27gmp/doc.go b/packages/go-anchor/ics27gmp/doc.go new file mode 100644 index 000000000..5e398195c --- /dev/null +++ b/packages/go-anchor/ics27gmp/doc.go @@ -0,0 +1,7 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains documentation and example usage for the generated code. + +package ics27_gmp + +// No documentation available from the IDL. +// Please refer to the IDL source or the program documentation for more information. diff --git a/packages/go-anchor/ics27gmp/errors.go b/packages/go-anchor/ics27gmp/errors.go new file mode 100644 index 000000000..16ae9b01f --- /dev/null +++ b/packages/go-anchor/ics27gmp/errors.go @@ -0,0 +1,4 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains errors. + +package ics27_gmp diff --git a/packages/go-anchor/ics27gmp/events.go b/packages/go-anchor/ics27gmp/events.go new file mode 100644 index 000000000..a11a81b72 --- /dev/null +++ b/packages/go-anchor/ics27gmp/events.go @@ -0,0 +1,261 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains parsers for the events defined in the IDL. + +package ics27_gmp + +import ( + "fmt" + binary "github.com/gagliardetto/binary" +) + +func ParseAnyEvent(eventData []byte) (any, error) { + decoder := binary.NewBorshDecoder(eventData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek event discriminator: %w", err) + } + switch discriminator { + case Event_GmpAccountCreated: + value := new(GmpAccountCreated) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event as GmpAccountCreated: %w", err) + } + return value, nil + case Event_GmpAcknowledgementProcessed: + value := new(GmpAcknowledgementProcessed) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event as GmpAcknowledgementProcessed: %w", err) + } + return value, nil + case Event_GmpAppInitialized: + value := new(GmpAppInitialized) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event as GmpAppInitialized: %w", err) + } + return value, nil + case Event_GmpAppPaused: + value := new(GmpAppPaused) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event as GmpAppPaused: %w", err) + } + return value, nil + case Event_GmpAppUnpaused: + value := new(GmpAppUnpaused) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event as GmpAppUnpaused: %w", err) + } + return value, nil + case Event_GmpCallSent: + value := new(GmpCallSent) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event as GmpCallSent: %w", err) + } + return value, nil + case Event_GmpExecutionCompleted: + value := new(GmpExecutionCompleted) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event as GmpExecutionCompleted: %w", err) + } + return value, nil + case Event_GmpExecutionFailed: + value := new(GmpExecutionFailed) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event as GmpExecutionFailed: %w", err) + } + return value, nil + case Event_GmpTimeoutProcessed: + value := new(GmpTimeoutProcessed) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event as GmpTimeoutProcessed: %w", err) + } + return value, nil + case Event_RouterCallerCreated: + value := new(RouterCallerCreated) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event as RouterCallerCreated: %w", err) + } + return value, nil + default: + return nil, fmt.Errorf("unknown discriminator: %s", binary.FormatDiscriminator(discriminator)) + } +} + +func ParseEvent_GmpAccountCreated(eventData []byte) (*GmpAccountCreated, error) { + decoder := binary.NewBorshDecoder(eventData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Event_GmpAccountCreated { + return nil, fmt.Errorf("expected discriminator %v, got %s", Event_GmpAccountCreated, binary.FormatDiscriminator(discriminator)) + } + event := new(GmpAccountCreated) + err = event.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event of type GmpAccountCreated: %w", err) + } + return event, nil +} + +func ParseEvent_GmpAcknowledgementProcessed(eventData []byte) (*GmpAcknowledgementProcessed, error) { + decoder := binary.NewBorshDecoder(eventData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Event_GmpAcknowledgementProcessed { + return nil, fmt.Errorf("expected discriminator %v, got %s", Event_GmpAcknowledgementProcessed, binary.FormatDiscriminator(discriminator)) + } + event := new(GmpAcknowledgementProcessed) + err = event.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event of type GmpAcknowledgementProcessed: %w", err) + } + return event, nil +} + +func ParseEvent_GmpAppInitialized(eventData []byte) (*GmpAppInitialized, error) { + decoder := binary.NewBorshDecoder(eventData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Event_GmpAppInitialized { + return nil, fmt.Errorf("expected discriminator %v, got %s", Event_GmpAppInitialized, binary.FormatDiscriminator(discriminator)) + } + event := new(GmpAppInitialized) + err = event.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event of type GmpAppInitialized: %w", err) + } + return event, nil +} + +func ParseEvent_GmpAppPaused(eventData []byte) (*GmpAppPaused, error) { + decoder := binary.NewBorshDecoder(eventData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Event_GmpAppPaused { + return nil, fmt.Errorf("expected discriminator %v, got %s", Event_GmpAppPaused, binary.FormatDiscriminator(discriminator)) + } + event := new(GmpAppPaused) + err = event.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event of type GmpAppPaused: %w", err) + } + return event, nil +} + +func ParseEvent_GmpAppUnpaused(eventData []byte) (*GmpAppUnpaused, error) { + decoder := binary.NewBorshDecoder(eventData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Event_GmpAppUnpaused { + return nil, fmt.Errorf("expected discriminator %v, got %s", Event_GmpAppUnpaused, binary.FormatDiscriminator(discriminator)) + } + event := new(GmpAppUnpaused) + err = event.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event of type GmpAppUnpaused: %w", err) + } + return event, nil +} + +func ParseEvent_GmpCallSent(eventData []byte) (*GmpCallSent, error) { + decoder := binary.NewBorshDecoder(eventData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Event_GmpCallSent { + return nil, fmt.Errorf("expected discriminator %v, got %s", Event_GmpCallSent, binary.FormatDiscriminator(discriminator)) + } + event := new(GmpCallSent) + err = event.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event of type GmpCallSent: %w", err) + } + return event, nil +} + +func ParseEvent_GmpExecutionCompleted(eventData []byte) (*GmpExecutionCompleted, error) { + decoder := binary.NewBorshDecoder(eventData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Event_GmpExecutionCompleted { + return nil, fmt.Errorf("expected discriminator %v, got %s", Event_GmpExecutionCompleted, binary.FormatDiscriminator(discriminator)) + } + event := new(GmpExecutionCompleted) + err = event.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event of type GmpExecutionCompleted: %w", err) + } + return event, nil +} + +func ParseEvent_GmpExecutionFailed(eventData []byte) (*GmpExecutionFailed, error) { + decoder := binary.NewBorshDecoder(eventData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Event_GmpExecutionFailed { + return nil, fmt.Errorf("expected discriminator %v, got %s", Event_GmpExecutionFailed, binary.FormatDiscriminator(discriminator)) + } + event := new(GmpExecutionFailed) + err = event.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event of type GmpExecutionFailed: %w", err) + } + return event, nil +} + +func ParseEvent_GmpTimeoutProcessed(eventData []byte) (*GmpTimeoutProcessed, error) { + decoder := binary.NewBorshDecoder(eventData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Event_GmpTimeoutProcessed { + return nil, fmt.Errorf("expected discriminator %v, got %s", Event_GmpTimeoutProcessed, binary.FormatDiscriminator(discriminator)) + } + event := new(GmpTimeoutProcessed) + err = event.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event of type GmpTimeoutProcessed: %w", err) + } + return event, nil +} + +func ParseEvent_RouterCallerCreated(eventData []byte) (*RouterCallerCreated, error) { + decoder := binary.NewBorshDecoder(eventData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Event_RouterCallerCreated { + return nil, fmt.Errorf("expected discriminator %v, got %s", Event_RouterCallerCreated, binary.FormatDiscriminator(discriminator)) + } + event := new(RouterCallerCreated) + err = event.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event of type RouterCallerCreated: %w", err) + } + return event, nil +} diff --git a/packages/go-anchor/ics27gmp/fetchers.go b/packages/go-anchor/ics27gmp/fetchers.go new file mode 100644 index 000000000..f6ab1b20d --- /dev/null +++ b/packages/go-anchor/ics27gmp/fetchers.go @@ -0,0 +1,4 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains fetcher functions. + +package ics27_gmp diff --git a/packages/go-anchor/ics27gmp/instructions.go b/packages/go-anchor/ics27gmp/instructions.go new file mode 100644 index 000000000..bd4a88685 --- /dev/null +++ b/packages/go-anchor/ics27gmp/instructions.go @@ -0,0 +1,381 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains instructions. + +package ics27_gmp + +import ( + "bytes" + "fmt" + errors "github.com/gagliardetto/anchor-go/errors" + binary "github.com/gagliardetto/binary" + solanago "github.com/gagliardetto/solana-go" +) + +// Builds a "initialize" instruction. +// Initialize the ICS27 GMP application +func NewInitializeInstruction( + // Params: + routerProgramParam solanago.PublicKey, + + // Accounts: + appStateAccount solanago.PublicKey, + routerCallerAccount solanago.PublicKey, + payerAccount solanago.PublicKey, + authorityAccount solanago.PublicKey, + systemProgramAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_Initialize[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `routerProgramParam`: + err = enc__.Encode(routerProgramParam) + if err != nil { + return nil, errors.NewField("routerProgramParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "app_state": Writable, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(appStateAccount, true, false)) + // Account 1 "router_caller": Writable, Non-signer, Required + // Router caller PDA that represents our app to the router + accounts__.Append(solanago.NewAccountMeta(routerCallerAccount, true, false)) + // Account 2 "payer": Writable, Signer, Required + accounts__.Append(solanago.NewAccountMeta(payerAccount, true, true)) + // Account 3 "authority": Read-only, Signer, Required + accounts__.Append(solanago.NewAccountMeta(authorityAccount, false, true)) + // Account 4 "system_program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + +// Builds a "on_acknowledgement_packet" instruction. +// IBC acknowledgement handler (called by router via CPI) +func NewOnAcknowledgementPacketInstruction( + // Params: + msgParam OnAcknowledgementPacketMsg, + + // Accounts: + appStateAccount solanago.PublicKey, + routerProgramAccount solanago.PublicKey, + payerAccount solanago.PublicKey, + systemProgramAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_OnAcknowledgementPacket[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `msgParam`: + err = enc__.Encode(msgParam) + if err != nil { + return nil, errors.NewField("msgParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "app_state": Read-only, Non-signer, Required + // App state account - validated by Anchor PDA constraints + accounts__.Append(solanago.NewAccountMeta(appStateAccount, false, false)) + // Account 1 "router_program": Read-only, Non-signer, Required + // Router program calling this instruction + accounts__.Append(solanago.NewAccountMeta(routerProgramAccount, false, false)) + // Account 2 "payer": Writable, Non-signer, Required + // Relayer fee payer (passed by router but not used in acknowledgement handler) + accounts__.Append(solanago.NewAccountMeta(payerAccount, true, false)) + // Account 3 "system_program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + +// Builds a "on_recv_packet" instruction. +// IBC packet receive handler (called by router via CPI) +func NewOnRecvPacketInstruction( + // Params: + msgParam OnRecvPacketMsg, + + // Accounts: + appStateAccount solanago.PublicKey, + routerProgramAccount solanago.PublicKey, + payerAccount solanago.PublicKey, + systemProgramAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_OnRecvPacket[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `msgParam`: + err = enc__.Encode(msgParam) + if err != nil { + return nil, errors.NewField("msgParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "app_state": Writable, Non-signer, Required + // App state account - validated by Anchor PDA constraints + accounts__.Append(solanago.NewAccountMeta(appStateAccount, true, false)) + // Account 1 "router_program": Read-only, Non-signer, Required + // Router program calling this instruction + accounts__.Append(solanago.NewAccountMeta(routerProgramAccount, false, false)) + // Account 2 "payer": Writable, Non-signer, Required + // Relayer fee payer - used for account creation rent + // NOTE: This cannot be the GMP account PDA because PDAs with data cannot + // be used as payers in System Program transfers. The relayer's fee payer + // is used for rent, while the GMP account PDA signs via `invoke_signed`. + accounts__.Append(solanago.NewAccountMeta(payerAccount, true, false)) + // Account 3 "system_program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + +// Builds a "on_timeout_packet" instruction. +// IBC timeout handler (called by router via CPI) +func NewOnTimeoutPacketInstruction( + // Params: + msgParam OnTimeoutPacketMsg, + + // Accounts: + appStateAccount solanago.PublicKey, + routerProgramAccount solanago.PublicKey, + payerAccount solanago.PublicKey, + systemProgramAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_OnTimeoutPacket[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `msgParam`: + err = enc__.Encode(msgParam) + if err != nil { + return nil, errors.NewField("msgParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "app_state": Read-only, Non-signer, Required + // App state account - validated by Anchor PDA constraints + accounts__.Append(solanago.NewAccountMeta(appStateAccount, false, false)) + // Account 1 "router_program": Read-only, Non-signer, Required + // Router program calling this instruction + accounts__.Append(solanago.NewAccountMeta(routerProgramAccount, false, false)) + // Account 2 "payer": Writable, Non-signer, Required + // Relayer fee payer (passed by router but not used in timeout handler) + accounts__.Append(solanago.NewAccountMeta(payerAccount, true, false)) + // Account 3 "system_program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + +// Builds a "pause_app" instruction. +// Pause the entire GMP app (admin only) +func NewPauseAppInstruction( + appStateAccount solanago.PublicKey, + authorityAccount solanago.PublicKey, +) (solanago.Instruction, error) { + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "app_state": Writable, Non-signer, Required + // App state account - validated by Anchor PDA constraints + accounts__.Append(solanago.NewAccountMeta(appStateAccount, true, false)) + // Account 1 "authority": Read-only, Signer, Required + accounts__.Append(solanago.NewAccountMeta(authorityAccount, false, true)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + nil, + ), nil +} + +// Builds a "send_call" instruction. +// Send a GMP call packet +func NewSendCallInstruction( + // Params: + msgParam SendCallMsg, + + // Accounts: + appStateAccount solanago.PublicKey, + senderAccount solanago.PublicKey, + payerAccount solanago.PublicKey, + routerProgramAccount solanago.PublicKey, + routerStateAccount solanago.PublicKey, + clientSequenceAccount solanago.PublicKey, + packetCommitmentAccount solanago.PublicKey, + routerCallerAccount solanago.PublicKey, + ibcAppAccount solanago.PublicKey, + clientAccount solanago.PublicKey, + systemProgramAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_SendCall[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `msgParam`: + err = enc__.Encode(msgParam) + if err != nil { + return nil, errors.NewField("msgParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "app_state": Writable, Non-signer, Required + // App state account - validated by Anchor PDA constraints + accounts__.Append(solanago.NewAccountMeta(appStateAccount, true, false)) + // Account 1 "sender": Read-only, Signer, Required + // Sender of the call + accounts__.Append(solanago.NewAccountMeta(senderAccount, false, true)) + // Account 2 "payer": Writable, Signer, Required + accounts__.Append(solanago.NewAccountMeta(payerAccount, true, true)) + // Account 3 "router_program": Read-only, Non-signer, Required + // Router program for sending packets + accounts__.Append(solanago.NewAccountMeta(routerProgramAccount, false, false)) + // Account 4 "router_state": Read-only, Non-signer, Required + // Router state account + accounts__.Append(solanago.NewAccountMeta(routerStateAccount, false, false)) + // Account 5 "client_sequence": Writable, Non-signer, Required + // Client sequence account for packet sequencing + accounts__.Append(solanago.NewAccountMeta(clientSequenceAccount, true, false)) + // Account 6 "packet_commitment": Writable, Non-signer, Required + // Packet commitment account to be created + accounts__.Append(solanago.NewAccountMeta(packetCommitmentAccount, true, false)) + // Account 7 "router_caller": Read-only, Non-signer, Required + // Router caller PDA that represents our app + accounts__.Append(solanago.NewAccountMeta(routerCallerAccount, false, false)) + // Account 8 "ibc_app": Read-only, Non-signer, Required + // IBC app registration account + accounts__.Append(solanago.NewAccountMeta(ibcAppAccount, false, false)) + // Account 9 "client": Read-only, Non-signer, Required + // Client account + accounts__.Append(solanago.NewAccountMeta(clientAccount, false, false)) + // Account 10 "system_program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + +// Builds a "unpause_app" instruction. +// Unpause the entire GMP app (admin only) +func NewUnpauseAppInstruction( + appStateAccount solanago.PublicKey, + authorityAccount solanago.PublicKey, +) (solanago.Instruction, error) { + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "app_state": Writable, Non-signer, Required + // App state account - validated by Anchor PDA constraints + accounts__.Append(solanago.NewAccountMeta(appStateAccount, true, false)) + // Account 1 "authority": Read-only, Signer, Required + accounts__.Append(solanago.NewAccountMeta(authorityAccount, false, true)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + nil, + ), nil +} + +// Builds a "update_authority" instruction. +// Update app authority (admin only) +func NewUpdateAuthorityInstruction( + appStateAccount solanago.PublicKey, + currentAuthorityAccount solanago.PublicKey, + newAuthorityAccount solanago.PublicKey, +) (solanago.Instruction, error) { + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "app_state": Writable, Non-signer, Required + // App state account - validated by Anchor PDA constraints + accounts__.Append(solanago.NewAccountMeta(appStateAccount, true, false)) + // Account 1 "current_authority": Read-only, Signer, Required + accounts__.Append(solanago.NewAccountMeta(currentAuthorityAccount, false, true)) + // Account 2 "new_authority": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(newAuthorityAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + nil, + ), nil +} diff --git a/packages/go-anchor/ics27gmp/program-id.go b/packages/go-anchor/ics27gmp/program-id.go new file mode 100644 index 000000000..ca65b3149 --- /dev/null +++ b/packages/go-anchor/ics27gmp/program-id.go @@ -0,0 +1,8 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains the program ID. + +package ics27_gmp + +import solanago "github.com/gagliardetto/solana-go" + +var ProgramID = solanago.MustPublicKeyFromBase58("3W3h4WSE8J9vFzVN8TGFGc9Uchbry3M4MBz4icdSWcFi") diff --git a/packages/go-anchor/ics27gmp/tests_test.go b/packages/go-anchor/ics27gmp/tests_test.go new file mode 100644 index 000000000..ee7deb873 --- /dev/null +++ b/packages/go-anchor/ics27gmp/tests_test.go @@ -0,0 +1,4 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains tests. + +package ics27_gmp diff --git a/packages/go-anchor/ics27gmp/types.go b/packages/go-anchor/ics27gmp/types.go new file mode 100644 index 000000000..a6fe5e3ff --- /dev/null +++ b/packages/go-anchor/ics27gmp/types.go @@ -0,0 +1,1560 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains parsers for the types defined in the IDL. + +package ics27_gmp + +import ( + "bytes" + "fmt" + errors "github.com/gagliardetto/anchor-go/errors" + binary "github.com/gagliardetto/binary" + solanago "github.com/gagliardetto/solana-go" +) + +// Event emitted when a new account is created +type GmpAccountCreated struct { + // Account address (PDA) + Account solanago.PublicKey `json:"account"` + + // Client ID + ClientId string `json:"clientId"` + + // Original sender + Sender string `json:"sender"` + + // Salt used for derivation + Salt []byte `json:"salt"` + + // Creation timestamp + CreatedAt int64 `json:"createdAt"` +} + +func (obj GmpAccountCreated) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `Account`: + err = encoder.Encode(obj.Account) + if err != nil { + return errors.NewField("Account", err) + } + // Serialize `ClientId`: + err = encoder.Encode(obj.ClientId) + if err != nil { + return errors.NewField("ClientId", err) + } + // Serialize `Sender`: + err = encoder.Encode(obj.Sender) + if err != nil { + return errors.NewField("Sender", err) + } + // Serialize `Salt`: + err = encoder.Encode(obj.Salt) + if err != nil { + return errors.NewField("Salt", err) + } + // Serialize `CreatedAt`: + err = encoder.Encode(obj.CreatedAt) + if err != nil { + return errors.NewField("CreatedAt", err) + } + return nil +} + +func (obj GmpAccountCreated) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding GmpAccountCreated: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *GmpAccountCreated) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `Account`: + err = decoder.Decode(&obj.Account) + if err != nil { + return errors.NewField("Account", err) + } + // Deserialize `ClientId`: + err = decoder.Decode(&obj.ClientId) + if err != nil { + return errors.NewField("ClientId", err) + } + // Deserialize `Sender`: + err = decoder.Decode(&obj.Sender) + if err != nil { + return errors.NewField("Sender", err) + } + // Deserialize `Salt`: + err = decoder.Decode(&obj.Salt) + if err != nil { + return errors.NewField("Salt", err) + } + // Deserialize `CreatedAt`: + err = decoder.Decode(&obj.CreatedAt) + if err != nil { + return errors.NewField("CreatedAt", err) + } + return nil +} + +func (obj *GmpAccountCreated) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling GmpAccountCreated: %w", err) + } + return nil +} + +func UnmarshalGmpAccountCreated(buf []byte) (*GmpAccountCreated, error) { + obj := new(GmpAccountCreated) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Event emitted when packet acknowledgement is processed +type GmpAcknowledgementProcessed struct { + // Original sender + Sender solanago.PublicKey `json:"sender"` + + // Packet sequence + Sequence uint64 `json:"sequence"` + + // Whether acknowledgement indicates success + AckSuccess bool `json:"ackSuccess"` + + // Processing timestamp + Timestamp int64 `json:"timestamp"` +} + +func (obj GmpAcknowledgementProcessed) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `Sender`: + err = encoder.Encode(obj.Sender) + if err != nil { + return errors.NewField("Sender", err) + } + // Serialize `Sequence`: + err = encoder.Encode(obj.Sequence) + if err != nil { + return errors.NewField("Sequence", err) + } + // Serialize `AckSuccess`: + err = encoder.Encode(obj.AckSuccess) + if err != nil { + return errors.NewField("AckSuccess", err) + } + // Serialize `Timestamp`: + err = encoder.Encode(obj.Timestamp) + if err != nil { + return errors.NewField("Timestamp", err) + } + return nil +} + +func (obj GmpAcknowledgementProcessed) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding GmpAcknowledgementProcessed: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *GmpAcknowledgementProcessed) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `Sender`: + err = decoder.Decode(&obj.Sender) + if err != nil { + return errors.NewField("Sender", err) + } + // Deserialize `Sequence`: + err = decoder.Decode(&obj.Sequence) + if err != nil { + return errors.NewField("Sequence", err) + } + // Deserialize `AckSuccess`: + err = decoder.Decode(&obj.AckSuccess) + if err != nil { + return errors.NewField("AckSuccess", err) + } + // Deserialize `Timestamp`: + err = decoder.Decode(&obj.Timestamp) + if err != nil { + return errors.NewField("Timestamp", err) + } + return nil +} + +func (obj *GmpAcknowledgementProcessed) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling GmpAcknowledgementProcessed: %w", err) + } + return nil +} + +func UnmarshalGmpAcknowledgementProcessed(buf []byte) (*GmpAcknowledgementProcessed, error) { + obj := new(GmpAcknowledgementProcessed) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Event emitted when GMP app is initialized +type GmpAppInitialized struct { + // Router program managing this app + RouterProgram solanago.PublicKey `json:"routerProgram"` + + // Administrative authority + Authority solanago.PublicKey `json:"authority"` + + // Port ID bound to this app + PortId string `json:"portId"` + + // App initialization timestamp + Timestamp int64 `json:"timestamp"` +} + +func (obj GmpAppInitialized) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `RouterProgram`: + err = encoder.Encode(obj.RouterProgram) + if err != nil { + return errors.NewField("RouterProgram", err) + } + // Serialize `Authority`: + err = encoder.Encode(obj.Authority) + if err != nil { + return errors.NewField("Authority", err) + } + // Serialize `PortId`: + err = encoder.Encode(obj.PortId) + if err != nil { + return errors.NewField("PortId", err) + } + // Serialize `Timestamp`: + err = encoder.Encode(obj.Timestamp) + if err != nil { + return errors.NewField("Timestamp", err) + } + return nil +} + +func (obj GmpAppInitialized) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding GmpAppInitialized: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *GmpAppInitialized) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `RouterProgram`: + err = decoder.Decode(&obj.RouterProgram) + if err != nil { + return errors.NewField("RouterProgram", err) + } + // Deserialize `Authority`: + err = decoder.Decode(&obj.Authority) + if err != nil { + return errors.NewField("Authority", err) + } + // Deserialize `PortId`: + err = decoder.Decode(&obj.PortId) + if err != nil { + return errors.NewField("PortId", err) + } + // Deserialize `Timestamp`: + err = decoder.Decode(&obj.Timestamp) + if err != nil { + return errors.NewField("Timestamp", err) + } + return nil +} + +func (obj *GmpAppInitialized) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling GmpAppInitialized: %w", err) + } + return nil +} + +func UnmarshalGmpAppInitialized(buf []byte) (*GmpAppInitialized, error) { + obj := new(GmpAppInitialized) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Event emitted when app is paused +type GmpAppPaused struct { + // Admin who paused the app + Admin solanago.PublicKey `json:"admin"` + + // Pause timestamp + Timestamp int64 `json:"timestamp"` +} + +func (obj GmpAppPaused) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `Admin`: + err = encoder.Encode(obj.Admin) + if err != nil { + return errors.NewField("Admin", err) + } + // Serialize `Timestamp`: + err = encoder.Encode(obj.Timestamp) + if err != nil { + return errors.NewField("Timestamp", err) + } + return nil +} + +func (obj GmpAppPaused) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding GmpAppPaused: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *GmpAppPaused) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `Admin`: + err = decoder.Decode(&obj.Admin) + if err != nil { + return errors.NewField("Admin", err) + } + // Deserialize `Timestamp`: + err = decoder.Decode(&obj.Timestamp) + if err != nil { + return errors.NewField("Timestamp", err) + } + return nil +} + +func (obj *GmpAppPaused) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling GmpAppPaused: %w", err) + } + return nil +} + +func UnmarshalGmpAppPaused(buf []byte) (*GmpAppPaused, error) { + obj := new(GmpAppPaused) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Main GMP application state +type GmpAppState struct { + // ICS26 Router program that manages this app + RouterProgram solanago.PublicKey `json:"routerProgram"` + + // Administrative authority + Authority solanago.PublicKey `json:"authority"` + + // Program version for upgrades + Version uint8 `json:"version"` + + // Emergency pause flag + Paused bool `json:"paused"` + + // PDA bump seed + Bump uint8 `json:"bump"` +} + +func (obj GmpAppState) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `RouterProgram`: + err = encoder.Encode(obj.RouterProgram) + if err != nil { + return errors.NewField("RouterProgram", err) + } + // Serialize `Authority`: + err = encoder.Encode(obj.Authority) + if err != nil { + return errors.NewField("Authority", err) + } + // Serialize `Version`: + err = encoder.Encode(obj.Version) + if err != nil { + return errors.NewField("Version", err) + } + // Serialize `Paused`: + err = encoder.Encode(obj.Paused) + if err != nil { + return errors.NewField("Paused", err) + } + // Serialize `Bump`: + err = encoder.Encode(obj.Bump) + if err != nil { + return errors.NewField("Bump", err) + } + return nil +} + +func (obj GmpAppState) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding GmpAppState: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *GmpAppState) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `RouterProgram`: + err = decoder.Decode(&obj.RouterProgram) + if err != nil { + return errors.NewField("RouterProgram", err) + } + // Deserialize `Authority`: + err = decoder.Decode(&obj.Authority) + if err != nil { + return errors.NewField("Authority", err) + } + // Deserialize `Version`: + err = decoder.Decode(&obj.Version) + if err != nil { + return errors.NewField("Version", err) + } + // Deserialize `Paused`: + err = decoder.Decode(&obj.Paused) + if err != nil { + return errors.NewField("Paused", err) + } + // Deserialize `Bump`: + err = decoder.Decode(&obj.Bump) + if err != nil { + return errors.NewField("Bump", err) + } + return nil +} + +func (obj *GmpAppState) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling GmpAppState: %w", err) + } + return nil +} + +func UnmarshalGmpAppState(buf []byte) (*GmpAppState, error) { + obj := new(GmpAppState) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Event emitted when app is unpaused +type GmpAppUnpaused struct { + // Admin who unpaused the app + Admin solanago.PublicKey `json:"admin"` + + // Unpause timestamp + Timestamp int64 `json:"timestamp"` +} + +func (obj GmpAppUnpaused) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `Admin`: + err = encoder.Encode(obj.Admin) + if err != nil { + return errors.NewField("Admin", err) + } + // Serialize `Timestamp`: + err = encoder.Encode(obj.Timestamp) + if err != nil { + return errors.NewField("Timestamp", err) + } + return nil +} + +func (obj GmpAppUnpaused) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding GmpAppUnpaused: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *GmpAppUnpaused) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `Admin`: + err = decoder.Decode(&obj.Admin) + if err != nil { + return errors.NewField("Admin", err) + } + // Deserialize `Timestamp`: + err = decoder.Decode(&obj.Timestamp) + if err != nil { + return errors.NewField("Timestamp", err) + } + return nil +} + +func (obj *GmpAppUnpaused) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling GmpAppUnpaused: %w", err) + } + return nil +} + +func UnmarshalGmpAppUnpaused(buf []byte) (*GmpAppUnpaused, error) { + obj := new(GmpAppUnpaused) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Event emitted when a GMP call is sent +type GmpCallSent struct { + // Packet sequence number + Sequence uint64 `json:"sequence"` + + // Sender of the call + Sender solanago.PublicKey `json:"sender"` + + // Target program to execute + Receiver solanago.PublicKey `json:"receiver"` + + // Source client ID + ClientId string `json:"clientId"` + + // Account salt used + Salt []byte `json:"salt"` + + // Payload size + PayloadSize uint64 `json:"payloadSize"` + + // Timeout timestamp + TimeoutTimestamp int64 `json:"timeoutTimestamp"` +} + +func (obj GmpCallSent) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `Sequence`: + err = encoder.Encode(obj.Sequence) + if err != nil { + return errors.NewField("Sequence", err) + } + // Serialize `Sender`: + err = encoder.Encode(obj.Sender) + if err != nil { + return errors.NewField("Sender", err) + } + // Serialize `Receiver`: + err = encoder.Encode(obj.Receiver) + if err != nil { + return errors.NewField("Receiver", err) + } + // Serialize `ClientId`: + err = encoder.Encode(obj.ClientId) + if err != nil { + return errors.NewField("ClientId", err) + } + // Serialize `Salt`: + err = encoder.Encode(obj.Salt) + if err != nil { + return errors.NewField("Salt", err) + } + // Serialize `PayloadSize`: + err = encoder.Encode(obj.PayloadSize) + if err != nil { + return errors.NewField("PayloadSize", err) + } + // Serialize `TimeoutTimestamp`: + err = encoder.Encode(obj.TimeoutTimestamp) + if err != nil { + return errors.NewField("TimeoutTimestamp", err) + } + return nil +} + +func (obj GmpCallSent) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding GmpCallSent: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *GmpCallSent) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `Sequence`: + err = decoder.Decode(&obj.Sequence) + if err != nil { + return errors.NewField("Sequence", err) + } + // Deserialize `Sender`: + err = decoder.Decode(&obj.Sender) + if err != nil { + return errors.NewField("Sender", err) + } + // Deserialize `Receiver`: + err = decoder.Decode(&obj.Receiver) + if err != nil { + return errors.NewField("Receiver", err) + } + // Deserialize `ClientId`: + err = decoder.Decode(&obj.ClientId) + if err != nil { + return errors.NewField("ClientId", err) + } + // Deserialize `Salt`: + err = decoder.Decode(&obj.Salt) + if err != nil { + return errors.NewField("Salt", err) + } + // Deserialize `PayloadSize`: + err = decoder.Decode(&obj.PayloadSize) + if err != nil { + return errors.NewField("PayloadSize", err) + } + // Deserialize `TimeoutTimestamp`: + err = decoder.Decode(&obj.TimeoutTimestamp) + if err != nil { + return errors.NewField("TimeoutTimestamp", err) + } + return nil +} + +func (obj *GmpCallSent) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling GmpCallSent: %w", err) + } + return nil +} + +func UnmarshalGmpCallSent(buf []byte) (*GmpCallSent, error) { + obj := new(GmpCallSent) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Event emitted when a packet is received and executed +type GmpExecutionCompleted struct { + // Account that executed the call + Account solanago.PublicKey `json:"account"` + + // Target program that was called + TargetProgram solanago.PublicKey `json:"targetProgram"` + + // Client ID + ClientId string `json:"clientId"` + + // Original sender + Sender string `json:"sender"` + + // Account nonce after execution + Nonce uint64 `json:"nonce"` + + // Whether execution succeeded + Success bool `json:"success"` + + // Result data size + ResultSize uint64 `json:"resultSize"` + + // Execution timestamp + Timestamp int64 `json:"timestamp"` +} + +func (obj GmpExecutionCompleted) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `Account`: + err = encoder.Encode(obj.Account) + if err != nil { + return errors.NewField("Account", err) + } + // Serialize `TargetProgram`: + err = encoder.Encode(obj.TargetProgram) + if err != nil { + return errors.NewField("TargetProgram", err) + } + // Serialize `ClientId`: + err = encoder.Encode(obj.ClientId) + if err != nil { + return errors.NewField("ClientId", err) + } + // Serialize `Sender`: + err = encoder.Encode(obj.Sender) + if err != nil { + return errors.NewField("Sender", err) + } + // Serialize `Nonce`: + err = encoder.Encode(obj.Nonce) + if err != nil { + return errors.NewField("Nonce", err) + } + // Serialize `Success`: + err = encoder.Encode(obj.Success) + if err != nil { + return errors.NewField("Success", err) + } + // Serialize `ResultSize`: + err = encoder.Encode(obj.ResultSize) + if err != nil { + return errors.NewField("ResultSize", err) + } + // Serialize `Timestamp`: + err = encoder.Encode(obj.Timestamp) + if err != nil { + return errors.NewField("Timestamp", err) + } + return nil +} + +func (obj GmpExecutionCompleted) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding GmpExecutionCompleted: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *GmpExecutionCompleted) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `Account`: + err = decoder.Decode(&obj.Account) + if err != nil { + return errors.NewField("Account", err) + } + // Deserialize `TargetProgram`: + err = decoder.Decode(&obj.TargetProgram) + if err != nil { + return errors.NewField("TargetProgram", err) + } + // Deserialize `ClientId`: + err = decoder.Decode(&obj.ClientId) + if err != nil { + return errors.NewField("ClientId", err) + } + // Deserialize `Sender`: + err = decoder.Decode(&obj.Sender) + if err != nil { + return errors.NewField("Sender", err) + } + // Deserialize `Nonce`: + err = decoder.Decode(&obj.Nonce) + if err != nil { + return errors.NewField("Nonce", err) + } + // Deserialize `Success`: + err = decoder.Decode(&obj.Success) + if err != nil { + return errors.NewField("Success", err) + } + // Deserialize `ResultSize`: + err = decoder.Decode(&obj.ResultSize) + if err != nil { + return errors.NewField("ResultSize", err) + } + // Deserialize `Timestamp`: + err = decoder.Decode(&obj.Timestamp) + if err != nil { + return errors.NewField("Timestamp", err) + } + return nil +} + +func (obj *GmpExecutionCompleted) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling GmpExecutionCompleted: %w", err) + } + return nil +} + +func UnmarshalGmpExecutionCompleted(buf []byte) (*GmpExecutionCompleted, error) { + obj := new(GmpExecutionCompleted) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Event emitted for execution failures +type GmpExecutionFailed struct { + // Account that failed execution + Account solanago.PublicKey `json:"account"` + + // Target program that failed + TargetProgram solanago.PublicKey `json:"targetProgram"` + + // Error code + ErrorCode uint32 `json:"errorCode"` + + // Error message + ErrorMessage string `json:"errorMessage"` + + // Failure timestamp + Timestamp int64 `json:"timestamp"` +} + +func (obj GmpExecutionFailed) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `Account`: + err = encoder.Encode(obj.Account) + if err != nil { + return errors.NewField("Account", err) + } + // Serialize `TargetProgram`: + err = encoder.Encode(obj.TargetProgram) + if err != nil { + return errors.NewField("TargetProgram", err) + } + // Serialize `ErrorCode`: + err = encoder.Encode(obj.ErrorCode) + if err != nil { + return errors.NewField("ErrorCode", err) + } + // Serialize `ErrorMessage`: + err = encoder.Encode(obj.ErrorMessage) + if err != nil { + return errors.NewField("ErrorMessage", err) + } + // Serialize `Timestamp`: + err = encoder.Encode(obj.Timestamp) + if err != nil { + return errors.NewField("Timestamp", err) + } + return nil +} + +func (obj GmpExecutionFailed) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding GmpExecutionFailed: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *GmpExecutionFailed) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `Account`: + err = decoder.Decode(&obj.Account) + if err != nil { + return errors.NewField("Account", err) + } + // Deserialize `TargetProgram`: + err = decoder.Decode(&obj.TargetProgram) + if err != nil { + return errors.NewField("TargetProgram", err) + } + // Deserialize `ErrorCode`: + err = decoder.Decode(&obj.ErrorCode) + if err != nil { + return errors.NewField("ErrorCode", err) + } + // Deserialize `ErrorMessage`: + err = decoder.Decode(&obj.ErrorMessage) + if err != nil { + return errors.NewField("ErrorMessage", err) + } + // Deserialize `Timestamp`: + err = decoder.Decode(&obj.Timestamp) + if err != nil { + return errors.NewField("Timestamp", err) + } + return nil +} + +func (obj *GmpExecutionFailed) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling GmpExecutionFailed: %w", err) + } + return nil +} + +func UnmarshalGmpExecutionFailed(buf []byte) (*GmpExecutionFailed, error) { + obj := new(GmpExecutionFailed) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Event emitted when packet timeout is processed +type GmpTimeoutProcessed struct { + // Original sender + Sender solanago.PublicKey `json:"sender"` + + // Packet sequence + Sequence uint64 `json:"sequence"` + + // Timeout height or timestamp + TimeoutInfo string `json:"timeoutInfo"` + + // Processing timestamp + Timestamp int64 `json:"timestamp"` +} + +func (obj GmpTimeoutProcessed) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `Sender`: + err = encoder.Encode(obj.Sender) + if err != nil { + return errors.NewField("Sender", err) + } + // Serialize `Sequence`: + err = encoder.Encode(obj.Sequence) + if err != nil { + return errors.NewField("Sequence", err) + } + // Serialize `TimeoutInfo`: + err = encoder.Encode(obj.TimeoutInfo) + if err != nil { + return errors.NewField("TimeoutInfo", err) + } + // Serialize `Timestamp`: + err = encoder.Encode(obj.Timestamp) + if err != nil { + return errors.NewField("Timestamp", err) + } + return nil +} + +func (obj GmpTimeoutProcessed) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding GmpTimeoutProcessed: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *GmpTimeoutProcessed) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `Sender`: + err = decoder.Decode(&obj.Sender) + if err != nil { + return errors.NewField("Sender", err) + } + // Deserialize `Sequence`: + err = decoder.Decode(&obj.Sequence) + if err != nil { + return errors.NewField("Sequence", err) + } + // Deserialize `TimeoutInfo`: + err = decoder.Decode(&obj.TimeoutInfo) + if err != nil { + return errors.NewField("TimeoutInfo", err) + } + // Deserialize `Timestamp`: + err = decoder.Decode(&obj.Timestamp) + if err != nil { + return errors.NewField("Timestamp", err) + } + return nil +} + +func (obj *GmpTimeoutProcessed) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling GmpTimeoutProcessed: %w", err) + } + return nil +} + +func UnmarshalGmpTimeoutProcessed(buf []byte) (*GmpTimeoutProcessed, error) { + obj := new(GmpTimeoutProcessed) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Message for onAcknowledgementPacket callback +// Sent from router to IBC app when an acknowledgement is received +type OnAcknowledgementPacketMsg struct { + SourceClient string `json:"sourceClient"` + DestClient string `json:"destClient"` + Sequence uint64 `json:"sequence"` + Payload Payload `json:"payload"` + Acknowledgement []byte `json:"acknowledgement"` + Relayer solanago.PublicKey `json:"relayer"` +} + +func (obj OnAcknowledgementPacketMsg) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `SourceClient`: + err = encoder.Encode(obj.SourceClient) + if err != nil { + return errors.NewField("SourceClient", err) + } + // Serialize `DestClient`: + err = encoder.Encode(obj.DestClient) + if err != nil { + return errors.NewField("DestClient", err) + } + // Serialize `Sequence`: + err = encoder.Encode(obj.Sequence) + if err != nil { + return errors.NewField("Sequence", err) + } + // Serialize `Payload`: + err = encoder.Encode(obj.Payload) + if err != nil { + return errors.NewField("Payload", err) + } + // Serialize `Acknowledgement`: + err = encoder.Encode(obj.Acknowledgement) + if err != nil { + return errors.NewField("Acknowledgement", err) + } + // Serialize `Relayer`: + err = encoder.Encode(obj.Relayer) + if err != nil { + return errors.NewField("Relayer", err) + } + return nil +} + +func (obj OnAcknowledgementPacketMsg) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding OnAcknowledgementPacketMsg: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *OnAcknowledgementPacketMsg) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `SourceClient`: + err = decoder.Decode(&obj.SourceClient) + if err != nil { + return errors.NewField("SourceClient", err) + } + // Deserialize `DestClient`: + err = decoder.Decode(&obj.DestClient) + if err != nil { + return errors.NewField("DestClient", err) + } + // Deserialize `Sequence`: + err = decoder.Decode(&obj.Sequence) + if err != nil { + return errors.NewField("Sequence", err) + } + // Deserialize `Payload`: + err = decoder.Decode(&obj.Payload) + if err != nil { + return errors.NewField("Payload", err) + } + // Deserialize `Acknowledgement`: + err = decoder.Decode(&obj.Acknowledgement) + if err != nil { + return errors.NewField("Acknowledgement", err) + } + // Deserialize `Relayer`: + err = decoder.Decode(&obj.Relayer) + if err != nil { + return errors.NewField("Relayer", err) + } + return nil +} + +func (obj *OnAcknowledgementPacketMsg) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling OnAcknowledgementPacketMsg: %w", err) + } + return nil +} + +func UnmarshalOnAcknowledgementPacketMsg(buf []byte) (*OnAcknowledgementPacketMsg, error) { + obj := new(OnAcknowledgementPacketMsg) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Message for onRecvPacket callback +// Sent from router to IBC app when a packet is received +type OnRecvPacketMsg struct { + SourceClient string `json:"sourceClient"` + DestClient string `json:"destClient"` + Sequence uint64 `json:"sequence"` + Payload Payload `json:"payload"` + Relayer solanago.PublicKey `json:"relayer"` +} + +func (obj OnRecvPacketMsg) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `SourceClient`: + err = encoder.Encode(obj.SourceClient) + if err != nil { + return errors.NewField("SourceClient", err) + } + // Serialize `DestClient`: + err = encoder.Encode(obj.DestClient) + if err != nil { + return errors.NewField("DestClient", err) + } + // Serialize `Sequence`: + err = encoder.Encode(obj.Sequence) + if err != nil { + return errors.NewField("Sequence", err) + } + // Serialize `Payload`: + err = encoder.Encode(obj.Payload) + if err != nil { + return errors.NewField("Payload", err) + } + // Serialize `Relayer`: + err = encoder.Encode(obj.Relayer) + if err != nil { + return errors.NewField("Relayer", err) + } + return nil +} + +func (obj OnRecvPacketMsg) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding OnRecvPacketMsg: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *OnRecvPacketMsg) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `SourceClient`: + err = decoder.Decode(&obj.SourceClient) + if err != nil { + return errors.NewField("SourceClient", err) + } + // Deserialize `DestClient`: + err = decoder.Decode(&obj.DestClient) + if err != nil { + return errors.NewField("DestClient", err) + } + // Deserialize `Sequence`: + err = decoder.Decode(&obj.Sequence) + if err != nil { + return errors.NewField("Sequence", err) + } + // Deserialize `Payload`: + err = decoder.Decode(&obj.Payload) + if err != nil { + return errors.NewField("Payload", err) + } + // Deserialize `Relayer`: + err = decoder.Decode(&obj.Relayer) + if err != nil { + return errors.NewField("Relayer", err) + } + return nil +} + +func (obj *OnRecvPacketMsg) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling OnRecvPacketMsg: %w", err) + } + return nil +} + +func UnmarshalOnRecvPacketMsg(buf []byte) (*OnRecvPacketMsg, error) { + obj := new(OnRecvPacketMsg) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Message for onTimeoutPacket callback +// Sent from router to IBC app when a packet times out +type OnTimeoutPacketMsg struct { + SourceClient string `json:"sourceClient"` + DestClient string `json:"destClient"` + Sequence uint64 `json:"sequence"` + Payload Payload `json:"payload"` + Relayer solanago.PublicKey `json:"relayer"` +} + +func (obj OnTimeoutPacketMsg) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `SourceClient`: + err = encoder.Encode(obj.SourceClient) + if err != nil { + return errors.NewField("SourceClient", err) + } + // Serialize `DestClient`: + err = encoder.Encode(obj.DestClient) + if err != nil { + return errors.NewField("DestClient", err) + } + // Serialize `Sequence`: + err = encoder.Encode(obj.Sequence) + if err != nil { + return errors.NewField("Sequence", err) + } + // Serialize `Payload`: + err = encoder.Encode(obj.Payload) + if err != nil { + return errors.NewField("Payload", err) + } + // Serialize `Relayer`: + err = encoder.Encode(obj.Relayer) + if err != nil { + return errors.NewField("Relayer", err) + } + return nil +} + +func (obj OnTimeoutPacketMsg) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding OnTimeoutPacketMsg: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *OnTimeoutPacketMsg) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `SourceClient`: + err = decoder.Decode(&obj.SourceClient) + if err != nil { + return errors.NewField("SourceClient", err) + } + // Deserialize `DestClient`: + err = decoder.Decode(&obj.DestClient) + if err != nil { + return errors.NewField("DestClient", err) + } + // Deserialize `Sequence`: + err = decoder.Decode(&obj.Sequence) + if err != nil { + return errors.NewField("Sequence", err) + } + // Deserialize `Payload`: + err = decoder.Decode(&obj.Payload) + if err != nil { + return errors.NewField("Payload", err) + } + // Deserialize `Relayer`: + err = decoder.Decode(&obj.Relayer) + if err != nil { + return errors.NewField("Relayer", err) + } + return nil +} + +func (obj *OnTimeoutPacketMsg) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling OnTimeoutPacketMsg: %w", err) + } + return nil +} + +func UnmarshalOnTimeoutPacketMsg(buf []byte) (*OnTimeoutPacketMsg, error) { + obj := new(OnTimeoutPacketMsg) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Payload structure shared between router and IBC apps +type Payload struct { + SourcePort string `json:"sourcePort"` + DestPort string `json:"destPort"` + Version string `json:"version"` + Encoding string `json:"encoding"` + Value []byte `json:"value"` +} + +func (obj Payload) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `SourcePort`: + err = encoder.Encode(obj.SourcePort) + if err != nil { + return errors.NewField("SourcePort", err) + } + // Serialize `DestPort`: + err = encoder.Encode(obj.DestPort) + if err != nil { + return errors.NewField("DestPort", err) + } + // Serialize `Version`: + err = encoder.Encode(obj.Version) + if err != nil { + return errors.NewField("Version", err) + } + // Serialize `Encoding`: + err = encoder.Encode(obj.Encoding) + if err != nil { + return errors.NewField("Encoding", err) + } + // Serialize `Value`: + err = encoder.Encode(obj.Value) + if err != nil { + return errors.NewField("Value", err) + } + return nil +} + +func (obj Payload) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding Payload: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *Payload) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `SourcePort`: + err = decoder.Decode(&obj.SourcePort) + if err != nil { + return errors.NewField("SourcePort", err) + } + // Deserialize `DestPort`: + err = decoder.Decode(&obj.DestPort) + if err != nil { + return errors.NewField("DestPort", err) + } + // Deserialize `Version`: + err = decoder.Decode(&obj.Version) + if err != nil { + return errors.NewField("Version", err) + } + // Deserialize `Encoding`: + err = decoder.Decode(&obj.Encoding) + if err != nil { + return errors.NewField("Encoding", err) + } + // Deserialize `Value`: + err = decoder.Decode(&obj.Value) + if err != nil { + return errors.NewField("Value", err) + } + return nil +} + +func (obj *Payload) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling Payload: %w", err) + } + return nil +} + +func UnmarshalPayload(buf []byte) (*Payload, error) { + obj := new(Payload) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Event emitted when router caller PDA is created +type RouterCallerCreated struct { + // Router caller PDA address + RouterCaller solanago.PublicKey `json:"routerCaller"` + + // PDA bump seed + Bump uint8 `json:"bump"` +} + +func (obj RouterCallerCreated) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `RouterCaller`: + err = encoder.Encode(obj.RouterCaller) + if err != nil { + return errors.NewField("RouterCaller", err) + } + // Serialize `Bump`: + err = encoder.Encode(obj.Bump) + if err != nil { + return errors.NewField("Bump", err) + } + return nil +} + +func (obj RouterCallerCreated) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding RouterCallerCreated: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *RouterCallerCreated) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `RouterCaller`: + err = decoder.Decode(&obj.RouterCaller) + if err != nil { + return errors.NewField("RouterCaller", err) + } + // Deserialize `Bump`: + err = decoder.Decode(&obj.Bump) + if err != nil { + return errors.NewField("Bump", err) + } + return nil +} + +func (obj *RouterCallerCreated) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling RouterCallerCreated: %w", err) + } + return nil +} + +func UnmarshalRouterCallerCreated(buf []byte) (*RouterCallerCreated, error) { + obj := new(RouterCallerCreated) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Send call message +type SendCallMsg struct { + // Source client identifier + SourceClient string `json:"sourceClient"` + + // Timeout timestamp (unix seconds) + TimeoutTimestamp int64 `json:"timeoutTimestamp"` + + // Receiver program + Receiver solanago.PublicKey `json:"receiver"` + + // Account salt + Salt []byte `json:"salt"` + + // Call payload (instruction data + accounts) + Payload []byte `json:"payload"` + + // Optional memo + Memo string `json:"memo"` +} + +func (obj SendCallMsg) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `SourceClient`: + err = encoder.Encode(obj.SourceClient) + if err != nil { + return errors.NewField("SourceClient", err) + } + // Serialize `TimeoutTimestamp`: + err = encoder.Encode(obj.TimeoutTimestamp) + if err != nil { + return errors.NewField("TimeoutTimestamp", err) + } + // Serialize `Receiver`: + err = encoder.Encode(obj.Receiver) + if err != nil { + return errors.NewField("Receiver", err) + } + // Serialize `Salt`: + err = encoder.Encode(obj.Salt) + if err != nil { + return errors.NewField("Salt", err) + } + // Serialize `Payload`: + err = encoder.Encode(obj.Payload) + if err != nil { + return errors.NewField("Payload", err) + } + // Serialize `Memo`: + err = encoder.Encode(obj.Memo) + if err != nil { + return errors.NewField("Memo", err) + } + return nil +} + +func (obj SendCallMsg) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding SendCallMsg: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *SendCallMsg) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `SourceClient`: + err = decoder.Decode(&obj.SourceClient) + if err != nil { + return errors.NewField("SourceClient", err) + } + // Deserialize `TimeoutTimestamp`: + err = decoder.Decode(&obj.TimeoutTimestamp) + if err != nil { + return errors.NewField("TimeoutTimestamp", err) + } + // Deserialize `Receiver`: + err = decoder.Decode(&obj.Receiver) + if err != nil { + return errors.NewField("Receiver", err) + } + // Deserialize `Salt`: + err = decoder.Decode(&obj.Salt) + if err != nil { + return errors.NewField("Salt", err) + } + // Deserialize `Payload`: + err = decoder.Decode(&obj.Payload) + if err != nil { + return errors.NewField("Payload", err) + } + // Deserialize `Memo`: + err = decoder.Decode(&obj.Memo) + if err != nil { + return errors.NewField("Memo", err) + } + return nil +} + +func (obj *SendCallMsg) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling SendCallMsg: %w", err) + } + return nil +} + +func UnmarshalSendCallMsg(buf []byte) (*SendCallMsg, error) { + obj := new(SendCallMsg) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} diff --git a/packages/relayer/lib/src/listener/solana.rs b/packages/relayer/lib/src/listener/solana.rs index a6f0d4709..f9f54a08b 100644 --- a/packages/relayer/lib/src/listener/solana.rs +++ b/packages/relayer/lib/src/listener/solana.rs @@ -82,7 +82,14 @@ impl ChainListenerService for ChainListener { for tx in tx_ids { let (tx, meta) = self .rpc_client - .get_transaction(&tx, UiTransactionEncoding::Json) + .get_transaction_with_config( + &tx, + solana_client::rpc_config::RpcTransactionConfig { + encoding: Some(UiTransactionEncoding::Json), + commitment: Some(CommitmentConfig::confirmed()), + max_supported_transaction_version: Some(0), + }, + ) .map_err(|e| anyhow::anyhow!("Failed to fetch Solana transaction: {e}")) .and_then(|tx| { tx.transaction diff --git a/packages/relayer/modules/cosmos-to-solana/Cargo.toml b/packages/relayer/modules/cosmos-to-solana/Cargo.toml index 289f6aeda..733be98de 100644 --- a/packages/relayer/modules/cosmos-to-solana/Cargo.toml +++ b/packages/relayer/modules/cosmos-to-solana/Cargo.toml @@ -38,3 +38,6 @@ ibc-client-tendermint.workspace = true prost.workspace = true solana-ibc-types.workspace = true solana-ibc-constants.workspace = true + +[build-dependencies] +prost-build.workspace = true diff --git a/packages/relayer/modules/cosmos-to-solana/build.rs b/packages/relayer/modules/cosmos-to-solana/build.rs new file mode 100644 index 000000000..3b4629b98 --- /dev/null +++ b/packages/relayer/modules/cosmos-to-solana/build.rs @@ -0,0 +1,27 @@ +use std::io::Result; + +fn main() -> Result<()> { + // Configure prost-build + let mut config = prost_build::Config::new(); + + // Note: prost already derives Eq for enums via ::prost::Enumeration + // Only add Eq to message types + config.type_attribute(".gmp.GMPPacketData", "#[derive(Eq)]"); + config.type_attribute(".gmp.GMPAcknowledgement", "#[derive(Eq)]"); + config.type_attribute(".solana.SolanaInstruction", "#[derive(Eq)]"); + config.type_attribute(".solana.SolanaAccountMeta", "#[derive(Eq)]"); + + // Compile proto files + config.compile_protos( + &[ + "../../../../proto/gmp/gmp.proto", + "../../../../proto/solana/solana_instruction.proto", + ], + &["../../../../proto"], + )?; + + println!("cargo:rerun-if-changed=../../../../proto/gmp/gmp.proto"); + println!("cargo:rerun-if-changed=../../../../proto/solana/solana_instruction.proto"); + + Ok(()) +} diff --git a/packages/relayer/modules/cosmos-to-solana/src/constants.rs b/packages/relayer/modules/cosmos-to-solana/src/constants.rs new file mode 100644 index 000000000..0c688b9a8 --- /dev/null +++ b/packages/relayer/modules/cosmos-to-solana/src/constants.rs @@ -0,0 +1,16 @@ +//! Constants for the Cosmos to Solana relayer + +/// Anchor account discriminator size (first 8 bytes of account data) +pub const ANCHOR_DISCRIMINATOR_SIZE: usize = 8; + +/// GMP (General Message Passing) port identifier +pub const GMP_PORT_ID: &str = "gmpport"; + +/// Protobuf encoding type for GMP packets +pub const PROTOBUF_ENCODING: &str = "application/x-protobuf"; + +/// JSON encoding type for IBC packets +pub const JSON_ENCODING: &str = "application/json"; + +/// GMP account state PDA seed +pub const GMP_ACCOUNT_STATE_SEED: &[u8] = b"gmp_account"; diff --git a/packages/relayer/modules/cosmos-to-solana/src/gmp.rs b/packages/relayer/modules/cosmos-to-solana/src/gmp.rs new file mode 100644 index 000000000..caa1f338b --- /dev/null +++ b/packages/relayer/modules/cosmos-to-solana/src/gmp.rs @@ -0,0 +1,154 @@ +//! GMP (General Message Passing) account extraction utilities +//! +//! This module handles extraction of accounts from GMP payloads for Solana transaction building. +//! GMP enables cross-chain message execution by encoding Solana instructions in IBC packets. + +use std::str::FromStr; + +use anyhow::Result; +use prost::Message; +use solana_sdk::{hash::hash, instruction::AccountMeta, pubkey::Pubkey}; + +use crate::constants::{GMP_ACCOUNT_STATE_SEED, GMP_PORT_ID, PROTOBUF_ENCODING}; +use crate::proto::{GmpPacketData, SolanaInstruction}; + +/// Extract GMP accounts from packet payload +/// +/// # Arguments +/// * `dest_port` - The destination port ID +/// * `encoding` - The payload encoding type +/// * `payload_value` - The raw payload data +/// * `source_client` - The source client ID +/// * `accounts` - Existing accounts list (used to extract IBC app program ID) +/// +/// # Returns +/// Vector of GMP accounts +/// +/// # Errors +/// Returns error if payload extraction fails +pub fn extract_gmp_accounts( + dest_port: &str, + encoding: &str, + payload_value: &[u8], + source_client: &str, + accounts: &[AccountMeta], +) -> Result> { + let (gmp_packet, receiver_pubkey) = match parse_gmp_packet(dest_port, encoding, payload_value) { + Some(result) => result?, + None => return Ok(Vec::new()), + }; + + // Derive account_state PDA + let ibc_app_program_id = accounts + .get(5) + .map(|acc| acc.pubkey) + .ok_or_else(|| anyhow::anyhow!("Missing ibc_app_program in existing accounts"))?; + + let account_state_pda = derive_account_state_pda( + source_client, + &gmp_packet.sender, + &gmp_packet.salt, + &ibc_app_program_id, + ); + + let mut account_metas = vec![ + AccountMeta { + pubkey: account_state_pda, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: receiver_pubkey, + is_signer: false, + is_writable: true, + }, + ]; + + // Parse SolanaInstruction and extract additional accounts + let solana_instruction = SolanaInstruction::decode(gmp_packet.payload.as_slice()) + .map_err(|e| anyhow::anyhow!("Failed to parse inner SolanaInstruction: {e}"))?; + + extract_instruction_accounts(&solana_instruction, &mut account_metas)?; + + tracing::info!( + "Found {} additional accounts from GMP payload for port {}", + account_metas.len(), + dest_port + ); + + Ok(account_metas) +} + +/// Parse and validate GMP packet from payload +/// +/// Returns `None` if payload is not a GMP payload, `Some(Ok(...))` if valid, `Some(Err(...))` if invalid +fn parse_gmp_packet( + port_id: &str, + encoding: &str, + payload_value: &[u8], +) -> Option> { + // Only process GMP port payloads + if port_id != GMP_PORT_ID || encoding != PROTOBUF_ENCODING { + tracing::debug!( + "No additional GMP accounts found in payload for port {} (non-GMP or parsing failed)", + port_id + ); + return None; + } + + let Ok(gmp_packet) = GmpPacketData::decode(payload_value) else { + tracing::debug!( + "No additional GMP accounts found in payload for port {} (non-GMP or parsing failed)", + port_id + ); + return None; + }; + + // Parse receiver as Solana Pubkey (target program) + let receiver_pubkey = match Pubkey::from_str(&gmp_packet.receiver) { + Ok(pubkey) => pubkey, + Err(e) => return Some(Err(anyhow::anyhow!("Invalid receiver pubkey: {e}"))), + }; + + tracing::info!( + "GMP account extraction: receiver from packet = {} (target program)", + receiver_pubkey + ); + + Some(Ok((gmp_packet, receiver_pubkey))) +} + +/// Derive the `account_state` PDA for a GMP sender +fn derive_account_state_pda( + source_client: &str, + sender: &str, + salt: &[u8], + ibc_app_program_id: &Pubkey, +) -> Pubkey { + let sender_hash = hash(sender.as_bytes()).to_bytes(); + let account_state_seeds = [ + GMP_ACCOUNT_STATE_SEED, + source_client.as_bytes(), + &sender_hash, + salt, + ]; + Pubkey::find_program_address(&account_state_seeds, ibc_app_program_id).0 +} + +/// Extract accounts from `SolanaInstruction` and add them to the account list +fn extract_instruction_accounts( + instruction: &SolanaInstruction, + account_metas: &mut Vec, +) -> Result<()> { + for account_meta in &instruction.accounts { + let pubkey = Pubkey::try_from(account_meta.pubkey.as_slice()) + .map_err(|e| anyhow::anyhow!("Invalid pubkey: {e}"))?; + + account_metas.push(AccountMeta { + pubkey, + is_signer: false, + is_writable: account_meta.is_writable, + }); + } + Ok(()) +} diff --git a/packages/relayer/modules/cosmos-to-solana/src/lib.rs b/packages/relayer/modules/cosmos-to-solana/src/lib.rs index 63f11ef78..125fe9726 100644 --- a/packages/relayer/modules/cosmos-to-solana/src/lib.rs +++ b/packages/relayer/modules/cosmos-to-solana/src/lib.rs @@ -3,6 +3,9 @@ #![deny(clippy::nursery, clippy::pedantic, warnings, unused_crate_dependencies)] #![allow(missing_docs, unused_crate_dependencies)] +pub mod constants; +pub mod gmp; +pub mod proto; pub mod tx_builder; use std::collections::HashMap; @@ -51,10 +54,12 @@ pub struct CosmosToSolanaConfig { pub solana_ics26_program_id: String, /// The Solana ICS07 Tendermint light client program ID. pub solana_ics07_program_id: String, - /// The Solana IBC app program ID. - pub solana_ibc_app_program_id: String, /// The Solana fee payer address. pub solana_fee_payer: String, + /// Address Lookup Table address for reducing transaction size (optional). + pub solana_alt_address: Option, + /// Whether to use mock WASM client on Cosmos for testing. + pub mock_wasm_client: bool, } impl CosmosToSolanaRelayerModuleService { @@ -72,11 +77,6 @@ impl CosmosToSolanaRelayerModuleService { .parse() .map_err(|e| anyhow::anyhow!("Invalid Solana ICS07 program ID: {}", e))?; - let ibc_app_program_id: Pubkey = config - .solana_ibc_app_program_id - .parse() - .map_err(|e| anyhow::anyhow!("Invalid Solana IBC app program ID: {}", e))?; - let target_listener = solana::ChainListener::new(config.target_rpc_url.clone(), solana_ics26_program_id); @@ -85,13 +85,20 @@ impl CosmosToSolanaRelayerModuleService { .parse() .map_err(|e| anyhow::anyhow!("Invalid fee payer address: {}", e))?; + let alt_address = config + .solana_alt_address + .as_ref() + .map(|addr| addr.parse()) + .transpose() + .map_err(|e| anyhow::anyhow!("Invalid ALT address: {}", e))?; + let tx_builder = tx_builder::TxBuilder::new( src_listener.client().clone(), target_listener.client().clone(), solana_ics07_program_id, solana_ics26_program_id, - ibc_app_program_id, fee_payer, + alt_address, )?; Ok(Self { @@ -221,17 +228,22 @@ impl RelayerService for CosmosToSolanaRelayerModuleService { ) -> Result, tonic::Status> { tracing::info!("Handling update client request for Cosmos to Solana..."); - let header_update = self + let chunked = self .tx_builder .update_client(request.into_inner().dst_client_id) .await .map_err(|e| tonic::Status::from_error(e.into()))?; + tracing::info!( + "Using chunked update client with {} chunks", + chunked.total_chunks + ); + let mut txs = Vec::new(); - for tx in header_update.chunk_txs { + for tx in chunked.chunk_txs { txs.push(tx); } - txs.push(header_update.assembly_tx); + txs.push(chunked.assembly_tx); Ok(Response::new(api::UpdateClientResponse { tx: vec![], diff --git a/packages/relayer/modules/cosmos-to-solana/src/proto.rs b/packages/relayer/modules/cosmos-to-solana/src/proto.rs new file mode 100644 index 000000000..d74eb24eb --- /dev/null +++ b/packages/relayer/modules/cosmos-to-solana/src/proto.rs @@ -0,0 +1,17 @@ +//! Generated Protobuf types for GMP relayer +//! +//! This module contains types generated from .proto files via prost-build. +//! The proto files are located in proto/gmp/ and proto/solana/. + +// Include generated code from build.rs +pub mod gmp { + include!(concat!(env!("OUT_DIR"), "/gmp.rs")); +} + +pub mod solana { + include!(concat!(env!("OUT_DIR"), "/solana.rs")); +} + +// Re-export for convenience +pub use gmp::GmpPacketData; +pub use solana::{SolanaAccountMeta, SolanaInstruction}; diff --git a/packages/relayer/modules/cosmos-to-solana/src/tx_builder.rs b/packages/relayer/modules/cosmos-to-solana/src/tx_builder.rs index e9b9bcd0b..ee5147de4 100644 --- a/packages/relayer/modules/cosmos-to-solana/src/tx_builder.rs +++ b/packages/relayer/modules/cosmos-to-solana/src/tx_builder.rs @@ -25,18 +25,22 @@ use ibc_eureka_relayer_lib::{ use prost::Message; use solana_client::rpc_client::RpcClient; use solana_sdk::{ + address_lookup_table::{state::AddressLookupTable, AddressLookupTableAccount}, commitment_config::CommitmentConfig, instruction::{AccountMeta, Instruction}, + message::{v0, VersionedMessage}, pubkey::Pubkey, - sysvar, - transaction::Transaction, + transaction::{Transaction, VersionedTransaction}, }; +use crate::constants::ANCHOR_DISCRIMINATOR_SIZE; +use crate::gmp; + use solana_ibc_types::{ - derive_client, derive_client_sequence, derive_ibc_app, derive_ics07_client_state, - derive_ics07_consensus_state, derive_packet_ack, derive_packet_commitment, - derive_packet_receipt, derive_payload_chunk, derive_proof_chunk, derive_router_state, - get_instruction_discriminator, + derive_app_state, derive_client, derive_client_sequence, derive_ibc_app, + derive_ics07_client_state, derive_ics07_consensus_state, derive_packet_ack, + derive_packet_commitment, derive_packet_receipt, derive_payload_chunk, derive_proof_chunk, + derive_router_state, get_instruction_discriminator, ics07::{ClientState, ConsensusState, ICS07_INITIALIZE_DISCRIMINATOR}, MsgAckPacket, MsgRecvPacket, MsgTimeoutPacket, MsgUploadChunk, }; @@ -45,6 +49,15 @@ use tendermint_rpc::{Client as _, HttpClient}; /// Maximum size for a chunk (matches `CHUNK_DATA_SIZE` in Solana program) const MAX_CHUNK_SIZE: usize = 700; +/// Maximum compute units allowed per Solana transaction +const MAX_COMPUTE_UNIT_LIMIT: u32 = 1_400_000; + +/// Priority fee in micro-lamports per compute unit +const DEFAULT_PRIORITY_FEE: u64 = 1000; + +/// Instruction discriminator for timeout packet +const TIMEOUT_PACKET_INSTRUCTION: &str = "timeout_packet"; + /// Parameters for uploading a header chunk (mirrors the Solana program's type) #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] struct UploadChunkParams { @@ -124,10 +137,10 @@ pub struct TxBuilder { pub solana_ics26_program_id: Pubkey, /// The Solana ICS07 program ID. pub solana_ics07_program_id: Pubkey, - /// The IBC app program ID. - pub ibc_app_program_id: Pubkey, /// The fee payer address for transactions. pub fee_payer: Pubkey, + /// Address Lookup Table address for reducing transaction size (optional). + pub alt_address: Option, } impl TxBuilder { @@ -142,16 +155,16 @@ impl TxBuilder { target_solana_client: Arc, solana_ics07_program_id: Pubkey, solana_ics26_program_id: Pubkey, - ibc_app_program_id: Pubkey, fee_payer: Pubkey, + alt_address: Option, ) -> Result { Ok(Self { src_tm_client, target_solana_client, solana_ics26_program_id, solana_ics07_program_id, - ibc_app_program_id, fee_payer, + alt_address, }) } @@ -214,23 +227,27 @@ impl TxBuilder { chain_id: &str, msg: &MsgRecvPacket, chunk_accounts: Vec, + payload_data: &[Vec], ) -> Result { - let [payload] = msg.packet.payloads.as_slice() else { - return Err(anyhow::anyhow!( - "Expected exactly one recv packet payload element" - )); + // Validate exactly one payload element (inline or metadata for chunked) + let dest_port = if msg.packet.payloads.is_empty() { + let [metadata] = msg.payloads.as_slice() else { + return Err(anyhow::anyhow!( + "Expected exactly one recv packet payload metadata element" + )); + }; + &metadata.dest_port + } else { + let [payload] = msg.packet.payloads.as_slice() else { + return Err(anyhow::anyhow!( + "Expected exactly one recv packet payload element" + )); + }; + &payload.dest_port }; let (router_state, _) = derive_router_state(self.solana_ics26_program_id); - let (ibc_app, _) = derive_ibc_app(&payload.dest_port, self.solana_ics26_program_id); - - // Use configured IBC app program ID - let ibc_app_program = self.ibc_app_program_id; - - let (app_state, _) = Pubkey::find_program_address( - &[b"app_state", payload.dest_port.as_bytes()], - &ibc_app_program, - ); + let (ibc_app, _) = derive_ibc_app(dest_port, self.solana_ics26_program_id); let (client_sequence, _) = derive_client_sequence(&msg.packet.dest_client, self.solana_ics26_program_id); @@ -254,31 +271,69 @@ impl TxBuilder { self.solana_ics07_program_id, ); + // Resolve the actual IBC app program ID for this port + let ibc_app_program_id = self.resolve_port_program_id(dest_port)?; + + // Derive the app state account for the resolved IBC app + let (ibc_app_state, _) = derive_app_state(dest_port, ibc_app_program_id); + + // Build base accounts list for recv_packet (matches router program's RecvPacket account structure) let mut accounts = vec![ AccountMeta::new_readonly(router_state, false), AccountMeta::new_readonly(ibc_app, false), AccountMeta::new(client_sequence, false), AccountMeta::new(packet_receipt, false), AccountMeta::new(packet_ack, false), - AccountMeta::new_readonly(ibc_app_program, false), - AccountMeta::new(app_state, false), - AccountMeta::new_readonly(self.solana_ics26_program_id, false), - AccountMeta::new_readonly(self.fee_payer, true), - AccountMeta::new(self.fee_payer, true), + AccountMeta::new_readonly(ibc_app_program_id, false), // IBC app program (e.g., ICS27 GMP) + AccountMeta::new(ibc_app_state, false), // IBC app state + AccountMeta::new_readonly(self.solana_ics26_program_id, false), // router program + AccountMeta::new_readonly(self.fee_payer, true), // relayer + AccountMeta::new(self.fee_payer, true), // payer AccountMeta::new_readonly(solana_sdk::system_program::id(), false), - AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(client, false), AccountMeta::new_readonly(self.solana_ics07_program_id, false), AccountMeta::new_readonly(client_state, false), AccountMeta::new_readonly(consensus_state, false), ]; - // TODO: fix closing - // Add chunk accounts as remaining_accounts (mutable since they'll be closed) + // Add chunk accounts FIRST as remaining_accounts (mutable since they'll be closed) + // The router's chunking logic expects chunk accounts at the beginning of remaining_accounts for chunk_account in chunk_accounts { accounts.push(AccountMeta::new(chunk_account, false)); } + // Extract GMP accounts if this is a GMP payload + // IMPORTANT: We only extract account information - the payload itself is NOT modified + // The packet must be forwarded exactly as received (IBC security invariant) + // GMP accounts are added AFTER chunk accounts to maintain correct ordering + let (dest_port_for_gmp, encoding, payload_value) = if msg.packet.payloads.is_empty() { + // Chunked payload case + let metadata = &msg.payloads[0]; + let data = &payload_data[0]; + ( + metadata.dest_port.as_str(), + metadata.encoding.as_str(), + data.as_slice(), + ) + } else { + // Inline payload case + let payload = &msg.packet.payloads[0]; + ( + payload.dest_port.as_str(), + payload.encoding.as_str(), + payload.value.as_slice(), + ) + }; + + let gmp_accounts = gmp::extract_gmp_accounts( + dest_port_for_gmp, + encoding, + payload_value, + &msg.packet.source_client, + &accounts, + )?; + accounts.extend(gmp_accounts); + let discriminator = get_instruction_discriminator("recv_packet"); let mut data = discriminator.to_vec(); data.extend_from_slice(&msg.try_to_vec()?); @@ -307,15 +362,36 @@ impl TxBuilder { let (router_state, _) = derive_router_state(solana_ics26_program_id); - let (ibc_app_pda, _) = derive_ibc_app("transfer", solana_ics26_program_id); + let [payload] = msg.packet.payloads.as_slice() else { + return Err(anyhow::anyhow!( + "Expected exactly one ack packet payload element" + )); + }; + + let source_port = payload.source_port.clone(); - // Use configured IBC app program ID - let ibc_app_program = self.ibc_app_program_id; + let (ibc_app_pda, _) = derive_ibc_app(&source_port, solana_ics26_program_id); + let ibc_app_account = self + .target_solana_client + .get_account(&ibc_app_pda) + .map_err(|e| anyhow::anyhow!("Failed to get IBC app account: {e}"))?; + + if ibc_app_account.data.len() < ANCHOR_DISCRIMINATOR_SIZE { + return Err(anyhow::anyhow!("Account data too short for IBCApp account")); + } + + // Deserialize IBCApp account using borsh (skip discriminator) + // Use deserialize instead of try_from_slice to handle extra bytes gracefully + let mut data = &ibc_app_account.data[ANCHOR_DISCRIMINATOR_SIZE..]; + let ibc_app = solana_ibc_types::IBCApp::deserialize(&mut data) + .map_err(|e| anyhow::anyhow!("Failed to deserialize IBCApp account: {e}"))?; + + let ibc_app_program = ibc_app.app_program_id; tracing::info!("IBC app program ID: {}", ibc_app_program); - let (app_state, _) = - Pubkey::find_program_address(&[b"app_state", b"transfer"], &ibc_app_program); + // Derive the app state PDA using the correct derivation (same as timeout handler) + let (app_state, _) = derive_app_state(&source_port, ibc_app_program); let (packet_commitment, _) = derive_packet_commitment( &msg.packet.source_client, @@ -323,6 +399,7 @@ impl TxBuilder { solana_ics26_program_id, ); + // Derive the client PDA using ICS26 client ID (from packet source_client) let (client, _) = derive_client(&msg.packet.source_client, solana_ics26_program_id); tracing::info!( "Router client PDA for '{}': {}", @@ -395,6 +472,7 @@ impl TxBuilder { /// Returns an error if serialization fails fn build_timeout_packet_instruction( &self, + chain_id: &str, msg: &MsgTimeoutPacket, chunk_accounts: Vec, ) -> Result { @@ -405,42 +483,129 @@ impl TxBuilder { msg.packet.sequence ); - let solana_ics26_program_id = self.solana_ics26_program_id; + let source_port = Self::extract_timeout_source_port(msg)?; + let accounts = self.build_timeout_accounts_with_derived_keys( + chain_id, + msg, + &source_port, + chunk_accounts, + )?; + let data = Self::build_timeout_instruction_data(msg)?; - let (router_state, _) = derive_router_state(solana_ics26_program_id); + Ok(Instruction { + program_id: self.solana_ics26_program_id, + accounts, + data, + }) + } - let (packet_commitment, _) = derive_packet_commitment( - &msg.packet.source_client, - msg.packet.sequence, - solana_ics26_program_id, + /// Extract source port from timeout packet message + fn extract_timeout_source_port(msg: &MsgTimeoutPacket) -> Result { + let [payload] = msg.packet.payloads.as_slice() else { + return Err(anyhow::anyhow!( + "Expected exactly one timeout packet payload element" + )); + }; + Ok(payload.source_port.clone()) + } + + /// Derive PDAs, log derivation info, and build accounts list for timeout packet instruction + #[allow(clippy::cognitive_complexity)] + fn build_timeout_accounts_with_derived_keys( + &self, + chain_id: &str, + msg: &MsgTimeoutPacket, + source_port: &str, + chunk_accounts: Vec, + ) -> Result> { + let program_id = self.solana_ics26_program_id; + + let (router_state, _) = derive_router_state(program_id); + let (ibc_app, _) = derive_ibc_app(source_port, program_id); + let (packet_commitment, _) = + derive_packet_commitment(&msg.packet.source_client, msg.packet.sequence, program_id); + + let ibc_app_program_id = self.resolve_port_program_id(source_port)?; + let (ibc_app_state, _) = derive_app_state(source_port, ibc_app_program_id); + let (client, _) = derive_client(&msg.packet.source_client, program_id); + let (client_state, _) = derive_ics07_client_state(chain_id, self.solana_ics07_program_id); + let (consensus_state, _) = derive_ics07_consensus_state( + client_state, + msg.proof.height, + self.solana_ics07_program_id, ); - let (client, _) = derive_client(&msg.packet.dest_client, solana_ics26_program_id); + tracing::info!("=== TIMEOUT PACKET CONSENSUS STATE DERIVATION ==="); + tracing::info!(" Chain ID: {}", chain_id); + tracing::info!(" Client state PDA: {}", client_state); + tracing::info!(" Proof height from message: {}", msg.proof.height); + tracing::info!(" Consensus state PDA: {}", consensus_state); + tracing::info!( + " This PDA should contain app_hash for height: {}", + msg.proof.height + ); + Ok(Self::assemble_timeout_accounts( + router_state, + ibc_app, + packet_commitment, + ibc_app_program_id, + ibc_app_state, + client, + client_state, + consensus_state, + self.fee_payer, + self.solana_ics26_program_id, + self.solana_ics07_program_id, + chunk_accounts, + )) + } + + /// Assemble timeout packet accounts vector + #[allow(clippy::too_many_arguments)] + fn assemble_timeout_accounts( + router_state: Pubkey, + ibc_app: Pubkey, + packet_commitment: Pubkey, + ibc_app_program_id: Pubkey, + ibc_app_state: Pubkey, + client: Pubkey, + client_state: Pubkey, + consensus_state: Pubkey, + fee_payer: Pubkey, + router_program_id: Pubkey, + light_client_program_id: Pubkey, + chunk_accounts: Vec, + ) -> Vec { let mut accounts = vec![ AccountMeta::new_readonly(router_state, false), - AccountMeta::new(packet_commitment, false), // Will be closed after timeout - AccountMeta::new_readonly(self.fee_payer, true), - AccountMeta::new(self.fee_payer, true), + AccountMeta::new_readonly(ibc_app, false), + AccountMeta::new(packet_commitment, false), + AccountMeta::new_readonly(ibc_app_program_id, false), + AccountMeta::new(ibc_app_state, false), + AccountMeta::new_readonly(router_program_id, false), + AccountMeta::new_readonly(fee_payer, true), + AccountMeta::new(fee_payer, true), AccountMeta::new_readonly(solana_sdk::system_program::id(), false), AccountMeta::new_readonly(client, false), + AccountMeta::new_readonly(light_client_program_id, false), + AccountMeta::new_readonly(client_state, false), + AccountMeta::new_readonly(consensus_state, false), ]; - // Add chunk accounts as remaining_accounts (mutable since they'll be closed) for chunk_account in chunk_accounts { accounts.push(AccountMeta::new(chunk_account, false)); } - // Build instruction data - let discriminator = get_instruction_discriminator("timeout_packet"); + accounts + } + + /// Build instruction data for timeout packet + fn build_timeout_instruction_data(msg: &MsgTimeoutPacket) -> Result> { + let discriminator = get_instruction_discriminator(TIMEOUT_PACKET_INSTRUCTION); let mut data = discriminator.to_vec(); data.extend_from_slice(&msg.try_to_vec()?); - - Ok(Instruction { - program_id: solana_ics26_program_id, - accounts, - data, - }) + Ok(data) } /// Fetch Cosmos client state from the light client on Solana. @@ -455,10 +620,10 @@ impl TxBuilder { .get_account(&client_state_pda) .context("Failed to fetch client state account")?; - let client_state = ClientState::try_from_slice(&account.data[8..]) + let client_state = ClientState::try_from_slice(&account.data[ANCHOR_DISCRIMINATOR_SIZE..]) .or_else(|_| { // If try_from_slice fails due to extra bytes, use deserialize which is more lenient - let mut data = &account.data[8..]; + let mut data = &account.data[ANCHOR_DISCRIMINATOR_SIZE..]; ClientState::deserialize(&mut data) }) .context("Failed to deserialize client state")?; @@ -466,10 +631,47 @@ impl TxBuilder { Ok(client_state) } + /// Helper function to split data into chunks fn split_into_chunks(data: &[u8]) -> Vec> { data.chunks(MAX_CHUNK_SIZE).map(<[u8]>::to_vec).collect() } + /// Resolve the IBC app program ID for a given port + /// + /// # Errors + /// + /// Returns an error if: + /// - Failed to fetch `IBCApp` account + /// - Failed to deserialize account data + fn resolve_port_program_id(&self, port_id: &str) -> Result { + let (ibc_app_account, _) = derive_ibc_app(port_id, self.solana_ics26_program_id); + + let account = self + .target_solana_client + .get_account(&ibc_app_account) + .map_err(|e| { + anyhow::anyhow!("Failed to fetch IBCApp account for port '{}': {e}", port_id) + })?; + + if account.data.len() < ANCHOR_DISCRIMINATOR_SIZE { + return Err(anyhow::anyhow!("Account data too short for IBCApp account")); + } + + // Deserialize IBCApp account using borsh (skip discriminator) + // Use deserialize instead of try_from_slice to handle extra bytes gracefully + let mut data = &account.data[ANCHOR_DISCRIMINATOR_SIZE..]; + let ibc_app = solana_ibc_types::IBCApp::deserialize(&mut data) + .map_err(|e| anyhow::anyhow!("Failed to deserialize IBCApp account: {e}"))?; + + tracing::info!( + "Resolved port '{}' to program ID: {}", + port_id, + ibc_app.app_program_id + ); + + Ok(ibc_app.app_program_id) + } + fn split_header_into_chunks(header_bytes: &[u8]) -> Vec> { Self::split_into_chunks(header_bytes) } @@ -501,10 +703,14 @@ impl TxBuilder { fn extend_compute_ix() -> Vec { let compute_budget_ix = - solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_limit(1_400_000); + solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_limit( + MAX_COMPUTE_UNIT_LIMIT, + ); let priority_fee_ix = - solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_price(1000); + solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_price( + DEFAULT_PRIORITY_FEE, + ); vec![compute_budget_ix, priority_fee_ix] } @@ -818,8 +1024,12 @@ impl TxBuilder { msg.proof.total_chunks, )?; - let recv_instruction = - self.build_recv_packet_instruction(chain_id, msg, remaining_account_pubkeys)?; + let recv_instruction = self.build_recv_packet_instruction( + chain_id, + msg, + remaining_account_pubkeys, + payload_data, + )?; let mut instructions = Self::extend_compute_ix(); instructions.push(recv_instruction); @@ -883,6 +1093,7 @@ impl TxBuilder { fn build_timeout_packet_chunked( &self, + chain_id: &str, msg: &MsgTimeoutPacket, payload_data: &[Vec], proof_data: &[u8], @@ -905,7 +1116,7 @@ impl TxBuilder { )?; let timeout_instruction = - self.build_timeout_packet_instruction(msg, remaining_account_pubkeys)?; + self.build_timeout_packet_instruction(chain_id, msg, remaining_account_pubkeys)?; let mut instructions = Self::extend_compute_ix(); instructions.push(timeout_instruction); @@ -954,11 +1165,21 @@ impl TxBuilder { // Find the maximum height among all source events // This is the height where the latest event (e.g., acknowledgment) was written + // For timeout proofs: src_events is empty, so we use (solana_latest_height - 1) + // This ensures we prove non-receipt at height N using consensus state at N+1 (which we have) let max_event_height = src_events .iter() .map(|e| e.height) .max() - .unwrap_or(solana_latest_height); + .unwrap_or_else(|| { + let timeout_height = solana_latest_height.saturating_sub(1); + tracing::info!( + "Timeout proof detected (no src_events). Proving non-receipt at Cosmos height {} using consensus state at {}", + timeout_height, + solana_latest_height + ); + timeout_height + }); tracing::info!("=== EVENT HEIGHTS ==="); tracing::info!(" Maximum event height from source: {}", max_event_height); @@ -1102,6 +1323,16 @@ impl TxBuilder { .context("proof too big to fit in u8")?; timeout_with_chunks.msg.proof.total_chunks = proof_total_chunks; + + // Update proof height from TM message (injected by inject_tendermint_proofs) + if let Some(proof_height) = &tm_msg.proof_height { + timeout_with_chunks.msg.proof.height = proof_height.revision_height; + tracing::info!( + "Updated timeout packet seq {} proof height to {} (from TM message)", + timeout_with_chunks.msg.packet.sequence, + proof_height.revision_height + ); + } } let mut all_txs = Vec::new(); @@ -1148,6 +1379,7 @@ impl TxBuilder { for timeout_with_chunks in timeout_msgs_with_chunks { // Build chunked transactions let chunked = self.build_timeout_packet_chunked( + &chain_id, &timeout_with_chunks.msg, &timeout_with_chunks.payload_chunks, &timeout_with_chunks.proof_chunks, @@ -1162,16 +1394,123 @@ impl TxBuilder { } fn create_tx_bytes(&self, instructions: &[Instruction]) -> Result> { - let mut tx = Transaction::new_with_payer(instructions, Some(&self.fee_payer)); + if instructions.is_empty() { + anyhow::bail!("No instructions to execute on Solana"); + } - let recent_blockhash = self - .target_solana_client + let recent_blockhash = self.get_recent_blockhash()?; + + self.alt_address.map_or_else( + || self.create_legacy_tx(instructions, recent_blockhash), + |alt_address| self.create_v0_tx_with_alt(instructions, recent_blockhash, alt_address), + ) + } + + fn get_recent_blockhash(&self) -> Result { + self.target_solana_client .get_latest_blockhash() - .map_err(|e| anyhow::anyhow!("Failed to get blockhash: {e}"))?; + .map_err(|e| anyhow::anyhow!("Failed to get blockhash: {e}")) + } + + fn create_v0_tx_with_alt( + &self, + instructions: &[Instruction], + recent_blockhash: solana_sdk::hash::Hash, + alt_address: Pubkey, + ) -> Result> { + tracing::info!( + "Building transaction with Address Lookup Table: {}", + alt_address + ); + let addresses = self.fetch_alt_addresses(alt_address)?; + + tracing::info!("ALT contains {} addresses", addresses.len()); + tracing::info!("ALT addresses: {:?}", addresses); + + let alt_account_for_compile = AddressLookupTableAccount { + key: alt_address, + addresses, + }; + + let v0_message = self.compile_v0_message_with_alt( + instructions, + recent_blockhash, + alt_account_for_compile, + )?; + + Self::log_v0_message_stats(&v0_message); + + Self::serialize_v0_transaction(v0_message) + } + + fn compile_v0_message_with_alt( + &self, + instructions: &[Instruction], + recent_blockhash: solana_sdk::hash::Hash, + alt_account: AddressLookupTableAccount, + ) -> Result { + v0::Message::try_compile( + &self.fee_payer, + instructions, + &[alt_account], + recent_blockhash, + ) + .map_err(|e| anyhow::anyhow!("Failed to compile v0 message with ALT: {e}")) + } + + fn serialize_v0_transaction(v0_message: v0::Message) -> Result> { + let num_signatures = v0_message.header.num_required_signatures as usize; + let versioned_tx = VersionedTransaction { + signatures: vec![solana_sdk::signature::Signature::default(); num_signatures], + message: VersionedMessage::V0(v0_message), + }; + + let serialized_tx = bincode::serialize(&versioned_tx)?; + tracing::warn!( + "Transaction size: {} bytes (limit: 1232 bytes raw, 1644 bytes encoded)", + serialized_tx.len() + ); + + Ok(serialized_tx) + } + + fn fetch_alt_addresses(&self, alt_address: Pubkey) -> Result> { + let alt_account = self + .target_solana_client + .get_account(&alt_address) + .map_err(|e| anyhow::anyhow!("Failed to fetch ALT account {}: {e}", alt_address))?; + + let lookup_table = AddressLookupTable::deserialize(&alt_account.data) + .map_err(|e| anyhow::anyhow!("Failed to deserialize ALT: {e}"))?; + + Ok(lookup_table.addresses.to_vec()) + } + + fn log_v0_message_stats(v0_message: &v0::Message) { + tracing::info!( + "Compiled v0 message: {} static accounts, {} ALT accounts", + v0_message.account_keys.len(), + v0_message + .address_table_lookups + .iter() + .map(|lookup| lookup.readonly_indexes.len() + lookup.writable_indexes.len()) + .sum::() + ); + + tracing::info!("Static account keys: {:?}", v0_message.account_keys); + } + + fn create_legacy_tx( + &self, + instructions: &[Instruction], + recent_blockhash: solana_sdk::hash::Hash, + ) -> Result> { + let mut tx = Transaction::new_with_payer(instructions, Some(&self.fee_payer)); tx.message.recent_blockhash = recent_blockhash; - Ok(bincode::serialize(&tx)?) + let versioned_tx = VersionedTransaction::from(tx); + Ok(bincode::serialize(&versioned_tx)?) } /// Create a new ICS07 Tendermint client on Solana diff --git a/packages/relayer/modules/solana-to-cosmos/src/lib.rs b/packages/relayer/modules/solana-to-cosmos/src/lib.rs index e28924482..2331a7fc3 100644 --- a/packages/relayer/modules/solana-to-cosmos/src/lib.rs +++ b/packages/relayer/modules/solana-to-cosmos/src/lib.rs @@ -60,9 +60,12 @@ pub struct SolanaToCosmosConfig { pub signer_address: String, /// The Solana ICS26 router program ID. pub solana_ics26_program_id: String, - /// Whether to run in mock mode. + /// Whether to use mock WASM client on Cosmos for testing. #[serde(default)] - pub mock: bool, + pub mock_wasm_client: bool, + /// Whether to use mock Solana light client updates for testing. + #[serde(default)] + pub mock_solana_client: bool, } impl SolanaToCosmosRelayerModuleService { @@ -78,7 +81,7 @@ impl SolanaToCosmosRelayerModuleService { let target_listener = cosmos_sdk::ChainListener::new(HttpClient::from_rpc_url(&config.target_rpc_url)); - let tx_builder = if config.mock { + let tx_builder = if config.mock_wasm_client { SolanaToCosmosTxBuilder::Mock(tx_builder::MockTxBuilder::new( src_listener.client().clone(), target_listener.client().clone(), diff --git a/programs/solana/Anchor.toml b/programs/solana/Anchor.toml index d28fb4348..57d1d2796 100644 --- a/programs/solana/Anchor.toml +++ b/programs/solana/Anchor.toml @@ -7,6 +7,7 @@ skip-lint = false [programs.localnet] ics07_tendermint = "8wQAC7oWLTxExhR49jYAzXZB39mu7WVVvkWJGgAMMjpV" ics26_router = "HsCyuYgKgoN9wUPiJyNZvvWg2N1uyZhDjvJfKJFu3jvU" +ics27_gmp = "FHsRDbCxXxv1wWsAFccaPz2BSphGEEcnVNHXCBTP92Am" [registry] url = "https://api.apr.dev" diff --git a/programs/solana/Cargo.lock b/programs/solana/Cargo.lock index 5f6c9818e..3f536ac09 100644 --- a/programs/solana/Cargo.lock +++ b/programs/solana/Cargo.lock @@ -195,7 +195,7 @@ dependencies = [ "anchor-syn", "anyhow", "bs58", - "heck", + "heck 0.3.3", "proc-macro2", "quote", "serde_json", @@ -269,7 +269,7 @@ checksum = "32e8599d21995f68e296265aa5ab0c3cef582fd58afec014d01bd0bce18a4418" dependencies = [ "anchor-lang-idl-spec", "anyhow", - "heck", + "heck 0.3.3", "regex", "serde", "serde_json", @@ -310,7 +310,7 @@ dependencies = [ "anyhow", "bs58", "cargo_toml", - "heck", + "heck 0.3.3", "proc-macro2", "quote", "serde", @@ -1160,6 +1160,7 @@ dependencies = [ "ibc-proto", "ics26-router", "prost", + "solana-ibc-macros", "solana-ibc-types", ] @@ -1260,6 +1261,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "feature-probe" version = "0.1.1" @@ -1296,6 +1313,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2551bf44bc5f776c15044b9b94153a00198be06743e262afaaa61f11ac7523a5" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.2" @@ -1475,6 +1498,19 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "gmp-counter-app" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "anchor-spl", + "bincode", + "mollusk-svm", + "mollusk-svm-bencher", + "solana-ibc-types", + "solana-sdk", +] + [[package]] name = "hash32" version = "0.3.1" @@ -1508,6 +1544,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1994,6 +2036,23 @@ dependencies = [ "solana-sdk", ] +[[package]] +name = "ics27-gmp" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "anchor-spl", + "bincode", + "gmp-counter-app", + "mollusk-svm", + "mollusk-svm-bencher", + "prost", + "prost-build", + "solana-ibc-macros", + "solana-ibc-types", + "solana-sdk", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -2276,6 +2335,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "litemap" version = "0.8.0" @@ -2356,6 +2421,7 @@ name = "mock-ibc-app" version = "0.1.0" dependencies = [ "anchor-lang", + "solana-ibc-macros", "solana-ibc-types", ] @@ -2459,6 +2525,12 @@ dependencies = [ "solana-rent", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "num" version = "0.2.1" @@ -2574,18 +2646,19 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" dependencies = [ "num_enum_derive", + "rustversion", ] [[package]] name = "num_enum_derive" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", @@ -2715,6 +2788,16 @@ dependencies = [ "num", ] +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2816,6 +2899,24 @@ dependencies = [ "prost-derive", ] +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck 0.5.0", + "itertools 0.14.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prost", + "prost-types", + "regex", + "tempfile", +] + [[package]] name = "prost-derive" version = "0.13.5" @@ -2829,6 +2930,15 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + [[package]] name = "qstring" version = "0.7.2" @@ -3143,6 +3253,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rustls" version = "0.23.27" @@ -3812,6 +3935,15 @@ dependencies = [ name = "solana-ibc-constants" version = "0.1.0" +[[package]] +name = "solana-ibc-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "solana-ibc-types" version = "0.1.0" @@ -4823,9 +4955,9 @@ dependencies = [ [[package]] name = "solana-zk-sdk" -version = "2.3.5" +version = "2.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a74a5cf6fd9ba73c1604ac2523976e10a1416c2867c512a86db4ff01f1ad48" +checksum = "97b9fc6ec37d16d0dccff708ed1dd6ea9ba61796700c3bb7c3b401973f10f63b" dependencies = [ "aes-gcm-siv", "base64 0.22.1", @@ -4918,9 +5050,9 @@ dependencies = [ [[package]] name = "spl-discriminator-syn" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c1f05593b7ca9eac7caca309720f2eafb96355e037e6d373b909a80fe7b69b9" +checksum = "5d1dbc82ab91422345b6df40a79e2b78c7bce1ebb366da323572dd60b7076b67" dependencies = [ "proc-macro2", "quote", @@ -5261,6 +5393,19 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "tendermint" version = "0.40.4" diff --git a/programs/solana/Cargo.toml b/programs/solana/Cargo.toml index 7529acb1a..82ee0f44f 100644 --- a/programs/solana/Cargo.toml +++ b/programs/solana/Cargo.toml @@ -2,12 +2,14 @@ members = [ "programs/ics07-tendermint", "programs/ics26-router", + "programs/ics27-gmp", + "programs/gmp-counter-app", "programs/mock-light-client", "programs/mock-ibc-app", "programs/dummy-ibc-app", "packages/solana-ibc-types", "packages/ics25-handler", - "packages/solana-ibc-constants", + "packages/solana-ibc-constants", "packages/solana-ibc-macros", ] resolver = "2" @@ -63,6 +65,7 @@ keywords = [ [workspace.dependencies] anchor-lang = { version = "0.31.1", default-features = false } +anchor-spl = { version = "0.31.1" } base64 = "0.22" tendermint-light-client-update-client = { path = "../../packages/tendermint-light-client/update-client", default-features = false } @@ -72,6 +75,7 @@ tendermint-light-client-uc-and-membership = { path = "../../packages/tendermint- solana-ibc-types = { path = "./packages/solana-ibc-types", default-features = false } ics25-handler = { path = "./packages/ics25-handler", default-features = false } solana-ibc-constants = { path = "./packages/solana-ibc-constants", default-features = false } +solana-ibc-macros = { path = "./packages/solana-ibc-macros" } ics26-router = { path = "./programs/ics26-router", default-features = false } ibc-client-tendermint = { version = "0.57", default-features = false } @@ -90,6 +94,9 @@ thiserror = { version = "2.0", default-features = false } hex = { version = "0.4", default-features = false } prost = { version = "0.13", default-features = false } +# Build dependencies +prost-build = { version = "0.13", default-features = false } + # Testing mollusk-svm = { version = "0.4.1", default-features = false } mollusk-svm-bencher = { version = "0.4.1", default-features = false } @@ -99,10 +106,21 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" bincode = "1.3" +[profile.dev] +debug = 0 +debug-assertions = true +overflow-checks = true +lto = "thin" +codegen-units = 1 +panic = "abort" +strip = "symbols" + [profile.release] overflow-checks = true lto = "fat" codegen-units = 1 +debug = 0 +strip = "symbols" [profile.release.build-override] opt-level = 3 incremental = false diff --git a/programs/solana/packages/solana-ibc-macros/Cargo.toml b/programs/solana/packages/solana-ibc-macros/Cargo.toml new file mode 100644 index 000000000..59866e091 --- /dev/null +++ b/programs/solana/packages/solana-ibc-macros/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "solana-ibc-macros" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +keywords.workspace = true +categories = ["cryptography::cryptocurrencies", "development-tools"] +description = "Procedural macros for IBC on Solana" +readme = "README.md" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", features = ["full", "extra-traits"] } + +[lints] +workspace = true diff --git a/programs/solana/packages/solana-ibc-macros/README.md b/programs/solana/packages/solana-ibc-macros/README.md new file mode 100644 index 000000000..a0dc7fc23 --- /dev/null +++ b/programs/solana/packages/solana-ibc-macros/README.md @@ -0,0 +1,48 @@ +# solana-ibc-macros + +Procedural macros for IBC applications on Solana. + +## Overview + +This crate provides the `#[ibc_app]` macro which validates that IBC applications implement all required callback functions with correct signatures at compile time. + +## Usage + +```rust +use solana_ibc_macros::ibc_app; + +#[ibc_app] +pub mod my_ibc_app { + use super::*; + + pub fn on_recv_packet( + ctx: Context, + msg: OnRecvPacketMsg, + ) -> Result> { + // Handle received packet + Ok(vec![]) + } + + pub fn on_acknowledgement_packet( + ctx: Context, + msg: OnAcknowledgementPacketMsg, + ) -> Result<()> { + // Handle acknowledgement + Ok(()) + } + + pub fn on_timeout_packet( + ctx: Context, + msg: OnTimeoutPacketMsg, + ) -> Result<()> { + // Handle timeout + Ok(()) + } +} +``` + +## Features + +- Compile-time validation of IBC callback function names and signatures +- Automatic generation of instruction discriminators +- Clear error messages for missing or misnamed callbacks diff --git a/programs/solana/packages/solana-ibc-macros/src/lib.rs b/programs/solana/packages/solana-ibc-macros/src/lib.rs new file mode 100644 index 000000000..800726c3b --- /dev/null +++ b/programs/solana/packages/solana-ibc-macros/src/lib.rs @@ -0,0 +1,424 @@ +//! Procedural macros for IBC applications on Solana +//! +//! This crate provides the `#[ibc_app]` macro which validates that IBC applications +//! implement all required callback functions with correct signatures. + +use proc_macro::TokenStream; +use quote::quote; +use std::collections::HashMap; +use syn::{parse_macro_input, FnArg, ItemFn, ItemMod, ReturnType, Type, TypePath}; + +// ============================================================================ +// Constants & Types +// ============================================================================ + +/// Configuration for each IBC callback +#[derive(Debug, Clone)] +struct CallbackConfig { + /// The function name + name: &'static str, + /// Expected message type name + msg_type: &'static str, + /// Expected return types (multiple allowed for flexibility) + return_types: &'static [ReturnTypeExpectation], + /// Discriminator name (for compile-time checks) + discriminator_name: &'static str, +} + +#[derive(Debug, Clone)] +enum ReturnTypeExpectation { + ResultUnit, + ResultVecU8, +} + +impl CallbackConfig { + const fn new( + name: &'static str, + msg_type: &'static str, + return_types: &'static [ReturnTypeExpectation], + discriminator_name: &'static str, + ) -> Self { + Self { + name, + msg_type, + return_types, + discriminator_name, + } + } +} + +/// IBC callback configurations +const IBC_CALLBACKS: &[CallbackConfig] = &[ + CallbackConfig::new( + "on_recv_packet", + "OnRecvPacketMsg", + &[ + ReturnTypeExpectation::ResultVecU8, + ReturnTypeExpectation::ResultUnit, + ], + "OnRecvPacket", + ), + CallbackConfig::new( + "on_acknowledgement_packet", + "OnAcknowledgementPacketMsg", + &[ReturnTypeExpectation::ResultUnit], + "OnAcknowledgementPacket", + ), + CallbackConfig::new( + "on_timeout_packet", + "OnTimeoutPacketMsg", + &[ReturnTypeExpectation::ResultUnit], + "OnTimeoutPacket", + ), +]; + +// ============================================================================ +// Main Macro Entry Point +// ============================================================================ + +/// Attribute macro for IBC applications +/// +/// This macro wraps Anchor's `#[program]` macro and adds compile-time validation +/// to ensure all required IBC callback functions are implemented with correct names. +/// +/// # Required Callbacks +/// +/// Your IBC app MUST implement these three functions: +/// +/// 1. `on_recv_packet` - Handle incoming packets from counterparty chain +/// 2. `on_acknowledgement_packet` - Handle acknowledgements for sent packets (NOT `on_ack_packet`) +/// 3. `on_timeout_packet` - Handle timeouts for sent packets +/// +/// # Example +/// +/// ```ignore +/// use solana_ibc_macros::ibc_app; +/// +/// declare_id!("..."); +/// +/// #[ibc_app] +/// pub mod my_ibc_app { +/// use super::*; +/// +/// pub fn on_recv_packet<'info>( +/// ctx: Context<'_, '_, '_, 'info, OnRecvPacket<'info>>, +/// msg: OnRecvPacketMsg, +/// ) -> Result> { +/// // Handle received packet +/// Ok(vec![]) +/// } +/// +/// pub fn on_acknowledgement_packet( +/// ctx: Context, +/// msg: OnAcknowledgementPacketMsg, +/// ) -> Result<()> { +/// // Handle acknowledgement +/// Ok(()) +/// } +/// +/// pub fn on_timeout_packet( +/// ctx: Context, +/// msg: OnTimeoutPacketMsg, +/// ) -> Result<()> { +/// // Handle timeout +/// Ok(()) +/// } +/// } +/// ``` +#[proc_macro_attribute] +pub fn ibc_app(_attr: TokenStream, item: TokenStream) -> TokenStream { + let module = parse_macro_input!(item as ItemMod); + + // Validate that all required callbacks are present with correct signatures + let validator = CallbackValidator::new(&module); + if let Err(e) = validator.validate() { + return e.to_compile_error().into(); + } + + // Generate output with compile-time assertions + generate_output(module) +} + +// ============================================================================ +// Validation Logic +// ============================================================================ + +struct CallbackValidator<'a> { + module: &'a ItemMod, + functions: HashMap, +} + +impl<'a> CallbackValidator<'a> { + fn new(module: &'a ItemMod) -> Self { + let functions = Self::collect_functions(module); + Self { module, functions } + } + + fn validate(&self) -> syn::Result<()> { + // Ensure module has content + self.validate_module_content()?; + + // Check for missing callbacks + self.validate_missing_callbacks()?; + + // Validate signatures of present callbacks + self.validate_callback_signatures()?; + + Ok(()) + } + + fn collect_functions(module: &'a ItemMod) -> HashMap { + let mut functions = HashMap::new(); + + if let Some((_, items)) = &module.content { + for item in items { + if let syn::Item::Fn(item_fn) = item { + functions.insert(item_fn.sig.ident.to_string(), item_fn); + } + } + } + + functions + } + + fn validate_module_content(&self) -> syn::Result<()> { + if self.module.content.is_none() { + return Err(syn::Error::new_spanned( + self.module, + "IBC app module must have a body with callback implementations", + )); + } + Ok(()) + } + + fn validate_missing_callbacks(&self) -> syn::Result<()> { + let missing_callbacks: Vec<_> = IBC_CALLBACKS + .iter() + .filter(|cb| !self.functions.contains_key(cb.name)) + .collect(); + + if missing_callbacks.is_empty() { + return Ok(()); + } + + let error_msg = self.build_missing_callbacks_error(&missing_callbacks); + Err(syn::Error::new_spanned(self.module, error_msg)) + } + + fn build_missing_callbacks_error(&self, missing: &[&CallbackConfig]) -> String { + let missing_names: Vec<_> = missing.iter().map(|cb| cb.name).collect(); + let mut error_msg = format!( + "IBC app is missing required callback function(s): {}", + missing_names.join(", ") + ); + + // Check for common naming mistakes + if let Some(suggestion) = self.check_common_mistakes(&missing_names) { + error_msg.push_str(&suggestion); + } + + error_msg + } + + fn check_common_mistakes(&self, missing: &[&str]) -> Option { + // Check for shortened acknowledgement callback name + if missing.contains(&"on_acknowledgement_packet") + && self.functions.contains_key("on_ack_packet") + { + return Some( + "\n\nFound 'on_ack_packet' but expected 'on_acknowledgement_packet'.\n\ + The router expects the full name 'on_acknowledgement_packet', not 'on_ack_packet'.".into() + ); + } + + // Could add more common mistake patterns here + None + } + + fn validate_callback_signatures(&self) -> syn::Result<()> { + for callback in IBC_CALLBACKS { + if let Some(item_fn) = self.functions.get(callback.name) { + SignatureValidator::new(callback, item_fn).validate()?; + } + } + Ok(()) + } +} + +// ============================================================================ +// Signature Validation +// ============================================================================ + +struct SignatureValidator<'a> { + config: &'a CallbackConfig, + item_fn: &'a ItemFn, +} + +impl<'a> SignatureValidator<'a> { + const fn new(config: &'a CallbackConfig, item_fn: &'a ItemFn) -> Self { + Self { config, item_fn } + } + + fn validate(&self) -> syn::Result<()> { + self.validate_parameter_count()?; + self.validate_return_type()?; + self.validate_message_parameter()?; + Ok(()) + } + + fn validate_parameter_count(&self) -> syn::Result<()> { + let sig = &self.item_fn.sig; + if sig.inputs.len() != 2 { + return Err(syn::Error::new_spanned( + &sig.inputs, + format!( + "Callback '{}' must have exactly 2 parameters (ctx: Context<...>, msg: {}), found {}", + self.config.name, + self.config.msg_type, + sig.inputs.len() + ), + )); + } + Ok(()) + } + + fn validate_return_type(&self) -> syn::Result<()> { + let sig = &self.item_fn.sig; + let return_type = &sig.output; + + let is_valid = self + .config + .return_types + .iter() + .any(|expected| match expected { + ReturnTypeExpectation::ResultUnit => is_result_unit(return_type), + ReturnTypeExpectation::ResultVecU8 => is_result_vec_u8(return_type), + }); + + if !is_valid { + let expected_types = self.format_expected_return_types(); + return Err(syn::Error::new_spanned( + return_type, + format!( + "Callback '{}' must return {}, found '{}'", + self.config.name, + expected_types, + quote::quote!(#return_type) + ), + )); + } + + Ok(()) + } + + fn format_expected_return_types(&self) -> String { + let type_strings: Vec<_> = self + .config + .return_types + .iter() + .map(|rt| match rt { + ReturnTypeExpectation::ResultUnit => "'Result<()>'", + ReturnTypeExpectation::ResultVecU8 => "'Result>'", + }) + .collect(); + + if type_strings.len() == 1 { + type_strings[0].to_string() + } else { + format!("one of: {}", type_strings.join(", ")) + } + } + + fn validate_message_parameter(&self) -> syn::Result<()> { + let sig = &self.item_fn.sig; + + if let Some(FnArg::Typed(pat_type)) = sig.inputs.iter().nth(1) { + if !type_ends_with(&pat_type.ty, self.config.msg_type) { + return Err(syn::Error::new_spanned( + &pat_type.ty, + format!( + "Callback '{}' second parameter must be of type '{}', found '{}'", + self.config.name, + self.config.msg_type, + quote::quote!(#(&pat_type.ty)) + ), + )); + } + } + + Ok(()) + } +} + +// ============================================================================ +// Type Checking Utilities +// ============================================================================ + +/// Check if return type is Result> +fn is_result_vec_u8(return_type: &ReturnType) -> bool { + matches_result_type(return_type) +} + +/// Check if return type is Result<()> +fn is_result_unit(return_type: &ReturnType) -> bool { + matches_result_type(return_type) +} + +/// Generic check if return type is a Result +fn matches_result_type(return_type: &ReturnType) -> bool { + if let ReturnType::Type(_, ty) = return_type { + if let Type::Path(TypePath { path, .. }) = &**ty { + if let Some(segment) = path.segments.last() { + return segment.ident == "Result"; + } + } + } + false +} + +/// Check if a type path ends with the expected identifier +fn type_ends_with(ty: &Type, expected: &str) -> bool { + if let Type::Path(TypePath { path, .. }) = ty { + if let Some(segment) = path.segments.last() { + return segment.ident == expected; + } + } + false +} + +// ============================================================================ +// Code Generation +// ============================================================================ + +fn generate_output(module: ItemMod) -> TokenStream { + let discriminator_checks = generate_discriminator_checks(); + + let output = quote! { + #[::anchor_lang::program] + #module + + // Compile-time check that instruction discriminators exist with correct names + const _: () = { + use ::anchor_lang::Discriminator; + #discriminator_checks + }; + }; + + TokenStream::from(output) +} + +fn generate_discriminator_checks() -> proc_macro2::TokenStream { + let checks = IBC_CALLBACKS.iter().map(|callback| { + let discriminator_path = format!("crate::instruction::{}", callback.discriminator_name); + let discriminator_ident: proc_macro2::TokenStream = discriminator_path.parse().unwrap(); + + quote! { + // Verify #callback.name discriminator exists + let _ = #discriminator_ident::DISCRIMINATOR; + } + }); + + quote! { + #(#checks)* + } +} diff --git a/programs/solana/packages/solana-ibc-types/src/ibc_app_interface.rs b/programs/solana/packages/solana-ibc-types/src/ibc_app_interface.rs new file mode 100644 index 000000000..b95ee4f60 --- /dev/null +++ b/programs/solana/packages/solana-ibc-types/src/ibc_app_interface.rs @@ -0,0 +1,23 @@ +//! IBC App Interface +//! +//! This module defines the trait that all IBC applications must implement +//! to be compatible with the ICS26 router. +//! +//! By implementing this trait, apps ensure they have all required callback +//! functions with the correct signatures at compile time. + +/// Standard instruction names for IBC app callbacks +/// These MUST match the function names in your #[ibc_app] module +pub mod instruction_names { + /// Instruction name for receiving packets + /// Your #[program] function MUST be named: `on_recv_packet` + pub const ON_RECV_PACKET: &str = "global:on_recv_packet"; + + /// Instruction name for acknowledgement callbacks + /// Your #[program] function MUST be named: `on_acknowledgement_packet` + pub const ON_ACKNOWLEDGEMENT_PACKET: &str = "global:on_acknowledgement_packet"; + + /// Instruction name for timeout callbacks + /// Your #[program] function MUST be named: `on_timeout_packet` + pub const ON_TIMEOUT_PACKET: &str = "global:on_timeout_packet"; +} diff --git a/programs/solana/packages/solana-ibc-types/src/lib.rs b/programs/solana/packages/solana-ibc-types/src/lib.rs index 0d78859ef..a9a3e26af 100644 --- a/programs/solana/packages/solana-ibc-types/src/lib.rs +++ b/programs/solana/packages/solana-ibc-types/src/lib.rs @@ -6,6 +6,7 @@ pub mod app_msgs; pub mod events; +pub mod ibc_app_interface; pub mod ics07; pub mod pda; pub mod router; @@ -33,3 +34,5 @@ pub use events::{ AckPacketEvent, ClientAddedEvent, ClientStatusUpdatedEvent, IBCAppAdded, NoopEvent, SendPacketEvent, TimeoutPacketEvent, WriteAcknowledgementEvent, }; + +pub use ibc_app_interface::instruction_names; diff --git a/programs/solana/programs/dummy-ibc-app/Cargo.toml b/programs/solana/programs/dummy-ibc-app/Cargo.toml index fb8c8adc1..942d18f75 100644 --- a/programs/solana/programs/dummy-ibc-app/Cargo.toml +++ b/programs/solana/programs/dummy-ibc-app/Cargo.toml @@ -27,6 +27,7 @@ default = [] [dependencies] anchor-lang = { workspace = true, features = ["init-if-needed"] } solana-ibc-types.workspace = true +solana-ibc-macros.workspace = true ics26-router = { workspace = true, features = ["cpi"] } prost = { workspace = true } ibc-proto = { workspace = true } diff --git a/programs/solana/programs/dummy-ibc-app/src/instructions/send_packet.rs b/programs/solana/programs/dummy-ibc-app/src/instructions/send_packet.rs index c66dac893..40e7ddd8b 100644 --- a/programs/solana/programs/dummy-ibc-app/src/instructions/send_packet.rs +++ b/programs/solana/programs/dummy-ibc-app/src/instructions/send_packet.rs @@ -84,9 +84,6 @@ pub struct SendPacket<'info> { pub system_program: Program<'info, System>, - /// Clock sysvar for timeout validation - pub clock: Sysvar<'info, Clock>, - /// PDA that acts as the router caller for CPI calls to the IBC router. #[account( seeds = [ROUTER_CALLER_SEED], @@ -97,7 +94,8 @@ pub struct SendPacket<'info> { pub fn send_packet(ctx: Context, msg: SendPacketMsg) -> Result<()> { let app_state = &mut ctx.accounts.app_state; - let clock = &ctx.accounts.clock; + // Get clock directly via syscall + let clock = Clock::get()?; // Validate timeout if msg.timeout_timestamp <= clock.unix_timestamp { @@ -127,7 +125,6 @@ pub fn send_packet(ctx: Context, msg: SendPacketMsg) -> Result<()> { app_caller: ctx.accounts.router_caller.to_account_info(), payer: ctx.accounts.user.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), - clock: ctx.accounts.clock.to_account_info(), client: ctx.accounts.client.to_account_info(), }; diff --git a/programs/solana/programs/dummy-ibc-app/src/instructions/send_transfer.rs b/programs/solana/programs/dummy-ibc-app/src/instructions/send_transfer.rs index ef6826a9e..a733d282c 100644 --- a/programs/solana/programs/dummy-ibc-app/src/instructions/send_transfer.rs +++ b/programs/solana/programs/dummy-ibc-app/src/instructions/send_transfer.rs @@ -105,9 +105,6 @@ pub struct SendTransfer<'info> { pub system_program: Program<'info, System>, - /// Clock sysvar for timeout validation - pub clock: Sysvar<'info, Clock>, - /// PDA that acts as the router caller for CPI calls to the IBC router. #[account( seeds = [ROUTER_CALLER_SEED], @@ -118,7 +115,8 @@ pub struct SendTransfer<'info> { pub fn send_transfer(ctx: Context, msg: SendTransferMsg) -> Result<()> { let app_state = &mut ctx.accounts.app_state; - let clock = &ctx.accounts.clock; + // Get clock directly via syscall + let clock = Clock::get()?; // No need to validate router_caller since it's a PDA derived by Anchor @@ -216,7 +214,6 @@ pub fn send_transfer(ctx: Context, msg: SendTransferMsg) -> Result app_caller: ctx.accounts.router_caller.to_account_info(), payer: ctx.accounts.user.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), - clock: ctx.accounts.clock.to_account_info(), client: ctx.accounts.client.to_account_info(), }; diff --git a/programs/solana/programs/dummy-ibc-app/src/lib.rs b/programs/solana/programs/dummy-ibc-app/src/lib.rs index c4d8ef038..6c0f59c28 100644 --- a/programs/solana/programs/dummy-ibc-app/src/lib.rs +++ b/programs/solana/programs/dummy-ibc-app/src/lib.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use solana_ibc_macros::ibc_app; use solana_ibc_types::{OnAcknowledgementPacketMsg, OnRecvPacketMsg, OnTimeoutPacketMsg}; declare_id!("5E73beFMq9QZvbwPN5i84psh2WcyJ9PgqF4avBaRDgCC"); @@ -26,7 +27,7 @@ pub use state::{PACKETS_ACKNOWLEDGED_OFFSET, PACKETS_RECEIVED_OFFSET, PACKETS_TI /// - `on_acknowledgement_packet`: Handles packet acknowledgements /// - `on_timeout_packet`: Handles packet timeouts /// -#[program] +#[ibc_app] pub mod dummy_ibc_app { use super::*; diff --git a/programs/solana/programs/gmp-counter-app/Cargo.toml b/programs/solana/programs/gmp-counter-app/Cargo.toml new file mode 100644 index 000000000..1e7d9d413 --- /dev/null +++ b/programs/solana/programs/gmp-counter-app/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "gmp-counter-app" +version.workspace = true +description = "GMP Counter App for testing cross-chain calls" +edition.workspace = true +license.workspace = true +repository.workspace = true +readme = "../../../README.md" +keywords = ["cosmos", "ibc", "solana", "gmp", "counter", "anchor"] +categories = ["cryptography", "blockchain"] + +[lints] +workspace = true + +[lib] +crate-type = ["cdylib", "lib"] +name = "gmp_counter_app" + +[features] +default = [] +cpi = ["no-entrypoint"] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] + +[dependencies] +anchor-lang = { workspace = true, features = ["init-if-needed"] } +anchor-spl.workspace = true +solana-ibc-types.workspace = true + +[dev-dependencies] +mollusk-svm.workspace = true +mollusk-svm-bencher.workspace = true +solana-sdk.workspace = true +bincode.workspace = true \ No newline at end of file diff --git a/programs/solana/programs/gmp-counter-app/Xargo.toml b/programs/solana/programs/gmp-counter-app/Xargo.toml new file mode 100644 index 000000000..1744f098a --- /dev/null +++ b/programs/solana/programs/gmp-counter-app/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/programs/solana/programs/gmp-counter-app/src/errors.rs b/programs/solana/programs/gmp-counter-app/src/errors.rs new file mode 100644 index 000000000..403b49ec7 --- /dev/null +++ b/programs/solana/programs/gmp-counter-app/src/errors.rs @@ -0,0 +1,22 @@ +use anchor_lang::prelude::*; + +#[error_code] +pub enum CounterError { + #[msg("Counter overflow occurred")] + CounterOverflow, + + #[msg("Counter underflow occurred")] + CounterUnderflow, + + #[msg("Invalid payload format")] + InvalidPayload, + + #[msg("Unauthorized GMP caller")] + UnauthorizedGMPCaller, + + #[msg("Counter not found for user")] + CounterNotFound, + + #[msg("Invalid instruction in payload")] + InvalidInstruction, +} diff --git a/programs/solana/programs/gmp-counter-app/src/instructions/counter_ops.rs b/programs/solana/programs/gmp-counter-app/src/instructions/counter_ops.rs new file mode 100644 index 000000000..b4b19d73b --- /dev/null +++ b/programs/solana/programs/gmp-counter-app/src/instructions/counter_ops.rs @@ -0,0 +1,134 @@ +use crate::errors::*; +use crate::state::*; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::program::set_return_data; + +/// Increment a user's counter +/// Note: `user_authority` must be a signer to ensure only the legitimate owner can increment their counter +#[derive(Accounts)] +pub struct IncrementCounter<'info> { + #[account( + mut, + seeds = [CounterAppState::SEED], + bump = app_state.bump + )] + pub app_state: Account<'info, CounterAppState>, + + #[account( + init_if_needed, + payer = payer, + space = UserCounter::INIT_SPACE, + seeds = [UserCounter::SEED, user_authority.key().as_ref()], + bump + )] + pub user_counter: Account<'info, UserCounter>, + + /// The user authority (`account_state` PDA for ICS27) + /// MUST be a signer to authorize operations on this user's counter + pub user_authority: Signer<'info>, + + #[account(mut)] + pub payer: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +/// Decrement a user's counter +#[derive(Accounts)] +#[instruction(user: Pubkey)] +pub struct DecrementCounter<'info> { + #[account( + mut, + seeds = [CounterAppState::SEED], + bump = app_state.bump + )] + pub app_state: Account<'info, CounterAppState>, + + #[account( + mut, + seeds = [UserCounter::SEED, user.as_ref()], + bump = user_counter.bump + )] + pub user_counter: Account<'info, UserCounter>, +} + +/// Get a user's counter value +#[derive(Accounts)] +#[instruction(user: Pubkey)] +pub struct GetCounter<'info> { + #[account( + seeds = [UserCounter::SEED, user.as_ref()], + bump = user_counter.bump + )] + pub user_counter: Account<'info, UserCounter>, +} + +pub fn increment(ctx: Context, amount: u64) -> Result<()> { + let app_state = &mut ctx.accounts.app_state; + let user_counter = &mut ctx.accounts.user_counter; + let user_authority = ctx.accounts.user_authority.key(); + let clock = Clock::get()?; + + // Initialize counter if it's new + if user_counter.user == Pubkey::default() { + user_counter.user = user_authority; + user_counter.count = 0; + user_counter.increments = 0; + user_counter.decrements = 0; + user_counter.bump = ctx.bumps.user_counter; + app_state.total_counters = app_state.total_counters.saturating_add(1); + } + + // Increment the counter + user_counter.increment(amount, clock.unix_timestamp)?; + + msg!( + "Incremented counter for user {} by {} to {}", + user_authority, + amount, + user_counter.count + ); + + // Return the new counter value + let result = user_counter.count.to_le_bytes(); + set_return_data(&result); + + Ok(()) +} + +pub fn decrement(ctx: Context, user: Pubkey, amount: u64) -> Result<()> { + let user_counter = &mut ctx.accounts.user_counter; + let clock = Clock::get()?; + + require!(user_counter.user == user, CounterError::CounterNotFound); + + // Decrement the counter + user_counter.decrement(amount, clock.unix_timestamp)?; + + msg!( + "Decremented counter for user {} by {} to {}", + user, + amount, + user_counter.count + ); + + // Return the new counter value + let result = user_counter.count.to_le_bytes(); + set_return_data(&result); + + Ok(()) +} + +pub fn get_counter(ctx: Context, user: Pubkey) -> Result<()> { + let user_counter = &ctx.accounts.user_counter; + + require!(user_counter.user == user, CounterError::CounterNotFound); + + msg!("Counter for user {}: {}", user, user_counter.count); + + // Return the counter value + let result = user_counter.count.to_le_bytes(); + set_return_data(&result); + + Ok(()) +} diff --git a/programs/solana/programs/gmp-counter-app/src/instructions/initialize.rs b/programs/solana/programs/gmp-counter-app/src/instructions/initialize.rs new file mode 100644 index 000000000..02c3908ed --- /dev/null +++ b/programs/solana/programs/gmp-counter-app/src/instructions/initialize.rs @@ -0,0 +1,32 @@ +use crate::state::*; +use anchor_lang::prelude::*; + +/// Initialize the counter app +#[derive(Accounts)] +pub struct Initialize<'info> { + #[account( + init, + payer = payer, + space = 8 + CounterAppState::INIT_SPACE, + seeds = [CounterAppState::SEED], + bump + )] + pub app_state: Account<'info, CounterAppState>, + + #[account(mut)] + pub payer: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +pub fn initialize(ctx: Context, authority: Pubkey) -> Result<()> { + let app_state = &mut ctx.accounts.app_state; + + app_state.authority = authority; + app_state.total_counters = 0; + app_state.total_gmp_calls = 0; + app_state.bump = ctx.bumps.app_state; + + msg!("Counter App initialized with authority: {}", authority); + Ok(()) +} diff --git a/programs/solana/programs/gmp-counter-app/src/instructions/mod.rs b/programs/solana/programs/gmp-counter-app/src/instructions/mod.rs new file mode 100644 index 000000000..0e765fb5e --- /dev/null +++ b/programs/solana/programs/gmp-counter-app/src/instructions/mod.rs @@ -0,0 +1,5 @@ +pub mod counter_ops; +pub mod initialize; + +pub use counter_ops::*; +pub use initialize::*; diff --git a/programs/solana/programs/gmp-counter-app/src/lib.rs b/programs/solana/programs/gmp-counter-app/src/lib.rs new file mode 100644 index 000000000..0d90fa336 --- /dev/null +++ b/programs/solana/programs/gmp-counter-app/src/lib.rs @@ -0,0 +1,46 @@ +use anchor_lang::prelude::*; + +declare_id!("GdEUjpVtKvHKStM3Hph6PnLSUMsJXvcVqugubhtQ5QUD"); + +pub mod errors; +pub mod instructions; +pub mod state; + +use instructions::*; + +/// GMP Counter App Program +/// +/// This program demonstrates a simple counter application that can be called +/// via the ICS27 GMP program through cross-chain IBC messages. +/// +/// It provides: +/// - `initialize`: Initialize the counter app +/// - `increment`: Increment a user's counter (called by GMP) +/// - `decrement`: Decrement a user's counter (called by GMP) +/// - `get_counter`: Get a user's current counter value +/// +#[program] +pub mod gmp_counter_app { + use super::*; + + /// Initialize the counter app + pub fn initialize(ctx: Context, authority: Pubkey) -> Result<()> { + instructions::initialize(ctx, authority) + } + + /// Increment a user's counter (typically called by GMP program) + /// The user is identified by the `user_authority` signer (ICS27 `account_state` PDA) + pub fn increment(ctx: Context, amount: u64) -> Result<()> { + instructions::increment(ctx, amount) + } + + /// Decrement a user's counter (typically called by GMP program) + pub fn decrement(ctx: Context, user: Pubkey, amount: u64) -> Result<()> { + instructions::decrement(ctx, user, amount) + } + + /// Get a user's counter value + pub fn get_counter(ctx: Context, user: Pubkey) -> Result<()> { + instructions::get_counter(ctx, user) + } +} diff --git a/programs/solana/programs/gmp-counter-app/src/state.rs b/programs/solana/programs/gmp-counter-app/src/state.rs new file mode 100644 index 000000000..9c0f76e25 --- /dev/null +++ b/programs/solana/programs/gmp-counter-app/src/state.rs @@ -0,0 +1,96 @@ +use anchor_lang::prelude::*; + +/// Global counter app state +#[account] +pub struct CounterAppState { + /// Authority that can manage the app + pub authority: Pubkey, + /// Total number of user counters created + pub total_counters: u64, + /// Total number of GMP calls processed + pub total_gmp_calls: u64, + /// Program bump seed + pub bump: u8, +} + +impl CounterAppState { + pub const SEED: &'static [u8] = b"counter_app_state"; + pub const INIT_SPACE: usize = 8 + // discriminator + 32 + // authority + 8 + // total_counters + 8 + // total_gmp_calls + 1; // bump +} + +/// Per-user counter state +#[account] +pub struct UserCounter { + /// User's public key + pub user: Pubkey, + /// Current counter value + pub count: u64, + /// Number of increments + pub increments: u64, + /// Number of decrements + pub decrements: u64, + /// Last updated timestamp + pub last_updated: i64, + /// PDA bump seed + pub bump: u8, +} + +impl UserCounter { + pub const SEED: &'static [u8] = b"user_counter"; + pub const INIT_SPACE: usize = 8 + // discriminator + 32 + // user + 8 + // count + 8 + // increments + 8 + // decrements + 8 + // last_updated + 1; // bump + + pub fn increment(&mut self, amount: u64, current_time: i64) -> Result<()> { + self.count = self + .count + .checked_add(amount) + .ok_or(crate::errors::CounterError::CounterOverflow)?; + self.increments = self.increments.saturating_add(1); + self.last_updated = current_time; + Ok(()) + } + + pub fn decrement(&mut self, amount: u64, current_time: i64) -> Result<()> { + self.count = self + .count + .checked_sub(amount) + .ok_or(crate::errors::CounterError::CounterUnderflow)?; + self.decrements = self.decrements.saturating_add(1); + self.last_updated = current_time; + Ok(()) + } +} + +/// GMP callback data for tracking calls +#[account] +pub struct GMPCallState { + /// User who initiated the call + pub user: Pubkey, + /// Original payload hash + pub payload_hash: [u8; 32], + /// Call timestamp + pub timestamp: i64, + /// Success status + pub success: bool, + /// PDA bump seed + pub bump: u8, +} + +impl GMPCallState { + pub const SEED: &'static [u8] = b"gmp_call_state"; + pub const INIT_SPACE: usize = 8 + // discriminator + 32 + // user + 32 + // payload_hash + 8 + // timestamp + 1 + // success + 1; // bump +} diff --git a/programs/solana/programs/gmp-counter-app/tests/counter_tests.rs b/programs/solana/programs/gmp-counter-app/tests/counter_tests.rs new file mode 100644 index 000000000..538513a37 --- /dev/null +++ b/programs/solana/programs/gmp-counter-app/tests/counter_tests.rs @@ -0,0 +1,327 @@ +use anchor_lang::{AnchorSerialize, Discriminator, InstructionData}; +use gmp_counter_app::{state::*, ID}; +use mollusk_svm::Mollusk; +use solana_sdk::{ + account::Account, + instruction::{AccountMeta, Instruction}, + native_loader, + pubkey::Pubkey, + system_program, +}; + +const fn get_gmp_counter_program_path() -> &'static str { + "../../target/deploy/gmp_counter_app" +} + +// Helper functions for account preparation +fn create_counter_app_state_account( + pubkey: Pubkey, + authority: Pubkey, + total_counters: u64, + total_gmp_calls: u64, + bump: u8, +) -> (Pubkey, Account) { + let app_state = CounterAppState { + authority, + total_counters, + total_gmp_calls, + bump, + }; + + let mut data = Vec::new(); + data.extend_from_slice(CounterAppState::DISCRIMINATOR); + app_state.serialize(&mut data).unwrap(); + + ( + pubkey, + Account { + lamports: 1_000_000, // Standard lamports for initialized accounts + data, + owner: ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +fn create_user_counter_account( + pubkey: Pubkey, + user: Pubkey, + count: u64, + increments: u64, + decrements: u64, + last_updated: i64, + bump: u8, +) -> (Pubkey, Account) { + let user_counter = UserCounter { + user, + count, + increments, + decrements, + last_updated, + bump, + }; + + let mut data = Vec::new(); + data.extend_from_slice(UserCounter::DISCRIMINATOR); + user_counter.serialize(&mut data).unwrap(); + + ( + pubkey, + Account { + lamports: 1_000_000, + data, + owner: ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +fn create_uninitialized_account(pubkey: Pubkey) -> (Pubkey, Account) { + ( + pubkey, + Account { + lamports: { + use solana_sdk::rent::Rent; + let account_size = 8 + UserCounter::INIT_SPACE; + Rent::default().minimum_balance(account_size) + }, + data: vec![], + owner: system_program::ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +const fn create_pda_for_init(pubkey: Pubkey) -> (Pubkey, Account) { + ( + pubkey, + Account { + lamports: 0, + data: vec![], + owner: system_program::ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +const fn create_payer_account(pubkey: Pubkey) -> (Pubkey, Account) { + ( + pubkey, + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +const fn create_system_program_account() -> (Pubkey, Account) { + ( + system_program::ID, + Account { + lamports: 0, + data: vec![], + owner: native_loader::ID, + executable: true, + rent_epoch: 0, + }, + ) +} + +#[test] +fn test_initialize_success() { + let mollusk = Mollusk::new(&ID, get_gmp_counter_program_path()); + + let authority = Pubkey::new_unique(); + let (app_state_pda, _bump) = Pubkey::find_program_address(&[CounterAppState::SEED], &ID); + let payer = Pubkey::new_unique(); + + let instruction_data = gmp_counter_app::instruction::Initialize { authority }; + + let instruction = Instruction { + program_id: ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_pda_for_init(app_state_pda), + create_payer_account(payer), + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!(!result.program_result.is_err()); +} + +#[test] +fn test_increment_counter_new_user() { + let mollusk = Mollusk::new(&ID, get_gmp_counter_program_path()); + + let authority = Pubkey::new_unique(); + let user_authority = Pubkey::new_unique(); // The ICS27 account_state PDA would be here + let (app_state_pda, app_state_bump) = + Pubkey::find_program_address(&[CounterAppState::SEED], &ID); + let (user_counter_pda, _) = + Pubkey::find_program_address(&[UserCounter::SEED, user_authority.as_ref()], &ID); + let payer = Pubkey::new_unique(); + + let instruction_data = gmp_counter_app::instruction::Increment { amount: 5 }; + + let instruction = Instruction { + program_id: ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new(user_counter_pda, false), + AccountMeta::new_readonly(user_authority, true), // user_authority must be signer + AccountMeta::new(payer, true), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_counter_app_state_account(app_state_pda, authority, 0, 0, app_state_bump), + create_uninitialized_account(user_counter_pda), + create_payer_account(user_authority), // user_authority needs lamports to sign + create_payer_account(payer), + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!(!result.program_result.is_err()); + + // Verify return data contains the new counter value (5) + assert!(!result.return_data.is_empty()); + assert_eq!(result.return_data.len(), 8); + let counter_value = u64::from_le_bytes(result.return_data.try_into().unwrap()); + assert_eq!(counter_value, 5); +} + +#[test] +fn test_decrement_counter_existing_user() { + let mollusk = Mollusk::new(&ID, get_gmp_counter_program_path()); + + let authority = Pubkey::new_unique(); + let user = Pubkey::new_unique(); + let (app_state_pda, app_state_bump) = + Pubkey::find_program_address(&[CounterAppState::SEED], &ID); + let (user_counter_pda, user_counter_bump) = + Pubkey::find_program_address(&[UserCounter::SEED, user.as_ref()], &ID); + + let instruction_data = gmp_counter_app::instruction::Decrement { user, amount: 3 }; + + let instruction = Instruction { + program_id: ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new(user_counter_pda, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_counter_app_state_account(app_state_pda, authority, 1, 0, app_state_bump), + create_user_counter_account( + user_counter_pda, + user, + 10, + 5, + 2, + 1_600_000_000, + user_counter_bump, + ), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!(!result.program_result.is_err()); + + // Verify return data contains the new counter value (7) + assert!(!result.return_data.is_empty()); + let counter_value = u64::from_le_bytes(result.return_data.try_into().unwrap()); + assert_eq!(counter_value, 7); +} + +#[test] +fn test_counter_underflow_fails() { + let mollusk = Mollusk::new(&ID, get_gmp_counter_program_path()); + + let authority = Pubkey::new_unique(); + let user = Pubkey::new_unique(); + let (app_state_pda, app_state_bump) = + Pubkey::find_program_address(&[CounterAppState::SEED], &ID); + let (user_counter_pda, user_counter_bump) = + Pubkey::find_program_address(&[UserCounter::SEED, user.as_ref()], &ID); + + let instruction_data = gmp_counter_app::instruction::Decrement { user, amount: 10 }; + + let instruction = Instruction { + program_id: ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new(user_counter_pda, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_counter_app_state_account(app_state_pda, authority, 1, 0, app_state_bump), + create_user_counter_account( + user_counter_pda, + user, + 5, + 2, + 1, + 1_600_000_000, + user_counter_bump, + ), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!(result.program_result.is_err()); // Should fail due to underflow +} + +#[test] +fn test_get_counter() { + let mollusk = Mollusk::new(&ID, get_gmp_counter_program_path()); + + let user = Pubkey::new_unique(); + let (user_counter_pda, user_counter_bump) = + Pubkey::find_program_address(&[UserCounter::SEED, user.as_ref()], &ID); + + let instruction_data = gmp_counter_app::instruction::GetCounter { user }; + + let instruction = Instruction { + program_id: ID, + accounts: vec![AccountMeta::new_readonly(user_counter_pda, false)], + data: instruction_data.data(), + }; + + let accounts = vec![create_user_counter_account( + user_counter_pda, + user, + 42, + 10, + 3, + 1_600_000_000, + user_counter_bump, + )]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!(!result.program_result.is_err()); + + // Verify return data contains the counter value (42) + assert!(!result.return_data.is_empty()); + let counter_value = u64::from_le_bytes(result.return_data.try_into().unwrap()); + assert_eq!(counter_value, 42); +} diff --git a/programs/solana/programs/ics07-tendermint/src/instructions/update_client.rs b/programs/solana/programs/ics07-tendermint/src/instructions/update_client.rs index 2acd20351..ff1e61542 100644 --- a/programs/solana/programs/ics07-tendermint/src/instructions/update_client.rs +++ b/programs/solana/programs/ics07-tendermint/src/instructions/update_client.rs @@ -541,17 +541,14 @@ mod tests { client_message, ); - let accounts = custom_accounts.map_or_else( - || { - setup_test_accounts_with_new_consensus_state( - initialized_accounts, - new_consensus_state_pda, - payer, - 100_000_000_000, - ) - }, - |custom_accounts| custom_accounts, - ); + let accounts = custom_accounts.unwrap_or_else(|| { + setup_test_accounts_with_new_consensus_state( + initialized_accounts, + new_consensus_state_pda, + payer, + 100_000_000_000, + ) + }); UpdateClientTestScenario { client_state_pda, diff --git a/programs/solana/programs/ics26-router/Cargo.toml b/programs/solana/programs/ics26-router/Cargo.toml index fd1da7895..9a55d07ff 100644 --- a/programs/solana/programs/ics26-router/Cargo.toml +++ b/programs/solana/programs/ics26-router/Cargo.toml @@ -26,9 +26,9 @@ idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] anchor-lang = { workspace = true, features = ["init-if-needed"] } +anchor-spl.workspace = true solana-ibc-types.workspace = true ics25-handler.workspace = true -anchor-spl = "0.31" sha2 = "0.10" hex = "0.4" diff --git a/programs/solana/programs/ics26-router/src/instructions/ack_packet.rs b/programs/solana/programs/ics26-router/src/instructions/ack_packet.rs index c62b451cb..ee4a65bb6 100644 --- a/programs/solana/programs/ics26-router/src/instructions/ack_packet.rs +++ b/programs/solana/programs/ics26-router/src/instructions/ack_packet.rs @@ -250,6 +250,7 @@ pub fn ack_packet<'info>( payload, &msg.acknowledgement, &ctx.accounts.relayer.key(), + ctx.remaining_accounts, )?; msg!("IBC app callback completed"); diff --git a/programs/solana/programs/ics26-router/src/instructions/recv_packet.rs b/programs/solana/programs/ics26-router/src/instructions/recv_packet.rs index 1fc05e57d..a2c38b538 100644 --- a/programs/solana/programs/ics26-router/src/instructions/recv_packet.rs +++ b/programs/solana/programs/ics26-router/src/instructions/recv_packet.rs @@ -75,8 +75,6 @@ pub struct RecvPacket<'info> { pub system_program: Program<'info, System>, - pub clock: Sysvar<'info, Clock>, - // Client for light client lookup #[account( seeds = [CLIENT_SEED, msg.packet.dest_client.as_bytes()], @@ -104,7 +102,8 @@ pub fn recv_packet<'info>( let packet_receipt = &mut ctx.accounts.packet_receipt; let packet_ack = &mut ctx.accounts.packet_ack; let client = &ctx.accounts.client; - let clock = &ctx.accounts.clock; + // Get clock directly via syscall + let clock = Clock::get()?; require!(!msg.payloads.is_empty(), RouterError::InvalidPayloadCount); @@ -217,6 +216,22 @@ pub fn recv_packet<'info>( _ => Ok(&packet.payloads[0]), }?; + // Calculate total chunk accounts that need to be filtered out before CPI + // Chunk accounts are at the beginning of remaining_accounts: + // - First: payload chunk accounts (total_payload_chunks) + // - Then: proof chunk accounts (msg.proof.total_chunks) + // - After chunks: IBC app-specific accounts (e.g., GMP accounts) + let total_chunk_accounts = total_payload_chunks + msg.proof.total_chunks as usize; + + // Filter out chunk accounts - only pass non-chunk accounts to the IBC app + // Chunk accounts are implementation details of the router's chunking mechanism + // and should not be visible to IBC applications + let app_remaining_accounts = if total_chunk_accounts > 0 { + &ctx.remaining_accounts[total_chunk_accounts..] + } else { + ctx.remaining_accounts + }; + let acknowledgement = match on_recv_packet_cpi( &ctx.accounts.ibc_app_program, &ctx.accounts.ibc_app_state, @@ -226,6 +241,7 @@ pub fn recv_packet<'info>( &packet, payload, &ctx.accounts.relayer.key(), + app_remaining_accounts, ) { Ok(ack) => { require!( @@ -274,7 +290,6 @@ mod tests { use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::program_error::ProgramError; use solana_sdk::pubkey::Pubkey; - use solana_sdk::sysvar::SysvarId; use solana_sdk::{clock::Clock, system_program}; #[test] @@ -312,10 +327,15 @@ mod tests { #[test] fn test_recv_packet_timeout_expired() { - let ctx = setup_recv_packet_test(true, -100); // Expired timeout + let mut ctx = setup_recv_packet_test(true, -100); // Expired timeout let mollusk = Mollusk::new(&crate::ID, crate::get_router_program_path()); + // Add Clock sysvar with current timestamp (1000) - packet timeout is 900 (expired) + let clock_data = create_clock_data(1000); + ctx.accounts + .push(create_clock_account_with_data(clock_data)); + let checks = vec![Check::err(ProgramError::Custom( ANCHOR_ERROR_OFFSET + RouterError::InvalidTimeoutTimestamp as u32, ))]; @@ -386,7 +406,6 @@ mod tests { let (client_sequence_pda, client_sequence_data) = setup_client_sequence(client_id, 0); let current_timestamp = 1000; - let clock_data = create_clock_data(current_timestamp); // Packet uses the source_client_id from params (could be different) // For tests, we'll simulate having already uploaded chunks @@ -465,7 +484,6 @@ mod tests { AccountMeta::new(relayer, true), AccountMeta::new(payer, true), AccountMeta::new_readonly(system_program::ID, false), - AccountMeta::new_readonly(Clock::id(), false), AccountMeta::new_readonly(client_pda, false), AccountMeta::new_readonly(light_client_program, false), AccountMeta::new_readonly(client_state, false), @@ -501,7 +519,6 @@ mod tests { signer_account.clone(), // relayer signer_account, // payer (same account as relayer) create_program_account(system_program::ID), - create_clock_account_with_data(clock_data), create_account(client_pda, client_data, crate::ID), create_bpf_program_account(light_client_program), create_account(client_state, vec![0u8; 100], light_client_program), diff --git a/programs/solana/programs/ics26-router/src/instructions/send_packet.rs b/programs/solana/programs/ics26-router/src/instructions/send_packet.rs index e33c9aae6..70c909e87 100644 --- a/programs/solana/programs/ics26-router/src/instructions/send_packet.rs +++ b/programs/solana/programs/ics26-router/src/instructions/send_packet.rs @@ -47,8 +47,6 @@ pub struct SendPacket<'info> { pub system_program: Program<'info, System>, - pub clock: Sysvar<'info, Clock>, - #[account( seeds = [CLIENT_SEED, msg.source_client.as_bytes()], bump, @@ -58,11 +56,11 @@ pub struct SendPacket<'info> { } pub fn send_packet(ctx: Context, msg: MsgSendPacket) -> Result { - // TODO: Support multi-payload packets #602 let ibc_app = &ctx.accounts.ibc_app; let client_sequence = &mut ctx.accounts.client_sequence; let packet_commitment = &mut ctx.accounts.packet_commitment; - let clock = &ctx.accounts.clock; + // Get clock directly via syscall + let clock = Clock::get()?; // Check if app_caller is authorized - it must be a PDA derived from the registered program // (since program IDs cannot sign transactions in Solana) @@ -121,7 +119,6 @@ mod tests { use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::program_error::ProgramError; use solana_sdk::pubkey::Pubkey; - use solana_sdk::sysvar::SysvarId; use solana_sdk::{clock::Clock, system_program}; struct SendPacketTestContext { @@ -178,8 +175,6 @@ mod tests { setup_client_sequence(params.client_id, params.initial_sequence); let (ibc_app_pda, ibc_app_data) = setup_ibc_app(params.port_id, app_program_id); - let clock_data = create_clock_data(params.current_timestamp); - let msg = MsgSendPacket { source_client: params.client_id.to_string(), timeout_timestamp: params.timeout_timestamp, @@ -211,7 +206,6 @@ mod tests { AccountMeta::new_readonly(app_caller, true), AccountMeta::new(payer, true), AccountMeta::new_readonly(system_program::ID, false), - AccountMeta::new_readonly(Clock::id(), false), AccountMeta::new_readonly(client_pda, false), ], data: crate::instruction::SendPacket { msg }.data(), @@ -225,7 +219,6 @@ mod tests { create_system_account(app_caller), // app_caller is a signer create_system_account(payer), // payer is also a signer create_program_account(system_program::ID), - create_clock_account_with_data(clock_data), create_account(client_pda, client_data, crate::ID), ]; @@ -337,7 +330,7 @@ mod tests { #[test] fn test_send_packet_invalid_timeout() { - let ctx = setup_send_packet_test_with_params(SendPacketTestParams { + let mut ctx = setup_send_packet_test_with_params(SendPacketTestParams { current_timestamp: 1000, timeout_timestamp: 900, // Past timestamp ..Default::default() @@ -345,6 +338,11 @@ mod tests { let mollusk = Mollusk::new(&crate::ID, crate::get_router_program_path()); + // Add Clock sysvar with current timestamp (1000) - packet timeout is 900 (expired) + let clock_data = create_clock_data(1000); + ctx.accounts + .push(create_clock_account_with_data(clock_data)); + let checks = vec![Check::err(ProgramError::Custom( ANCHOR_ERROR_OFFSET + RouterError::InvalidTimeoutTimestamp as u32, ))]; @@ -431,8 +429,6 @@ mod tests { let (client_sequence_pda_2, client_sequence_data_2) = setup_client_sequence(client_id_2, 20); - let clock_data = create_clock_data(1000); - // Test sending packet on client 1 let msg_1 = MsgSendPacket { source_client: client_id_1.to_string(), @@ -465,7 +461,6 @@ mod tests { AccountMeta::new_readonly(app_caller_pda, true), AccountMeta::new(payer, true), AccountMeta::new_readonly(system_program::ID, false), - AccountMeta::new_readonly(Clock::id(), false), AccountMeta::new_readonly(client_pda_1, false), ], data: crate::instruction::SendPacket { msg: msg_1 }.data(), @@ -479,7 +474,6 @@ mod tests { create_system_account(app_caller_pda), create_system_account(payer), create_program_account(system_program::ID), - create_clock_account_with_data(clock_data.clone()), create_account(client_pda_1, client_data_1, crate::ID), ]; @@ -524,7 +518,6 @@ mod tests { AccountMeta::new_readonly(app_caller_pda, true), AccountMeta::new(payer, true), AccountMeta::new_readonly(system_program::ID, false), - AccountMeta::new_readonly(Clock::id(), false), AccountMeta::new_readonly(client_pda_2, false), ], data: crate::instruction::SendPacket { msg: msg_2 }.data(), @@ -538,7 +531,6 @@ mod tests { create_system_account(app_caller_pda), create_system_account(payer), create_program_account(system_program::ID), - create_clock_account_with_data(clock_data), create_account(client_pda_2, client_data_2, crate::ID), ]; diff --git a/programs/solana/programs/ics26-router/src/instructions/timeout_packet.rs b/programs/solana/programs/ics26-router/src/instructions/timeout_packet.rs index 4266757a5..c3b08bc9b 100644 --- a/programs/solana/programs/ics26-router/src/instructions/timeout_packet.rs +++ b/programs/solana/programs/ics26-router/src/instructions/timeout_packet.rs @@ -80,6 +80,7 @@ pub fn timeout_packet<'info>( ctx: Context<'_, '_, '_, 'info, TimeoutPacket<'info>>, msg: MsgTimeoutPacket, ) -> Result<()> { + // TODO: Support multi-payload packets #602 let router_state = &ctx.accounts.router_state; let packet_commitment_account = &ctx.accounts.packet_commitment; let client = &ctx.accounts.client; @@ -202,6 +203,7 @@ pub fn timeout_packet<'info>( &packet, payload, &ctx.accounts.relayer.key(), + ctx.remaining_accounts, )?; // Close the account and return rent to payer diff --git a/programs/solana/programs/ics26-router/src/router_cpi/ibc_app_cpi.rs b/programs/solana/programs/ics26-router/src/router_cpi/ibc_app_cpi.rs index 248242e51..31be552e0 100644 --- a/programs/solana/programs/ics26-router/src/router_cpi/ibc_app_cpi.rs +++ b/programs/solana/programs/ics26-router/src/router_cpi/ibc_app_cpi.rs @@ -5,7 +5,9 @@ use anchor_lang::prelude::*; use anchor_lang::solana_program::instruction::Instruction; use anchor_lang::solana_program::program::get_return_data; use anchor_lang::solana_program::program::invoke; -use solana_ibc_types::{OnAcknowledgementPacketMsg, OnRecvPacketMsg, OnTimeoutPacketMsg, Payload}; +use solana_ibc_types::{ + instruction_names, OnAcknowledgementPacketMsg, OnRecvPacketMsg, OnTimeoutPacketMsg, Payload, +}; // TODO: Params struct /// CPI helper for calling IBC app's `on_recv_packet` instruction @@ -19,6 +21,7 @@ pub fn on_recv_packet_cpi<'a>( packet: &Packet, payload: &Payload, relayer: &Pubkey, + remaining_accounts: &[AccountInfo<'a>], ) -> Result> { let msg = OnRecvPacketMsg { source_client: packet.source_client.clone(), @@ -34,8 +37,9 @@ pub fn on_recv_packet_cpi<'a>( router_program, payer, system_program, - "global:on_recv_packet", + instruction_names::ON_RECV_PACKET, msg, + remaining_accounts, )?; // Get the return data (acknowledgement) @@ -63,6 +67,7 @@ pub fn on_acknowledgement_packet_cpi<'a>( payload: &Payload, acknowledgement: &[u8], relayer: &Pubkey, + remaining_accounts: &[AccountInfo<'a>], ) -> Result<()> { let msg = OnAcknowledgementPacketMsg { source_client: packet.source_client.clone(), @@ -79,8 +84,9 @@ pub fn on_acknowledgement_packet_cpi<'a>( router_program, payer, system_program, - "global:on_acknowledgement_packet", + instruction_names::ON_ACKNOWLEDGEMENT_PACKET, msg, + remaining_accounts, ) } @@ -96,6 +102,7 @@ pub fn on_timeout_packet_cpi<'a>( packet: &Packet, payload: &Payload, relayer: &Pubkey, + remaining_accounts: &[AccountInfo<'a>], ) -> Result<()> { let msg = OnTimeoutPacketMsg { source_client: packet.source_client.clone(), @@ -111,12 +118,14 @@ pub fn on_timeout_packet_cpi<'a>( router_program, payer, system_program, - "global:on_timeout_packet", + instruction_names::ON_TIMEOUT_PACKET, msg, + remaining_accounts, ) } /// Generic CPI helper for calling IBC app instructions +#[allow(clippy::too_many_arguments)] fn call_ibc_app_cpi<'a, T: AnchorSerialize>( ibc_app_program: &AccountInfo<'a>, app_state: &AccountInfo<'a>, @@ -125,6 +134,7 @@ fn call_ibc_app_cpi<'a, T: AnchorSerialize>( system_program: &AccountInfo<'a>, discriminator: &str, msg: T, + remaining_accounts: &[AccountInfo<'a>], ) -> Result<()> { let mut instruction_data = Vec::with_capacity(IBC_CPI_INSTRUCTION_CAPACITY); instruction_data.extend_from_slice( @@ -133,26 +143,39 @@ fn call_ibc_app_cpi<'a, T: AnchorSerialize>( ); msg.serialize(&mut instruction_data)?; - // Create the instruction + // Create the instruction with fixed accounts plus remaining accounts + let mut account_metas = vec![ + AccountMeta::new(*app_state.key, false), + AccountMeta::new_readonly(*router_program.key, false), + AccountMeta::new(*payer.key, true), + AccountMeta::new_readonly(*system_program.key, false), + ]; + + // Add remaining accounts to instruction + for account_info in remaining_accounts { + account_metas.push(AccountMeta { + pubkey: *account_info.key, + is_signer: account_info.is_signer, + is_writable: account_info.is_writable, + }); + } + let instruction = Instruction { program_id: *ibc_app_program.key, - accounts: vec![ - AccountMeta::new(*app_state.key, false), - AccountMeta::new_readonly(*router_program.key, false), // router_program account - AccountMeta::new(*payer.key, true), // payer account - AccountMeta::new_readonly(*system_program.key, false), // system_program account - ], + accounts: account_metas, data: instruction_data, }; - // Invoke the CPI - let account_infos = &[ + // Build account_infos array with both fixed and remaining accounts + let mut account_infos = vec![ app_state.clone(), - router_program.clone(), // Pass the router program for auth check + router_program.clone(), payer.clone(), system_program.clone(), ]; - invoke(&instruction, account_infos)?; + account_infos.extend_from_slice(remaining_accounts); + + invoke(&instruction, &account_infos)?; Ok(()) } diff --git a/programs/solana/programs/ics27-gmp/Cargo.toml b/programs/solana/programs/ics27-gmp/Cargo.toml new file mode 100644 index 000000000..9b142eaad --- /dev/null +++ b/programs/solana/programs/ics27-gmp/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "ics27-gmp" +version.workspace = true +description = "ICS27 General Message Passing (GMP) for Solana" +edition.workspace = true +license.workspace = true +repository.workspace = true +readme = "../../../README.md" +keywords = ["cosmos", "ibc", "solana", "gmp", "anchor"] +categories = ["cryptography", "blockchain"] + +[lints] +workspace = true + +[lib] +crate-type = ["cdylib", "lib"] +name = "ics27_gmp" + +[features] +default = [] +cpi = ["no-entrypoint"] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] + +[dependencies] +anchor-lang = { workspace = true, features = ["init-if-needed"] } +anchor-spl.workspace = true +solana-ibc-types.workspace = true +solana-ibc-macros.workspace = true +prost = { workspace = true, features = ["prost-derive"] } + +[build-dependencies] +prost-build = { workspace = true } + +[dev-dependencies] +mollusk-svm.workspace = true +mollusk-svm-bencher.workspace = true +solana-sdk.workspace = true +bincode.workspace = true +gmp-counter-app = { path = "../gmp-counter-app", features = ["cpi"] } \ No newline at end of file diff --git a/programs/solana/programs/ics27-gmp/Xargo.toml b/programs/solana/programs/ics27-gmp/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/programs/solana/programs/ics27-gmp/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/programs/solana/programs/ics27-gmp/build.rs b/programs/solana/programs/ics27-gmp/build.rs new file mode 100644 index 000000000..72e56b16b --- /dev/null +++ b/programs/solana/programs/ics27-gmp/build.rs @@ -0,0 +1,27 @@ +use std::io::Result; + +fn main() -> Result<()> { + // Configure prost-build + let mut config = prost_build::Config::new(); + + // Note: prost already derives Eq for enums via ::prost::Enumeration + // Only add Eq to message types + config.type_attribute(".gmp.GMPAcknowledgement", "#[derive(Eq)]"); + config.type_attribute(".gmp.GMPPacketData", "#[derive(Eq)]"); + config.type_attribute(".solana.SolanaInstruction", "#[derive(Eq)]"); + config.type_attribute(".solana.SolanaAccountMeta", "#[derive(Eq)]"); + + // Compile proto files + config.compile_protos( + &[ + "../../../../proto/gmp/gmp.proto", + "../../../../proto/solana/solana_instruction.proto", + ], + &["../../../../proto"], + )?; + + println!("cargo:rerun-if-changed=../../../../proto/gmp/gmp.proto"); + println!("cargo:rerun-if-changed=../../../../proto/solana/solana_instruction.proto"); + + Ok(()) +} diff --git a/programs/solana/programs/ics27-gmp/src/constants.rs b/programs/solana/programs/ics27-gmp/src/constants.rs new file mode 100644 index 000000000..c9a32d8c2 --- /dev/null +++ b/programs/solana/programs/ics27-gmp/src/constants.rs @@ -0,0 +1,47 @@ +/// Program constants for ICS27 GMP +/// +/// Port ID for this GMP app instance (fixed at compile time) +pub const GMP_PORT_ID: &str = "gmpport"; + +/// Seed for the main GMP application state PDA +/// Note: This follows the standard IBC app pattern: [`APP_STATE_SEED`, `port_id`] +pub const GMP_APP_STATE_SEED: &[u8] = b"app_state"; + +/// Seed for individual account state PDAs +pub const ACCOUNT_STATE_SEED: &[u8] = b"gmp_account"; + +/// Maximum length for client ID +pub const MAX_CLIENT_ID_LENGTH: usize = 32; + +/// Maximum length for sender address (supports both Ethereum hex and Cosmos bech32) +pub const MAX_SENDER_LENGTH: usize = 128; // Supports bech32 addresses up to ~90 chars + +/// Maximum length for salt +pub const MAX_SALT_LENGTH: usize = 8; + +/// Maximum length for port ID +pub const MAX_PORT_ID_LENGTH: usize = 128; + +/// Maximum length for memo +pub const MAX_MEMO_LENGTH: usize = 256; + +/// Maximum length for execution payload +pub const MAX_PAYLOAD_LENGTH: usize = 1024; + +/// ICS27 version (must match Cosmos GMP module version) +pub const ICS27_VERSION: &str = "ics27-2"; + +/// ICS27 encoding (must match Cosmos IBC-Go's `EncodingProtobuf` constant) +pub const ICS27_ENCODING: &str = "application/x-protobuf"; + +/// Maximum timeout duration (24 hours in seconds) +pub const MAX_TIMEOUT_DURATION: i64 = 86400; + +/// Minimum timeout duration (1 minute in seconds) +pub const MIN_TIMEOUT_DURATION: i64 = 60; + +/// Universal error acknowledgement bytes +pub const ACK_ERROR: &[u8] = b"error"; + +/// Anchor discriminator size (8 bytes) +pub const DISCRIMINATOR_SIZE: usize = 8; diff --git a/programs/solana/programs/ics27-gmp/src/errors.rs b/programs/solana/programs/ics27-gmp/src/errors.rs new file mode 100644 index 000000000..92e595a08 --- /dev/null +++ b/programs/solana/programs/ics27-gmp/src/errors.rs @@ -0,0 +1,131 @@ +use anchor_lang::prelude::*; + +/// Custom errors for ICS27 GMP program +#[error_code] +pub enum GMPError { + #[msg("App is currently paused")] + AppPaused = 6000, + + #[msg("Invalid router program")] + InvalidRouter, + + #[msg("Execution payload is empty")] + EmptyPayload, + + #[msg("Invalid timeout timestamp")] + InvalidTimeout, + + #[msg("Timeout too far in future")] + TimeoutTooLong, + + #[msg("Timeout too soon")] + TimeoutTooSoon, + + #[msg("Unauthorized sender")] + UnauthorizedSender, + + #[msg("Wrong counterparty client")] + WrongCounterpartyClient, + + #[msg("Invalid salt")] + InvalidSalt, + + #[msg("Target program is not executable")] + TargetNotExecutable, + + #[msg("Insufficient accounts provided")] + InsufficientAccounts, + + #[msg("Account key mismatch")] + AccountKeyMismatch, + + #[msg("Insufficient account permissions")] + InsufficientAccountPermissions, + + #[msg("Unauthorized signer")] + UnauthorizedSigner, + + #[msg("Execution too expensive")] + ExecutionTooExpensive, + + #[msg("Invalid account address derivation")] + InvalidAccountAddress, + + #[msg("Unauthorized admin operation")] + UnauthorizedAdmin, + + #[msg("Invalid packet data format")] + InvalidPacketData, + + #[msg("Unauthorized router calling")] + UnauthorizedRouter, + + #[msg("Port ID too long")] + PortIdTooLong, + + #[msg("Client ID too long")] + ClientIdTooLong, + + #[msg("Sender address too long")] + SenderTooLong, + + #[msg("Salt too long")] + SaltTooLong, + + #[msg("Memo too long")] + MemoTooLong, + + #[msg("Payload too long")] + PayloadTooLong, + + #[msg("Invalid execution payload format")] + InvalidExecutionPayload, + + #[msg("Account already exists")] + AccountAlreadyExists, + + #[msg("Failed to parse packet data")] + PacketDataParseError, + + #[msg("Packet data validation failed")] + PacketDataValidationFailed, + + #[msg("Target program execution failed")] + TargetExecutionFailed, + + #[msg("Invalid acknowledgement format")] + InvalidAcknowledgement, + + #[msg("Compute budget exceeded")] + ComputeBudgetExceeded, + + #[msg("Too many accounts in execution payload")] + TooManyAccounts, + + #[msg("Invalid program ID format")] + InvalidProgramId, + + #[msg("Invalid account key format")] + InvalidAccountKey, + + #[msg("Invalid router CPI call")] + InvalidRouterCall, + + #[msg("Insufficient funds for account creation")] + InsufficientFunds, + + #[msg("Invalid payer position")] + InvalidPayerPosition, + + #[msg("Invalid IBC version")] + InvalidVersion, + + #[msg("Invalid IBC port")] + InvalidPort, + + #[msg("Invalid IBC encoding")] + InvalidEncoding, + + #[msg("Failed to parse sequence from router account")] + SequenceParseError, +} diff --git a/programs/solana/programs/ics27-gmp/src/events.rs b/programs/solana/programs/ics27-gmp/src/events.rs new file mode 100644 index 000000000..07f925b23 --- /dev/null +++ b/programs/solana/programs/ics27-gmp/src/events.rs @@ -0,0 +1,137 @@ +use anchor_lang::prelude::*; + +/// Event emitted when GMP app is initialized +#[event] +pub struct GMPAppInitialized { + /// Router program managing this app + pub router_program: Pubkey, + /// Administrative authority + pub authority: Pubkey, + /// Port ID bound to this app + pub port_id: String, + /// App initialization timestamp + pub timestamp: i64, +} + +/// Event emitted when a GMP call is sent +#[event] +pub struct GMPCallSent { + /// Packet sequence number + pub sequence: u64, + /// Sender of the call + pub sender: Pubkey, + /// Target program to execute + pub receiver: Pubkey, + /// Source client ID + pub client_id: String, + /// Account salt used + pub salt: Vec, + /// Payload size + pub payload_size: u64, + /// Timeout timestamp + pub timeout_timestamp: i64, +} + +/// Event emitted when a packet is received and executed +#[event] +pub struct GMPExecutionCompleted { + /// Account that executed the call + pub account: Pubkey, + /// Target program that was called + pub target_program: Pubkey, + /// Client ID + pub client_id: String, + /// Original sender + pub sender: String, + /// Account nonce after execution + pub nonce: u64, + /// Whether execution succeeded + pub success: bool, + /// Result data size + pub result_size: u64, + /// Execution timestamp + pub timestamp: i64, +} + +/// Event emitted when a new account is created +#[event] +pub struct GMPAccountCreated { + /// Account address (PDA) + pub account: Pubkey, + /// Client ID + pub client_id: String, + /// Original sender + pub sender: String, + /// Salt used for derivation + pub salt: Vec, + /// Creation timestamp + pub created_at: i64, +} + +/// Event emitted when app is paused +#[event] +pub struct GMPAppPaused { + /// Admin who paused the app + pub admin: Pubkey, + /// Pause timestamp + pub timestamp: i64, +} + +/// Event emitted when app is unpaused +#[event] +pub struct GMPAppUnpaused { + /// Admin who unpaused the app + pub admin: Pubkey, + /// Unpause timestamp + pub timestamp: i64, +} + +/// Event emitted when packet acknowledgement is processed +#[event] +pub struct GMPAcknowledgementProcessed { + /// Original sender + pub sender: Pubkey, + /// Packet sequence + pub sequence: u64, + /// Whether acknowledgement indicates success + pub ack_success: bool, + /// Processing timestamp + pub timestamp: i64, +} + +/// Event emitted when packet timeout is processed +#[event] +pub struct GMPTimeoutProcessed { + /// Original sender + pub sender: Pubkey, + /// Packet sequence + pub sequence: u64, + /// Timeout height or timestamp + pub timeout_info: String, + /// Processing timestamp + pub timestamp: i64, +} + +/// Event emitted for execution failures +#[event] +pub struct GMPExecutionFailed { + /// Account that failed execution + pub account: Pubkey, + /// Target program that failed + pub target_program: Pubkey, + /// Error code + pub error_code: u32, + /// Error message + pub error_message: String, + /// Failure timestamp + pub timestamp: i64, +} + +/// Event emitted when router caller PDA is created +#[event] +pub struct RouterCallerCreated { + /// Router caller PDA address + pub router_caller: Pubkey, + /// PDA bump seed + pub bump: u8, +} diff --git a/programs/solana/programs/ics27-gmp/src/instructions/admin.rs b/programs/solana/programs/ics27-gmp/src/instructions/admin.rs new file mode 100644 index 000000000..d3229301c --- /dev/null +++ b/programs/solana/programs/ics27-gmp/src/instructions/admin.rs @@ -0,0 +1,467 @@ +use crate::constants::*; +use crate::errors::GMPError; +use crate::events::{GMPAppPaused, GMPAppUnpaused}; +use crate::state::GMPAppState; +use anchor_lang::prelude::*; + +/// Pause the entire GMP app (admin only) +#[derive(Accounts)] +pub struct PauseApp<'info> { + /// App state account - validated by Anchor PDA constraints + #[account( + mut, + seeds = [GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + bump = app_state.bump + )] + pub app_state: Account<'info, GMPAppState>, + + #[account( + constraint = authority.key() == app_state.authority @ GMPError::UnauthorizedAdmin + )] + pub authority: Signer<'info>, +} + +pub fn pause_app(ctx: Context) -> Result<()> { + let clock = Clock::get()?; + let app_state = &mut ctx.accounts.app_state; + + app_state.paused = true; + + emit!(GMPAppPaused { + admin: ctx.accounts.authority.key(), + timestamp: clock.unix_timestamp, + }); + + msg!("GMP app paused by admin: {}", ctx.accounts.authority.key()); + + Ok(()) +} + +/// Unpause the entire GMP app (admin only) +#[derive(Accounts)] +pub struct UnpauseApp<'info> { + /// App state account - validated by Anchor PDA constraints + #[account( + mut, + seeds = [GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + bump = app_state.bump + )] + pub app_state: Account<'info, GMPAppState>, + + #[account( + constraint = authority.key() == app_state.authority @ GMPError::UnauthorizedAdmin + )] + pub authority: Signer<'info>, +} + +pub fn unpause_app(ctx: Context) -> Result<()> { + let clock = Clock::get()?; + let app_state = &mut ctx.accounts.app_state; + + app_state.paused = false; + + emit!(GMPAppUnpaused { + admin: ctx.accounts.authority.key(), + timestamp: clock.unix_timestamp, + }); + + msg!( + "GMP app unpaused by admin: {}", + ctx.accounts.authority.key() + ); + + Ok(()) +} + +/// Update app authority (admin only) +#[derive(Accounts)] +pub struct UpdateAuthority<'info> { + /// App state account - validated by Anchor PDA constraints + #[account( + mut, + seeds = [GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + bump = app_state.bump + )] + pub app_state: Account<'info, GMPAppState>, + + #[account( + constraint = current_authority.key() == app_state.authority @ GMPError::UnauthorizedAdmin + )] + pub current_authority: Signer<'info>, + + /// CHECK: New authority can be any valid Pubkey + pub new_authority: AccountInfo<'info>, +} + +pub fn update_authority(ctx: Context) -> Result<()> { + let app_state = &mut ctx.accounts.app_state; + let old_authority = app_state.authority; + + app_state.authority = ctx.accounts.new_authority.key(); + + msg!( + "GMP app authority updated: {} -> {}", + old_authority, + app_state.authority + ); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::*; + use anchor_lang::InstructionData; + use mollusk_svm::Mollusk; + use solana_sdk::{ + account::Account as SolanaAccount, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + system_program, + }; + + // ======================================================================== + // Initialize Tests + // ======================================================================== + + #[test] + fn test_initialize_success() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let router_program = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let (app_state_pda, _bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + let (router_caller_pda, _) = Pubkey::find_program_address(&[b"router_caller"], &crate::ID); + let payer = authority; + + let instruction_data = crate::instruction::Initialize { router_program }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new(router_caller_pda, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(authority, true), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_pda_for_init(app_state_pda), + create_pda_for_init(router_caller_pda), + create_payer_account(payer), + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!(!result.program_result.is_err(), "Initialize should succeed"); + } + + // ======================================================================== + // Pause/Unpause App Tests + // ======================================================================== + + #[test] + fn test_pause_app_success() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + + let (app_state_pda, app_state_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + + let instruction_data = crate::instruction::PauseApp {}; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(authority, true), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_gmp_app_state_account( + app_state_pda, + router_program, + authority, + app_state_bump, + false, // not paused + ), + create_authority_account(authority), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + !result.program_result.is_err(), + "Pause app should succeed: {:?}", + result.program_result + ); + + let app_state_account = result.get_account(&app_state_pda).unwrap(); + let app_state_data = &app_state_account.data[crate::constants::DISCRIMINATOR_SIZE..]; + let app_state = GMPAppState::try_from_slice(app_state_data).unwrap(); + assert!(app_state.paused, "App should be paused"); + } + + #[test] + fn test_unpause_app_success() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + + let (app_state_pda, app_state_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + + let app_state = GMPAppState { + router_program, + authority, + version: 1, + paused: true, + bump: app_state_bump, + }; + + let mut data = Vec::new(); + data.extend_from_slice(GMPAppState::DISCRIMINATOR); + app_state.serialize(&mut data).unwrap(); + + let instruction_data = crate::instruction::UnpauseApp {}; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(authority, true), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + ( + app_state_pda, + SolanaAccount { + lamports: 1_000_000, + data, + owner: crate::ID, + executable: false, + rent_epoch: 0, + }, + ), + create_authority_account(authority), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + !result.program_result.is_err(), + "Unpause app should succeed: {:?}", + result.program_result + ); + + let app_state_account = result.get_account(&app_state_pda).unwrap(); + let app_state_data = &app_state_account.data[crate::constants::DISCRIMINATOR_SIZE..]; + let app_state = GMPAppState::try_from_slice(app_state_data).unwrap(); + assert!(!app_state.paused, "App should be unpaused"); + } + + #[test] + fn test_pause_app_unauthorized() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let wrong_authority = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + + let (app_state_pda, app_state_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + + let instruction_data = crate::instruction::PauseApp {}; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(wrong_authority, true), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_gmp_app_state_account( + app_state_pda, + router_program, + authority, + app_state_bump, + false, // not paused + ), + create_authority_account(wrong_authority), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "Pause app should fail with wrong authority" + ); + } + + // ======================================================================== + // Update Authority Tests + // ======================================================================== + + #[test] + fn test_update_authority_success() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let current_authority = Pubkey::new_unique(); + let new_authority = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + + let (app_state_pda, app_state_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + + let instruction_data = crate::instruction::UpdateAuthority {}; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(current_authority, true), + AccountMeta::new_readonly(new_authority, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_gmp_app_state_account( + app_state_pda, + router_program, + current_authority, + app_state_bump, + false, // not paused + ), + create_authority_account(current_authority), + create_authority_account(new_authority), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + !result.program_result.is_err(), + "Update authority should succeed: {:?}", + result.program_result + ); + + let app_state_account = result.get_account(&app_state_pda).unwrap(); + let app_state_data = &app_state_account.data[crate::constants::DISCRIMINATOR_SIZE..]; + let app_state = GMPAppState::try_from_slice(app_state_data).unwrap(); + assert_eq!( + app_state.authority, new_authority, + "Authority should be updated" + ); + } + + #[test] + fn test_update_authority_unauthorized() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let current_authority = Pubkey::new_unique(); + let wrong_authority = Pubkey::new_unique(); + let new_authority = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + + let (app_state_pda, app_state_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + + let instruction_data = crate::instruction::UpdateAuthority {}; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(wrong_authority, true), + AccountMeta::new_readonly(new_authority, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_gmp_app_state_account( + app_state_pda, + router_program, + current_authority, + app_state_bump, + false, // not paused + ), + create_authority_account(wrong_authority), + create_authority_account(new_authority), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "Update authority should fail with wrong authority" + ); + } + + #[test] + fn test_pause_app_invalid_pda() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + + let (_correct_app_state_pda, _correct_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + + // Use wrong PDA + let wrong_app_state_pda = Pubkey::new_unique(); + + let instruction_data = crate::instruction::PauseApp {}; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(wrong_app_state_pda, false), // Wrong PDA! + AccountMeta::new_readonly(authority, true), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + // Create account state at wrong PDA for testing + create_gmp_app_state_account( + wrong_app_state_pda, + router_program, + authority, + 255u8, + false, // not paused + ), + create_authority_account(authority), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "PauseApp should fail with invalid app_state PDA" + ); + } +} diff --git a/programs/solana/programs/ics27-gmp/src/instructions/initialize.rs b/programs/solana/programs/ics27-gmp/src/instructions/initialize.rs new file mode 100644 index 000000000..a44401909 --- /dev/null +++ b/programs/solana/programs/ics27-gmp/src/instructions/initialize.rs @@ -0,0 +1,211 @@ +use crate::constants::*; +use crate::events::{GMPAppInitialized, RouterCallerCreated}; +use crate::state::GMPAppState; +use anchor_lang::prelude::*; + +/// Initialize the ICS27 GMP application +#[derive(Accounts)] +#[instruction(router_program: Pubkey)] +pub struct Initialize<'info> { + #[account( + init, + payer = payer, + space = 8 + GMPAppState::INIT_SPACE, + seeds = [GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + bump + )] + pub app_state: Account<'info, GMPAppState>, + + /// Router caller PDA that represents our app to the router + /// CHECK: This is a PDA that just needs to exist for router authorization + #[account( + init, + payer = payer, + space = 8, + seeds = [b"router_caller"], + bump, + )] + pub router_caller: AccountInfo<'info>, + + #[account(mut)] + pub payer: Signer<'info>, + + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, +} + +pub fn initialize(ctx: Context, router_program: Pubkey) -> Result<()> { + let app_state = &mut ctx.accounts.app_state; + let clock = Clock::get()?; + + // Initialize app state + app_state.router_program = router_program; + app_state.authority = ctx.accounts.authority.key(); + app_state.version = 1; + app_state.paused = false; + app_state.bump = ctx.bumps.app_state; + + // Emit initialization events + emit!(GMPAppInitialized { + router_program, + authority: app_state.authority, + port_id: GMP_PORT_ID.to_string(), + timestamp: clock.unix_timestamp, + }); + + emit!(RouterCallerCreated { + router_caller: ctx.accounts.router_caller.key(), + bump: ctx.bumps.router_caller, + }); + + msg!( + "ICS27 GMP app initialized with router: {}, port_id: {}, router_caller: {}", + router_program, + GMP_PORT_ID, + ctx.accounts.router_caller.key() + ); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use anchor_lang::InstructionData; + use mollusk_svm::result::Check; + use mollusk_svm::Mollusk; + use solana_sdk::instruction::{AccountMeta, Instruction}; + use solana_sdk::pubkey::Pubkey; + use solana_sdk::system_program; + + fn create_initialize_instruction( + app_state: Pubkey, + router_caller: Pubkey, + payer: Pubkey, + authority: Pubkey, + router_program: Pubkey, + ) -> Instruction { + let instruction_data = crate::instruction::Initialize { router_program }; + + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(app_state, false), + AccountMeta::new(router_caller, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(authority, true), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: instruction_data.data(), + } + } + + #[test] + fn test_initialize_success() { + let authority = Pubkey::new_unique(); + let payer = authority; + let router_program = Pubkey::new_unique(); + + let (app_state_pda, _) = + Pubkey::find_program_address(&[GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], &crate::ID); + + let (router_caller_pda, _) = Pubkey::find_program_address(&[b"router_caller"], &crate::ID); + + let instruction = create_initialize_instruction( + app_state_pda, + router_caller_pda, + payer, + authority, + router_program, + ); + + let accounts = vec![ + (app_state_pda, solana_sdk::account::Account::default()), + (router_caller_pda, solana_sdk::account::Account::default()), + ( + payer, + solana_sdk::account::Account { + lamports: 1_000_000_000, + owner: system_program::ID, + ..Default::default() + }, + ), + ( + system_program::ID, + solana_sdk::account::Account { + lamports: 1, + executable: true, + owner: solana_sdk::native_loader::ID, + ..Default::default() + }, + ), + ]; + + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let checks = vec![ + Check::success(), + Check::account(&app_state_pda).owner(&crate::ID).build(), + ]; + + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } + + #[test] + fn test_initialize_already_initialized() { + let authority = Pubkey::new_unique(); + let payer = authority; + let router_program = Pubkey::new_unique(); + + let (app_state_pda, _) = + Pubkey::find_program_address(&[GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], &crate::ID); + + let (router_caller_pda, _) = Pubkey::find_program_address(&[b"router_caller"], &crate::ID); + + let instruction = create_initialize_instruction( + app_state_pda, + router_caller_pda, + payer, + authority, + router_program, + ); + + // Create accounts that are already initialized (owned by program, not system) + let accounts = vec![ + ( + app_state_pda, + solana_sdk::account::Account { + lamports: 1_000_000, + data: vec![0; 100], // Already has data + owner: crate::ID, // Already owned by program + ..Default::default() + }, + ), + (router_caller_pda, solana_sdk::account::Account::default()), + ( + payer, + solana_sdk::account::Account { + lamports: 1_000_000_000, + owner: system_program::ID, + ..Default::default() + }, + ), + ( + system_program::ID, + solana_sdk::account::Account { + lamports: 1, + executable: true, + owner: solana_sdk::native_loader::ID, + ..Default::default() + }, + ), + ]; + + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "Initialize should fail when account already initialized" + ); + } +} diff --git a/programs/solana/programs/ics27-gmp/src/instructions/mod.rs b/programs/solana/programs/ics27-gmp/src/instructions/mod.rs new file mode 100644 index 000000000..8a7d72c9a --- /dev/null +++ b/programs/solana/programs/ics27-gmp/src/instructions/mod.rs @@ -0,0 +1,13 @@ +pub mod admin; +pub mod initialize; +pub mod on_ack_packet; +pub mod on_recv_packet; +pub mod on_timeout_packet; +pub mod send_call; + +pub use admin::*; +pub use initialize::*; +pub use on_ack_packet::*; +pub use on_recv_packet::*; +pub use on_timeout_packet::*; +pub use send_call::*; diff --git a/programs/solana/programs/ics27-gmp/src/instructions/on_ack_packet.rs b/programs/solana/programs/ics27-gmp/src/instructions/on_ack_packet.rs new file mode 100644 index 000000000..f4ebf274d --- /dev/null +++ b/programs/solana/programs/ics27-gmp/src/instructions/on_ack_packet.rs @@ -0,0 +1,364 @@ +use crate::constants::*; +use crate::errors::GMPError; +use crate::events::GMPAcknowledgementProcessed; +use crate::state::GMPAppState; +use anchor_lang::prelude::*; + +/// Process IBC packet acknowledgement (called by router via CPI) +#[derive(Accounts)] +#[instruction(msg: solana_ibc_types::OnAcknowledgementPacketMsg)] +pub struct OnAckPacket<'info> { + /// App state account - validated by Anchor PDA constraints + #[account( + seeds = [GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + bump = app_state.bump + )] + pub app_state: Account<'info, GMPAppState>, + + /// Router program calling this instruction + /// CHECK: Validated in handler + pub router_program: UncheckedAccount<'info>, + + /// Relayer fee payer (passed by router but not used in acknowledgement handler) + /// CHECK: Router always passes this account + #[account(mut)] + pub payer: UncheckedAccount<'info>, + + /// CHECK: System program (passed by router but not used in acknowledgement handler) + pub system_program: UncheckedAccount<'info>, +} + +pub fn on_acknowledgement_packet( + ctx: Context, + msg: solana_ibc_types::OnAcknowledgementPacketMsg, +) -> Result<()> { + let clock = Clock::get()?; + let app_state = &ctx.accounts.app_state; + + // Validate router program + require!( + ctx.accounts.router_program.key() == app_state.router_program, + GMPError::UnauthorizedRouter + ); + + // Check if app is operational + app_state.can_operate()?; + + // Parse packet data and acknowledgement from router message + let (packet_data, acknowledgement) = crate::router_cpi::parse_ack_data_from_router_cpi(&msg)?; + let sequence = msg.sequence; + + // Validate packet data + packet_data.validate()?; + + // Convert cross-chain sender address to deterministic Solana pubkey + let sender = crate::utils::derive_pubkey_from_address(&packet_data.sender)?; + + // Determine if acknowledgement indicates success + let ack_success = is_acknowledgement_success(&acknowledgement); + + emit!(GMPAcknowledgementProcessed { + sender, + sequence, + ack_success, + timestamp: clock.unix_timestamp, + }); + + if ack_success { + msg!( + "GMP call acknowledged successfully: sender={}, sequence={}", + sender, + sequence + ); + } else { + msg!( + "GMP call acknowledged with error: sender={}, sequence={}", + sender, + sequence + ); + } + + Ok(()) +} + +/// Determine if acknowledgement indicates success +fn is_acknowledgement_success(acknowledgement: &[u8]) -> bool { + // Parse the acknowledgement to determine success/failure + // This depends on the specific acknowledgement format used + + if acknowledgement.is_empty() { + return false; + } + + // Check for universal error acknowledgement + if acknowledgement == ACK_ERROR { + return false; + } + + // Try to parse as GMPAcknowledgement + // If we can't parse it, assume it's a success (non-empty, non-error) + crate::state::GMPAcknowledgement::try_from_slice(acknowledgement) + .map(|ack| ack.success) + .unwrap_or(true) +} + +#[cfg(test)] +mod tests { + use crate::constants::GMP_PORT_ID; + use crate::test_utils::*; + use anchor_lang::InstructionData; + use mollusk_svm::Mollusk; + use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + }; + + #[test] + fn test_on_ack_packet_app_paused() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let (app_state_pda, app_state_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + + let ack_msg = solana_ibc_types::OnAcknowledgementPacketMsg { + source_client: "cosmoshub-1".to_string(), + dest_client: "solana-1".to_string(), + sequence: 1, + payload: solana_ibc_types::Payload { + source_port: GMP_PORT_ID.to_string(), + dest_port: GMP_PORT_ID.to_string(), + version: "gmp-1".to_string(), + encoding: "proto3".to_string(), + value: vec![], + }, + acknowledgement: vec![1, 2, 3], + relayer: Pubkey::new_unique(), + }; + + let instruction_data = crate::instruction::OnAcknowledgementPacket { msg: ack_msg }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new_readonly(app_state_pda, false), + AccountMeta::new_readonly(router_program, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_gmp_app_state_account( + app_state_pda, + router_program, + authority, + app_state_bump, + true, // paused + ), + create_router_program_account(router_program), + create_authority_account(payer), + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "OnAckPacket should fail when app is paused" + ); + } + + #[test] + fn test_on_ack_packet_unauthorized_router() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + let wrong_router = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let (app_state_pda, app_state_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + + let ack_msg = solana_ibc_types::OnAcknowledgementPacketMsg { + source_client: "cosmoshub-1".to_string(), + dest_client: "solana-1".to_string(), + sequence: 1, + payload: solana_ibc_types::Payload { + source_port: GMP_PORT_ID.to_string(), + dest_port: GMP_PORT_ID.to_string(), + version: "gmp-1".to_string(), + encoding: "proto3".to_string(), + value: vec![], + }, + acknowledgement: vec![1, 2, 3], + relayer: Pubkey::new_unique(), + }; + + let instruction_data = crate::instruction::OnAcknowledgementPacket { msg: ack_msg }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new_readonly(app_state_pda, false), + AccountMeta::new_readonly(wrong_router, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_gmp_app_state_account( + app_state_pda, + router_program, + authority, + app_state_bump, + false, // not paused + ), + create_router_program_account(wrong_router), + create_authority_account(payer), + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "OnAckPacket should fail with unauthorized router" + ); + } + + #[test] + fn test_on_ack_packet_invalid_app_state_pda() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let port_id = "gmpport".to_string(); + + let (_correct_app_state_pda, _correct_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, port_id.as_bytes()], + &crate::ID, + ); + + // Use wrong PDA + let wrong_app_state_pda = Pubkey::new_unique(); + + let ack_msg = solana_ibc_types::OnAcknowledgementPacketMsg { + source_client: "cosmoshub-1".to_string(), + dest_client: "solana-1".to_string(), + sequence: 1, + payload: solana_ibc_types::Payload { + source_port: GMP_PORT_ID.to_string(), + dest_port: GMP_PORT_ID.to_string(), + version: "gmp-1".to_string(), + encoding: "proto3".to_string(), + value: vec![], + }, + acknowledgement: vec![1, 2, 3], + relayer: Pubkey::new_unique(), + }; + + let instruction_data = crate::instruction::OnAcknowledgementPacket { msg: ack_msg }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new_readonly(wrong_app_state_pda, false), // Wrong PDA! + AccountMeta::new_readonly(router_program, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ], + data: instruction_data.data(), + }; + + // Create account state at wrong PDA for testing + let wrong_bump = 255u8; + let accounts = vec![ + create_gmp_app_state_account( + wrong_app_state_pda, + router_program, + authority, + wrong_bump, + false, // not paused + ), + create_router_program_account(router_program), + create_authority_account(payer), + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "OnAckPacket should fail with invalid app_state PDA" + ); + } + + #[test] + fn test_on_ack_packet_invalid_packet_data() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let (app_state_pda, app_state_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + + // Create acknowledgement message with invalid packet data in payload.value + let ack_msg = solana_ibc_types::OnAcknowledgementPacketMsg { + source_client: "cosmoshub-1".to_string(), + dest_client: "solana-1".to_string(), + sequence: 1, + payload: solana_ibc_types::Payload { + source_port: GMP_PORT_ID.to_string(), + dest_port: GMP_PORT_ID.to_string(), + version: "gmp-1".to_string(), + encoding: "proto3".to_string(), + value: vec![0xFF, 0xFF, 0xFF], // Invalid/malformed packet data! + }, + acknowledgement: vec![1, 2, 3], + relayer: Pubkey::new_unique(), + }; + + let instruction_data = crate::instruction::OnAcknowledgementPacket { msg: ack_msg }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new_readonly(app_state_pda, false), + AccountMeta::new_readonly(router_program, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_gmp_app_state_account( + app_state_pda, + router_program, + authority, + app_state_bump, + false, // not paused + ), + create_router_program_account(router_program), + create_authority_account(payer), + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "OnAckPacket should fail with invalid/malformed packet data" + ); + } +} diff --git a/programs/solana/programs/ics27-gmp/src/instructions/on_recv_packet.rs b/programs/solana/programs/ics27-gmp/src/instructions/on_recv_packet.rs new file mode 100644 index 000000000..fe914a822 --- /dev/null +++ b/programs/solana/programs/ics27-gmp/src/instructions/on_recv_packet.rs @@ -0,0 +1,1435 @@ +use crate::constants::*; +use crate::errors::GMPError; +use crate::events::{GMPAccountCreated, GMPExecutionCompleted, GMPExecutionFailed}; +use crate::state::{ + AccountState, GMPAcknowledgement, GMPAppState, GMPPacketData, SolanaInstruction, +}; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::{hash::hash, instruction::Instruction, program::invoke_signed}; + +const EXECUTION_SUCCESS_RESULT: &[u8] = b"execution_success"; + +/// Receive IBC packet and execute call (called by router via CPI) +#[derive(Accounts)] +#[instruction(msg: solana_ibc_types::OnRecvPacketMsg)] +pub struct OnRecvPacket<'info> { + /// App state account - validated by Anchor PDA constraints + #[account( + mut, + seeds = [GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + bump = app_state.bump + )] + pub app_state: Account<'info, GMPAppState>, + + /// Router program calling this instruction + /// CHECK: Validated in handler + pub router_program: UncheckedAccount<'info>, + + /// Relayer fee payer - used for account creation rent + /// NOTE: This cannot be the GMP account PDA because PDAs with data cannot + /// be used as payers in System Program transfers. The relayer's fee payer + /// is used for rent, while the GMP account PDA signs via `invoke_signed`. + /// CHECK: Validated for sufficient funds when account creation is needed + #[account(mut)] + pub payer: UncheckedAccount<'info>, + + /// CHECK: System program + pub system_program: UncheckedAccount<'info>, + // Additional accounts (accessed via remaining_accounts from relayer): + // - [0]: account_state - GMP account PDA (created if needed, signs via invoke_signed) + // - [1]: target_program - Target program to execute + // - [2+]: accounts from payload - All accounts required by target program +} + +pub fn on_recv_packet<'info>( + ctx: Context<'_, '_, '_, 'info, OnRecvPacket<'info>>, + msg: solana_ibc_types::OnRecvPacketMsg, +) -> Result> { + let clock = Clock::get()?; + let current_time = clock.unix_timestamp; + let app_state = &mut ctx.accounts.app_state; + + // Validate router program + require!( + ctx.accounts.router_program.key() == app_state.router_program, + GMPError::UnauthorizedRouter + ); + + // Check if app is operational + app_state.can_operate()?; + + // Validate IBC payload fields (matching Solidity ICS27GMP validations) + // See: ICS27GMP.sol lines 115-130 + + // Validate version + require!( + msg.payload.version == ICS27_VERSION, + GMPError::InvalidVersion + ); + + // Validate source port + require!( + msg.payload.source_port == GMP_PORT_ID, + GMPError::InvalidPort + ); + + // Validate encoding + require!( + msg.payload.encoding == ICS27_ENCODING, + GMPError::InvalidEncoding + ); + + // Validate dest port + require!(msg.payload.dest_port == GMP_PORT_ID, GMPError::InvalidPort); + + // Parse packet data from router message + let packet_data = crate::router_cpi::parse_packet_data_from_router_cpi(&msg)?; + + // Validate packet data + packet_data.validate()?; + + // Get account and target program from remaining accounts + // The router passes these as the first two remaining accounts + require!( + ctx.remaining_accounts.len() >= 2, + GMPError::InsufficientAccounts + ); + + // Work around lifetime issues by accessing items inline + if ctx.remaining_accounts.len() < 2 { + return Err(GMPError::InsufficientAccounts.into()); + } + + // Parse receiver as Solana Pubkey (for incoming packets, receiver is a Solana address) + let receiver_pubkey = + Pubkey::try_from(packet_data.receiver.as_str()).map_err(|_| GMPError::InvalidAccountKey)?; + + // Validate target program matches packet data + require!( + ctx.remaining_accounts[1].key() == receiver_pubkey, + GMPError::AccountKeyMismatch + ); + + // Derive expected account address + let (expected_account_address, _bump) = AccountState::derive_address( + &packet_data.client_id, + &packet_data.sender, + &packet_data.salt, + ctx.program_id, + )?; + + // Validate account info matches derived address + require!( + ctx.remaining_accounts[0].key() == expected_account_address, + GMPError::InvalidAccountAddress + ); + + // Validate account ownership (security check) + if !ctx.remaining_accounts[0].data_is_empty() { + crate::utils::validate_account_ownership(&ctx.remaining_accounts[0], ctx.program_id)?; + } + + // Get or create account state using utility function + let account_info = &ctx.remaining_accounts[0]; + let payer_account_info = ctx.accounts.payer.to_account_info(); + let system_program_account_info = ctx.accounts.system_program.to_account_info(); + let (mut account_state, is_new_account) = crate::utils::get_or_create_account( + account_info, + &packet_data.client_id, + &packet_data.sender, + &packet_data.salt, + &payer_account_info, + &system_program_account_info, + ctx.program_id, + current_time, + _bump, + )?; + + // Validate execution authority + validate_execution_authority(&account_state, &packet_data)?; + + // Parse the SolanaInstruction from Protobuf payload + // The payload contains the target program ID, all required accounts, and instruction data + let solana_instruction = SolanaInstruction::try_from_slice(&packet_data.payload)?; + solana_instruction.validate()?; + + // Prepare signer seeds for invoke_signed + // The GMP account PDA will sign for the target program execution + // NOTE: Sender is hashed to fit Solana's 32-byte PDA seed constraint + let sender_hash = hash(account_state.sender.as_bytes()).to_bytes(); + let client_id_bytes = account_state.client_id.as_bytes().to_vec(); + let salt_bytes = account_state.salt.clone(); + let bump = account_state.bump; + + // Build signer seeds on stack for invoke_signed + let bump_array = [bump]; + let signer_seeds: &[&[u8]] = &[ + ACCOUNT_STATE_SEED, // b"gmp_account" + &client_id_bytes, // Source chain client ID + &sender_hash, // Hashed sender address (32 bytes) + &salt_bytes, // User-provided salt + &bump_array, // PDA bump seed + ]; + + // Execute with nonce protection and record result + let execution_result = execute_with_nonce_protection(&mut account_state, current_time, || { + execute_target_program( + &solana_instruction, + ctx.remaining_accounts, + signer_seeds, + &ctx.accounts.payer, + ) + }); + + // Save account state using utility function + let account_info = &ctx.remaining_accounts[0]; + crate::utils::save_account_state(account_info, &account_state)?; + + // Emit event for new accounts + if is_new_account { + emit!(GMPAccountCreated { + account: ctx.remaining_accounts[0].key(), + client_id: packet_data.client_id.clone(), + sender: packet_data.sender.clone(), + salt: packet_data.salt.clone(), + created_at: current_time, + }); + } + + // Handle execution result and create acknowledgement + match execution_result { + Ok(result) => { + emit!(GMPExecutionCompleted { + account: ctx.remaining_accounts[0].key(), + target_program: ctx.remaining_accounts[1].key(), + client_id: packet_data.client_id.clone(), + sender: packet_data.sender.clone(), + nonce: account_state.nonce, + success: true, + result_size: result.len() as u64, + timestamp: current_time, + }); + + let ack = GMPAcknowledgement::success(result); + Ok(ack.try_to_vec()?) + } + Err(e) => { + let error_msg = format!("Execution failed: {e:?}"); + + emit!(GMPExecutionFailed { + account: ctx.remaining_accounts[0].key(), + target_program: ctx.remaining_accounts[1].key(), + error_code: 0, // Simplified error code handling + error_message: error_msg.clone(), + timestamp: current_time, + }); + + // Return error acknowledgement instead of failing transaction + // This matches Ethereum behavior + let ack = GMPAcknowledgement::error(error_msg); + Ok(ack.try_to_vec()?) + } + } +} + +/// Validate that the execution is authorized for this GMP account +/// Only the original creator (same `client_id`, sender, and salt) can execute via this account +/// +/// Note: This function is public primarily for testing purposes +pub fn validate_execution_authority( + account_state: &AccountState, + packet_data: &GMPPacketData, +) -> Result<()> { + // Ensure the packet comes from the same source chain client + require!( + account_state.client_id == packet_data.client_id, + GMPError::WrongCounterpartyClient + ); + + // Ensure the packet is from the original sender who created this account + require!( + account_state.sender == packet_data.sender, + GMPError::UnauthorizedSender + ); + + // Ensure the salt matches (for deterministic PDA derivation) + require!( + account_state.salt == packet_data.salt, + GMPError::InvalidSalt + ); + + Ok(()) +} + +/// Execute with nonce protection and error handling +/// Increments the nonce before execution to prevent replay attacks +/// +/// Note: This function is public primarily for testing purposes +pub fn execute_with_nonce_protection( + account_state: &mut AccountState, + current_time: i64, + execution_fn: F, +) -> Result +where + F: FnOnce() -> Result, +{ + // Increment nonce BEFORE execution to prevent replay attacks + // Even if execution fails, the nonce is incremented + account_state.execute_nonce_increment(current_time); + + // Execute the target program + execution_fn() +} + +/// Execute target program with GMP account PDA as signer +fn execute_target_program<'a>( + solana_instruction: &SolanaInstruction, + remaining_accounts: &[AccountInfo<'a>], + signer_seeds: &[&[u8]], + payer: &AccountInfo<'a>, +) -> Result> { + let program_id = solana_instruction.get_program_id()?; + let mut account_metas = solana_instruction.to_account_metas()?; + + let payer_position = inject_payer_if_needed( + &mut account_metas, + solana_instruction.payer_position, + payer.key, + )?; + + let target_account_infos = + map_and_validate_accounts(&account_metas, remaining_accounts, payer_position, payer)?; + + let instruction = Instruction { + program_id, + accounts: account_metas, + data: solana_instruction.data.clone(), + }; + + invoke_signed(&instruction, &target_account_infos, &[signer_seeds]) + .map(|()| EXECUTION_SUCCESS_RESULT.to_vec()) + .map_err(|_| GMPError::TargetExecutionFailed.into()) +} + +/// Inject payer into account metas if specified +/// +/// Note: This function is public primarily for testing purposes +pub fn inject_payer_if_needed( + account_metas: &mut Vec, + payer_position: Option, + payer_key: &Pubkey, +) -> Result> { + match payer_position { + None => Ok(None), + Some(pos) if (pos as usize) <= account_metas.len() => { + let pos_usize = pos as usize; + account_metas.insert(pos_usize, AccountMeta::new(*payer_key, true)); + Ok(Some(pos_usize)) + } + _ => Err(GMPError::InvalidPayerPosition.into()), + } +} + +/// Calculate offset for account mapping based on payer injection position +/// +/// Note: This function is public primarily for testing purposes +pub const fn calculate_account_offset( + account_index: usize, + payer_position: Option, + base_offset: usize, +) -> usize { + match payer_position { + Some(pos) if account_index > pos => base_offset - 1, + _ => base_offset, + } +} + +/// Map account metas to account infos and validate permissions +fn map_and_validate_accounts<'a>( + account_metas: &[AccountMeta], + remaining_accounts: &[AccountInfo<'a>], + payer_position: Option, + payer: &AccountInfo<'a>, +) -> Result>> { + const TARGET_ACCOUNTS_OFFSET: usize = 2; + + require!( + account_metas.len() + TARGET_ACCOUNTS_OFFSET <= remaining_accounts.len() + 1, + GMPError::InsufficientAccounts + ); + + let mut target_account_infos = Vec::new(); + + for (i, meta) in account_metas.iter().enumerate() { + let account_info = if Some(i) == payer_position { + payer + } else { + let offset = calculate_account_offset(i, payer_position, TARGET_ACCOUNTS_OFFSET); + &remaining_accounts[i + offset] + }; + + require!( + account_info.key() == meta.pubkey, + GMPError::AccountKeyMismatch + ); + + if meta.is_writable && !account_info.is_writable { + return Err(GMPError::InsufficientAccountPermissions.into()); + } + + target_account_infos.push(account_info.clone()); + } + + Ok(target_account_infos) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::state::{AccountState, GMPPacketData, SolanaAccountMeta, SolanaInstruction}; + use crate::test_utils::*; + use anchor_lang::InstructionData; + use mollusk_svm::Mollusk; + use solana_sdk::{ + instruction::{AccountMeta, Instruction as SolanaInstructionSDK}, + pubkey::Pubkey, + system_program, + }; + + #[test] + fn test_validate_execution_authority_success() { + let client_id = "cosmoshub-1"; + let sender = "cosmos1test"; + let salt = b"salt"; + + let (_account_pda, bump) = + AccountState::derive_address(client_id, sender, salt, &crate::ID).unwrap(); + + let account_state = AccountState { + client_id: client_id.to_string(), + sender: sender.to_string(), + salt: salt.to_vec(), + nonce: 0, + created_at: 1_600_000_000, + last_executed_at: 1_600_000_000, + execution_count: 0, + bump, + }; + + let packet_data = GMPPacketData { + client_id: client_id.to_string(), + sender: sender.to_string(), + receiver: Pubkey::new_unique().to_string(), + salt: salt.to_vec(), + payload: vec![1, 2, 3], + memo: String::new(), + }; + + assert!(validate_execution_authority(&account_state, &packet_data).is_ok()); + } + + #[test] + fn test_validate_execution_authority_wrong_client() { + let (_account_pda, bump) = + AccountState::derive_address("cosmoshub-1", "cosmos1test", b"", &crate::ID).unwrap(); + + let account_state = AccountState { + client_id: "cosmoshub-1".to_string(), + sender: "cosmos1test".to_string(), + salt: vec![], + nonce: 0, + created_at: 1_600_000_000, + last_executed_at: 1_600_000_000, + execution_count: 0, + bump, + }; + + let packet_data = GMPPacketData { + client_id: "different-client".to_string(), + sender: "cosmos1test".to_string(), + receiver: Pubkey::new_unique().to_string(), + salt: vec![], + payload: vec![1, 2, 3], + memo: String::new(), + }; + + assert!(validate_execution_authority(&account_state, &packet_data).is_err()); + } + + #[test] + fn test_execute_with_nonce_protection_increments_on_success() { + let (_account_pda, bump) = + AccountState::derive_address("cosmoshub-1", "cosmos1test", b"", &crate::ID).unwrap(); + + let mut account_state = AccountState { + client_id: "cosmoshub-1".to_string(), + sender: "cosmos1test".to_string(), + salt: vec![], + nonce: 5, + created_at: 1_600_000_000, + last_executed_at: 1_600_000_000, + execution_count: 10, + bump, + }; + + let current_time = 1_700_000_000; + let old_nonce = account_state.nonce; + + let result = execute_with_nonce_protection(&mut account_state, current_time, || { + Ok("success".to_string()) + }); + + assert!(result.is_ok()); + assert_eq!(account_state.nonce, old_nonce + 1); + } + + #[test] + fn test_inject_payer_at_beginning() { + let payer_key = Pubkey::new_unique(); + let mut account_metas = vec![ + AccountMeta::new(Pubkey::new_unique(), false), + AccountMeta::new_readonly(Pubkey::new_unique(), false), + ]; + + let result = inject_payer_if_needed(&mut account_metas, Some(0), &payer_key); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), Some(0)); + assert_eq!(account_metas.len(), 3); + assert_eq!(account_metas[0].pubkey, payer_key); + } + + #[test] + fn test_inject_payer_none() { + let payer_key = Pubkey::new_unique(); + let mut account_metas = vec![ + AccountMeta::new(Pubkey::new_unique(), false), + AccountMeta::new_readonly(Pubkey::new_unique(), false), + ]; + + let original_len = account_metas.len(); + let result = inject_payer_if_needed(&mut account_metas, None, &payer_key); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), None); + assert_eq!(account_metas.len(), original_len); + } + + #[test] + fn test_calculate_account_offset_no_payer() { + assert_eq!(calculate_account_offset(0, None, 2), 2); + assert_eq!(calculate_account_offset(5, None, 2), 2); + } + + #[test] + fn test_calculate_account_offset_with_payer_after() { + assert_eq!(calculate_account_offset(3, Some(2), 2), 1); + assert_eq!(calculate_account_offset(4, Some(2), 2), 1); + } + + #[test] + fn test_on_recv_packet_app_paused() { + let ctx = create_gmp_test_context(); + + let client_id = "cosmoshub-1"; + let sender = "cosmos1test"; + let salt = vec![1u8, 2, 3]; + + let (account_state_pda, _account_bump) = + AccountState::derive_address(client_id, sender, &salt, &crate::ID).unwrap(); + + let packet_data = + create_gmp_packet_data(client_id, sender, system_program::ID, salt, vec![]); + let packet_data_bytes = packet_data.try_to_vec().unwrap(); + + let recv_msg = create_recv_packet_msg(client_id, packet_data_bytes, 1); + let instruction = create_recv_packet_instruction( + ctx.app_state_pda, + ctx.router_program, + ctx.payer, + recv_msg, + ); + + let accounts = vec![ + create_gmp_app_state_account( + ctx.app_state_pda, + ctx.router_program, + ctx.authority, + ctx.app_state_bump, + true, // paused + ), + create_router_program_account(ctx.router_program), + create_authority_account(ctx.payer), + create_system_program_account(), + create_uninitialized_account_for_pda(account_state_pda), + create_system_program_account(), + ]; + + let result = ctx.mollusk.process_instruction(&instruction, &accounts); + assert!(result.program_result.is_err()); + } + + #[test] + fn test_on_recv_packet_frozen_account() { + let ctx = create_gmp_test_context(); + + let client_id = "cosmoshub-1"; + let sender = "cosmos1test"; + let salt = vec![1u8, 2, 3]; + + let (account_state_pda, account_bump) = + AccountState::derive_address(client_id, sender, &salt, &crate::ID).unwrap(); + + let packet_data = + create_gmp_packet_data(client_id, sender, system_program::ID, salt.clone(), vec![]); + let packet_data_bytes = packet_data.try_to_vec().unwrap(); + + let recv_msg = create_recv_packet_msg(client_id, packet_data_bytes, 1); + let instruction = create_recv_packet_instruction( + ctx.app_state_pda, + ctx.router_program, + ctx.payer, + recv_msg, + ); + + let accounts = vec![ + create_gmp_app_state_account( + ctx.app_state_pda, + ctx.router_program, + ctx.authority, + ctx.app_state_bump, + false, // not paused + ), + create_router_program_account(ctx.router_program), + create_authority_account(ctx.payer), + create_system_program_account(), + create_account_state( + account_state_pda, + client_id.to_string(), + sender.to_string(), + salt, + account_bump, + ), + create_system_program_account(), + ]; + + let result = ctx.mollusk.process_instruction(&instruction, &accounts); + assert!(result.program_result.is_err()); + } + + #[test] + fn test_on_recv_packet_unauthorized_router() { + let ctx = create_gmp_test_context(); + let wrong_router = Pubkey::new_unique(); + + let client_id = "cosmoshub-1"; + let sender = "cosmos1test"; + let salt = vec![1u8, 2, 3]; + + let (account_state_pda, _account_bump) = + AccountState::derive_address(client_id, sender, &salt, &crate::ID).unwrap(); + + let packet_data = + create_gmp_packet_data(client_id, sender, system_program::ID, salt, vec![]); + let packet_data_bytes = packet_data.try_to_vec().unwrap(); + + let recv_msg = create_recv_packet_msg(client_id, packet_data_bytes, 1); + let instruction = + create_recv_packet_instruction(ctx.app_state_pda, wrong_router, ctx.payer, recv_msg); + + let accounts = vec![ + create_gmp_app_state_account( + ctx.app_state_pda, + ctx.router_program, // State has correct router + ctx.authority, + ctx.app_state_bump, + false, // not paused + ), + create_router_program_account(wrong_router), + create_authority_account(ctx.payer), + create_system_program_account(), + create_uninitialized_account_for_pda(account_state_pda), + create_system_program_account(), + ]; + + let result = ctx.mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "OnRecvPacket should fail with unauthorized router" + ); + } + + #[test] + fn test_on_recv_packet_invalid_app_state_pda() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let port_id = "gmpport".to_string(); + + let (_correct_app_state_pda, _correct_bump) = + Pubkey::find_program_address(&[GMP_APP_STATE_SEED, port_id.as_bytes()], &crate::ID); + + // Use wrong PDA + let wrong_app_state_pda = Pubkey::new_unique(); + + let client_id = "cosmoshub-1"; + let sender = "cosmos1test"; + let salt = vec![1u8, 2, 3]; + + let (account_state_pda, _account_bump) = + AccountState::derive_address(client_id, sender, &salt, &crate::ID).unwrap(); + + let packet_data = GMPPacketData { + client_id: client_id.to_string(), + sender: sender.to_string(), + receiver: system_program::ID.to_string(), + salt, + payload: vec![], + memo: String::new(), + }; + + let packet_data_bytes = packet_data.try_to_vec().unwrap(); + + let recv_msg = solana_ibc_types::OnRecvPacketMsg { + source_client: client_id.to_string(), + dest_client: "solana-1".to_string(), + sequence: 1, + payload: solana_ibc_types::Payload { + source_port: GMP_PORT_ID.to_string(), + dest_port: GMP_PORT_ID.to_string(), + version: ICS27_VERSION.to_string(), + encoding: ICS27_ENCODING.to_string(), + value: packet_data_bytes, + }, + relayer: Pubkey::new_unique(), + }; + + let instruction_data = crate::instruction::OnRecvPacket { msg: recv_msg }; + + let instruction = SolanaInstructionSDK { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(wrong_app_state_pda, false), // Wrong PDA! + AccountMeta::new_readonly(router_program, false), + AccountMeta::new(payer, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: instruction_data.data(), + }; + + // Create account state at wrong PDA for testing + let wrong_bump = 255u8; + let accounts = vec![ + create_gmp_app_state_account( + wrong_app_state_pda, + router_program, + authority, + wrong_bump, + false, // not paused + ), + create_router_program_account(router_program), + create_authority_account(payer), + create_system_program_account(), + create_uninitialized_account_for_pda(account_state_pda), + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "OnRecvPacket should fail with invalid app_state PDA" + ); + } + + #[test] + fn test_on_recv_packet_wrong_sender() { + let ctx = create_gmp_test_context(); + + let client_id = "cosmoshub-1"; + let original_sender = "cosmos1original"; + let wrong_sender = "cosmos1attacker"; + let salt = vec![1u8, 2, 3]; + + // Account was created by original_sender + let (account_state_pda, account_bump) = + AccountState::derive_address(client_id, original_sender, &salt, &crate::ID).unwrap(); + + // Packet claims to be from wrong_sender + let packet_data = create_gmp_packet_data( + client_id, + wrong_sender, + system_program::ID, + salt.clone(), + vec![], + ); + let packet_data_bytes = packet_data.try_to_vec().unwrap(); + + let recv_msg = create_recv_packet_msg(client_id, packet_data_bytes, 1); + let instruction = create_recv_packet_instruction( + ctx.app_state_pda, + ctx.router_program, + ctx.payer, + recv_msg, + ); + + let accounts = vec![ + create_gmp_app_state_account( + ctx.app_state_pda, + ctx.router_program, + ctx.authority, + ctx.app_state_bump, + false, // not paused + ), + create_router_program_account(ctx.router_program), + create_authority_account(ctx.payer), + create_system_program_account(), + create_account_state( + account_state_pda, + client_id.to_string(), + original_sender.to_string(), // Account owned by original sender + salt, + account_bump, + ), + create_system_program_account(), + ]; + + let result = ctx.mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "OnRecvPacket should fail when sender doesn't match account owner" + ); + } + + #[test] + fn test_on_recv_packet_wrong_salt() { + let ctx = create_gmp_test_context(); + + let client_id = "cosmoshub-1"; + let sender = "cosmos1test"; + let original_salt = vec![1u8, 2, 3]; + let wrong_salt = vec![4u8, 5, 6]; + + // Account was created with original_salt + let (account_state_pda, account_bump) = + AccountState::derive_address(client_id, sender, &original_salt, &crate::ID).unwrap(); + + // Packet uses wrong_salt + let packet_data = + create_gmp_packet_data(client_id, sender, system_program::ID, wrong_salt, vec![]); + let packet_data_bytes = packet_data.try_to_vec().unwrap(); + + let recv_msg = create_recv_packet_msg(client_id, packet_data_bytes, 1); + let instruction = create_recv_packet_instruction( + ctx.app_state_pda, + ctx.router_program, + ctx.payer, + recv_msg, + ); + + let accounts = vec![ + create_gmp_app_state_account( + ctx.app_state_pda, + ctx.router_program, + ctx.authority, + ctx.app_state_bump, + false, // not paused + ), + create_router_program_account(ctx.router_program), + create_authority_account(ctx.payer), + create_system_program_account(), + create_account_state( + account_state_pda, + client_id.to_string(), + sender.to_string(), + original_salt, // Account has original salt + account_bump, + ), + create_system_program_account(), + ]; + + let result = ctx.mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "OnRecvPacket should fail when salt doesn't match" + ); + } + + #[test] + fn test_on_recv_packet_insufficient_accounts() { + let ctx = create_gmp_test_context(); + + let client_id = "cosmoshub-1"; + let sender = "cosmos1test"; + let salt = vec![1u8, 2, 3]; + + let packet_data = + create_gmp_packet_data(client_id, sender, system_program::ID, salt, vec![]); + let packet_data_bytes = packet_data.try_to_vec().unwrap(); + + let recv_msg = create_recv_packet_msg(client_id, packet_data_bytes, 1); + let instruction = create_recv_packet_instruction( + ctx.app_state_pda, + ctx.router_program, + ctx.payer, + recv_msg, + ); + + let accounts = vec![ + create_gmp_app_state_account( + ctx.app_state_pda, + ctx.router_program, + ctx.authority, + ctx.app_state_bump, + false, // not paused + ), + create_router_program_account(ctx.router_program), + create_authority_account(ctx.payer), + create_system_program_account(), + // Missing remaining accounts! + ]; + + let result = ctx.mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "OnRecvPacket should fail with insufficient accounts" + ); + } + + #[test] + fn test_on_recv_packet_invalid_version() { + let ctx = create_gmp_test_context(); + + let client_id = "cosmoshub-1"; + let sender = "cosmos1test"; + let salt = vec![1u8, 2, 3]; + + let (account_state_pda, _account_bump) = + AccountState::derive_address(client_id, sender, &salt, &crate::ID).unwrap(); + + let packet_data = + create_gmp_packet_data(client_id, sender, system_program::ID, salt, vec![1, 2, 3]); + let packet_data_bytes = packet_data.try_to_vec().unwrap(); + + // Create custom recv_msg with invalid version + let recv_msg = solana_ibc_types::OnRecvPacketMsg { + source_client: client_id.to_string(), + dest_client: "solana-1".to_string(), + sequence: 1, + payload: solana_ibc_types::Payload { + source_port: GMP_PORT_ID.to_string(), + dest_port: GMP_PORT_ID.to_string(), + version: "wrong-version".to_string(), // Invalid version! + encoding: ICS27_ENCODING.to_string(), + value: packet_data_bytes, + }, + relayer: Pubkey::new_unique(), + }; + + let instruction = create_recv_packet_instruction( + ctx.app_state_pda, + ctx.router_program, + ctx.payer, + recv_msg, + ); + + let accounts = vec![ + create_gmp_app_state_account( + ctx.app_state_pda, + ctx.router_program, + ctx.authority, + ctx.app_state_bump, + false, // not paused + ), + create_router_program_account(ctx.router_program), + create_authority_account(ctx.payer), + create_system_program_account(), + create_uninitialized_account_for_pda(account_state_pda), + create_system_program_account(), + ]; + + let result = ctx.mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "OnRecvPacket should fail with invalid version" + ); + } + + #[test] + fn test_on_recv_packet_invalid_source_port() { + let ctx = create_gmp_test_context(); + + let client_id = "cosmoshub-1"; + let sender = "cosmos1test"; + let salt = vec![1u8, 2, 3]; + + let (account_state_pda, _account_bump) = + AccountState::derive_address(client_id, sender, &salt, &crate::ID).unwrap(); + + let packet_data = + create_gmp_packet_data(client_id, sender, system_program::ID, salt, vec![1, 2, 3]); + let packet_data_bytes = packet_data.try_to_vec().unwrap(); + + // Create custom recv_msg with invalid source port + let recv_msg = solana_ibc_types::OnRecvPacketMsg { + source_client: client_id.to_string(), + dest_client: "solana-1".to_string(), + sequence: 1, + payload: solana_ibc_types::Payload { + source_port: "transfer".to_string(), // Invalid source port! + dest_port: GMP_PORT_ID.to_string(), + version: ICS27_VERSION.to_string(), + encoding: ICS27_ENCODING.to_string(), + value: packet_data_bytes, + }, + relayer: Pubkey::new_unique(), + }; + + let instruction = create_recv_packet_instruction( + ctx.app_state_pda, + ctx.router_program, + ctx.payer, + recv_msg, + ); + + let accounts = vec![ + create_gmp_app_state_account( + ctx.app_state_pda, + ctx.router_program, + ctx.authority, + ctx.app_state_bump, + false, // not paused + ), + create_router_program_account(ctx.router_program), + create_authority_account(ctx.payer), + create_system_program_account(), + create_uninitialized_account_for_pda(account_state_pda), + create_system_program_account(), + ]; + + let result = ctx.mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "OnRecvPacket should fail with invalid source port" + ); + } + + #[test] + fn test_on_recv_packet_invalid_encoding() { + let ctx = create_gmp_test_context(); + + let client_id = "cosmoshub-1"; + let sender = "cosmos1test"; + let salt = vec![1u8, 2, 3]; + + let (account_state_pda, _account_bump) = + AccountState::derive_address(client_id, sender, &salt, &crate::ID).unwrap(); + + let packet_data = + create_gmp_packet_data(client_id, sender, system_program::ID, salt, vec![1, 2, 3]); + let packet_data_bytes = packet_data.try_to_vec().unwrap(); + + // Create custom recv_msg with invalid encoding + let recv_msg = solana_ibc_types::OnRecvPacketMsg { + source_client: client_id.to_string(), + dest_client: "solana-1".to_string(), + sequence: 1, + payload: solana_ibc_types::Payload { + source_port: GMP_PORT_ID.to_string(), + dest_port: GMP_PORT_ID.to_string(), + version: ICS27_VERSION.to_string(), + encoding: "application/json".to_string(), // Invalid encoding! + value: packet_data_bytes, + }, + relayer: Pubkey::new_unique(), + }; + + let instruction = create_recv_packet_instruction( + ctx.app_state_pda, + ctx.router_program, + ctx.payer, + recv_msg, + ); + + let accounts = vec![ + create_gmp_app_state_account( + ctx.app_state_pda, + ctx.router_program, + ctx.authority, + ctx.app_state_bump, + false, // not paused + ), + create_router_program_account(ctx.router_program), + create_authority_account(ctx.payer), + create_system_program_account(), + create_uninitialized_account_for_pda(account_state_pda), + create_system_program_account(), + ]; + + let result = ctx.mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "OnRecvPacket should fail with invalid encoding" + ); + } + + #[test] + fn test_on_recv_packet_invalid_dest_port() { + let ctx = create_gmp_test_context(); + + let client_id = "cosmoshub-1"; + let sender = "cosmos1test"; + let salt = vec![1u8, 2, 3]; + + let (account_state_pda, _account_bump) = + AccountState::derive_address(client_id, sender, &salt, &crate::ID).unwrap(); + + let packet_data = + create_gmp_packet_data(client_id, sender, system_program::ID, salt, vec![1, 2, 3]); + let packet_data_bytes = packet_data.try_to_vec().unwrap(); + + // Create custom recv_msg with invalid dest port + let recv_msg = solana_ibc_types::OnRecvPacketMsg { + source_client: client_id.to_string(), + dest_client: "solana-1".to_string(), + sequence: 1, + payload: solana_ibc_types::Payload { + source_port: GMP_PORT_ID.to_string(), + dest_port: "transfer".to_string(), // Invalid dest port! + version: ICS27_VERSION.to_string(), + encoding: ICS27_ENCODING.to_string(), + value: packet_data_bytes, + }, + relayer: Pubkey::new_unique(), + }; + + let instruction = create_recv_packet_instruction( + ctx.app_state_pda, + ctx.router_program, + ctx.payer, + recv_msg, + ); + + let accounts = vec![ + create_gmp_app_state_account( + ctx.app_state_pda, + ctx.router_program, + ctx.authority, + ctx.app_state_bump, + false, // not paused + ), + create_router_program_account(ctx.router_program), + create_authority_account(ctx.payer), + create_system_program_account(), + create_uninitialized_account_for_pda(account_state_pda), + create_system_program_account(), + ]; + + let result = ctx.mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "OnRecvPacket should fail with invalid dest port" + ); + } + + #[test] + fn test_on_recv_packet_account_key_mismatch() { + let ctx = create_gmp_test_context(); + + let client_id = "cosmoshub-1"; + let sender = "cosmos1test"; + let salt = vec![1u8, 2, 3]; + + let (expected_account_state_pda, _account_bump) = + AccountState::derive_address(client_id, sender, &salt, &crate::ID).unwrap(); + + // Use a different account key than expected + let wrong_account_key = Pubkey::new_unique(); + + let packet_data = + create_gmp_packet_data(client_id, sender, system_program::ID, salt, vec![]); + let packet_data_bytes = packet_data.try_to_vec().unwrap(); + + let recv_msg = create_recv_packet_msg(client_id, packet_data_bytes, 1); + let instruction = create_recv_packet_instruction( + ctx.app_state_pda, + ctx.router_program, + ctx.payer, + recv_msg, + ); + + let accounts = vec![ + create_gmp_app_state_account( + ctx.app_state_pda, + ctx.router_program, + ctx.authority, + ctx.app_state_bump, + false, // not paused + ), + create_router_program_account(ctx.router_program), + create_authority_account(ctx.payer), + create_system_program_account(), + create_uninitialized_account_for_pda(wrong_account_key), // Wrong account key! + create_system_program_account(), + ]; + + let result = ctx.mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "OnRecvPacket should fail when account key doesn't match expected PDA (expected: {expected_account_state_pda}, got: {wrong_account_key})" + ); + } + + #[test] + fn test_on_recv_packet_success_with_cpi() { + use gmp_counter_app::ID as COUNTER_APP_ID; + use prost::Message as ProstMessage; + use solana_sdk::account::Account; + use solana_sdk::bpf_loader_upgradeable; + + // Create Mollusk instance and load both programs + let mut mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + // Add the counter app program so CPI will work + // Use BPF loader upgradeable for Anchor programs + mollusk.add_program( + &COUNTER_APP_ID, + "../../target/deploy/gmp_counter_app", + &bpf_loader_upgradeable::ID, + ); + + let authority = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let (app_state_pda, app_state_bump) = + Pubkey::find_program_address(&[GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], &crate::ID); + + // Create packet data that will call the counter app + let client_id = "cosmoshub-1"; + let sender = "cosmos1test"; + let salt = vec![1u8, 2, 3]; + + let (account_state_pda, _account_bump) = + AccountState::derive_address(client_id, sender, &salt, &crate::ID).unwrap(); + + // Counter app state and user counter PDAs + let (counter_app_state_pda, counter_app_state_bump) = Pubkey::find_program_address( + &[gmp_counter_app::state::CounterAppState::SEED], + &COUNTER_APP_ID, + ); + + let (user_counter_pda, _user_counter_bump) = Pubkey::find_program_address( + &[ + gmp_counter_app::state::UserCounter::SEED, + account_state_pda.as_ref(), + ], + &COUNTER_APP_ID, + ); + + // Create SolanaInstruction that will increment the counter + let counter_instruction = gmp_counter_app::instruction::Increment { amount: 5 }; + let counter_instruction_data = anchor_lang::InstructionData::data(&counter_instruction); + + // Build SolanaInstruction for the payload + let solana_instruction = SolanaInstruction { + program_id: COUNTER_APP_ID.to_bytes().to_vec(), + accounts: vec![ + // app_state + SolanaAccountMeta { + pubkey: counter_app_state_pda.to_bytes().to_vec(), + is_signer: false, + is_writable: true, + }, + // user_counter + SolanaAccountMeta { + pubkey: user_counter_pda.to_bytes().to_vec(), + is_signer: false, + is_writable: true, + }, + // user_authority (account_state_pda will sign via invoke_signed) + SolanaAccountMeta { + pubkey: account_state_pda.to_bytes().to_vec(), + is_signer: true, + is_writable: false, + }, + // payer will be injected at position 3 by GMP + // system_program + SolanaAccountMeta { + pubkey: system_program::ID.to_bytes().to_vec(), + is_signer: false, + is_writable: false, + }, + ], + data: counter_instruction_data, + payer_position: Some(3), // Inject payer at position 3 + }; + + let mut solana_instruction_bytes = Vec::new(); + solana_instruction + .encode(&mut solana_instruction_bytes) + .unwrap(); + + // Create GMPPacketData with the counter instruction as payload using protobuf + let proto_packet_data = crate::proto::GmpPacketData { + sender: sender.to_string(), + receiver: COUNTER_APP_ID.to_string(), + salt: salt.clone(), + payload: solana_instruction_bytes, + memo: String::new(), + }; + + let mut packet_data_bytes = Vec::new(); + proto_packet_data.encode(&mut packet_data_bytes).unwrap(); + + let recv_msg = solana_ibc_types::OnRecvPacketMsg { + source_client: client_id.to_string(), + dest_client: "solana-1".to_string(), + sequence: 1, + payload: solana_ibc_types::Payload { + source_port: GMP_PORT_ID.to_string(), + dest_port: GMP_PORT_ID.to_string(), + version: ICS27_VERSION.to_string(), + encoding: ICS27_ENCODING.to_string(), + value: packet_data_bytes, + }, + relayer: Pubkey::new_unique(), + }; + + let instruction_data = crate::instruction::OnRecvPacket { msg: recv_msg }; + + let instruction = SolanaInstructionSDK { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(router_program, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(system_program::ID, false), + // Remaining accounts for CPI: + AccountMeta::new(account_state_pda, false), // [0] account_state (GMP account) + AccountMeta::new_readonly(COUNTER_APP_ID, false), // [1] target_program + AccountMeta::new(counter_app_state_pda, false), // [2] counter app state + AccountMeta::new(user_counter_pda, false), // [3] user counter + AccountMeta::new_readonly(account_state_pda, false), // [4] user_authority (same as [0]) + AccountMeta::new_readonly(system_program::ID, false), // [5] system program + ], + data: instruction_data.data(), + }; + + // Create counter app state + let counter_app_state = gmp_counter_app::state::CounterAppState { + authority, + total_counters: 0, + total_gmp_calls: 0, + bump: counter_app_state_bump, + }; + let mut counter_app_state_data = Vec::new(); + counter_app_state_data + .extend_from_slice(gmp_counter_app::state::CounterAppState::DISCRIMINATOR); + counter_app_state + .serialize(&mut counter_app_state_data) + .unwrap(); + + let accounts = vec![ + create_gmp_app_state_account( + app_state_pda, + router_program, + authority, + app_state_bump, + false, // not paused + ), + create_router_program_account(router_program), + create_authority_account(payer), + create_system_program_account(), + // Remaining accounts + create_uninitialized_account_for_pda(account_state_pda), // Account state will be created + // Counter app program (loaded via mollusk.add_program()) + ( + COUNTER_APP_ID, + Account { + lamports: 1_000_000, + data: vec![], + owner: bpf_loader_upgradeable::ID, + executable: true, + rent_epoch: 0, + }, + ), + ( + counter_app_state_pda, + Account { + lamports: 1_000_000, + data: counter_app_state_data, + owner: COUNTER_APP_ID, + executable: false, + rent_epoch: 0, + }, + ), + create_uninitialized_account_for_pda(user_counter_pda), // User counter will be created + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + + // OnRecvPacket should succeed even if CPI fails (returns error ack instead) + // This is the correct behavior - OnRecvPacket never fails the transaction, + // it returns success/error acks + assert!( + !result.program_result.is_err(), + "OnRecvPacket instruction should succeed (returns ack even on CPI failure): {:?}", + result.program_result + ); + + // Verify acknowledgement is returned + assert!( + !result.return_data.is_empty(), + "Should return acknowledgement" + ); + + // Parse the acknowledgement and verify CPI succeeded + // The ack is protobuf-encoded + // The return data in Mollusk is just the raw bytes, but OnRecvPacket uses + // anchor's return mechanism which prefixes with length + // Skip the first 4 bytes (u32 length prefix) that Anchor adds + let ack_bytes = if result.return_data.len() > 4 { + &result.return_data[4..] + } else { + &result.return_data[..] + }; + + let ack = crate::state::GMPAcknowledgement::decode(ack_bytes).unwrap(); + + assert!( + ack.success, + "CPI execution should succeed, but got error: {}", + ack.error + ); + + // Verify account state was created and has correct data + let acc = result + .get_account(&account_state_pda) + .expect("Account state should be created"); + + assert_eq!( + acc.owner, + crate::ID, + "Account should be owned by GMP program" + ); + assert!(!acc.data.is_empty(), "Account should have data"); + + // Check discriminator + assert_eq!( + &acc.data[0..crate::constants::DISCRIMINATOR_SIZE], + AccountState::DISCRIMINATOR, + "Should have correct discriminator" + ); + + // Deserialize and verify account state + let account_state = + AccountState::try_deserialize(&mut &acc.data[crate::constants::DISCRIMINATOR_SIZE..]) + .expect("Failed to deserialize account state"); + assert_eq!(account_state.nonce, 1, "Nonce should be incremented to 1"); + assert_eq!(account_state.client_id, client_id); + assert_eq!(account_state.sender, sender); + assert_eq!(account_state.salt, salt); + } +} diff --git a/programs/solana/programs/ics27-gmp/src/instructions/on_timeout_packet.rs b/programs/solana/programs/ics27-gmp/src/instructions/on_timeout_packet.rs new file mode 100644 index 000000000..7b44ec226 --- /dev/null +++ b/programs/solana/programs/ics27-gmp/src/instructions/on_timeout_packet.rs @@ -0,0 +1,322 @@ +use crate::constants::*; +use crate::errors::GMPError; +use crate::events::GMPTimeoutProcessed; +use crate::state::GMPAppState; +use anchor_lang::prelude::*; + +/// Process IBC packet timeout (called by router via CPI) +#[derive(Accounts)] +#[instruction(msg: solana_ibc_types::OnTimeoutPacketMsg)] +pub struct OnTimeoutPacket<'info> { + /// App state account - validated by Anchor PDA constraints + #[account( + seeds = [GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + bump = app_state.bump + )] + pub app_state: Account<'info, GMPAppState>, + + /// Router program calling this instruction + /// CHECK: Validated in handler + pub router_program: UncheckedAccount<'info>, + + /// Relayer fee payer (passed by router but not used in timeout handler) + /// CHECK: Router always passes this account + #[account(mut)] + pub payer: UncheckedAccount<'info>, + + /// CHECK: System program (passed by router but not used in timeout handler) + pub system_program: UncheckedAccount<'info>, +} + +pub fn on_timeout_packet( + ctx: Context, + msg: solana_ibc_types::OnTimeoutPacketMsg, +) -> Result<()> { + let clock = Clock::get()?; + let app_state = &ctx.accounts.app_state; + + // Validate router program + require!( + ctx.accounts.router_program.key() == app_state.router_program, + GMPError::UnauthorizedRouter + ); + + // Check if app is operational + app_state.can_operate()?; + + // Parse packet data from router message + let packet_data = crate::router_cpi::parse_timeout_data_from_router_cpi(&msg)?; + let sequence = msg.sequence; + + // Validate packet data + packet_data.validate()?; + + // Convert cross-chain sender address to deterministic Solana pubkey + let sender = crate::utils::derive_pubkey_from_address(&packet_data.sender)?; + + emit!(GMPTimeoutProcessed { + sender, + sequence, + timeout_info: format!("timestamp:{}", clock.unix_timestamp), + timestamp: clock.unix_timestamp, + }); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::constants::GMP_PORT_ID; + use crate::test_utils::*; + use anchor_lang::InstructionData; + use mollusk_svm::Mollusk; + use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + }; + + #[test] + fn test_on_timeout_packet_app_paused() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let (app_state_pda, app_state_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + + let timeout_msg = solana_ibc_types::OnTimeoutPacketMsg { + source_client: "cosmoshub-1".to_string(), + dest_client: "solana-1".to_string(), + sequence: 1, + payload: solana_ibc_types::Payload { + source_port: GMP_PORT_ID.to_string(), + dest_port: GMP_PORT_ID.to_string(), + version: "gmp-1".to_string(), + encoding: "proto3".to_string(), + value: vec![], + }, + relayer: Pubkey::new_unique(), + }; + + let instruction_data = crate::instruction::OnTimeoutPacket { msg: timeout_msg }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new_readonly(app_state_pda, false), + AccountMeta::new_readonly(router_program, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_gmp_app_state_account( + app_state_pda, + router_program, + authority, + app_state_bump, + true, // paused + ), + create_router_program_account(router_program), + create_authority_account(payer), + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "OnTimeoutPacket should fail when app is paused" + ); + } + + #[test] + fn test_on_timeout_packet_unauthorized_router() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + let wrong_router = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let (app_state_pda, app_state_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + + let timeout_msg = solana_ibc_types::OnTimeoutPacketMsg { + source_client: "cosmoshub-1".to_string(), + dest_client: "solana-1".to_string(), + sequence: 1, + payload: solana_ibc_types::Payload { + source_port: GMP_PORT_ID.to_string(), + dest_port: GMP_PORT_ID.to_string(), + version: "gmp-1".to_string(), + encoding: "proto3".to_string(), + value: vec![], + }, + relayer: Pubkey::new_unique(), + }; + + let instruction_data = crate::instruction::OnTimeoutPacket { msg: timeout_msg }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new_readonly(app_state_pda, false), + AccountMeta::new_readonly(wrong_router, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_gmp_app_state_account( + app_state_pda, + router_program, + authority, + app_state_bump, + false, // not paused + ), + create_router_program_account(wrong_router), + create_authority_account(payer), + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "OnTimeoutPacket should fail with unauthorized router" + ); + } + + #[test] + fn test_on_timeout_packet_invalid_app_state_pda() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let port_id = "gmpport".to_string(); + + let (_correct_app_state_pda, _correct_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, port_id.as_bytes()], + &crate::ID, + ); + + // Use wrong PDA + let wrong_app_state_pda = Pubkey::new_unique(); + + let timeout_msg = solana_ibc_types::OnTimeoutPacketMsg { + source_client: "cosmoshub-1".to_string(), + dest_client: "solana-1".to_string(), + sequence: 1, + payload: solana_ibc_types::Payload { + source_port: GMP_PORT_ID.to_string(), + dest_port: GMP_PORT_ID.to_string(), + version: "gmp-1".to_string(), + encoding: "proto3".to_string(), + value: vec![], + }, + relayer: Pubkey::new_unique(), + }; + + let instruction_data = crate::instruction::OnTimeoutPacket { msg: timeout_msg }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new_readonly(wrong_app_state_pda, false), // Wrong PDA! + AccountMeta::new_readonly(router_program, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ], + data: instruction_data.data(), + }; + + // Create account state at wrong PDA for testing + let wrong_bump = 255u8; + let accounts = vec![ + create_gmp_app_state_account( + wrong_app_state_pda, + router_program, + authority, + wrong_bump, + false, // not paused + ), + create_router_program_account(router_program), + create_authority_account(payer), + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "OnTimeoutPacket should fail with invalid app_state PDA" + ); + } + + #[test] + fn test_on_timeout_packet_invalid_packet_data() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let (app_state_pda, app_state_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + + // Create timeout message with invalid packet data in payload.value + let timeout_msg = solana_ibc_types::OnTimeoutPacketMsg { + source_client: "cosmoshub-1".to_string(), + dest_client: "solana-1".to_string(), + sequence: 1, + payload: solana_ibc_types::Payload { + source_port: GMP_PORT_ID.to_string(), + dest_port: GMP_PORT_ID.to_string(), + version: "gmp-1".to_string(), + encoding: "proto3".to_string(), + value: vec![0xFF, 0xFF, 0xFF], // Invalid/malformed packet data! + }, + relayer: Pubkey::new_unique(), + }; + + let instruction_data = crate::instruction::OnTimeoutPacket { msg: timeout_msg }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new_readonly(app_state_pda, false), + AccountMeta::new_readonly(router_program, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_gmp_app_state_account( + app_state_pda, + router_program, + authority, + app_state_bump, + false, // not paused + ), + create_router_program_account(router_program), + create_authority_account(payer), + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "OnTimeoutPacket should fail with invalid/malformed packet data" + ); + } +} diff --git a/programs/solana/programs/ics27-gmp/src/instructions/send_call.rs b/programs/solana/programs/ics27-gmp/src/instructions/send_call.rs new file mode 100644 index 000000000..68794cb9e --- /dev/null +++ b/programs/solana/programs/ics27-gmp/src/instructions/send_call.rs @@ -0,0 +1,707 @@ +use crate::constants::*; +use crate::errors::GMPError; +use crate::events::GMPCallSent; +use crate::proto::GmpPacketData; +use crate::state::{GMPAppState, SendCallMsg}; +use anchor_lang::prelude::*; +use prost::Message as ProstMessage; +use solana_ibc_types::{MsgSendPacket, Payload}; + +/// Send a GMP call packet +#[derive(Accounts)] +#[instruction(msg: SendCallMsg)] +pub struct SendCall<'info> { + /// App state account - validated by Anchor PDA constraints + #[account( + mut, + seeds = [GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + bump = app_state.bump + )] + pub app_state: Account<'info, GMPAppState>, + + /// Sender of the call + pub sender: Signer<'info>, + + #[account(mut)] + pub payer: Signer<'info>, + + /// Router program for sending packets + /// CHECK: Validated against `app_state` + #[account( + constraint = router_program.key() == app_state.router_program @ GMPError::InvalidRouter + )] + pub router_program: AccountInfo<'info>, + + /// Router state account + /// CHECK: Router program validates this + #[account()] + pub router_state: AccountInfo<'info>, + + /// Client sequence account for packet sequencing + /// CHECK: Router program validates this + #[account(mut)] + pub client_sequence: AccountInfo<'info>, + + /// Packet commitment account to be created + /// CHECK: Router program validates this + #[account(mut)] + pub packet_commitment: AccountInfo<'info>, + + /// Router caller PDA that represents our app + /// CHECK: This is a PDA derived with `router_caller` seeds + #[account( + seeds = [b"router_caller"], + bump, + )] + pub router_caller: AccountInfo<'info>, + + /// IBC app registration account + /// CHECK: Router program validates this + #[account()] + pub ibc_app: AccountInfo<'info>, + + /// Client account + /// CHECK: Router program validates this + #[account()] + pub client: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} + +pub fn send_call(ctx: Context, msg: SendCallMsg) -> Result { + let clock = Clock::get()?; + let current_time = clock.unix_timestamp; + let app_state = &mut ctx.accounts.app_state; + + // Check if app is operational + app_state.can_operate()?; + + // Validate message + msg.validate(current_time)?; + + // Create protobuf packet data (matching Ethereum format - no client_id) + // Note: Empty receiver (system program / all zeros) indicates Cosmos SDK message execution + let receiver_str = if msg.receiver == Pubkey::default() { + String::new() // Empty string for Cosmos SDK messages + } else { + msg.receiver.to_string() + }; + + let proto_packet_data = GmpPacketData { + sender: ctx.accounts.sender.key().to_string(), + receiver: receiver_str, + salt: msg.salt.clone(), + payload: msg.payload.clone(), + memo: msg.memo.clone(), + }; + + // Encode using protobuf + let mut packet_data_bytes = Vec::new(); + proto_packet_data + .encode(&mut packet_data_bytes) + .map_err(|_| GMPError::InvalidPacketData)?; + + // Create IBC packet payload + let ibc_payload = Payload { + source_port: GMP_PORT_ID.to_string(), + dest_port: GMP_PORT_ID.to_string(), + version: ICS27_VERSION.to_string(), + encoding: ICS27_ENCODING.to_string(), + value: packet_data_bytes, + }; + + // Create send packet message for router + let router_msg = MsgSendPacket { + source_client: msg.source_client.clone(), + timeout_timestamp: msg.timeout_timestamp, + payload: ibc_payload, + }; + + // Call router via CPI to actually send the packet + let sequence = crate::router_cpi::send_packet_cpi( + &ctx.accounts.router_program, + &ctx.accounts.router_state, + &ctx.accounts.client_sequence, + &ctx.accounts.packet_commitment, + &ctx.accounts.router_caller.to_account_info(), + &ctx.accounts.payer.to_account_info(), + &ctx.accounts.ibc_app, + &ctx.accounts.client, + &ctx.accounts.system_program.to_account_info(), + router_msg, + ctx.bumps.router_caller, + )?; + + // Emit event + emit!(GMPCallSent { + sequence, + sender: ctx.accounts.sender.key(), + receiver: msg.receiver, + client_id: msg.source_client, + salt: msg.salt, + payload_size: msg.payload.len() as u64, + timeout_timestamp: msg.timeout_timestamp, + }); + + msg!( + "GMP call sent: sender={}, receiver={}, sequence={}", + ctx.accounts.sender.key(), + msg.receiver, + sequence + ); + + Ok(sequence) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::*; + use anchor_lang::InstructionData; + use mollusk_svm::Mollusk; + use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + system_program, + }; + + #[test] + fn test_send_call_app_paused() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let sender = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + let router_state = Pubkey::new_unique(); + let client_sequence = Pubkey::new_unique(); + let packet_commitment = Pubkey::new_unique(); + let ibc_app = Pubkey::new_unique(); + let client = Pubkey::new_unique(); + let (app_state_pda, app_state_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + + let (router_caller_pda, _router_caller_bump) = + Pubkey::find_program_address(&[b"router_caller"], &crate::ID); + + let msg = SendCallMsg { + source_client: "cosmoshub-1".to_string(), + receiver: Pubkey::new_unique(), + salt: vec![1, 2, 3], + payload: vec![4, 5, 6], + timeout_timestamp: 9_999_999_999, + memo: String::new(), + }; + + let instruction_data = crate::instruction::SendCall { msg }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(sender, true), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(router_program, false), + AccountMeta::new_readonly(router_state, false), + AccountMeta::new(client_sequence, false), + AccountMeta::new(packet_commitment, false), + AccountMeta::new_readonly(router_caller_pda, false), + AccountMeta::new_readonly(ibc_app, false), + AccountMeta::new_readonly(client, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_gmp_app_state_account( + app_state_pda, + router_program, + authority, + app_state_bump, + true, // paused + ), + create_authority_account(sender), + create_authority_account(payer), + create_router_program_account(router_program), + create_authority_account(router_state), + create_authority_account(client_sequence), + create_authority_account(packet_commitment), + create_authority_account(router_caller_pda), + create_authority_account(ibc_app), + create_authority_account(client), + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "SendCall should fail when app is paused" + ); + } + + #[test] + fn test_send_call_invalid_timeout() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let sender = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + let router_state = Pubkey::new_unique(); + let client_sequence = Pubkey::new_unique(); + let packet_commitment = Pubkey::new_unique(); + let ibc_app = Pubkey::new_unique(); + let client = Pubkey::new_unique(); + let (app_state_pda, app_state_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + + let (router_caller_pda, _router_caller_bump) = + Pubkey::find_program_address(&[b"router_caller"], &crate::ID); + + let msg = SendCallMsg { + source_client: "cosmoshub-1".to_string(), + receiver: Pubkey::new_unique(), + salt: vec![1, 2, 3], + payload: vec![4, 5, 6], + timeout_timestamp: 1_000_000, // Timeout in the past + memo: String::new(), + }; + + let instruction_data = crate::instruction::SendCall { msg }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(sender, true), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(router_program, false), + AccountMeta::new_readonly(router_state, false), + AccountMeta::new(client_sequence, false), + AccountMeta::new(packet_commitment, false), + AccountMeta::new_readonly(router_caller_pda, false), + AccountMeta::new_readonly(ibc_app, false), + AccountMeta::new_readonly(client, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_gmp_app_state_account( + app_state_pda, + router_program, + authority, + app_state_bump, + false, // not paused + ), + create_authority_account(sender), + create_authority_account(payer), + create_router_program_account(router_program), + create_authority_account(router_state), + create_authority_account(client_sequence), + create_authority_account(packet_commitment), + create_authority_account(router_caller_pda), + create_authority_account(ibc_app), + create_authority_account(client), + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "SendCall should fail with timeout in the past" + ); + } + + #[test] + fn test_send_call_invalid_app_state_pda() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let sender = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + let router_state = Pubkey::new_unique(); + let client_sequence = Pubkey::new_unique(); + let packet_commitment = Pubkey::new_unique(); + let ibc_app = Pubkey::new_unique(); + let client = Pubkey::new_unique(); + let port_id = "gmpport".to_string(); + + let (_correct_app_state_pda, app_state_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, port_id.as_bytes()], + &crate::ID, + ); + + // Use wrong PDA in instruction + let wrong_app_state_pda = Pubkey::new_unique(); + + let (router_caller_pda, _) = Pubkey::find_program_address(&[b"router_caller"], &crate::ID); + + let msg = SendCallMsg { + source_client: "cosmoshub-1".to_string(), + receiver: Pubkey::new_unique(), + salt: vec![1, 2, 3], + payload: vec![4, 5, 6], + timeout_timestamp: 9_999_999_999, + memo: String::new(), + }; + + let instruction_data = crate::instruction::SendCall { msg }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(wrong_app_state_pda, false), // Wrong PDA! + AccountMeta::new_readonly(sender, true), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(router_program, false), + AccountMeta::new_readonly(router_state, false), + AccountMeta::new(client_sequence, false), + AccountMeta::new(packet_commitment, false), + AccountMeta::new_readonly(router_caller_pda, false), + AccountMeta::new_readonly(ibc_app, false), + AccountMeta::new_readonly(client, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_gmp_app_state_account( + wrong_app_state_pda, + router_program, + authority, + app_state_bump, + false, + ), + create_authority_account(sender), + create_authority_account(payer), + create_router_program_account(router_program), + create_authority_account(router_state), + create_authority_account(client_sequence), + create_authority_account(packet_commitment), + create_authority_account(router_caller_pda), + create_authority_account(ibc_app), + create_authority_account(client), + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "SendCall should fail with invalid app_state PDA" + ); + } + + #[test] + fn test_send_call_wrong_router_program() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let sender = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let correct_router_program = Pubkey::new_unique(); + let wrong_router_program = Pubkey::new_unique(); // Different router! + let router_state = Pubkey::new_unique(); + let client_sequence = Pubkey::new_unique(); + let packet_commitment = Pubkey::new_unique(); + let ibc_app = Pubkey::new_unique(); + let client = Pubkey::new_unique(); + let (app_state_pda, app_state_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + + let (router_caller_pda, _) = Pubkey::find_program_address(&[b"router_caller"], &crate::ID); + + let msg = SendCallMsg { + source_client: "cosmoshub-1".to_string(), + receiver: Pubkey::new_unique(), + salt: vec![1, 2, 3], + payload: vec![4, 5, 6], + timeout_timestamp: 9_999_999_999, + memo: String::new(), + }; + + let instruction_data = crate::instruction::SendCall { msg }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(sender, true), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(wrong_router_program, false), // Wrong router! + AccountMeta::new_readonly(router_state, false), + AccountMeta::new(client_sequence, false), + AccountMeta::new(packet_commitment, false), + AccountMeta::new_readonly(router_caller_pda, false), + AccountMeta::new_readonly(ibc_app, false), + AccountMeta::new_readonly(client, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_gmp_app_state_account( + app_state_pda, + correct_router_program, // Stored in state + authority, + app_state_bump, + false, + ), + create_authority_account(sender), + create_authority_account(payer), + create_router_program_account(wrong_router_program), // Wrong one passed + create_authority_account(router_state), + create_authority_account(client_sequence), + create_authority_account(packet_commitment), + create_authority_account(router_caller_pda), + create_authority_account(ibc_app), + create_authority_account(client), + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "SendCall should fail with wrong router_program" + ); + } + + #[test] + fn test_send_call_payload_too_large() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let sender = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + let router_state = Pubkey::new_unique(); + let client_sequence = Pubkey::new_unique(); + let packet_commitment = Pubkey::new_unique(); + let ibc_app = Pubkey::new_unique(); + let client = Pubkey::new_unique(); + let (app_state_pda, app_state_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + + let (router_caller_pda, _) = Pubkey::find_program_address(&[b"router_caller"], &crate::ID); + + let msg = SendCallMsg { + source_client: "cosmoshub-1".to_string(), + receiver: Pubkey::new_unique(), + salt: vec![1, 2, 3], + payload: vec![0; crate::constants::MAX_PAYLOAD_LENGTH + 1], // Exceeds limit! + timeout_timestamp: 9_999_999_999, + memo: String::new(), + }; + + let instruction_data = crate::instruction::SendCall { msg }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(sender, true), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(router_program, false), + AccountMeta::new_readonly(router_state, false), + AccountMeta::new(client_sequence, false), + AccountMeta::new(packet_commitment, false), + AccountMeta::new_readonly(router_caller_pda, false), + AccountMeta::new_readonly(ibc_app, false), + AccountMeta::new_readonly(client, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_gmp_app_state_account( + app_state_pda, + router_program, + authority, + app_state_bump, + false, + ), + create_authority_account(sender), + create_authority_account(payer), + create_router_program_account(router_program), + create_authority_account(router_state), + create_authority_account(client_sequence), + create_authority_account(packet_commitment), + create_authority_account(router_caller_pda), + create_authority_account(ibc_app), + create_authority_account(client), + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "SendCall should fail with payload too large" + ); + } + + #[test] + fn test_send_call_empty_payload() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let sender = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + let router_state = Pubkey::new_unique(); + let client_sequence = Pubkey::new_unique(); + let packet_commitment = Pubkey::new_unique(); + let ibc_app = Pubkey::new_unique(); + let client = Pubkey::new_unique(); + let (app_state_pda, app_state_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + + let (router_caller_pda, _) = Pubkey::find_program_address(&[b"router_caller"], &crate::ID); + + let msg = SendCallMsg { + source_client: "cosmoshub-1".to_string(), + receiver: Pubkey::new_unique(), + salt: vec![1, 2, 3], + payload: vec![], // Empty payload! + timeout_timestamp: 9_999_999_999, + memo: String::new(), + }; + + let instruction_data = crate::instruction::SendCall { msg }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(sender, true), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(router_program, false), + AccountMeta::new_readonly(router_state, false), + AccountMeta::new(client_sequence, false), + AccountMeta::new(packet_commitment, false), + AccountMeta::new_readonly(router_caller_pda, false), + AccountMeta::new_readonly(ibc_app, false), + AccountMeta::new_readonly(client, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_gmp_app_state_account( + app_state_pda, + router_program, + authority, + app_state_bump, + false, + ), + create_authority_account(sender), + create_authority_account(payer), + create_router_program_account(router_program), + create_authority_account(router_state), + create_authority_account(client_sequence), + create_authority_account(packet_commitment), + create_authority_account(router_caller_pda), + create_authority_account(ibc_app), + create_authority_account(client), + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "SendCall should fail with empty payload" + ); + } + + #[test] + fn test_send_call_empty_client_id() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let sender = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + let router_state = Pubkey::new_unique(); + let client_sequence = Pubkey::new_unique(); + let packet_commitment = Pubkey::new_unique(); + let ibc_app = Pubkey::new_unique(); + let client = Pubkey::new_unique(); + let (app_state_pda, app_state_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + + let (router_caller_pda, _) = Pubkey::find_program_address(&[b"router_caller"], &crate::ID); + + let msg = SendCallMsg { + source_client: String::new(), // Empty client ID! + receiver: Pubkey::new_unique(), + salt: vec![1, 2, 3], + payload: vec![4, 5, 6], + timeout_timestamp: 9_999_999_999, + memo: String::new(), + }; + + let instruction_data = crate::instruction::SendCall { msg }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(sender, true), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(router_program, false), + AccountMeta::new_readonly(router_state, false), + AccountMeta::new(client_sequence, false), + AccountMeta::new(packet_commitment, false), + AccountMeta::new_readonly(router_caller_pda, false), + AccountMeta::new_readonly(ibc_app, false), + AccountMeta::new_readonly(client, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: instruction_data.data(), + }; + + let accounts = vec![ + create_gmp_app_state_account( + app_state_pda, + router_program, + authority, + app_state_bump, + false, + ), + create_authority_account(sender), + create_authority_account(payer), + create_router_program_account(router_program), + create_authority_account(router_state), + create_authority_account(client_sequence), + create_authority_account(packet_commitment), + create_authority_account(router_caller_pda), + create_authority_account(ibc_app), + create_authority_account(client), + create_system_program_account(), + ]; + + let result = mollusk.process_instruction(&instruction, &accounts); + assert!( + result.program_result.is_err(), + "SendCall should fail with empty client_id" + ); + } +} diff --git a/programs/solana/programs/ics27-gmp/src/lib.rs b/programs/solana/programs/ics27-gmp/src/lib.rs new file mode 100644 index 000000000..66076b13c --- /dev/null +++ b/programs/solana/programs/ics27-gmp/src/lib.rs @@ -0,0 +1,84 @@ +use anchor_lang::prelude::*; +use solana_ibc_macros::ibc_app; + +pub mod constants; +pub mod errors; +pub mod events; +pub mod instructions; +pub mod proto; +pub mod router_cpi; +pub mod state; +pub mod utils; + +#[cfg(test)] +pub mod test_utils; + +use instructions::*; +use state::SendCallMsg; + +declare_id!("3W3h4WSE8J9vFzVN8TGFGc9Uchbry3M4MBz4icdSWcFi"); + +#[cfg(test)] +pub fn get_gmp_program_path() -> &'static str { + use std::sync::OnceLock; + static PATH: OnceLock = OnceLock::new(); + + PATH.get_or_init(|| { + std::env::var("GMP_PROGRAM_PATH") + .unwrap_or_else(|_| "../../target/deploy/ics27_gmp".to_string()) + }) +} + +#[ibc_app] +pub mod ics27_gmp { + use super::*; + + /// Initialize the ICS27 GMP application + pub fn initialize(ctx: Context, router_program: Pubkey) -> Result<()> { + instructions::initialize(ctx, router_program) + } + + /// Send a GMP call packet + pub fn send_call(ctx: Context, msg: SendCallMsg) -> Result { + instructions::send_call(ctx, msg) + } + + /// IBC packet receive handler (called by router via CPI) + pub fn on_recv_packet<'info>( + ctx: Context<'_, '_, '_, 'info, OnRecvPacket<'info>>, + msg: solana_ibc_types::OnRecvPacketMsg, + ) -> Result> { + instructions::on_recv_packet(ctx, msg) + } + + /// IBC acknowledgement handler (called by router via CPI) + pub fn on_acknowledgement_packet( + ctx: Context, + msg: solana_ibc_types::OnAcknowledgementPacketMsg, + ) -> Result<()> { + instructions::on_acknowledgement_packet(ctx, msg) + } + + /// IBC timeout handler (called by router via CPI) + pub fn on_timeout_packet( + ctx: Context, + msg: solana_ibc_types::OnTimeoutPacketMsg, + ) -> Result<()> { + instructions::on_timeout_packet(ctx, msg) + } + + /// Pause the entire GMP app (admin only) + pub fn pause_app(ctx: Context) -> Result<()> { + instructions::pause_app(ctx) + } + + /// Unpause the entire GMP app (admin only) + pub fn unpause_app(ctx: Context) -> Result<()> { + instructions::unpause_app(ctx) + } + + /// Update app authority (admin only) + pub fn update_authority(ctx: Context) -> Result<()> { + instructions::update_authority(ctx) + } +} diff --git a/programs/solana/programs/ics27-gmp/src/proto.rs b/programs/solana/programs/ics27-gmp/src/proto.rs new file mode 100644 index 000000000..5beba5d60 --- /dev/null +++ b/programs/solana/programs/ics27-gmp/src/proto.rs @@ -0,0 +1,17 @@ +//! Generated Protobuf types for GMP +//! +//! This module contains types generated from .proto files via prost-build. +//! The proto files are located in proto/gmp/ and proto/solana/. + +// Include generated code from build.rs +pub mod gmp { + include!(concat!(env!("OUT_DIR"), "/gmp.rs")); +} + +pub mod solana { + include!(concat!(env!("OUT_DIR"), "/solana.rs")); +} + +// Re-export for convenience +pub use gmp::{GmpAcknowledgement, GmpPacketData}; +pub use solana::{SolanaAccountMeta, SolanaInstruction}; diff --git a/programs/solana/programs/ics27-gmp/src/router_cpi.rs b/programs/solana/programs/ics27-gmp/src/router_cpi.rs new file mode 100644 index 000000000..4cbbb55ac --- /dev/null +++ b/programs/solana/programs/ics27-gmp/src/router_cpi.rs @@ -0,0 +1,175 @@ +use crate::errors::GMPError; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::{instruction::Instruction, program::invoke_signed}; +use solana_ibc_types::MsgSendPacket; + +/// Send IBC packet via CPI to the ICS26 router +/// This function creates and sends a GMP packet from Solana to another chain +#[allow(clippy::too_many_arguments)] +pub fn send_packet_cpi<'a>( + router_program: &AccountInfo<'a>, + router_state: &AccountInfo<'a>, + client_sequence: &AccountInfo<'a>, + packet_commitment: &AccountInfo<'a>, + router_caller: &AccountInfo<'a>, + payer: &AccountInfo<'a>, + ibc_app: &AccountInfo<'a>, + client: &AccountInfo<'a>, + system_program: &AccountInfo<'a>, + msg: MsgSendPacket, + router_caller_bump: u8, +) -> Result { + // Build instruction data with Anchor discriminator + let mut instruction_data = Vec::with_capacity(256); + + // Anchor instruction discriminator: first 8 bytes of hash of "global:send_packet" + let discriminator = anchor_lang::solana_program::hash::hash(b"global:send_packet").to_bytes(); + instruction_data.extend_from_slice(&discriminator[..8]); + + // Append serialized MsgSendPacket data + msg.serialize(&mut instruction_data)?; + + // Build CPI instruction for router's send_packet + let instruction = Instruction { + program_id: *router_program.key, + accounts: vec![ + AccountMeta::new_readonly(*router_state.key, false), + AccountMeta::new_readonly(*ibc_app.key, false), + AccountMeta::new(*client_sequence.key, false), + AccountMeta::new(*packet_commitment.key, false), + AccountMeta::new_readonly(*router_caller.key, true), // GMP's router_caller PDA signs + AccountMeta::new(*payer.key, true), + AccountMeta::new_readonly(*system_program.key, false), + AccountMeta::new_readonly(*client.key, false), + ], + data: instruction_data, + }; + + // Router caller PDA signer seeds + let signer_seeds = &[b"router_caller".as_slice(), &[router_caller_bump]]; + + // Execute CPI to router with PDA signing + let account_infos = &[ + router_state.clone(), + ibc_app.clone(), + client_sequence.clone(), + packet_commitment.clone(), + router_caller.clone(), + payer.clone(), + system_program.clone(), + client.clone(), + ]; + + invoke_signed(&instruction, account_infos, &[signer_seeds])?; + + // Read sequence number from updated client_sequence account + // The router increments the sequence after sending the packet + let client_sequence_data = client_sequence.try_borrow_data()?; + if client_sequence_data.len() >= 16 { + // Account layout: 8 bytes Anchor discriminator + 8 bytes u64 sequence + let sequence_bytes = &client_sequence_data[8..16]; + let current_sequence = u64::from_le_bytes( + sequence_bytes + .try_into() + .map_err(|_| GMPError::SequenceParseError)?, + ); + // Return the sequence that was just used (current - 1) + Ok(current_sequence.saturating_sub(1)) + } else { + Err(GMPError::SequenceParseError.into()) + } +} + +/// Parse GMP packet data from router CPI call +/// +/// Extracts and validates `GMPPacketData` from the Protobuf-encoded IBC packet payload. +/// The router passes `OnRecvPacketMsg` which contains the source chain client ID and +/// Protobuf-encoded packet data. This function decodes the Protobuf payload and +/// combines it with the IBC context to create the full `GMPPacketData` structure. +/// +/// Note: Port ID validation should be done by the caller using `app_state.port_id` +/// Note: Receiver is kept as string - caller must parse to Pubkey when needed (incoming packets) +pub fn parse_packet_data_from_router_cpi( + msg: &solana_ibc_types::OnRecvPacketMsg, +) -> Result> { + // Decode Protobuf payload from IBC packet + let proto_data = decode_gmp_packet_data(msg.payload.value.as_slice())?; + + // Construct full GMPPacketData by combining Protobuf data with IBC context + let packet_data = Box::new(crate::state::GMPPacketData { + client_id: msg.source_client.clone(), // From IBC context (e.g., "07-tendermint-0") + sender: proto_data.sender, + receiver: proto_data.receiver, // Keep as string (Solana Pubkey base58 for incoming packets) + salt: proto_data.salt, + payload: proto_data.payload, // SolanaInstruction (Protobuf-encoded) + memo: proto_data.memo, + }); + + // Validate all fields (lengths, non-empty checks, etc.) + packet_data.validate()?; + + Ok(packet_data) +} + +/// Parse acknowledgement data from router CPI call +/// +/// Extracts packet data and acknowledgement from `OnAcknowledgementPacketMsg`. +/// Used when the destination chain sends back an acknowledgement for a packet +/// that was previously sent from this Solana chain. +/// +/// Note: For acks of outgoing packets, receiver is a Cosmos address or empty string +pub fn parse_ack_data_from_router_cpi( + msg: &solana_ibc_types::OnAcknowledgementPacketMsg, +) -> Result<(Box, Vec)> { + let proto_data = decode_gmp_packet_data(msg.payload.value.as_slice())?; + + let packet_data = Box::new(crate::state::GMPPacketData { + client_id: msg.source_client.clone(), + sender: proto_data.sender, + receiver: proto_data.receiver, // Keep as string (Cosmos address for outgoing packets) + salt: proto_data.salt, + payload: proto_data.payload, + memo: proto_data.memo, + }); + + packet_data.validate()?; + + // Return both the original packet data and the acknowledgement from destination chain + Ok((packet_data, msg.acknowledgement.clone())) +} + +/// Parse timeout data from router CPI call +/// +/// Extracts packet data from `OnTimeoutPacketMsg` when a packet times out. +/// This occurs when the packet was not delivered to the destination chain +/// within the specified timeout period, and can be proven via timeout proof. +/// +/// Note: For timeouts of outgoing packets, receiver is a Cosmos address or empty string +pub fn parse_timeout_data_from_router_cpi( + msg: &solana_ibc_types::OnTimeoutPacketMsg, +) -> Result> { + // Decode original packet data that timed out + let proto_data = decode_gmp_packet_data(msg.payload.value.as_slice())?; + + let packet_data = Box::new(crate::state::GMPPacketData { + client_id: msg.source_client.clone(), + sender: proto_data.sender, + receiver: proto_data.receiver, // Keep as string (Cosmos address for outgoing packets) + salt: proto_data.salt, + payload: proto_data.payload, + memo: proto_data.memo, + }); + + packet_data.validate()?; + + Ok(packet_data) +} + +/// Decode GMP packet data from protobuf payload with error logging +fn decode_gmp_packet_data(payload: &[u8]) -> Result { + use prost::Message; + crate::proto::GmpPacketData::decode(payload).map_err(|e| { + msg!("Failed to decode GMP packet data: {}", e); + GMPError::PacketDataParseError.into() + }) +} diff --git a/programs/solana/programs/ics27-gmp/src/state.rs b/programs/solana/programs/ics27-gmp/src/state.rs new file mode 100644 index 000000000..8378aabb7 --- /dev/null +++ b/programs/solana/programs/ics27-gmp/src/state.rs @@ -0,0 +1,322 @@ +use crate::constants::*; +use crate::errors::GMPError; +use anchor_lang::prelude::*; + +/// Main GMP application state +#[account] +#[derive(InitSpace)] +pub struct GMPAppState { + /// ICS26 Router program that manages this app + pub router_program: Pubkey, + + /// Administrative authority + pub authority: Pubkey, + + /// Program version for upgrades + pub version: u8, + + /// Emergency pause flag + pub paused: bool, + + /// PDA bump seed + pub bump: u8, +} + +impl GMPAppState { + /// Get signer seeds for this app state + /// Seeds: [`b"app_state`", `GMP_PORT_ID.as_bytes()`, bump] + pub fn signer_seeds(&self) -> Vec> { + vec![ + GMP_APP_STATE_SEED.to_vec(), + GMP_PORT_ID.as_bytes().to_vec(), + vec![self.bump], + ] + } + + /// Check if app is operational + pub fn can_operate(&self) -> Result<()> { + require!(!self.paused, GMPError::AppPaused); + Ok(()) + } +} + +/// Individual account state managed as PDAs +#[account] +#[derive(InitSpace)] +pub struct AccountState { + /// Client ID that created this account + #[max_len(MAX_CLIENT_ID_LENGTH)] + pub client_id: String, + + /// Original sender (checksummed hex address from source chain) + #[max_len(MAX_SENDER_LENGTH)] + pub sender: String, + + /// Salt for unique account generation + #[max_len(MAX_SALT_LENGTH)] + pub salt: Vec, + + /// Execution nonce for replay protection + pub nonce: u64, + + /// Account creation timestamp + pub created_at: i64, + + /// Last execution timestamp + pub last_executed_at: i64, + + /// Total successful executions + pub execution_count: u64, + + /// PDA bump seed + pub bump: u8, +} + +impl AccountState { + /// Derive PDA address for an account + /// Note: If sender is >32 bytes, it will be hashed to fit Solana's PDA seed constraints + pub fn derive_address( + client_id: &str, + sender: &str, + salt: &[u8], + program_id: &Pubkey, + ) -> Result<(Pubkey, u8)> { + use anchor_lang::solana_program::hash::hash; + + require!( + client_id.len() <= MAX_CLIENT_ID_LENGTH, + GMPError::ClientIdTooLong + ); + require!(sender.len() <= MAX_SENDER_LENGTH, GMPError::SenderTooLong); + require!(salt.len() <= MAX_SALT_LENGTH, GMPError::SaltTooLong); + + // Always hash the sender to ensure consistent PDA derivation regardless of address format + // This makes the derivation deterministic and supports any address length + let sender_hash = hash(sender.as_bytes()).to_bytes(); + + let (address, bump) = Pubkey::find_program_address( + &[ACCOUNT_STATE_SEED, client_id.as_bytes(), &sender_hash, salt], + program_id, + ); + + Ok((address, bump)) + } + + /// Get signer seeds for CPI calls + /// Note: Sender is always hashed to match PDA derivation + pub fn signer_seeds(&self) -> Vec> { + use anchor_lang::solana_program::hash::hash; + + let sender_hash = hash(self.sender.as_bytes()).to_bytes(); + + vec![ + ACCOUNT_STATE_SEED.to_vec(), + self.client_id.as_bytes().to_vec(), + sender_hash.to_vec(), + self.salt.clone(), + vec![self.bump], + ] + } + + /// Increment nonce and update execution stats + #[allow(clippy::missing_const_for_fn)] + pub fn execute_nonce_increment(&mut self, current_time: i64) { + self.nonce = self.nonce.saturating_add(1); + self.last_executed_at = current_time; + self.execution_count = self.execution_count.saturating_add(1); + } +} + +/// GMP packet data structure +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct GMPPacketData { + /// Client ID for account derivation + pub client_id: String, + + /// Original sender address (hex string) + pub sender: String, + + /// Target receiver address + /// - For incoming packets (Cosmos → Solana): Solana Pubkey as base58 string + /// - For outgoing packets (Solana → Cosmos): Cosmos address (bech32) or empty string + pub receiver: String, + + /// Salt for account uniqueness + pub salt: Vec, + + /// Serialized execution payload + pub payload: Vec, + + /// Optional memo field + pub memo: String, +} + +impl GMPPacketData { + /// Validate packet data + pub fn validate(&self) -> Result<()> { + require!(!self.client_id.is_empty(), GMPError::InvalidPacketData); + require!( + self.client_id.len() <= MAX_CLIENT_ID_LENGTH, + GMPError::ClientIdTooLong + ); + + require!(!self.sender.is_empty(), GMPError::InvalidPacketData); + require!( + self.sender.len() <= MAX_SENDER_LENGTH, + GMPError::SenderTooLong + ); + + require!(self.salt.len() <= MAX_SALT_LENGTH, GMPError::SaltTooLong); + + require!(!self.payload.is_empty(), GMPError::EmptyPayload); + require!( + self.payload.len() <= MAX_PAYLOAD_LENGTH, + GMPError::PayloadTooLong + ); + + require!(self.memo.len() <= MAX_MEMO_LENGTH, GMPError::MemoTooLong); + + Ok(()) + } +} + +/// Send call message +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct SendCallMsg { + /// Source client identifier + pub source_client: String, + + /// Timeout timestamp (unix seconds) + pub timeout_timestamp: i64, + + /// Receiver program + pub receiver: Pubkey, + + /// Account salt + pub salt: Vec, + + /// Call payload (instruction data + accounts) + pub payload: Vec, + + /// Optional memo + pub memo: String, +} + +impl SendCallMsg { + /// Validate send call message + pub fn validate(&self, current_time: i64) -> Result<()> { + require!(!self.source_client.is_empty(), GMPError::InvalidPacketData); + require!( + self.source_client.len() <= MAX_CLIENT_ID_LENGTH, + GMPError::ClientIdTooLong + ); + + require!(self.salt.len() <= MAX_SALT_LENGTH, GMPError::SaltTooLong); + + require!(!self.payload.is_empty(), GMPError::EmptyPayload); + require!( + self.payload.len() <= MAX_PAYLOAD_LENGTH, + GMPError::PayloadTooLong + ); + + require!(self.memo.len() <= MAX_MEMO_LENGTH, GMPError::MemoTooLong); + + require!( + self.timeout_timestamp > current_time + MIN_TIMEOUT_DURATION, + GMPError::TimeoutTooSoon + ); + require!( + self.timeout_timestamp < current_time + MAX_TIMEOUT_DURATION, + GMPError::TimeoutTooLong + ); + + Ok(()) + } +} + +// Re-export generated Protobuf types +pub use crate::proto::{ + GmpAcknowledgement as GMPAcknowledgement, SolanaAccountMeta, SolanaInstruction, +}; + +/// Helper methods for `SolanaInstruction` +impl SolanaInstruction { + /// Parse Solana instruction from Protobuf-encoded bytes + pub fn try_from_slice(data: &[u8]) -> Result { + use prost::Message; + Self::decode(data).map_err(|_| GMPError::InvalidExecutionPayload.into()) + } + + /// Validate Solana instruction fields + pub fn validate(&self) -> Result<()> { + require!(self.program_id.len() == 32, GMPError::InvalidProgramId); + require!(!self.data.is_empty(), GMPError::EmptyPayload); + require!(self.accounts.len() <= 32, GMPError::TooManyAccounts); + + // Validate all account pubkeys are 32 bytes + for account in &self.accounts { + require!(account.pubkey.len() == 32, GMPError::InvalidAccountKey); + } + + Ok(()) + } + + /// Convert Protobuf account metas to Anchor `AccountMeta` format + pub fn to_account_metas(&self) -> Result> { + let mut account_metas = Vec::new(); + for meta in &self.accounts { + let pubkey = Pubkey::try_from(meta.pubkey.as_slice()) + .map_err(|_| GMPError::InvalidAccountKey)?; + + // Use is_signer directly from the protobuf + // This indicates whether the account should sign at CPI instruction level + account_metas.push(AccountMeta { + pubkey, + is_signer: meta.is_signer, + is_writable: meta.is_writable, + }); + } + Ok(account_metas) + } + + /// Extract program ID as Solana Pubkey + pub fn get_program_id(&self) -> Result { + Pubkey::try_from(self.program_id.as_slice()).map_err(|_| GMPError::InvalidProgramId.into()) + } +} + +/// Helper methods for `GMPAcknowledgement` +impl GMPAcknowledgement { + /// Create success acknowledgement + pub const fn success(data: Vec) -> Self { + Self { + success: true, + data, + error: String::new(), // Proto uses empty string instead of Option + } + } + + /// Create error acknowledgement + pub const fn error(message: String) -> Self { + Self { + success: false, + data: Vec::new(), + error: message, + } + } + + /// Serialize to Protobuf bytes (compatible with Borsh `try_to_vec`) + pub fn try_to_vec(&self) -> Result> { + use prost::Message; + let mut buf = Vec::new(); + self.encode(&mut buf) + .map_err(|_| GMPError::InvalidExecutionPayload)?; + Ok(buf) + } + + /// Deserialize from Protobuf bytes (compatible with Borsh `try_from_slice`) + pub fn try_from_slice(data: &[u8]) -> Result { + use prost::Message; + Self::decode(data).map_err(|_| GMPError::InvalidExecutionPayload.into()) + } +} diff --git a/programs/solana/programs/ics27-gmp/src/test_utils.rs b/programs/solana/programs/ics27-gmp/src/test_utils.rs new file mode 100644 index 000000000..79a59b54b --- /dev/null +++ b/programs/solana/programs/ics27-gmp/src/test_utils.rs @@ -0,0 +1,273 @@ +use crate::constants::{GMP_PORT_ID, ICS27_ENCODING, ICS27_VERSION}; +use crate::state::{AccountState, GMPAppState, GMPPacketData}; +use anchor_lang::{AnchorSerialize, Discriminator, InstructionData}; +use mollusk_svm::Mollusk; +use solana_sdk::{ + account::Account as SolanaAccount, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + system_program, +}; + +pub fn create_gmp_app_state_account( + pubkey: Pubkey, + router_program: Pubkey, + authority: Pubkey, + bump: u8, + paused: bool, +) -> (Pubkey, SolanaAccount) { + let app_state = GMPAppState { + router_program, + authority, + version: 1, + paused, + bump, + }; + + let mut data = Vec::new(); + data.extend_from_slice(GMPAppState::DISCRIMINATOR); + app_state.serialize(&mut data).unwrap(); + + ( + pubkey, + SolanaAccount { + lamports: 1_000_000, + data, + owner: crate::ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +pub fn create_account_state( + pubkey: Pubkey, + client_id: String, + sender: String, + salt: Vec, + bump: u8, +) -> (Pubkey, SolanaAccount) { + let account_state = AccountState { + client_id, + sender, + salt, + nonce: 0, + created_at: 1_600_000_000, + last_executed_at: 1_600_000_000, + execution_count: 0, + bump, + }; + + let mut data = Vec::new(); + data.extend_from_slice(AccountState::DISCRIMINATOR); + account_state.serialize(&mut data).unwrap(); + + ( + pubkey, + SolanaAccount { + lamports: 1_000_000, + data, + owner: crate::ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +pub fn create_account_state_with_nonce( + pubkey: Pubkey, + client_id: String, + sender: String, + salt: Vec, + nonce: u64, + bump: u8, +) -> (Pubkey, SolanaAccount) { + let account_state = AccountState { + client_id, + sender, + salt, + nonce, + created_at: 1_600_000_000, + last_executed_at: 1_600_000_000, + execution_count: 0, + bump, + }; + + let mut data = Vec::new(); + data.extend_from_slice(AccountState::DISCRIMINATOR); + account_state.serialize(&mut data).unwrap(); + + ( + pubkey, + SolanaAccount { + lamports: 1_000_000, + data, + owner: crate::ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +pub const fn create_authority_account(pubkey: Pubkey) -> (Pubkey, SolanaAccount) { + ( + pubkey, + SolanaAccount { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +pub const fn create_router_program_account(pubkey: Pubkey) -> (Pubkey, SolanaAccount) { + ( + pubkey, + SolanaAccount { + lamports: 1_000_000, + data: vec![], + owner: solana_sdk::native_loader::ID, + executable: true, + rent_epoch: 0, + }, + ) +} + +pub const fn create_pda_for_init(pubkey: Pubkey) -> (Pubkey, SolanaAccount) { + ( + pubkey, + SolanaAccount { + lamports: 0, + data: vec![], + owner: system_program::ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +pub const fn create_payer_account(pubkey: Pubkey) -> (Pubkey, SolanaAccount) { + ( + pubkey, + SolanaAccount { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +pub const fn create_system_program_account() -> (Pubkey, SolanaAccount) { + ( + system_program::ID, + SolanaAccount { + lamports: 0, + data: vec![], + owner: solana_sdk::native_loader::ID, + executable: true, + rent_epoch: 0, + }, + ) +} + +pub const fn create_uninitialized_account_for_pda(pubkey: Pubkey) -> (Pubkey, SolanaAccount) { + ( + pubkey, + SolanaAccount { + lamports: 0, + data: vec![], + owner: system_program::ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +pub struct GmpTestContext { + pub mollusk: Mollusk, + pub authority: Pubkey, + pub router_program: Pubkey, + pub payer: Pubkey, + pub app_state_pda: Pubkey, + pub app_state_bump: u8, +} + +pub fn create_gmp_test_context() -> GmpTestContext { + let authority = Pubkey::new_unique(); + let router_program = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + let (app_state_pda, app_state_bump) = Pubkey::find_program_address( + &[crate::constants::GMP_APP_STATE_SEED, GMP_PORT_ID.as_bytes()], + &crate::ID, + ); + + GmpTestContext { + mollusk: Mollusk::new(&crate::ID, crate::get_gmp_program_path()), + authority, + router_program, + payer, + app_state_pda, + app_state_bump, + } +} + +pub fn create_gmp_packet_data( + client_id: &str, + sender: &str, + receiver: Pubkey, + salt: Vec, + payload: Vec, +) -> GMPPacketData { + GMPPacketData { + client_id: client_id.to_string(), + sender: sender.to_string(), + receiver: receiver.to_string(), + salt, + payload, + memo: String::new(), + } +} + +pub fn create_recv_packet_msg( + client_id: &str, + packet_data_bytes: Vec, + sequence: u64, +) -> solana_ibc_types::OnRecvPacketMsg { + solana_ibc_types::OnRecvPacketMsg { + source_client: client_id.to_string(), + dest_client: "solana-1".to_string(), + sequence, + payload: solana_ibc_types::Payload { + source_port: GMP_PORT_ID.to_string(), + dest_port: GMP_PORT_ID.to_string(), + version: ICS27_VERSION.to_string(), + encoding: ICS27_ENCODING.to_string(), + value: packet_data_bytes, + }, + relayer: Pubkey::new_unique(), + } +} + +pub fn create_recv_packet_instruction( + app_state_pda: Pubkey, + router_program: Pubkey, + payer: Pubkey, + msg: solana_ibc_types::OnRecvPacketMsg, +) -> Instruction { + let instruction_data = crate::instruction::OnRecvPacket { msg }; + + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(router_program, false), + AccountMeta::new(payer, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: instruction_data.data(), + } +} diff --git a/programs/solana/programs/ics27-gmp/src/utils.rs b/programs/solana/programs/ics27-gmp/src/utils.rs new file mode 100644 index 000000000..36a87a404 --- /dev/null +++ b/programs/solana/programs/ics27-gmp/src/utils.rs @@ -0,0 +1,3 @@ +pub mod account_management; + +pub use account_management::*; diff --git a/programs/solana/programs/ics27-gmp/src/utils/account_management.rs b/programs/solana/programs/ics27-gmp/src/utils/account_management.rs new file mode 100644 index 000000000..227d49b1d --- /dev/null +++ b/programs/solana/programs/ics27-gmp/src/utils/account_management.rs @@ -0,0 +1,147 @@ +use crate::errors::GMPError; +use crate::state::AccountState; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::{hash::hash, program::invoke_signed, system_instruction}; +use anchor_lang::Discriminator; + +const ADDRESS_HASH_PREFIX: &[u8] = b"gmp_sender_address"; + +/// Get or create account state with proper PDA validation +#[allow(clippy::too_many_arguments)] +pub fn get_or_create_account<'a>( + account_info: &AccountInfo<'a>, + client_id: &str, + sender: &str, + salt: &[u8], + payer: &AccountInfo<'a>, + _system_program: &AccountInfo<'a>, + program_id: &Pubkey, + current_time: i64, + expected_bump: u8, +) -> Result<(AccountState, bool)> { + // Derive expected address and validate + let (expected_address, derived_bump) = + AccountState::derive_address(client_id, sender, salt, program_id)?; + + require!( + account_info.key() == expected_address, + GMPError::InvalidAccountAddress + ); + + require!( + expected_bump == derived_bump, + GMPError::InvalidAccountAddress + ); + + // Check if account already exists + if account_info.data_is_empty() { + // Create new account + let account_size = crate::constants::DISCRIMINATOR_SIZE + AccountState::INIT_SPACE; + let rent = Rent::get()?; + let required_lamports = rent.minimum_balance(account_size); + + // Create account via system program using invoke_signed + // The account_info is a PDA, so we need to sign for it + let sender_hash = hash(sender.as_bytes()).to_bytes(); + let signer_seeds: &[&[u8]] = &[ + crate::constants::ACCOUNT_STATE_SEED, + client_id.as_bytes(), + &sender_hash, + salt, + &[expected_bump], + ]; + + invoke_signed( + &system_instruction::create_account( + payer.key, + account_info.key, + required_lamports, + account_size as u64, + program_id, + ), + &[payer.clone(), account_info.clone()], + &[signer_seeds], + )?; + + // Initialize account state + let account_state = AccountState { + client_id: client_id.to_string(), + sender: sender.to_string(), + salt: salt.to_vec(), + nonce: 0, + created_at: current_time, + last_executed_at: 0, + execution_count: 0, + bump: expected_bump, + }; + + // Serialize and write the account state + save_account_state(account_info, &account_state)?; + + Ok((account_state, true)) + } else { + // Load existing account + let account_data = account_info.try_borrow_data()?; + + // Skip discriminator (8 bytes) and deserialize + let account_state = AccountState::try_deserialize( + &mut &account_data[crate::constants::DISCRIMINATOR_SIZE..], + )?; + + Ok((account_state, false)) + } +} + +/// Save account state to account data +pub fn save_account_state( + account_info: &AccountInfo<'_>, + account_state: &AccountState, +) -> Result<()> { + let mut account_data = account_info.try_borrow_mut_data()?; + + // Write discriminator + account_data[0..crate::constants::DISCRIMINATOR_SIZE] + .copy_from_slice(AccountState::DISCRIMINATOR); + + // Serialize account state after discriminator + account_state.try_serialize(&mut &mut account_data[crate::constants::DISCRIMINATOR_SIZE..])?; + + Ok(()) +} + +/// Validate account ownership and program +pub fn validate_account_ownership( + account_info: &AccountInfo<'_>, + program_id: &Pubkey, +) -> Result<()> { + require!( + account_info.owner == program_id, + GMPError::InvalidAccountAddress + ); + Ok(()) +} + +/// Calculate account rent requirement +pub fn calculate_account_rent() -> Result { + let account_size = crate::constants::DISCRIMINATOR_SIZE + AccountState::INIT_SPACE; + let rent = Rent::get()?; + Ok(rent.minimum_balance(account_size)) +} + +/// Convert any cross-chain address string to a deterministic Solana Pubkey +/// Uses hashing to ensure the same address always maps to the same Pubkey +pub fn derive_pubkey_from_address(sender_address: &str) -> Result { + // Validate input is not empty + if sender_address.is_empty() { + return Err(GMPError::InvalidPacketData.into()); + } + + // Use deterministic hash-based derivation for any address format + // This is simple, reliable, and works for all blockchain address formats + let mut seed_data = Vec::new(); + seed_data.extend_from_slice(ADDRESS_HASH_PREFIX); + seed_data.extend_from_slice(sender_address.as_bytes()); + + let hash_result = hash(&seed_data); + Ok(Pubkey::new_from_array(hash_result.to_bytes())) +} diff --git a/programs/solana/programs/ics27-gmp/tests/gmp_tests.rs b/programs/solana/programs/ics27-gmp/tests/gmp_tests.rs new file mode 100644 index 000000000..9b05311ff --- /dev/null +++ b/programs/solana/programs/ics27-gmp/tests/gmp_tests.rs @@ -0,0 +1,420 @@ +use ics27_gmp::{state::*, ID}; +use solana_sdk::pubkey::Pubkey; + +// ============================================================================ +// Account State Tests +// ============================================================================ + +#[test] +fn test_account_state_derive_address() { + let client_id = "cosmoshub-1"; + let sender = "cosmos1abc123def456"; + let salt = b"salt123"; + + let (address1, bump1) = AccountState::derive_address(client_id, sender, salt, &ID).unwrap(); + let (address2, bump2) = AccountState::derive_address(client_id, sender, salt, &ID).unwrap(); + + // Same inputs should produce same PDA + assert_eq!(address1, address2); + assert_eq!(bump1, bump2); + + // Different sender should produce different PDA + let (address3, _) = + AccountState::derive_address(client_id, "cosmos1different", salt, &ID).unwrap(); + assert_ne!(address1, address3); +} + +#[test] +fn test_account_state_long_sender_hashed() { + // Test with very long sender address (>32 bytes) + let client_id = "cosmoshub-1"; + let long_sender = "cosmos1".to_string() + &"a".repeat(120); + let salt = b""; + + // Should succeed by hashing the long sender + let result = AccountState::derive_address(client_id, &long_sender, salt, &ID); + assert!(result.is_ok(), "Should handle long sender by hashing"); +} + +#[test] +fn test_account_state_validation() { + let client_id = "a".repeat(33); // Exceeds MAX_CLIENT_ID_LENGTH + let sender = "cosmos1test"; + let salt = b"salt"; + + let result = AccountState::derive_address(&client_id, sender, salt, &ID); + assert!(result.is_err(), "Should fail with client ID too long"); +} + +// ============================================================================ +// Packet Data Validation Tests +// ============================================================================ + +#[test] +fn test_gmp_packet_data_validation_success() { + let packet_data = GMPPacketData { + client_id: "cosmoshub-1".to_string(), + sender: "cosmos1abc".to_string(), + receiver: Pubkey::new_unique().to_string(), + salt: vec![1, 2, 3], + payload: vec![1, 2, 3, 4, 5], + memo: "test memo".to_string(), + }; + + assert!( + packet_data.validate().is_ok(), + "Valid packet should pass validation" + ); +} + +#[test] +fn test_gmp_packet_data_validation_empty_client_id() { + let packet_data = GMPPacketData { + client_id: String::new(), + sender: "cosmos1abc".to_string(), + receiver: Pubkey::new_unique().to_string(), + salt: vec![], + payload: vec![1, 2, 3], + memo: String::new(), + }; + + assert!( + packet_data.validate().is_err(), + "Empty client ID should fail" + ); +} + +#[test] +fn test_gmp_packet_data_validation_empty_sender() { + let packet_data = GMPPacketData { + client_id: "cosmoshub-1".to_string(), + sender: String::new(), + receiver: Pubkey::new_unique().to_string(), + salt: vec![], + payload: vec![1, 2, 3], + memo: String::new(), + }; + + assert!(packet_data.validate().is_err(), "Empty sender should fail"); +} + +#[test] +fn test_gmp_packet_data_validation_empty_payload() { + let packet_data = GMPPacketData { + client_id: "cosmoshub-1".to_string(), + sender: "cosmos1abc".to_string(), + receiver: Pubkey::new_unique().to_string(), + salt: vec![], + payload: vec![], + memo: String::new(), + }; + + assert!(packet_data.validate().is_err(), "Empty payload should fail"); +} + +#[test] +fn test_gmp_packet_data_validation_payload_too_long() { + let packet_data = GMPPacketData { + client_id: "cosmoshub-1".to_string(), + sender: "cosmos1abc".to_string(), + receiver: Pubkey::new_unique().to_string(), + salt: vec![], + payload: vec![0; 1025], // Exceeds MAX_PAYLOAD_LENGTH + memo: String::new(), + }; + + assert!( + packet_data.validate().is_err(), + "Payload too long should fail" + ); +} + +#[test] +fn test_gmp_packet_data_validation_memo_too_long() { + let packet_data = GMPPacketData { + client_id: "cosmoshub-1".to_string(), + sender: "cosmos1abc".to_string(), + receiver: Pubkey::new_unique().to_string(), + salt: vec![], + payload: vec![1, 2, 3], + memo: "a".repeat(257), // Exceeds MAX_MEMO_LENGTH + }; + + assert!(packet_data.validate().is_err(), "Memo too long should fail"); +} + +#[test] +fn test_gmp_packet_data_validation_salt_too_long() { + let packet_data = GMPPacketData { + client_id: "cosmoshub-1".to_string(), + sender: "cosmos1abc".to_string(), + receiver: Pubkey::new_unique().to_string(), + salt: vec![0; 9], // Exceeds MAX_SALT_LENGTH + payload: vec![1, 2, 3], + memo: String::new(), + }; + + assert!(packet_data.validate().is_err(), "Salt too long should fail"); +} + +// ============================================================================ +// Send Call Message Validation Tests +// ============================================================================ + +#[test] +fn test_send_call_msg_validation_success() { + let current_time = 1_000_000; + let msg = SendCallMsg { + source_client: "cosmoshub-1".to_string(), + timeout_timestamp: current_time + 3600, // 1 hour from now + receiver: Pubkey::new_unique(), + salt: vec![1, 2, 3], + payload: vec![1, 2, 3, 4, 5], + memo: "test memo".to_string(), + }; + + assert!( + msg.validate(current_time).is_ok(), + "Valid message should pass validation" + ); +} + +#[test] +fn test_send_call_msg_validation_timeout_too_soon() { + let current_time = 1_000_000; + let msg = SendCallMsg { + source_client: "cosmoshub-1".to_string(), + timeout_timestamp: current_time + 30, // Less than MIN_TIMEOUT_DURATION + receiver: Pubkey::new_unique(), + salt: vec![], + payload: vec![1, 2, 3], + memo: String::new(), + }; + + assert!( + msg.validate(current_time).is_err(), + "Timeout too soon should fail" + ); +} + +#[test] +fn test_send_call_msg_validation_timeout_too_long() { + let current_time = 1_000_000; + let msg = SendCallMsg { + source_client: "cosmoshub-1".to_string(), + timeout_timestamp: current_time + 90_000, // More than MAX_TIMEOUT_DURATION + receiver: Pubkey::new_unique(), + salt: vec![], + payload: vec![1, 2, 3], + memo: String::new(), + }; + + assert!( + msg.validate(current_time).is_err(), + "Timeout too long should fail" + ); +} + +// ============================================================================ +// Solana Instruction Extension Tests +// ============================================================================ + +#[test] +fn test_solana_instruction_validation_success() { + let instruction = ics27_gmp::proto::SolanaInstruction { + program_id: vec![1; 32], + accounts: vec![ics27_gmp::proto::SolanaAccountMeta { + pubkey: vec![2; 32], + is_signer: true, + is_writable: true, + }], + data: vec![1, 2, 3], + payer_position: None, + }; + + assert!( + instruction.validate().is_ok(), + "Valid instruction should pass" + ); +} + +#[test] +fn test_solana_instruction_validation_invalid_program_id() { + let instruction = ics27_gmp::proto::SolanaInstruction { + program_id: vec![1; 20], // Invalid length + accounts: vec![], + data: vec![1, 2, 3], + payer_position: None, + }; + + assert!( + instruction.validate().is_err(), + "Invalid program ID should fail" + ); +} + +#[test] +fn test_solana_instruction_validation_empty_data() { + let instruction = ics27_gmp::proto::SolanaInstruction { + program_id: vec![1; 32], + accounts: vec![], + data: vec![], + payer_position: None, + }; + + assert!(instruction.validate().is_err(), "Empty data should fail"); +} + +#[test] +fn test_solana_instruction_validation_too_many_accounts() { + let instruction = ics27_gmp::proto::SolanaInstruction { + program_id: vec![1; 32], + accounts: vec![ + ics27_gmp::proto::SolanaAccountMeta { + pubkey: vec![2; 32], + is_signer: false, + is_writable: false, + }; + 33 // More than 32 accounts + ], + data: vec![1, 2, 3], + payer_position: None, + }; + + assert!( + instruction.validate().is_err(), + "Too many accounts should fail" + ); +} + +#[test] +fn test_solana_instruction_to_account_metas() { + let instruction = ics27_gmp::proto::SolanaInstruction { + program_id: vec![1; 32], + accounts: vec![ + ics27_gmp::proto::SolanaAccountMeta { + pubkey: Pubkey::new_unique().to_bytes().to_vec(), + is_signer: true, + is_writable: true, + }, + ics27_gmp::proto::SolanaAccountMeta { + pubkey: Pubkey::new_unique().to_bytes().to_vec(), + is_signer: false, + is_writable: false, + }, + ], + data: vec![1, 2, 3], + payer_position: None, + }; + + let metas = instruction.to_account_metas().unwrap(); + assert_eq!(metas.len(), 2); + assert!(metas[0].is_signer); + assert!(metas[0].is_writable); + assert!(!metas[1].is_signer); + assert!(!metas[1].is_writable); +} + +// ============================================================================ +// GMP Acknowledgement Tests +// ============================================================================ + +#[test] +fn test_gmp_acknowledgement_success() { + let data = vec![1, 2, 3, 4]; + let ack = ics27_gmp::proto::GmpAcknowledgement::success(data.clone()); + + assert!(ack.success); + assert_eq!(ack.data, data); + assert!(ack.error.is_empty()); +} + +#[test] +fn test_gmp_acknowledgement_error() { + let error_msg = "execution failed".to_string(); + let ack = ics27_gmp::proto::GmpAcknowledgement::error(error_msg.clone()); + + assert!(!ack.success); + assert!(ack.data.is_empty()); + assert_eq!(ack.error, error_msg); +} + +#[test] +fn test_gmp_acknowledgement_serialization() { + let ack = ics27_gmp::proto::GmpAcknowledgement::success(vec![1, 2, 3]); + let serialized = ack.try_to_vec().unwrap(); + + let deserialized = ics27_gmp::proto::GmpAcknowledgement::try_from_slice(&serialized).unwrap(); + assert_eq!(ack.success, deserialized.success); + assert_eq!(ack.data, deserialized.data); +} + +// ============================================================================ +// App State Operation Tests +// ============================================================================ + +#[test] +fn test_app_state_can_operate_when_not_paused() { + let router_program = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let (_app_state_pda, bump) = + Pubkey::find_program_address(&[ics27_gmp::constants::GMP_APP_STATE_SEED, b"gmpport"], &ID); + + let app_state = GMPAppState { + router_program, + authority, + version: 1, + paused: false, + bump, + }; + + assert!( + app_state.can_operate().is_ok(), + "App should be operational when not paused" + ); +} + +#[test] +fn test_app_state_cannot_operate_when_paused() { + let router_program = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let (_app_state_pda, bump) = + Pubkey::find_program_address(&[ics27_gmp::constants::GMP_APP_STATE_SEED, b"gmpport"], &ID); + + let app_state = GMPAppState { + router_program, + authority, + version: 1, + paused: true, // Paused + bump, + }; + + assert!( + app_state.can_operate().is_err(), + "App should not be operational when paused" + ); +} + +#[test] +fn test_account_state_nonce_increment() { + let (_account_pda, bump) = + AccountState::derive_address("cosmoshub-1", "cosmos1test", b"", &ID).unwrap(); + + let mut account_state = AccountState { + client_id: "cosmoshub-1".to_string(), + sender: "cosmos1test".to_string(), + salt: vec![], + nonce: 5, + created_at: 1_600_000_000, + last_executed_at: 1_600_000_000, + execution_count: 10, + bump, + }; + + let current_time = 1_700_000_000; + account_state.execute_nonce_increment(current_time); + + assert_eq!(account_state.nonce, 6); + assert_eq!(account_state.last_executed_at, current_time); + assert_eq!(account_state.execution_count, 11); +} diff --git a/programs/solana/programs/mock-ibc-app/Cargo.toml b/programs/solana/programs/mock-ibc-app/Cargo.toml index 30db4d2f6..dd16e2903 100644 --- a/programs/solana/programs/mock-ibc-app/Cargo.toml +++ b/programs/solana/programs/mock-ibc-app/Cargo.toml @@ -27,3 +27,4 @@ idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang.workspace = true solana-ibc-types.workspace = true +solana-ibc-macros.workspace = true diff --git a/programs/solana/programs/mock-ibc-app/src/lib.rs b/programs/solana/programs/mock-ibc-app/src/lib.rs index d5d8636a7..cb52eea90 100644 --- a/programs/solana/programs/mock-ibc-app/src/lib.rs +++ b/programs/solana/programs/mock-ibc-app/src/lib.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use solana_ibc_macros::ibc_app; use solana_ibc_types::{OnAcknowledgementPacketMsg, OnRecvPacketMsg, OnTimeoutPacketMsg}; declare_id!("9qnEj3T1NsaGkN3Sj7hgJZiKrVbKVBNmVphJ6PW1PDAB"); @@ -8,7 +9,7 @@ declare_id!("9qnEj3T1NsaGkN3Sj7hgJZiKrVbKVBNmVphJ6PW1PDAB"); /// This program is a minimal implementation of the IBC app interface /// used only for testing the router. It has no state and no logic, /// just accepts the calls and returns success. -#[program] +#[ibc_app] pub mod mock_ibc_app { use super::*; use anchor_lang::solana_program::program::set_return_data; diff --git a/programs/solana/programs/mock-light-client/src/lib.rs b/programs/solana/programs/mock-light-client/src/lib.rs index 5a7248c8a..0b361ed7f 100644 --- a/programs/solana/programs/mock-light-client/src/lib.rs +++ b/programs/solana/programs/mock-light-client/src/lib.rs @@ -36,9 +36,9 @@ pub mod mock_light_client { ) -> Result<()> { msg!("Mock light client: verify_non_membership always returns success"); - // For non-membership (timeout), return a timestamp value - // Using 2000 as a mock timestamp (greater than typical test timeout values) - let timestamp: u64 = 2000; + // For non-membership (timeout), return a very large timestamp + // Using u64::MAX to ensure it's always greater than any test timeout value + let timestamp: u64 = u64::MAX; let timestamp_bytes = timestamp.to_le_bytes(); set_return_data(×tamp_bytes); diff --git a/proto/gmp/gmp.proto b/proto/gmp/gmp.proto new file mode 100644 index 000000000..e1a67ac21 --- /dev/null +++ b/proto/gmp/gmp.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +package gmp; + +option go_package = "types/gmp"; + +// `GMPPacketData` is the packet data sent over IBC for General Message Passing +// This is the inner packet data that gets wrapped in the IBC packet payload +message GMPPacketData { + // Source chain sender address (e.g., cosmos1...) + string sender = 1; + + // Target program ID on destination chain (base58 Solana pubkey) + string receiver = 2; + + // Salt for GMP account uniqueness (allows multiple accounts per sender) + bytes salt = 3; + + // Protobuf-encoded execution payload + // For Solana: This contains a `SolanaInstruction` + bytes payload = 4; + + // Optional memo field + string memo = 5; +} + +// `GMPAcknowledgement` is returned after packet execution on the destination chain +message GMPAcknowledgement { + // Whether execution succeeded + bool success = 1; + + // Result data from execution + bytes data = 2; + + // Error message if failed + string error = 3; +} diff --git a/proto/solana/solana_instruction.proto b/proto/solana/solana_instruction.proto new file mode 100644 index 000000000..b7d762e56 --- /dev/null +++ b/proto/solana/solana_instruction.proto @@ -0,0 +1,44 @@ +syntax = "proto3"; + +package solana; + +option go_package = "types/solana"; + +// `SolanaInstruction` represents a Solana instruction for cross-chain execution +message SolanaInstruction { + // Target Solana program ID (32 bytes) + bytes program_id = 1; + + // ALL accounts that will be accessed during execution + repeated SolanaAccountMeta accounts = 2; + + // Instruction data + bytes data = 3; + + // Optional: Position to inject relayer payer account for rent payment (0-indexed) + // + // - If not set: No payer injection (use for programs that don't create accounts) + // - If set to N: Inject payer at index N in the final account list + // + // Example: `payer_position=2` means payer will be at index 2 in the accounts array. + // The relayer's fee payer will be inserted at this position as a signer. + optional uint32 payer_position = 4; +} + +// `SolanaAccountMeta` represents account metadata for Solana instructions. +// +// Note: `is_signer` indicates whether the account should be a signer at the CPI instruction level. +// PDAs are marked `is_signer=true` even though they don't sign at the transaction level. +message SolanaAccountMeta { + // Account public key (32 bytes) + bytes pubkey = 1; + + // Should this account be a signer in the instruction? + // + // For PDAs: true (signs via `invoke_signed` during CPI) + // For regular accounts: false (doesn't sign) + bool is_signer = 2; + + // Will this account be modified? + bool is_writable = 3; +} \ No newline at end of file