feat: implement message v0 compilation (#27524)

* feat: add PublicKey.unique method for tests

* feat: add MessageAccountKeys class

* feat: add CompiledKeys class for message compilation

* feat: implement message compilation using CompiledKeys
This commit is contained in:
Justin Starry 2022-09-06 22:43:22 -05:00 committed by GitHub
parent 8c1093534e
commit 3374f41201
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 833 additions and 0 deletions

View File

@ -0,0 +1,79 @@
import {LoadedAddresses} from '../connection';
import {PublicKey} from '../publickey';
import {TransactionInstruction} from '../transaction';
import {MessageCompiledInstruction} from './index';
export type AccountKeysFromLookups = LoadedAddresses;
export class MessageAccountKeys {
staticAccountKeys: Array<PublicKey>;
accountKeysFromLookups?: AccountKeysFromLookups;
constructor(
staticAccountKeys: Array<PublicKey>,
accountKeysFromLookups?: AccountKeysFromLookups,
) {
this.staticAccountKeys = staticAccountKeys;
this.accountKeysFromLookups = accountKeysFromLookups;
}
keySegments(): Array<Array<PublicKey>> {
const keySegments = [this.staticAccountKeys];
if (this.accountKeysFromLookups) {
keySegments.push(this.accountKeysFromLookups.writable);
keySegments.push(this.accountKeysFromLookups.readonly);
}
return keySegments;
}
get(index: number): PublicKey | undefined {
for (const keySegment of this.keySegments()) {
if (index < keySegment.length) {
return keySegment[index];
} else {
index -= keySegment.length;
}
}
return;
}
get length(): number {
return this.keySegments().flat().length;
}
compileInstructions(
instructions: Array<TransactionInstruction>,
): Array<MessageCompiledInstruction> {
// Bail early if any account indexes would overflow a u8
const U8_MAX = 255;
if (this.length > U8_MAX + 1) {
throw new Error('Account index overflow encountered during compilation');
}
const keyIndexMap = new Map();
this.keySegments()
.flat()
.forEach((key, index) => {
keyIndexMap.set(key.toBase58(), index);
});
const findKeyIndex = (key: PublicKey) => {
const keyIndex = keyIndexMap.get(key.toBase58());
if (keyIndex === undefined)
throw new Error(
'Encountered an unknown instruction account key during compilation',
);
return keyIndex;
};
return instructions.map((instruction): MessageCompiledInstruction => {
return {
programIdIndex: findKeyIndex(instruction.programId),
accountKeyIndexes: instruction.keys.map(meta =>
findKeyIndex(meta.pubkey),
),
data: instruction.data,
};
});
}
}

View File

@ -0,0 +1,165 @@
import {MessageHeader, MessageAddressTableLookup} from './index';
import {AccountKeysFromLookups} from './account-keys';
import {AddressLookupTableAccount} from '../programs';
import {TransactionInstruction} from '../transaction';
import assert from '../utils/assert';
import {PublicKey} from '../publickey';
export type CompiledKeyMeta = {
isSigner: boolean;
isWritable: boolean;
isInvoked: boolean;
};
type KeyMetaMap = Map<string, CompiledKeyMeta>;
export class CompiledKeys {
payer: PublicKey;
keyMetaMap: KeyMetaMap;
constructor(payer: PublicKey, keyMetaMap: KeyMetaMap) {
this.payer = payer;
this.keyMetaMap = keyMetaMap;
}
static compile(
instructions: Array<TransactionInstruction>,
payer: PublicKey,
): CompiledKeys {
const keyMetaMap: KeyMetaMap = new Map();
const getOrInsertDefault = (pubkey: PublicKey): CompiledKeyMeta => {
const address = pubkey.toBase58();
let keyMeta = keyMetaMap.get(address);
if (keyMeta === undefined) {
keyMeta = {
isSigner: false,
isWritable: false,
isInvoked: false,
};
keyMetaMap.set(address, keyMeta);
}
return keyMeta;
};
const payerKeyMeta = getOrInsertDefault(payer);
payerKeyMeta.isSigner = true;
payerKeyMeta.isWritable = true;
for (const ix of instructions) {
getOrInsertDefault(ix.programId).isInvoked = true;
for (const accountMeta of ix.keys) {
const keyMeta = getOrInsertDefault(accountMeta.pubkey);
keyMeta.isSigner ||= accountMeta.isSigner;
keyMeta.isWritable ||= accountMeta.isWritable;
}
}
return new CompiledKeys(payer, keyMetaMap);
}
getMessageComponents(): [MessageHeader, Array<PublicKey>] {
const mapEntries = [...this.keyMetaMap.entries()];
assert(mapEntries.length <= 256, 'Max static account keys length exceeded');
const writableSigners = mapEntries.filter(
([, meta]) => meta.isSigner && meta.isWritable,
);
const readonlySigners = mapEntries.filter(
([, meta]) => meta.isSigner && !meta.isWritable,
);
const writableNonSigners = mapEntries.filter(
([, meta]) => !meta.isSigner && meta.isWritable,
);
const readonlyNonSigners = mapEntries.filter(
([, meta]) => !meta.isSigner && !meta.isWritable,
);
const header: MessageHeader = {
numRequiredSignatures: writableSigners.length + readonlySigners.length,
numReadonlySignedAccounts: readonlySigners.length,
numReadonlyUnsignedAccounts: readonlyNonSigners.length,
};
// sanity checks
{
assert(
writableSigners.length > 0,
'Expected at least one writable signer key',
);
const [payerAddress] = writableSigners[0];
assert(
payerAddress === this.payer.toBase58(),
'Expected first writable signer key to be the fee payer',
);
}
const staticAccountKeys = [
...writableSigners.map(([address]) => new PublicKey(address)),
...readonlySigners.map(([address]) => new PublicKey(address)),
...writableNonSigners.map(([address]) => new PublicKey(address)),
...readonlyNonSigners.map(([address]) => new PublicKey(address)),
];
return [header, staticAccountKeys];
}
extractTableLookup(
lookupTable: AddressLookupTableAccount,
): [MessageAddressTableLookup, AccountKeysFromLookups] | undefined {
const [writableIndexes, drainedWritableKeys] =
this.drainKeysFoundInLookupTable(
lookupTable.state.addresses,
keyMeta =>
!keyMeta.isSigner && !keyMeta.isInvoked && keyMeta.isWritable,
);
const [readonlyIndexes, drainedReadonlyKeys] =
this.drainKeysFoundInLookupTable(
lookupTable.state.addresses,
keyMeta =>
!keyMeta.isSigner && !keyMeta.isInvoked && !keyMeta.isWritable,
);
// Don't extract lookup if no keys were found
if (writableIndexes.length === 0 && readonlyIndexes.length === 0) {
return;
}
return [
{
accountKey: lookupTable.key,
writableIndexes,
readonlyIndexes,
},
{
writable: drainedWritableKeys,
readonly: drainedReadonlyKeys,
},
];
}
/** @internal */
private drainKeysFoundInLookupTable(
lookupTableEntries: Array<PublicKey>,
keyMetaFilter: (keyMeta: CompiledKeyMeta) => boolean,
): [Array<number>, Array<PublicKey>] {
const lookupTableIndexes = new Array();
const drainedKeys = new Array();
for (const [address, keyMeta] of this.keyMetaMap.entries()) {
if (keyMetaFilter(keyMeta)) {
const key = new PublicKey(address);
const lookupTableIndex = lookupTableEntries.findIndex(entry =>
entry.equals(key),
);
if (lookupTableIndex >= 0) {
assert(lookupTableIndex < 256, 'Max lookup table index exceeded');
lookupTableIndexes.push(lookupTableIndex);
drainedKeys.push(key);
this.keyMetaMap.delete(address);
}
}
}
return [lookupTableIndexes, drainedKeys];
}
}

View File

@ -1,5 +1,7 @@
import {PublicKey} from '../publickey';
export * from './account-keys';
// note: compiled-keys is internal and doesn't need to be exported
export * from './legacy';
export * from './versioned';
export * from './v0';

View File

@ -12,6 +12,10 @@ import {PublicKey, PUBLIC_KEY_LENGTH} from '../publickey';
import * as shortvec from '../utils/shortvec-encoding';
import assert from '../utils/assert';
import {PACKET_DATA_SIZE, VERSION_PREFIX_MASK} from '../transaction/constants';
import {TransactionInstruction} from '../transaction';
import {AddressLookupTableAccount} from '../programs';
import {CompiledKeys} from './compiled-keys';
import {AccountKeysFromLookups, MessageAccountKeys} from './account-keys';
/**
* Message constructor arguments
@ -29,6 +33,13 @@ export type MessageV0Args = {
addressTableLookups: MessageAddressTableLookup[];
};
export type CompileV0Args = {
payerKey: PublicKey;
instructions: Array<TransactionInstruction>;
recentBlockhash: Blockhash;
addressLookupTableAccounts?: Array<AddressLookupTableAccount>;
};
export class MessageV0 {
header: MessageHeader;
staticAccountKeys: Array<PublicKey>;
@ -48,6 +59,42 @@ export class MessageV0 {
return 0;
}
static compile(args: CompileV0Args): MessageV0 {
const compiledKeys = CompiledKeys.compile(args.instructions, args.payerKey);
const addressTableLookups = new Array<MessageAddressTableLookup>();
const accountKeysFromLookups: AccountKeysFromLookups = {
writable: new Array(),
readonly: new Array(),
};
const lookupTableAccounts = args.addressLookupTableAccounts || [];
for (const lookupTable of lookupTableAccounts) {
const extractResult = compiledKeys.extractTableLookup(lookupTable);
if (extractResult !== undefined) {
const [addressTableLookup, {writable, readonly}] = extractResult;
addressTableLookups.push(addressTableLookup);
accountKeysFromLookups.writable.push(...writable);
accountKeysFromLookups.readonly.push(...readonly);
}
}
const [header, staticAccountKeys] = compiledKeys.getMessageComponents();
const accountKeys = new MessageAccountKeys(
staticAccountKeys,
accountKeysFromLookups,
);
const compiledInstructions = accountKeys.compileInstructions(
args.instructions,
);
return new MessageV0({
header,
staticAccountKeys,
recentBlockhash: args.recentBlockhash,
compiledInstructions,
addressTableLookups,
});
}
serialize(): Uint8Array {
const encodedStaticAccountKeysLength = Array<number>();
shortvec.encodeLength(

View File

@ -40,6 +40,9 @@ function isPublicKeyData(value: PublicKeyInitData): value is PublicKeyData {
return (value as PublicKeyData)._bn !== undefined;
}
// local counter used by PublicKey.unique()
let uniquePublicKeyCounter = 1;
/**
* A public key
*/
@ -73,6 +76,15 @@ export class PublicKey extends Struct {
}
}
/**
* Returns a unique PublicKey for tests and benchmarks using acounter
*/
static unique(): PublicKey {
const key = new PublicKey(uniquePublicKeyCounter);
uniquePublicKeyCounter += 1;
return new PublicKey(key.toBuffer());
}
/**
* Default public key value. (All zeros)
*/

View File

@ -0,0 +1,180 @@
import {expect} from 'chai';
import {
MessageAccountKeys,
MessageCompiledInstruction,
} from '../../src/message';
import {PublicKey} from '../../src/publickey';
import {TransactionInstruction} from '../../src/transaction';
function createTestKeys(count: number): Array<PublicKey> {
return new Array(count).fill(0).map(() => PublicKey.unique());
}
describe('MessageAccountKeys', () => {
it('keySegments', () => {
const keys = createTestKeys(6);
const staticAccountKeys = keys.slice(0, 3);
const accountKeysFromLookups = {
writable: [keys[3], keys[4]],
readonly: [keys[5]],
};
const accountKeys = new MessageAccountKeys(
staticAccountKeys,
accountKeysFromLookups,
);
const expectedSegments = [
staticAccountKeys,
accountKeysFromLookups.writable,
accountKeysFromLookups.readonly,
];
expect(expectedSegments).to.eql(accountKeys.keySegments());
});
it('get', () => {
const keys = createTestKeys(3);
const accountKeys = new MessageAccountKeys(keys);
expect(accountKeys.get(0)).to.eq(keys[0]);
expect(accountKeys.get(1)).to.eq(keys[1]);
expect(accountKeys.get(2)).to.eq(keys[2]);
expect(accountKeys.get(3)).to.be.undefined;
});
it('get with loaded addresses', () => {
const keys = createTestKeys(6);
const staticAccountKeys = keys.slice(0, 3);
const accountKeysFromLookups = {
writable: [keys[3], keys[4]],
readonly: [keys[5]],
};
const accountKeys = new MessageAccountKeys(
staticAccountKeys,
accountKeysFromLookups,
);
expect(accountKeys.get(0)).to.eq(keys[0]);
expect(accountKeys.get(1)).to.eq(keys[1]);
expect(accountKeys.get(2)).to.eq(keys[2]);
expect(accountKeys.get(3)).to.eq(keys[3]);
expect(accountKeys.get(4)).to.eq(keys[4]);
expect(accountKeys.get(5)).to.eq(keys[5]);
});
it('length', () => {
const keys = createTestKeys(6);
const accountKeys = new MessageAccountKeys(keys);
expect(accountKeys.length).to.eq(6);
});
it('length with loaded addresses', () => {
const keys = createTestKeys(6);
const accountKeys = new MessageAccountKeys(keys.slice(0, 3), {
writable: [],
readonly: keys.slice(3, 6),
});
expect(accountKeys.length).to.eq(6);
});
it('compileInstructions', () => {
const keys = createTestKeys(3);
const staticAccountKeys = [keys[0]];
const accountKeysFromLookups = {
writable: [keys[1]],
readonly: [keys[2]],
};
const accountKeys = new MessageAccountKeys(
staticAccountKeys,
accountKeysFromLookups,
);
const instruction = new TransactionInstruction({
programId: keys[0],
keys: [
{
pubkey: keys[1],
isSigner: true,
isWritable: true,
},
{
pubkey: keys[2],
isSigner: true,
isWritable: true,
},
],
data: Buffer.alloc(0),
});
const expectedInstruction: MessageCompiledInstruction = {
programIdIndex: 0,
accountKeyIndexes: [1, 2],
data: new Uint8Array(0),
};
expect(accountKeys.compileInstructions([instruction])).to.eql([
expectedInstruction,
]);
});
it('compileInstructions with unknown key', () => {
const keys = createTestKeys(3);
const staticAccountKeys = [keys[0]];
const accountKeysFromLookups = {
writable: [keys[1]],
readonly: [keys[2]],
};
const accountKeys = new MessageAccountKeys(
staticAccountKeys,
accountKeysFromLookups,
);
const unknownKey = PublicKey.unique();
const testInstructions = [
new TransactionInstruction({
programId: unknownKey,
keys: [],
data: Buffer.alloc(0),
}),
new TransactionInstruction({
programId: keys[0],
keys: [
{
pubkey: keys[1],
isSigner: true,
isWritable: true,
},
{
pubkey: unknownKey,
isSigner: true,
isWritable: true,
},
],
data: Buffer.alloc(0),
}),
];
for (const instruction of testInstructions) {
expect(() => accountKeys.compileInstructions([instruction])).to.throw(
'Encountered an unknown instruction account key during compilation',
);
}
});
it('compileInstructions with too many account keys', () => {
const keys = createTestKeys(257);
const accountKeys = new MessageAccountKeys(keys.slice(0, 256), {
writable: [keys[256]],
readonly: [],
});
expect(() => accountKeys.compileInstructions([])).to.throw(
'Account index overflow encountered during compilation',
);
});
});

View File

@ -0,0 +1,243 @@
import {expect} from 'chai';
import {CompiledKeyMeta, CompiledKeys} from '../../src/message/compiled-keys';
import {AddressLookupTableAccount} from '../../src/programs';
import {PublicKey} from '../../src/publickey';
import {AccountMeta, TransactionInstruction} from '../../src/transaction';
function createTestKeys(count: number): Array<PublicKey> {
return new Array(count).fill(0).map(() => PublicKey.unique());
}
function createTestLookupTable(
addresses: Array<PublicKey>,
): AddressLookupTableAccount {
const U64_MAX = 2n ** 64n - 1n;
return new AddressLookupTableAccount({
key: PublicKey.unique(),
state: {
lastExtendedSlot: 0,
lastExtendedSlotStartIndex: 0,
deactivationSlot: U64_MAX,
authority: PublicKey.unique(),
addresses,
},
});
}
describe('CompiledKeys', () => {
it('compile', () => {
const payer = PublicKey.unique();
const keys = createTestKeys(4);
const programIds = createTestKeys(4);
const compiledKeys = CompiledKeys.compile(
[
new TransactionInstruction({
programId: programIds[0],
keys: [
createAccountMeta(keys[0], false, false),
createAccountMeta(keys[1], true, false),
createAccountMeta(keys[2], false, true),
createAccountMeta(keys[3], true, true),
// duplicate the account metas
createAccountMeta(keys[0], false, false),
createAccountMeta(keys[1], true, false),
createAccountMeta(keys[2], false, true),
createAccountMeta(keys[3], true, true),
// reference program ids
createAccountMeta(programIds[0], false, false),
createAccountMeta(programIds[1], true, false),
createAccountMeta(programIds[2], false, true),
createAccountMeta(programIds[3], true, true),
],
}),
new TransactionInstruction({programId: programIds[1], keys: []}),
new TransactionInstruction({programId: programIds[2], keys: []}),
new TransactionInstruction({programId: programIds[3], keys: []}),
],
payer,
);
const map = new Map<string, CompiledKeyMeta>();
setMapEntry(map, payer, true, true, false);
setMapEntry(map, keys[0], false, false, false);
setMapEntry(map, keys[1], true, false, false);
setMapEntry(map, keys[2], false, true, false);
setMapEntry(map, keys[3], true, true, false);
setMapEntry(map, programIds[0], false, false, true);
setMapEntry(map, programIds[1], true, false, true);
setMapEntry(map, programIds[2], false, true, true);
setMapEntry(map, programIds[3], true, true, true);
expect(compiledKeys.keyMetaMap).to.eql(map);
expect(compiledKeys.payer).to.eq(payer);
});
it('compile with dup payer', () => {
const [payer, programId] = createTestKeys(2);
const compiledKeys = CompiledKeys.compile(
[
new TransactionInstruction({
programId: programId,
keys: [createAccountMeta(payer, false, false)],
}),
],
payer,
);
const map = new Map<string, CompiledKeyMeta>();
setMapEntry(map, payer, true, true, false);
setMapEntry(map, programId, false, false, true);
expect(compiledKeys.keyMetaMap).to.eql(map);
expect(compiledKeys.payer).to.eq(payer);
});
it('compile with dup key', () => {
const [payer, key, programId] = createTestKeys(3);
const compiledKeys = CompiledKeys.compile(
[
new TransactionInstruction({
programId: programId,
keys: [
createAccountMeta(key, false, false),
createAccountMeta(key, true, true),
],
}),
],
payer,
);
const map = new Map<string, CompiledKeyMeta>();
setMapEntry(map, payer, true, true, false);
setMapEntry(map, key, true, true, false);
setMapEntry(map, programId, false, false, true);
expect(compiledKeys.keyMetaMap).to.eql(map);
expect(compiledKeys.payer).to.eq(payer);
});
it('getMessageComponents', () => {
const keys = createTestKeys(4);
const payer = keys[0];
const map = new Map<string, CompiledKeyMeta>();
setMapEntry(map, payer, true, true, false);
setMapEntry(map, keys[1], true, false, false);
setMapEntry(map, keys[2], false, true, false);
setMapEntry(map, keys[3], false, false, false);
const compiledKeys = new CompiledKeys(payer, map);
const [header, staticAccountKeys] = compiledKeys.getMessageComponents();
expect(staticAccountKeys).to.eql(keys);
expect(header).to.eql({
numRequiredSignatures: 2,
numReadonlySignedAccounts: 1,
numReadonlyUnsignedAccounts: 1,
});
});
it('getMessageComponents with overflow', () => {
const keys = createTestKeys(257);
const map = new Map<string, CompiledKeyMeta>();
for (const key of keys) {
setMapEntry(map, key, true, true, false);
}
const compiledKeys = new CompiledKeys(keys[0], map);
expect(() => compiledKeys.getMessageComponents()).to.throw(
'Max static account keys length exceeded',
);
});
it('extractTableLookup', () => {
const keys = createTestKeys(6);
const map = new Map<string, CompiledKeyMeta>();
setMapEntry(map, keys[0], true, true, false);
setMapEntry(map, keys[1], true, false, false);
setMapEntry(map, keys[2], false, true, false);
setMapEntry(map, keys[3], false, false, false);
setMapEntry(map, keys[4], true, false, true);
setMapEntry(map, keys[5], false, false, true);
const lookupTable = createTestLookupTable([...keys, ...keys]);
const compiledKeys = new CompiledKeys(keys[0], map);
const extractResult = compiledKeys.extractTableLookup(lookupTable);
if (extractResult === undefined) {
expect(extractResult).to.not.be.undefined;
return;
}
const [tableLookup, extractedAddresses] = extractResult;
expect(tableLookup).to.eql({
accountKey: lookupTable.key,
writableIndexes: [2],
readonlyIndexes: [3],
});
expect(extractedAddresses).to.eql({
writable: [keys[2]],
readonly: [keys[3]],
});
});
it('extractTableLookup no extractable keys found', () => {
const keys = createTestKeys(6);
const map = new Map<string, CompiledKeyMeta>();
setMapEntry(map, keys[0], true, true, false);
setMapEntry(map, keys[1], true, false, false);
setMapEntry(map, keys[2], true, true, true);
setMapEntry(map, keys[3], true, false, true);
setMapEntry(map, keys[4], false, true, true);
setMapEntry(map, keys[5], false, false, true);
const lookupTable = createTestLookupTable(keys);
const compiledKeys = new CompiledKeys(keys[0], map);
const extractResult = compiledKeys.extractTableLookup(lookupTable);
expect(extractResult).to.be.undefined;
});
it('extractTableLookup with empty lookup table', () => {
const keys = createTestKeys(2);
const map = new Map<string, CompiledKeyMeta>();
setMapEntry(map, keys[0], true, true, false);
setMapEntry(map, keys[1], false, false, false);
const lookupTable = createTestLookupTable([]);
const compiledKeys = new CompiledKeys(keys[0], map);
const extractResult = compiledKeys.extractTableLookup(lookupTable);
expect(extractResult).to.be.undefined;
});
it('extractTableLookup with invalid lookup table', () => {
const keys = createTestKeys(257);
const map = new Map<string, CompiledKeyMeta>();
setMapEntry(map, keys[0], true, true, false);
setMapEntry(map, keys[256], false, false, false);
const lookupTable = createTestLookupTable(keys);
const compiledKeys = new CompiledKeys(keys[0], map);
expect(() => compiledKeys.extractTableLookup(lookupTable)).to.throw(
'Max lookup table index exceeded',
);
});
});
function setMapEntry(
map: Map<string, CompiledKeyMeta>,
pubkey: PublicKey,
isSigner: boolean,
isWritable: boolean,
isInvoked: boolean,
) {
map.set(pubkey.toBase58(), {
isSigner,
isWritable,
isInvoked,
});
}
function createAccountMeta(
pubkey: PublicKey,
isSigner: boolean,
isWritable: boolean,
): AccountMeta {
return {
pubkey,
isSigner,
isWritable,
};
}

View File

@ -1,9 +1,114 @@
import bs58 from 'bs58';
import {expect} from 'chai';
import {sha256} from '@noble/hashes/sha256';
import {MessageV0} from '../../src/message';
import {TransactionInstruction} from '../../src/transaction';
import {PublicKey} from '../../src/publickey';
import {AddressLookupTableAccount} from '../../src/programs';
function createTestKeys(count: number): Array<PublicKey> {
return new Array(count).fill(0).map(() => PublicKey.unique());
}
function createTestLookupTable(
addresses: Array<PublicKey>,
): AddressLookupTableAccount {
const U64_MAX = 2n ** 64n - 1n;
return new AddressLookupTableAccount({
key: PublicKey.unique(),
state: {
lastExtendedSlot: 0,
lastExtendedSlotStartIndex: 0,
deactivationSlot: U64_MAX,
authority: PublicKey.unique(),
addresses,
},
});
}
describe('MessageV0', () => {
it('compile', () => {
const keys = createTestKeys(7);
const recentBlockhash = bs58.encode(sha256('test'));
const payerKey = keys[0];
const instructions = [
new TransactionInstruction({
programId: keys[4],
keys: [
{pubkey: keys[1], isSigner: true, isWritable: true},
{pubkey: keys[2], isSigner: false, isWritable: false},
{pubkey: keys[3], isSigner: false, isWritable: false},
],
data: Buffer.alloc(1),
}),
new TransactionInstruction({
programId: keys[1],
keys: [
{pubkey: keys[2], isSigner: true, isWritable: false},
{pubkey: keys[3], isSigner: false, isWritable: true},
],
data: Buffer.alloc(2),
}),
new TransactionInstruction({
programId: keys[3],
keys: [
{pubkey: keys[5], isSigner: false, isWritable: true},
{pubkey: keys[6], isSigner: false, isWritable: false},
],
data: Buffer.alloc(3),
}),
];
const lookupTable = createTestLookupTable(keys);
const message = MessageV0.compile({
payerKey,
recentBlockhash,
instructions,
addressLookupTableAccounts: [lookupTable],
});
expect(message.staticAccountKeys).to.eql([
payerKey, // payer is first
keys[1], // other writable signer
keys[2], // sole readonly signer
keys[3], // sole writable non-signer
keys[4], // sole readonly non-signer
]);
expect(message.header).to.eql({
numRequiredSignatures: 3,
numReadonlySignedAccounts: 1,
numReadonlyUnsignedAccounts: 1,
});
// only keys 5 and 6 are eligible to be referenced by a lookup table
// because they are not invoked and are not signers
expect(message.addressTableLookups).to.eql([
{
accountKey: lookupTable.key,
writableIndexes: [5],
readonlyIndexes: [6],
},
]);
expect(message.compiledInstructions).to.eql([
{
programIdIndex: 4,
accountKeyIndexes: [1, 2, 3],
data: new Uint8Array(1),
},
{
programIdIndex: 1,
accountKeyIndexes: [2, 3],
data: new Uint8Array(2),
},
{
programIdIndex: 3,
accountKeyIndexes: [5, 6],
data: new Uint8Array(3),
},
]);
expect(message.recentBlockhash).to.eq(recentBlockhash);
});
it('serialize and deserialize', () => {
const messageV0 = new MessageV0({
header: {