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)
)
Previous
Account Data