From 3374f41201caa1bcd81a46376c9f2e049d21609f Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Tue, 6 Sep 2022 22:43:22 -0500 Subject: [PATCH] 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 --- web3.js/src/message/account-keys.ts | 79 ++++++ web3.js/src/message/compiled-keys.ts | 165 ++++++++++++ web3.js/src/message/index.ts | 2 + web3.js/src/message/v0.ts | 47 ++++ web3.js/src/publickey.ts | 12 + .../test/message-tests/account-keys.test.ts | 180 +++++++++++++ .../test/message-tests/compiled-keys.test.ts | 243 ++++++++++++++++++ web3.js/test/message-tests/v0.test.ts | 105 ++++++++ 8 files changed, 833 insertions(+) create mode 100644 web3.js/src/message/account-keys.ts create mode 100644 web3.js/src/message/compiled-keys.ts create mode 100644 web3.js/test/message-tests/account-keys.test.ts create mode 100644 web3.js/test/message-tests/compiled-keys.test.ts diff --git a/web3.js/src/message/account-keys.ts b/web3.js/src/message/account-keys.ts new file mode 100644 index 000000000..73a6ca56e --- /dev/null +++ b/web3.js/src/message/account-keys.ts @@ -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; + accountKeysFromLookups?: AccountKeysFromLookups; + + constructor( + staticAccountKeys: Array, + accountKeysFromLookups?: AccountKeysFromLookups, + ) { + this.staticAccountKeys = staticAccountKeys; + this.accountKeysFromLookups = accountKeysFromLookups; + } + + keySegments(): Array> { + 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, + ): Array { + // 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, + }; + }); + } +} diff --git a/web3.js/src/message/compiled-keys.ts b/web3.js/src/message/compiled-keys.ts new file mode 100644 index 000000000..a0cf88ea3 --- /dev/null +++ b/web3.js/src/message/compiled-keys.ts @@ -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; + +export class CompiledKeys { + payer: PublicKey; + keyMetaMap: KeyMetaMap; + + constructor(payer: PublicKey, keyMetaMap: KeyMetaMap) { + this.payer = payer; + this.keyMetaMap = keyMetaMap; + } + + static compile( + instructions: Array, + 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] { + 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, + keyMetaFilter: (keyMeta: CompiledKeyMeta) => boolean, + ): [Array, Array] { + 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]; + } +} diff --git a/web3.js/src/message/index.ts b/web3.js/src/message/index.ts index 24f7a1dcb..294a90b17 100644 --- a/web3.js/src/message/index.ts +++ b/web3.js/src/message/index.ts @@ -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'; diff --git a/web3.js/src/message/v0.ts b/web3.js/src/message/v0.ts index ea770df3a..800ad162c 100644 --- a/web3.js/src/message/v0.ts +++ b/web3.js/src/message/v0.ts @@ -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; + recentBlockhash: Blockhash; + addressLookupTableAccounts?: Array; +}; + export class MessageV0 { header: MessageHeader; staticAccountKeys: Array; @@ -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(); + 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(); shortvec.encodeLength( diff --git a/web3.js/src/publickey.ts b/web3.js/src/publickey.ts index f1bf76b22..0fad902f3 100644 --- a/web3.js/src/publickey.ts +++ b/web3.js/src/publickey.ts @@ -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) */ diff --git a/web3.js/test/message-tests/account-keys.test.ts b/web3.js/test/message-tests/account-keys.test.ts new file mode 100644 index 000000000..ec132e4d9 --- /dev/null +++ b/web3.js/test/message-tests/account-keys.test.ts @@ -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 { + 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', + ); + }); +}); diff --git a/web3.js/test/message-tests/compiled-keys.test.ts b/web3.js/test/message-tests/compiled-keys.test.ts new file mode 100644 index 000000000..a44f7bdd6 --- /dev/null +++ b/web3.js/test/message-tests/compiled-keys.test.ts @@ -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 { + return new Array(count).fill(0).map(() => PublicKey.unique()); +} + +function createTestLookupTable( + addresses: Array, +): 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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, + 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, + }; +} diff --git a/web3.js/test/message-tests/v0.test.ts b/web3.js/test/message-tests/v0.test.ts index 9cd3b1897..8538b64cc 100644 --- a/web3.js/test/message-tests/v0.test.ts +++ b/web3.js/test/message-tests/v0.test.ts @@ -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 { + return new Array(count).fill(0).map(() => PublicKey.unique()); +} + +function createTestLookupTable( + addresses: Array, +): 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: {