//! A future Solana message format.
//!
//! This crate defines two versions of `Message` in their own modules:
//! [`legacy`] and [`v0`]. `legacy` is the current version as of Solana 1.10.0.
//! `v0` is a [future message format] that encodes more account keys into a
//! transaction than the legacy format.
//!
//! [`legacy`]: crate::message::legacy
//! [`v0`]: crate::message::v0
//! [future message format]: https://docs.solana.com/proposals/versioned-transactions

use crate::{
    address_lookup_table_account::AddressLookupTableAccount,
    bpf_loader_upgradeable,
    hash::Hash,
    instruction::{CompiledInstruction, Instruction},
    message::{
        compiled_keys::{CompileError, CompiledKeys},
        legacy::is_builtin_key_or_sysvar,
        AccountKeys, MessageHeader, MESSAGE_VERSION_PREFIX,
    },
    pubkey::Pubkey,
    sanitize::SanitizeError,
    short_vec,
};
pub use loaded::*;

mod loaded;

/// Address table lookups describe an on-chain address lookup table to use
/// for loading more readonly and writable accounts in a single tx.
#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone, AbiExample)]
#[serde(rename_all = "camelCase")]
pub struct MessageAddressTableLookup {
    /// Address lookup table account key
    pub account_key: Pubkey,
    /// List of indexes used to load writable account addresses
    #[serde(with = "short_vec")]
    pub writable_indexes: Vec<u8>,
    /// List of indexes used to load readonly account addresses
    #[serde(with = "short_vec")]
    pub readonly_indexes: Vec<u8>,
}

/// A Solana transaction message (v0).
///
/// This message format supports succinct account loading with
/// on-chain address lookup tables.
///
/// See the [`message`] module documentation for further description.
///
/// [`message`]: crate::message
#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone, AbiExample)]
#[serde(rename_all = "camelCase")]
pub struct Message {
    /// The message header, identifying signed and read-only `account_keys`.
    /// Header values only describe static `account_keys`, they do not describe
    /// any additional account keys loaded via address table lookups.
    pub header: MessageHeader,

    /// List of accounts loaded by this transaction.
    #[serde(with = "short_vec")]
    pub account_keys: Vec<Pubkey>,

    /// The blockhash of a recent block.
    pub recent_blockhash: Hash,

    /// Instructions that invoke a designated program, are executed in sequence,
    /// and committed in one atomic transaction if all succeed.
    ///
    /// # Notes
    ///
    /// Program indexes must index into the list of message `account_keys` because
    /// program id's cannot be dynamically loaded from a lookup table.
    ///
    /// Account indexes must index into the list of addresses
    /// constructed from the concatenation of three key lists:
    ///   1) message `account_keys`
    ///   2) ordered list of keys loaded from `writable` lookup table indexes
    ///   3) ordered list of keys loaded from `readable` lookup table indexes
    #[serde(with = "short_vec")]
    pub instructions: Vec<CompiledInstruction>,

    /// List of address table lookups used to load additional accounts
    /// for this transaction.
    #[serde(with = "short_vec")]
    pub address_table_lookups: Vec<MessageAddressTableLookup>,
}

impl Message {
    /// Sanitize message fields and compiled instruction indexes
    pub fn sanitize(&self) -> Result<(), SanitizeError> {
        let num_static_account_keys = self.account_keys.len();
        if usize::from(self.header.num_required_signatures)
            .saturating_add(usize::from(self.header.num_readonly_unsigned_accounts))
            > num_static_account_keys
        {
            return Err(SanitizeError::IndexOutOfBounds);
        }

        // there should be at least 1 RW fee-payer account.
        if self.header.num_readonly_signed_accounts >= self.header.num_required_signatures {
            return Err(SanitizeError::InvalidValue);
        }

        let num_dynamic_account_keys = {
            let mut total_lookup_keys: usize = 0;
            for lookup in &self.address_table_lookups {
                let num_lookup_indexes = lookup
                    .writable_indexes
                    .len()
                    .saturating_add(lookup.readonly_indexes.len());

                // each lookup table must be used to load at least one account
                if num_lookup_indexes == 0 {
                    return Err(SanitizeError::InvalidValue);
                }

                total_lookup_keys = total_lookup_keys.saturating_add(num_lookup_indexes);
            }
            total_lookup_keys
        };

        // this is redundant with the above sanitization checks which require that:
        // 1) the header describes at least 1 RW account
        // 2) the header doesn't describe more account keys than the number of account keys
        if num_static_account_keys == 0 {
            return Err(SanitizeError::InvalidValue);
        }

        // the combined number of static and dynamic account keys must be <= 256
        // since account indices are encoded as `u8`
        // Note that this is different from the per-transaction account load cap
        // as defined in `Bank::get_transaction_account_lock_limit`
        let total_account_keys = num_static_account_keys.saturating_add(num_dynamic_account_keys);
        if total_account_keys > 256 {
            return Err(SanitizeError::IndexOutOfBounds);
        }

        // `expect` is safe because of earlier check that
        // `num_static_account_keys` is non-zero
        let max_account_ix = total_account_keys
            .checked_sub(1)
            .expect("message doesn't contain any account keys");

        // reject program ids loaded from lookup tables so that
        // static analysis on program instructions can be performed
        // without loading on-chain data from a bank
        let max_program_id_ix =
            // `expect` is safe because of earlier check that
            // `num_static_account_keys` is non-zero
            num_static_account_keys
                .checked_sub(1)
                .expect("message doesn't contain any static account keys");

        for ci in &self.instructions {
            if usize::from(ci.program_id_index) > max_program_id_ix {
                return Err(SanitizeError::IndexOutOfBounds);
            }
            // A program cannot be a payer.
            if ci.program_id_index == 0 {
                return Err(SanitizeError::IndexOutOfBounds);
            }
            for ai in &ci.accounts {
                if usize::from(*ai) > max_account_ix {
                    return Err(SanitizeError::IndexOutOfBounds);
                }
            }
        }

        Ok(())
    }
}

impl Message {
    /// Create a signable transaction message from a `payer` public key,
    /// `recent_blockhash`, list of `instructions`, and a list of
    /// `address_lookup_table_accounts`.
    ///
    /// # Examples
    ///
    /// This example uses the [`solana_rpc_client`], [`solana_sdk`], and [`anyhow`] crates.
    ///
    /// [`solana_rpc_client`]: https://docs.rs/solana-rpc-client
    /// [`solana_sdk`]: https://docs.rs/solana-sdk
    /// [`anyhow`]: https://docs.rs/anyhow
    ///
    /// ```
    /// # use solana_program::example_mocks::{
    /// #     solana_rpc_client,
    /// #     solana_sdk,
    /// # };
    /// # use std::borrow::Cow;
    /// # use solana_sdk::account::Account;
    /// use anyhow::Result;
    /// use solana_rpc_client::rpc_client::RpcClient;
    /// use solana_program::address_lookup_table::{self, state::{AddressLookupTable, LookupTableMeta}};
    /// use solana_sdk::{
    ///      address_lookup_table_account::AddressLookupTableAccount,
    ///      instruction::{AccountMeta, Instruction},
    ///      message::{VersionedMessage, v0},
    ///      pubkey::Pubkey,
    ///      signature::{Keypair, Signer},
    ///      transaction::VersionedTransaction,
    /// };
    ///
    /// fn create_tx_with_address_table_lookup(
    ///     client: &RpcClient,
    ///     instruction: Instruction,
    ///     address_lookup_table_key: Pubkey,
    ///     payer: &Keypair,
    /// ) -> Result<VersionedTransaction> {
    ///     # client.set_get_account_response(address_lookup_table_key, Account {
    ///     #   lamports: 1,
    ///     #   data: AddressLookupTable {
    ///     #     meta: LookupTableMeta::default(),
    ///     #     addresses: Cow::Owned(instruction.accounts.iter().map(|meta| meta.pubkey).collect()),
    ///     #   }.serialize_for_tests().unwrap(),
    ///     #   owner: address_lookup_table::program::id(),
    ///     #   executable: false,
    ///     #   rent_epoch: 1,
    ///     # });
    ///     let raw_account = client.get_account(&address_lookup_table_key)?;
    ///     let address_lookup_table = AddressLookupTable::deserialize(&raw_account.data)?;
    ///     let address_lookup_table_account = AddressLookupTableAccount {
    ///         key: address_lookup_table_key,
    ///         addresses: address_lookup_table.addresses.to_vec(),
    ///     };
    ///
    ///     let blockhash = client.get_latest_blockhash()?;
    ///     let tx = VersionedTransaction::try_new(
    ///         VersionedMessage::V0(v0::Message::try_compile(
    ///             &payer.pubkey(),
    ///             &[instruction],
    ///             &[address_lookup_table_account],
    ///             blockhash,
    ///         )?),
    ///         &[payer],
    ///     )?;
    ///
    ///     # assert!(tx.message.address_table_lookups().unwrap().len() > 0);
    ///     Ok(tx)
    /// }
    /// #
    /// # let client = RpcClient::new(String::new());
    /// # let payer = Keypair::new();
    /// # let address_lookup_table_key = Pubkey::new_unique();
    /// # let instruction = Instruction::new_with_bincode(Pubkey::new_unique(), &(), vec![
    /// #   AccountMeta::new(Pubkey::new_unique(), false),
    /// # ]);
    /// # create_tx_with_address_table_lookup(&client, instruction, address_lookup_table_key, &payer)?;
    /// # Ok::<(), anyhow::Error>(())
    /// ```
    pub fn try_compile(
        payer: &Pubkey,
        instructions: &[Instruction],
        address_lookup_table_accounts: &[AddressLookupTableAccount],
        recent_blockhash: Hash,
    ) -> Result<Self, CompileError> {
        let mut compiled_keys = CompiledKeys::compile(instructions, Some(*payer));

        let mut address_table_lookups = Vec::with_capacity(address_lookup_table_accounts.len());
        let mut loaded_addresses_list = Vec::with_capacity(address_lookup_table_accounts.len());
        for lookup_table_account in address_lookup_table_accounts {
            if let Some((lookup, loaded_addresses)) =
                compiled_keys.try_extract_table_lookup(lookup_table_account)?
            {
                address_table_lookups.push(lookup);
                loaded_addresses_list.push(loaded_addresses);
            }
        }

        let (header, static_keys) = compiled_keys.try_into_message_components()?;
        let dynamic_keys = loaded_addresses_list.into_iter().collect();
        let account_keys = AccountKeys::new(&static_keys, Some(&dynamic_keys));
        let instructions = account_keys.try_compile_instructions(instructions)?;

        Ok(Self {
            header,
            account_keys: static_keys,
            recent_blockhash,
            instructions,
            address_table_lookups,
        })
    }

    /// Serialize this message with a version #0 prefix using bincode encoding.
    pub fn serialize(&self) -> Vec<u8> {
        bincode::serialize(&(MESSAGE_VERSION_PREFIX, self)).unwrap()
    }

    /// Returns true if the account at the specified index is called as a program by an instruction
    pub fn is_key_called_as_program(&self, key_index: usize) -> bool {
        if let Ok(key_index) = u8::try_from(key_index) {
            self.instructions
                .iter()
                .any(|ix| ix.program_id_index == key_index)
        } else {
            false
        }
    }

    /// Returns true if the account at the specified index was requested to be
    /// writable.  This method should not be used directly.
    fn is_writable_index(&self, key_index: usize) -> bool {
        let header = &self.header;
        let num_account_keys = self.account_keys.len();
        let num_signed_accounts = usize::from(header.num_required_signatures);
        if key_index >= num_account_keys {
            let loaded_addresses_index = key_index.saturating_sub(num_account_keys);
            let num_writable_dynamic_addresses = self
                .address_table_lookups
                .iter()
                .map(|lookup| lookup.writable_indexes.len())
                .sum();
            loaded_addresses_index < num_writable_dynamic_addresses
        } else if key_index >= num_signed_accounts {
            let num_unsigned_accounts = num_account_keys.saturating_sub(num_signed_accounts);
            let num_writable_unsigned_accounts = num_unsigned_accounts
                .saturating_sub(usize::from(header.num_readonly_unsigned_accounts));
            let unsigned_account_index = key_index.saturating_sub(num_signed_accounts);
            unsigned_account_index < num_writable_unsigned_accounts
        } else {
            let num_writable_signed_accounts = num_signed_accounts
                .saturating_sub(usize::from(header.num_readonly_signed_accounts));
            key_index < num_writable_signed_accounts
        }
    }

    /// Returns true if any static account key is the bpf upgradeable loader
    fn is_upgradeable_loader_in_static_keys(&self) -> bool {
        self.account_keys
            .iter()
            .any(|&key| key == bpf_loader_upgradeable::id())
    }

    /// Returns true if the account at the specified index was requested as writable.
    /// Before loading addresses, we can't demote write locks for dynamically loaded
    /// addresses so this should not be used by the runtime.
    pub fn is_maybe_writable(&self, key_index: usize) -> bool {
        self.is_writable_index(key_index)
            && !{
                // demote reserved ids
                self.account_keys
                    .get(key_index)
                    .map(is_builtin_key_or_sysvar)
                    .unwrap_or_default()
            }
            && !{
                // demote program ids
                self.is_key_called_as_program(key_index)
                    && !self.is_upgradeable_loader_in_static_keys()
            }
    }
}

#[cfg(test)]
mod tests {
    use {
        super::*,
        crate::{instruction::AccountMeta, message::VersionedMessage},
    };

    #[test]
    fn test_sanitize() {
        assert!(Message {
            header: MessageHeader {
                num_required_signatures: 1,
                ..MessageHeader::default()
            },
            account_keys: vec![Pubkey::new_unique()],
            ..Message::default()
        }
        .sanitize()
        .is_ok());
    }

    #[test]
    fn test_sanitize_with_instruction() {
        assert!(Message {
            header: MessageHeader {
                num_required_signatures: 1,
                ..MessageHeader::default()
            },
            account_keys: vec![Pubkey::new_unique(), Pubkey::new_unique()],
            instructions: vec![CompiledInstruction {
                program_id_index: 1,
                accounts: vec![0],
                data: vec![]
            }],
            ..Message::default()
        }
        .sanitize()
        .is_ok());
    }

    #[test]
    fn test_sanitize_with_table_lookup() {
        assert!(Message {
            header: MessageHeader {
                num_required_signatures: 1,
                ..MessageHeader::default()
            },
            account_keys: vec![Pubkey::new_unique()],
            address_table_lookups: vec![MessageAddressTableLookup {
                account_key: Pubkey::new_unique(),
                writable_indexes: vec![1, 2, 3],
                readonly_indexes: vec![0],
            }],
            ..Message::default()
        }
        .sanitize()
        .is_ok());
    }

    #[test]
    fn test_sanitize_with_table_lookup_and_ix_with_dynamic_program_id() {
        let message = Message {
            header: MessageHeader {
                num_required_signatures: 1,
                ..MessageHeader::default()
            },
            account_keys: vec![Pubkey::new_unique()],
            address_table_lookups: vec![MessageAddressTableLookup {
                account_key: Pubkey::new_unique(),
                writable_indexes: vec![1, 2, 3],
                readonly_indexes: vec![0],
            }],
            instructions: vec![CompiledInstruction {
                program_id_index: 4,
                accounts: vec![0, 1, 2, 3],
                data: vec![],
            }],
            ..Message::default()
        };

        assert!(message.sanitize().is_err());
    }

    #[test]
    fn test_sanitize_with_table_lookup_and_ix_with_static_program_id() {
        assert!(Message {
            header: MessageHeader {
                num_required_signatures: 1,
                ..MessageHeader::default()
            },
            account_keys: vec![Pubkey::new_unique(), Pubkey::new_unique()],
            address_table_lookups: vec![MessageAddressTableLookup {
                account_key: Pubkey::new_unique(),
                writable_indexes: vec![1, 2, 3],
                readonly_indexes: vec![0],
            }],
            instructions: vec![CompiledInstruction {
                program_id_index: 1,
                accounts: vec![2, 3, 4, 5],
                data: vec![]
            }],
            ..Message::default()
        }
        .sanitize()
        .is_ok());
    }

    #[test]
    fn test_sanitize_without_signer() {
        assert!(Message {
            header: MessageHeader::default(),
            account_keys: vec![Pubkey::new_unique()],
            ..Message::default()
        }
        .sanitize()
        .is_err());
    }

    #[test]
    fn test_sanitize_without_writable_signer() {
        assert!(Message {
            header: MessageHeader {
                num_required_signatures: 1,
                num_readonly_signed_accounts: 1,
                ..MessageHeader::default()
            },
            account_keys: vec![Pubkey::new_unique()],
            ..Message::default()
        }
        .sanitize()
        .is_err());
    }

    #[test]
    fn test_sanitize_with_empty_table_lookup() {
        assert!(Message {
            header: MessageHeader {
                num_required_signatures: 1,
                ..MessageHeader::default()
            },
            account_keys: vec![Pubkey::new_unique()],
            address_table_lookups: vec![MessageAddressTableLookup {
                account_key: Pubkey::new_unique(),
                writable_indexes: vec![],
                readonly_indexes: vec![],
            }],
            ..Message::default()
        }
        .sanitize()
        .is_err());
    }

    #[test]
    fn test_sanitize_with_max_account_keys() {
        assert!(Message {
            header: MessageHeader {
                num_required_signatures: 1,
                ..MessageHeader::default()
            },
            account_keys: (0..=u8::MAX).map(|_| Pubkey::new_unique()).collect(),
            ..Message::default()
        }
        .sanitize()
        .is_ok());
    }

    #[test]
    fn test_sanitize_with_too_many_account_keys() {
        assert!(Message {
            header: MessageHeader {
                num_required_signatures: 1,
                ..MessageHeader::default()
            },
            account_keys: (0..=256).map(|_| Pubkey::new_unique()).collect(),
            ..Message::default()
        }
        .sanitize()
        .is_err());
    }

    #[test]
    fn test_sanitize_with_max_table_loaded_keys() {
        assert!(Message {
            header: MessageHeader {
                num_required_signatures: 1,
                ..MessageHeader::default()
            },
            account_keys: vec![Pubkey::new_unique()],
            address_table_lookups: vec![MessageAddressTableLookup {
                account_key: Pubkey::new_unique(),
                writable_indexes: (0..=254).step_by(2).collect(),
                readonly_indexes: (1..=254).step_by(2).collect(),
            }],
            ..Message::default()
        }
        .sanitize()
        .is_ok());
    }

    #[test]
    fn test_sanitize_with_too_many_table_loaded_keys() {
        assert!(Message {
            header: MessageHeader {
                num_required_signatures: 1,
                ..MessageHeader::default()
            },
            account_keys: vec![Pubkey::new_unique()],
            address_table_lookups: vec![MessageAddressTableLookup {
                account_key: Pubkey::new_unique(),
                writable_indexes: (0..=255).step_by(2).collect(),
                readonly_indexes: (1..=255).step_by(2).collect(),
            }],
            ..Message::default()
        }
        .sanitize()
        .is_err());
    }

    #[test]
    fn test_sanitize_with_invalid_ix_program_id() {
        let message = Message {
            header: MessageHeader {
                num_required_signatures: 1,
                ..MessageHeader::default()
            },
            account_keys: vec![Pubkey::new_unique()],
            address_table_lookups: vec![MessageAddressTableLookup {
                account_key: Pubkey::new_unique(),
                writable_indexes: vec![0],
                readonly_indexes: vec![],
            }],
            instructions: vec![CompiledInstruction {
                program_id_index: 2,
                accounts: vec![],
                data: vec![],
            }],
            ..Message::default()
        };

        assert!(message.sanitize().is_err());
    }

    #[test]
    fn test_sanitize_with_invalid_ix_account() {
        assert!(Message {
            header: MessageHeader {
                num_required_signatures: 1,
                ..MessageHeader::default()
            },
            account_keys: vec![Pubkey::new_unique(), Pubkey::new_unique()],
            address_table_lookups: vec![MessageAddressTableLookup {
                account_key: Pubkey::new_unique(),
                writable_indexes: vec![],
                readonly_indexes: vec![0],
            }],
            instructions: vec![CompiledInstruction {
                program_id_index: 1,
                accounts: vec![3],
                data: vec![]
            }],
            ..Message::default()
        }
        .sanitize()
        .is_err());
    }

    #[test]
    fn test_serialize() {
        let message = Message::default();
        let versioned_msg = VersionedMessage::V0(message.clone());
        assert_eq!(message.serialize(), versioned_msg.serialize());
    }

    #[test]
    fn test_try_compile() {
        let mut keys = vec![];
        keys.resize_with(7, Pubkey::new_unique);

        let payer = keys[0];
        let program_id = keys[6];
        let instructions = vec![Instruction {
            program_id,
            accounts: vec![
                AccountMeta::new(keys[1], true),
                AccountMeta::new_readonly(keys[2], true),
                AccountMeta::new(keys[3], false),
                AccountMeta::new(keys[4], false), // loaded from lut
                AccountMeta::new_readonly(keys[5], false), // loaded from lut
            ],
            data: vec![],
        }];
        let address_lookup_table_accounts = vec![
            AddressLookupTableAccount {
                key: Pubkey::new_unique(),
                addresses: vec![keys[4], keys[5], keys[6]],
            },
            AddressLookupTableAccount {
                key: Pubkey::new_unique(),
                addresses: vec![],
            },
        ];

        let recent_blockhash = Hash::new_unique();
        assert_eq!(
            Message::try_compile(
                &payer,
                &instructions,
                &address_lookup_table_accounts,
                recent_blockhash
            ),
            Ok(Message {
                header: MessageHeader {
                    num_required_signatures: 3,
                    num_readonly_signed_accounts: 1,
                    num_readonly_unsigned_accounts: 1
                },
                recent_blockhash,
                account_keys: vec![keys[0], keys[1], keys[2], keys[3], program_id],
                instructions: vec![CompiledInstruction {
                    program_id_index: 4,
                    accounts: vec![1, 2, 3, 5, 6],
                    data: vec![],
                },],
                address_table_lookups: vec![MessageAddressTableLookup {
                    account_key: address_lookup_table_accounts[0].key,
                    writable_indexes: vec![0],
                    readonly_indexes: vec![1],
                }],
            })
        );
    }
}
