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:
parent
8c1093534e
commit
3374f41201
|
@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
*/
|
||||
|
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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: {
|
||||
|
|
Loading…
Reference in New Issue