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:
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.
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.
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.
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 withvalid_from
, this field ensures that transactions can be precisely scheduled within a specific time window.
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:
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.
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.
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 InnerInstruction
struct 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 UserInstruction
struct 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:
-
Serialize UserInstruction:
serializeInIx(userIx)
: This function serializes theUserInstruction
object (userIx
) into a byte array. In a Solana program using Rust, this would be done using Borsh serialization as shown above.
-
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 serializedUserInstruction
.
-
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 serializedUserInstruction
.
-
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 serializedUserInstruction
.
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 UserInstruction
once 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
-
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 serializedUserInstruction
object.Result<(), ProgramError>
: Returns a result type to handle execution success or errors.
-
Deserialization:
let user_instruction: UserInstruction = UserInstruction::try_from_slice(instruction_data)?;
: Deserializes the byte array into aUserInstruction
object using Borsh.
-
Processing the UserInstruction:
- Iterates over each
InnerInstruction
within theUserInstruction
. - Processes each inner instruction (e.g., invoking another program using CPI).
- Iterates over each
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.