Assertions
Assert Account Info
AssertAccountInfo Instruction
The AccountInfoAssertion
exposes the fields accessible by the AccountInfo struct passed into the rust entrypoint during runtime. The struct itself looks like
pub struct AccountInfo<'a> {
/// Public key of the account
pub key: &'a Pubkey,
/// The lamports in the account. Modifiable by programs.
pub lamports: Rc<RefCell<&'a mut u64>>,
/// The data held in this account. Modifiable by programs.
pub data: Rc<RefCell<&'a mut [u8]>>,
/// Program that owns this account
pub owner: &'a Pubkey,
/// The epoch at which this account will next owe rent
pub rent_epoch: Epoch,
/// Was the transaction signed by this account's public key?
pub is_signer: bool,
/// Is the account writable?
pub is_writable: bool,
/// This account's data contains a loaded program (and is read-only)
pub executable: bool,
}
Lighthouse exposes asserting on these AccountInfo
through the assertion types AssertAccountInfo
and AssertAccountDelta
(which is discussed in here).
Lamports
It's possible to make assertions on the value of lamports of an account at runtime.
Lamport assertion instruction
const ix = getAssertAccountInfoInstruction({
targetAccount,
assertion: accountInfoAssertion('Lamports', {
value: 5_000_000,
operator: IntegerOperator.GreaterThan,
}),
})
Owner
It's possible to make assertions on which programs owns the account.
Account owner assertion instruction
const ix = getAssertAccountInfoInstruction({
targetAccount,
assertion: accountInfoAssertion('Owner', {
value: SystemProgram.programId,
operator: EquatableOperator.Equal,
}),
})
KnownOwner
It's possible to make assertions on which programs owns the account using KnownOwner which is a enum of common program ids. This reduces the size of instruction data you need to pack into your transaction by 31 bytes.
pub enum KnownProgram {
System,
Token,
Token2022,
Rent,
Stake,
Vote,
BpfLoader,
UpgradeableLoader,
SysvarConfig,
}
Here is an example of asserting that the account is owned by the system program.
KnownOwner account owner assertion instruction
const ix = getAssertAccountInfoInstruction({
targetAccount,
assertion: accountInfoAssertion('KnownOwner', {
value: KnownProgram.System,
operator: EquatableOperator.Equal,
}),
})
RentEpoch
It's possible to assert the
Rent Epoch assertion instruction
const ix = getAssertAccountInfoInstruction({
targetAccount,
assertion: accountInfoAssertion('RentEpoch', {
value: 0,
operator: IntegerOperator.Equal,
}),
})
IsSigner
It's possible to get whether an account is a signer in the runtime.
IsSigner assertion instruction
const ix = getAssertAccountInfoInstruction({
targetAccount,
assertion: accountInfoAssertion('IsSigner', {
value: true,
operator: EquatableOperator.Equal,
}),
})
IsWritable
It's possible to get whether an account is writable in the runtime.
IsWritable assertion instruction
const ix = getAssertAccountInfoInstruction({
targetAccount,
assertion: accountInfoAssertion('IsWritable', {
value: true,
operator: EquatableOperator.Equal,
}),
})
Executable
It's possible to get whether an account is an executable account.
Executable assertion instruction
const ix = getAssertAccountInfoInstruction({
targetAccount,
assertion: accountInfoAssertion('IsWritable', {
value: true,
operator: EquatableOperator.Equal,
}),
})
VerifyDatahash
To save transaction space it is possible to assert on account data by hashing a slice of the account data and passing it into the VerifyDatahash
assertion. This costs more compute but is very useful if you need to verify that a writable account matches exactly what you expected.
Fields of the VerifyDatahash
assertion are:
expected_hash
- the expected keccak hash which will be compared to what the lighthouse program hashes at runtime. If they do not match the program will throw a AssertionFailed error.
start
- the start index of the account data slice to be hashed. If None
, start is 0.
length
- the length of the slice to be hashed where the end index of the slice will be start + length
. If None
, length is (length of account data) - start
.
The following is an example using the entire account data.
import { keccak_256 } from 'js-sha3'
const accountDataHash = Buffer.from(keccak_256.digest(accountDataBuffer))
const ix = getAssertAccountInfoInstruction({
targetAccount,
assertion: accountInfoAssertion('VerifyDatahash', {
expectedHash: Buffer.from(accountDataHash),
start: null,
length: null,
}),
})
const transaction = await pipe(
createTransaction({ version: 0 }),
(tx) => setTransactionFeePayer(signer.address, tx),
(tx) => appendTransactionInstructions([ix], tx),
(tx) => setTransactionLifetimeUsingBlockhash(recentBlockhash, tx),
(tx) => signTransaction([signer.keyPair], tx)
)
The following is an example using start and length.
VerifyDatahash
const accountDataHash = Buffer.from(
keccak_256.digest(accountDataBuffer.subarray(128, 256))
)
const ix = getAssertAccountInfoInstruction({
targetAccount,
assertion: accountInfoAssertion('VerifyDatahash', {
expectedHash: Buffer.from(accountDataHash),
start: 128,
length: 128,
}),
})
const transaction = await pipe(
createTransaction({ version: 0 }),
(tx) => setTransactionFeePayer(signer.address, tx),
(tx) => appendTransactionInstructions([ix], tx),
(tx) => setTransactionLifetimeUsingBlockhash(recentBlockhash, tx),
(tx) => signTransaction([signer.keyPair], tx)
)
AssertAccountInfoMulti Instruction
To save transaction space there is an instruction AssertAccountInfoMulti which allows you join all your assertions into one vector. This elimiates duplicating instruction data.
AssertAccountInfoMulti instruction
const ix = getAssertAccountInfoMultiInstruction({
targetAccount,
logLevel: LogLevel.PlaintextMessage,
assertions: [
accountInfoAssertion('Owner', {
value: SystemProgram.programId,
operator: 'Equal',
}),
accountInfoAssertion('KnownOwner', {
value: KnownProgram.System,
operator: 'Equal',
}),
accountInfoAssertion('Lamports', {
value: userPrebalance - 5000,
operator: 'Equal',
}),
accountInfoAssertion('DataLength', {
value: 0,
operator: 'Equal',
}),
accountInfoAssertion('Executable', {
value: true,
operator: 'NotEqual',
}),
accountInfoAssertion('Executable', {
value: false,
operator: 'Equal',
}),
accountInfoAssertion('Executable', {
value: true,
operator: 'NotEqual',
}),
accountInfoAssertion('RentEpoch', {
value: account.rentEpoch,
operator: 'Equal',
}),
],
})
const tx = await pipe(
createTransaction({ version: 0 }),
(tx) => setTransactionFeePayer(signer.address, tx),
(tx) => appendTransactionInstructions([ix], tx),
(tx) => setTransactionLifetimeUsingBlockhash(recentBlockhash, tx),
(tx) => signTransaction([signer.keyPair], tx)
)