sRFC 30: Account Abstraction Interfaces

Summary

This proposal aims to introduce a standardized interface for Account Abstraction (AA) in the Solana ecosystem. By following this interface, any protocol interested in AA development on Solana can unify the operation object, allowing users to focus on their intended instructions rather than dealing with different AA provider interfaces.

Motivation

Currently, only a few protocols in the Solana ecosystem are developing Account Abstraction (AA) solutions, each with varying interfaces. This early stage presents an ideal opportunity to create a unified interface for AA operations. A standardized interface will simplify integration, reduce fragmentation, and enhance the overall developer and user experience. By focusing on constructing a unified operation object, EOAs (Externally Owned Accounts) can ensure interoperability and streamline interactions across different AA vendors, ultimately reducing complexity and fostering a more cohesive ecosystem.

Specification

UserInstruction Struct

pub struct UserInstruction {
    pub inner_instructions: Vec<InnerInstruction>,
    // additional fields for replay attack
    pub nonce: u64,
    // fields for timestamp validation
    pub valid_from: u64,
    pub valid_until: u64,
    // executed with modulars ids
    pub modulars: Vec<u32>,
}

pub struct InnerInstruction {
    // conventional Solana Instruction fields, except data
    pub program_id: Pubkey, //_dstAddress
    pub keys: Vec<AccMeta>, // solana req
    pub data: Vec<u8>,      //_payload
}

UserInstruction Struct Fields Explanation

The UserInstruction struct is designed to encapsulate the necessary information for executing a series of instructions on the Solana blockchain while addressing concerns such as replay attacks and timestamp validation. Here is a detailed explanation of each field:

  1. inner_instructions: Vec<InnerInstruction>
  • Type: Vector of InnerInstruction structs

  • Purpose: Holds a list of inner instructions that define the specific operations to be executed on the blockchain. Each InnerInstruction represents a single action, such as transferring tokens or invoking a contract.

  • Details: This field is primarily used for multi-call operations, allowing multiple inner instructions to be batched together. By grouping multiple inner instructions into a single UserInstruction, the transaction size is reduced, and the most relevant fields are batched together.

  1. nonce: u64
  • Type: Unsigned 64-bit integer

  • Purpose: Provides a mechanism to prevent replay attacks by ensuring that each UserInstruction has a unique value.

  • Details: The nonce should be incremented for each new UserInstruction sent by the same user or entity. If an instruction with the same nonce is detected, it can be rejected as a replay attack.

  1. valid_from: u64
  • Type: Unsigned 64-bit integer (timestamp)

  • Purpose: Specifies the earliest time at which the UserInstruction can be executed.

  • Details: This field ensures that the instruction is not processed before a certain time, providing control over the timing of execution. This feature allows for future-dated transactions, meaning a transaction can be set to execute at a future time. Such transactions can be executed by anyone once the valid_from time is reached, enabling scheduled operations and enhancing automation capabilities on the Solana network.

  1. valid_until: u64
  • Type: Unsigned 64-bit integer (timestamp)

  • Purpose: Specifies the latest time at which the UserInstruction can be executed.

  • Details: This field prevents the execution of stale instructions by setting an expiration time. If the current time exceeds valid_until, the instruction should be rejected. Combined with valid_from, this field ensures that transactions can be precisely scheduled within a specific time window.

  1. modulars: Vec<u32>
  • Type: Vector of unsigned 32-bit integers

  • Purpose: Identifies the modular components or modules that are relevant for the execution of this UserInstruction.

  • Details: This field allows for the extension and customization of the execution logic by specifying which modules should be considered during execution. Each module might represent different aspects of the operation, such as pre-execution hooks, post-execution hooks, validation, authorization, or custom business logic. The modular approach enables flexible and reusable execution patterns, ensuring that additional logic can be incorporated before and after the main execution flow.

Customization and Security

The inclusion of nonce, valid_from, and valid_until fields not only provides replay attack prevention and timestamp validation but also allows for customized signatures within the hash value, instead of relying on block hash signatures in Solana. This design enables greater flexibility and security in transaction validation and execution.

InnerInstruction Struct Fields Explanation

The InnerInstruction struct represents individual instructions that make up a UserInstruction. Here is a detailed explanation of its fields:

  1. program_id: Pubkey
  • Type: Public key

  • Purpose: Specifies the Solana program to be called by this instruction.

  • Details: The program_id determines which on-chain program will process the instruction. In the context of Solana, this field is used to perform a Cross-Program Invocation (CPI) call, which allows one program to invoke another program on the blockchain.

  1. keys: Vec<AccMeta>
  • Type: Vector of AccMeta

  • Purpose: Lists the accounts that are read or written by the instruction.

  • Details: Each AccMeta includes information about the account’s role in the instruction (read-only, writable, signer, etc.). This ensures that all necessary accounts are correctly referenced and used during execution. Proper account metadata is crucial for CPIs to ensure that the invoked program has access to the required accounts with the correct permissions.

  1. data: Vec<u8>
  • Type: Vector of unsigned 8-bit integers (bytes)

  • Purpose: Contains the instruction-specific data, typically the parameters or payload to be processed by the program.

  • Details: This field encodes the actual operation or command to be executed by the target program. The data field allows for the passing of specific parameters required for the execution of the CPI call, enabling the target program to understand and process the instruction accordingly.

UserInstruction Summary

  • inner_instructions: List of operations to be performed.

  • nonce: Unique identifier to prevent replay attacks.

  • valid_from: Start time for when the instruction can be executed.

  • valid_until: Expiry time after which the instruction cannot be executed.

  • modulars: Identifiers for additional modules relevant to the execution.

In the context of Solana and the provided code snippet, serializeInIx refers to the serialization of the InnerInstruction object into a format that can be hashed. This serialization process converts the InnerInstructionstruct into a byte array that can be processed further, such as being hashed using Keccak256.

Explanation of Serialization with UserInstruction

In the context of Solana and the provided code snippet, serializeInIx refers to the serialization of the UserInstruction object into a format that can be hashed. This serialization process converts the UserInstructionstruct into a byte array that can be processed further, such as being hashed using Keccak256.

Serialization using Borsh

Borsh (Binary Object Representation Serializer for Hashing) is a binary serialization format designed to serialize data structures in a compact, deterministic manner. In Rust, Borsh is commonly used for serializing data structures before storing them on-chain or hashing them.

To serialize a UserInstruction using Borsh, you would typically implement the BorshSerialize trait for the struct, if not already provided, and then use the .try_to_vec() method to serialize it.

Example:

use borsh::{BorshSerialize, BorshDeserialize};

#[derive(BorshSerialize, BorshDeserialize)]
pub struct UserInstruction {
    pub inner_instructions: Vec<InnerInstruction>,
    pub nonce: u64,
    pub valid_from: u64,
    pub valid_until: u64,
    pub modulars: Vec<u32>,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct InnerInstruction {
    pub program_id: Pubkey,
    pub keys: Vec<AccMeta>,
    pub data: Vec<u8>,
}

// Usagelet serialized_user_ix = user_ix.try_to_vec().unwrap(); // Serialize the UserInstruction

Explanation of the Hash Construction

The provided hash construction uses Keccak256, a cryptographic hash function, to create a unique hash for the UserInstruction object, including its associated program ID and serialized inner instructions.

Here’s a breakdown of the code:

ethers.keccak256(
    Buffer.concat([
        Buffer.from(aaFactory.programId.toBytes()),
        ethers.getBytes(ethers.keccak256(ethers.hexlify(serializeInIx(userIx)))),
    ])
);

Summary Steps Explained:

  1. Serialize UserInstruction:

    • serializeInIx(userIx): This function serializes the UserInstruction object (userIx) into a byte array. In a Solana program using Rust, this would be done using Borsh serialization as shown above.
  2. Convert to Hex and Hash:

    • ethers.hexlify(serializeInIx(userIx)): Converts the serialized byte array into a hex string.
    • ethers.keccak256(...): Computes the Keccak256 hash of the hex string representing the serialized UserInstruction.
  3. Concatenate with Program ID:

    • Buffer.from(aaFactory.programId.toBytes()): Converts the program ID to bytes.
    • Buffer.concat([...]): Concatenates the bytes of the program ID and the Keccak256 hash of the serialized UserInstruction.
  4. Final Hash:

    • ethers.keccak256(Buffer.concat([...])): Computes the Keccak256 hash of the concatenated buffer, producing the final hash that includes both the program ID and the serialized UserInstruction.

Execution Function

In the context of Account Abstraction (AA) on Solana, the execution function is a standardized interface that allows any UserInstruction signed by the user to be executed across different AA provider programs. This unification is crucial for ensuring interoperability and reducing complexity for developers and users.

Purpose

The purpose of the execution function is to provide a common method that AA provider programs can implement to process and execute UserInstruction objects. By following this standard interface, users can sign a UserInstructiononce and use it with any compliant AA provider program, fostering a more seamless and consistent experience across the Solana ecosystem.

Function Definition

pub fn execute_operation(
    accounts: &[AccountMeta],
    instruction_data: &[u8],
) -> Result<(), ProgramError> {
    // Deserialize the UserInstruction 
    let user_instruction: UserInstruction = UserInstruction::try_from_slice(instruction_data)?; 
    
    // Logic to process and execute the UserInstruction 
    for inner_instruction in user_instruction.inner_instructions { 
        // Process each inner instruction 
        // Invoking another program with CPI 
    }
    
    Ok(())
}

Explanation

  1. Function Signature:

    • accounts: &[AccountMeta]: A slice of account metadata that provides the necessary account information for the execution context.
    • instruction_data: &[u8]: A slice of bytes that represents the serialized UserInstruction object.
    • Result<(), ProgramError>: Returns a result type to handle execution success or errors.
  2. Deserialization:

    • let user_instruction: UserInstruction = UserInstruction::try_from_slice(instruction_data)?;: Deserializes the byte array into a UserInstruction object using Borsh.
  3. Processing the UserInstruction:

    • Iterates over each InnerInstruction within the UserInstruction.
    • Processes each inner instruction (e.g., invoking another program using CPI).

Conclusion

The design choices for the UserInstruction and InnerInstruction structs, as well as the unified execution interface, prioritize flexibility, extensibility, simplicity, and security. These design choices include:

  • UserInstruction can encapsulate multiple InnerInstruction objects, enabling complex operations to be executed atomically. The modulars field allows for customizable pre-execution and post-execution hooks, validation, authorization, and other business logic.

  • The unified execution interface (execute_operation) standardizes processing across different AA provider programs, fostering interoperability and simplifying development. This standardization ensures a consistent experience for developers and users, reducing fragmentation and complexity across different AA providers.

  • Security is enhanced through the nonce field, preventing replay attacks by ensuring unique UserInstruction instances, and valid_from and valid_until fields, providing timestamp validation to prevent executing stale instructions.

  • Efficiency is achieved by batching multiple InnerInstruction objects into a single UserInstruction, reducing transaction size and overhead, improving network efficiency. Keccak256 hashing, including the program ID, ensures cryptographic consistency and unique, secure instructions.

By adopting this standardized interface for Account Abstraction (AA) in the Solana ecosystem, the operation object can be unified across different AA provider programs. This unification enhances AA capabilities, streamlines user interactions, and simplifies the development process for protocols and users alike, fostering a more integrated, efficient, and user-friendly blockchain environment. The Solana ecosystem benefits from improved interoperability, consistency, flexibility, security, and efficiency, ultimately contributing to a more robust blockchain infrastructure.

4 Likes