From b3707ac6ba9110933ad16c835486f36765c17e09 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Wed, 31 Aug 2022 10:43:19 -0400 Subject: [PATCH 1/5] feat: add PublicKey.unique method for tests --- web3.js/src/publickey.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/web3.js/src/publickey.ts b/web3.js/src/publickey.ts index f1bf76b22a94ab..0fad902f34af0e 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) */ From 286548f966e536dc5efc54b76ee4badabe39f1f8 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Tue, 16 Aug 2022 13:31:32 +0100 Subject: [PATCH 2/5] feat: add MessageAccountKeys class --- web3.js/src/message/account-keys.ts | 79 ++++++++ web3.js/src/message/index.ts | 1 + .../test/message-tests/account-keys.test.ts | 180 ++++++++++++++++++ 3 files changed, 260 insertions(+) create mode 100644 web3.js/src/message/account-keys.ts create mode 100644 web3.js/test/message-tests/account-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 00000000000000..73a6ca56ee41e2 --- /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/index.ts b/web3.js/src/message/index.ts index 24f7a1dcb843ac..5389a89d251194 100644 --- a/web3.js/src/message/index.ts +++ b/web3.js/src/message/index.ts @@ -1,5 +1,6 @@ import {PublicKey} from '../publickey'; +export * from './account-keys'; export * from './legacy'; export * from './versioned'; export * from './v0'; 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 00000000000000..ec132e4d96573a --- /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', + ); + }); +}); From d8829f3edb0b80eff778566527e7179fa4a1b58e Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Wed, 31 Aug 2022 11:12:44 -0400 Subject: [PATCH 3/5] feat: add CompiledKeys class for message compilation --- web3.js/src/message/compiled-keys.ts | 165 ++++++++++++ web3.js/src/message/index.ts | 1 + .../test/message-tests/compiled-keys.test.ts | 243 ++++++++++++++++++ 3 files changed, 409 insertions(+) create mode 100644 web3.js/src/message/compiled-keys.ts create mode 100644 web3.js/test/message-tests/compiled-keys.test.ts diff --git a/web3.js/src/message/compiled-keys.ts b/web3.js/src/message/compiled-keys.ts new file mode 100644 index 00000000000000..a0cf88ea34377a --- /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 5389a89d251194..294a90b17e25ee 100644 --- a/web3.js/src/message/index.ts +++ b/web3.js/src/message/index.ts @@ -1,6 +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/test/message-tests/compiled-keys.test.ts b/web3.js/test/message-tests/compiled-keys.test.ts new file mode 100644 index 00000000000000..a44f7bdd6cfcb6 --- /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, + }; +} From c159dca828146678f67794a1111b7968bd6187f9 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Wed, 31 Aug 2022 11:13:41 -0400 Subject: [PATCH 4/5] feat: implement message compilation using CompiledKeys --- web3.js/src/message/v0.ts | 47 ++++++++++++ web3.js/test/message-tests/v0.test.ts | 105 ++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/web3.js/src/message/v0.ts b/web3.js/src/message/v0.ts index ea770df3a9b203..800ad162c163be 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/test/message-tests/v0.test.ts b/web3.js/test/message-tests/v0.test.ts index 9cd3b189735b73..8538b64ccb5fb5 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: { From 72c6339cedcb666c8e8be625e4f0de1d5cbe4ca6 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Wed, 31 Aug 2022 10:43:19 -0400 Subject: [PATCH 5/5] feat: implement message v0 decompilation --- web3.js/src/message/legacy.ts | 36 ++++- web3.js/src/message/v0.ts | 90 +++++++++++ web3.js/src/transaction/index.ts | 1 + web3.js/src/transaction/message.ts | 147 ++++++++++++++++++ web3.js/test/message-tests/legacy.test.ts | 91 +++++++++++ web3.js/test/message-tests/v0.test.ts | 142 ++++++++++++++++- .../test/transaction-tests/message.test.ts | 89 +++++++++++ 7 files changed, 593 insertions(+), 3 deletions(-) create mode 100644 web3.js/src/transaction/message.ts create mode 100644 web3.js/test/message-tests/legacy.test.ts create mode 100644 web3.js/test/transaction-tests/message.test.ts diff --git a/web3.js/src/message/legacy.ts b/web3.js/src/message/legacy.ts index 38faa6320a0dbd..8e5116fb51b637 100644 --- a/web3.js/src/message/legacy.ts +++ b/web3.js/src/message/legacy.ts @@ -13,6 +13,9 @@ import { MessageAddressTableLookup, MessageCompiledInstruction, } from './index'; +import {TransactionInstruction} from '../transaction'; +import {CompiledKeys} from './compiled-keys'; +import {MessageAccountKeys} from './account-keys'; /** * An instruction to execute by a program @@ -37,13 +40,19 @@ export type MessageArgs = { /** The message header, identifying signed and read-only `accountKeys` */ header: MessageHeader; /** All the account keys used by this transaction */ - accountKeys: string[]; + accountKeys: string[] | PublicKey[]; /** The hash of a recent ledger block */ recentBlockhash: Blockhash; /** Instructions that will be executed in sequence and committed in one atomic transaction if all succeed. */ instructions: CompiledInstruction[]; }; +export type CompileLegacyArgs = { + payerKey: PublicKey; + instructions: Array; + recentBlockhash: Blockhash; +}; + /** * List of instructions to be processed atomically */ @@ -93,6 +102,29 @@ export class Message { return []; } + getAccountKeys(): MessageAccountKeys { + return new MessageAccountKeys(this.staticAccountKeys); + } + + static compile(args: CompileLegacyArgs): Message { + const compiledKeys = CompiledKeys.compile(args.instructions, args.payerKey); + const [header, staticAccountKeys] = compiledKeys.getMessageComponents(); + const accountKeys = new MessageAccountKeys(staticAccountKeys); + const instructions = accountKeys.compileInstructions(args.instructions).map( + (ix: MessageCompiledInstruction): CompiledInstruction => ({ + programIdIndex: ix.programIdIndex, + accounts: ix.accountKeyIndexes, + data: bs58.encode(ix.data), + }), + ); + return new Message({ + header, + accountKeys: staticAccountKeys, + recentBlockhash: args.recentBlockhash, + instructions, + }); + } + isAccountSigner(index: number): boolean { return index < this.header.numRequiredSignatures; } @@ -250,7 +282,7 @@ export class Message { for (let i = 0; i < accountCount; i++) { const account = byteArray.slice(0, PUBLIC_KEY_LENGTH); byteArray = byteArray.slice(PUBLIC_KEY_LENGTH); - accountKeys.push(bs58.encode(Buffer.from(account))); + accountKeys.push(new PublicKey(Buffer.from(account))); } const recentBlockhash = byteArray.slice(0, PUBLIC_KEY_LENGTH); diff --git a/web3.js/src/message/v0.ts b/web3.js/src/message/v0.ts index 800ad162c163be..0f452602063cff 100644 --- a/web3.js/src/message/v0.ts +++ b/web3.js/src/message/v0.ts @@ -40,6 +40,14 @@ export type CompileV0Args = { addressLookupTableAccounts?: Array; }; +export type GetAccountKeysArgs = + | { + accountKeysFromLookups: AccountKeysFromLookups; + } + | { + addressLookupTableAccounts: AddressLookupTableAccount[]; + }; + export class MessageV0 { header: MessageHeader; staticAccountKeys: Array; @@ -59,6 +67,88 @@ export class MessageV0 { return 0; } + get numAccountKeysFromLookups(): number { + let count = 0; + for (const lookup of this.addressTableLookups) { + count += lookup.readonlyIndexes.length + lookup.writableIndexes.length; + } + return count; + } + + getAccountKeys(args?: GetAccountKeysArgs): MessageAccountKeys { + let accountKeysFromLookups: AccountKeysFromLookups | undefined; + if (args && 'accountKeysFromLookups' in args) { + if ( + this.numAccountKeysFromLookups != + args.accountKeysFromLookups.writable.length + + args.accountKeysFromLookups.readonly.length + ) { + throw new Error( + 'Failed to get account keys because of a mismatch in the number of account keys from lookups', + ); + } + accountKeysFromLookups = args.accountKeysFromLookups; + } else if (args && 'addressLookupTableAccounts' in args) { + accountKeysFromLookups = this.resolveAddressTableLookups( + args.addressLookupTableAccounts, + ); + } else if (this.addressTableLookups.length > 0) { + throw new Error( + 'Failed to get account keys because address table lookups were not resolved', + ); + } + return new MessageAccountKeys( + this.staticAccountKeys, + accountKeysFromLookups, + ); + } + + resolveAddressTableLookups( + addressLookupTableAccounts: AddressLookupTableAccount[], + ): AccountKeysFromLookups { + const accountKeysFromLookups: AccountKeysFromLookups = { + writable: [], + readonly: [], + }; + + for (const tableLookup of this.addressTableLookups) { + const tableAccount = addressLookupTableAccounts.find(account => + account.key.equals(tableLookup.accountKey), + ); + if (!tableAccount) { + throw new Error( + `Failed to find address lookup table account for table key ${tableLookup.accountKey.toBase58()}`, + ); + } + + for (const index of tableLookup.writableIndexes) { + if (index < tableAccount.state.addresses.length) { + accountKeysFromLookups.writable.push( + tableAccount.state.addresses[index], + ); + } else { + throw new Error( + `Failed to find address for index ${index} in address lookup table ${tableLookup.accountKey.toBase58()}`, + ); + } + } + + for (const index of tableLookup.readonlyIndexes) { + if (index < tableAccount.state.addresses.length) { + accountKeysFromLookups.readonly.push( + tableAccount.state.addresses[index], + ); + } else { + throw new Error( + `Failed to find address for index ${index} in address lookup table ${tableLookup.accountKey.toBase58()}`, + ); + } + } + } + + return accountKeysFromLookups; + } + static compile(args: CompileV0Args): MessageV0 { const compiledKeys = CompiledKeys.compile(args.instructions, args.payerKey); diff --git a/web3.js/src/transaction/index.ts b/web3.js/src/transaction/index.ts index 2f5c19cb2510a5..88d1cb1700ea62 100644 --- a/web3.js/src/transaction/index.ts +++ b/web3.js/src/transaction/index.ts @@ -1,4 +1,5 @@ export * from './constants'; export * from './expiry-custom-errors'; export * from './legacy'; +export * from './message'; export * from './versioned'; diff --git a/web3.js/src/transaction/message.ts b/web3.js/src/transaction/message.ts new file mode 100644 index 00000000000000..2b0a69572b1b21 --- /dev/null +++ b/web3.js/src/transaction/message.ts @@ -0,0 +1,147 @@ +import { + AccountKeysFromLookups, + MessageAccountKeys, +} from '../message/account-keys'; +import assert from '../utils/assert'; +import {toBuffer} from '../utils/to-buffer'; +import {Blockhash} from '../blockhash'; +import {Message, MessageV0, VersionedMessage} from '../message'; +import {AddressLookupTableAccount} from '../programs'; +import {AccountMeta, TransactionInstruction} from './legacy'; + +export type TransactionMessageArgs = { + accountKeys: MessageAccountKeys; + instructions: Array; + recentBlockhash: Blockhash; +}; + +export type DecompileArgs = + | { + accountKeysFromLookups: AccountKeysFromLookups; + } + | { + addressLookupTableAccounts: AddressLookupTableAccount[]; + }; + +export class TransactionMessage { + accountKeys: MessageAccountKeys; + instructions: Array; + recentBlockhash: Blockhash; + + constructor(args: TransactionMessageArgs) { + this.accountKeys = args.accountKeys; + this.instructions = args.instructions; + this.recentBlockhash = args.recentBlockhash; + } + + static decompile( + message: VersionedMessage, + args?: DecompileArgs, + ): TransactionMessage { + const {header, compiledInstructions, recentBlockhash} = message; + + const { + numRequiredSignatures, + numReadonlySignedAccounts, + numReadonlyUnsignedAccounts, + } = header; + + const numWritableSignedAccounts = + numRequiredSignatures - numReadonlySignedAccounts; + assert(numWritableSignedAccounts > 0, 'Message header is invalid'); + + const numWritableUnsignedAccounts = + message.staticAccountKeys.length - numReadonlyUnsignedAccounts; + assert(numWritableUnsignedAccounts >= 0, 'Message header is invalid'); + + const accountKeys = message.getAccountKeys(args); + const instructions: TransactionInstruction[] = []; + for (const compiledIx of compiledInstructions) { + const keys: AccountMeta[] = []; + + for (const keyIndex of compiledIx.accountKeyIndexes) { + const pubkey = accountKeys.get(keyIndex); + if (pubkey === undefined) { + throw new Error( + `Failed to find key for account key index ${keyIndex}`, + ); + } + + const isSigner = keyIndex < numRequiredSignatures; + + let isWritable; + if (isSigner) { + isWritable = keyIndex < numWritableSignedAccounts; + } else if (keyIndex < accountKeys.staticAccountKeys.length) { + isWritable = + keyIndex - numRequiredSignatures < numWritableUnsignedAccounts; + } else { + isWritable = + keyIndex - accountKeys.staticAccountKeys.length < + // accountKeysFromLookups cannot be undefined because we already found a pubkey for this index above + accountKeys.accountKeysFromLookups!.writable.length; + } + + keys.push({ + pubkey, + isSigner: keyIndex < header.numRequiredSignatures, + isWritable, + }); + } + + const programId = accountKeys.get(compiledIx.programIdIndex); + if (programId === undefined) { + throw new Error( + `Failed to find program id for program id index ${compiledIx.programIdIndex}`, + ); + } + + instructions.push( + new TransactionInstruction({ + programId, + data: toBuffer(compiledIx.data), + keys, + }), + ); + } + + return new TransactionMessage({ + accountKeys, + instructions, + recentBlockhash, + }); + } + + compileToLegacyMessage(): Message { + const payerKey = this.accountKeys.get(0); + if (payerKey === undefined) { + throw new Error( + 'Failed to compile message because no account keys were found', + ); + } + + return Message.compile({ + payerKey, + recentBlockhash: this.recentBlockhash, + instructions: this.instructions, + }); + } + + compileToV0Message( + addressLookupTableAccounts?: AddressLookupTableAccount[], + ): MessageV0 { + const payerKey = this.accountKeys.get(0); + if (payerKey === undefined) { + throw new Error( + 'Failed to compile message because no account keys were found', + ); + } + + return MessageV0.compile({ + payerKey, + recentBlockhash: this.recentBlockhash, + instructions: this.instructions, + addressLookupTableAccounts, + }); + } +} diff --git a/web3.js/test/message-tests/legacy.test.ts b/web3.js/test/message-tests/legacy.test.ts new file mode 100644 index 00000000000000..a602c189698bf0 --- /dev/null +++ b/web3.js/test/message-tests/legacy.test.ts @@ -0,0 +1,91 @@ +import bs58 from 'bs58'; +import {expect} from 'chai'; +import {sha256} from '@noble/hashes/sha256'; + +import {Message} from '../../src/message'; +import {TransactionInstruction} from '../../src/transaction'; +import {PublicKey} from '../../src/publickey'; + +function createTestKeys(count: number): Array { + return new Array(count).fill(0).map(() => PublicKey.unique()); +} + +describe('Message', () => { + it('compile', () => { + const keys = createTestKeys(5); + 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), + }), + ]; + + const message = Message.compile({ + payerKey, + recentBlockhash, + instructions, + }); + + expect(message.accountKeys).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, + }); + expect(message.addressTableLookups.length).to.eq(0); + expect(message.instructions).to.eql([ + { + programIdIndex: 4, + accounts: [1, 2, 3], + data: bs58.encode(Buffer.alloc(1)), + }, + { + programIdIndex: 1, + accounts: [2, 3], + data: bs58.encode(Buffer.alloc(2)), + }, + ]); + expect(message.recentBlockhash).to.eq(recentBlockhash); + }); + + it('compile without instructions', () => { + const payerKey = PublicKey.unique(); + const recentBlockhash = bs58.encode(sha256('test')); + const message = Message.compile({ + payerKey, + instructions: [], + recentBlockhash, + }); + + expect(message.accountKeys).to.eql([payerKey]); + expect(message.header).to.eql({ + numRequiredSignatures: 1, + numReadonlySignedAccounts: 0, + numReadonlyUnsignedAccounts: 0, + }); + expect(message.addressTableLookups.length).to.eq(0); + expect(message.instructions.length).to.eq(0); + expect(message.recentBlockhash).to.eq(recentBlockhash); + }); +}); diff --git a/web3.js/test/message-tests/v0.test.ts b/web3.js/test/message-tests/v0.test.ts index 8538b64ccb5fb5..1f95b1d3b7d682 100644 --- a/web3.js/test/message-tests/v0.test.ts +++ b/web3.js/test/message-tests/v0.test.ts @@ -2,7 +2,11 @@ import bs58 from 'bs58'; import {expect} from 'chai'; import {sha256} from '@noble/hashes/sha256'; -import {MessageV0} from '../../src/message'; +import { + MessageAccountKeys, + MessageAddressTableLookup, + MessageV0, +} from '../../src/message'; import {TransactionInstruction} from '../../src/transaction'; import {PublicKey} from '../../src/publickey'; import {AddressLookupTableAccount} from '../../src/programs'; @@ -28,6 +32,142 @@ function createTestLookupTable( } describe('MessageV0', () => { + it('numAccountKeysFromLookups', () => { + const message = MessageV0.compile({ + payerKey: PublicKey.unique(), + recentBlockhash: '', + instructions: [], + }); + expect(message.numAccountKeysFromLookups).to.eq(0); + + message.addressTableLookups = [ + { + accountKey: PublicKey.unique(), + writableIndexes: [0], + readonlyIndexes: [1], + }, + { + accountKey: PublicKey.unique(), + writableIndexes: [0, 2], + readonlyIndexes: [], + }, + ]; + expect(message.numAccountKeysFromLookups).to.eq(4); + }); + + it('getAccountKeys', () => { + const staticAccountKeys = createTestKeys(3); + const lookupTable = createTestLookupTable(createTestKeys(2)); + const message = new MessageV0({ + header: { + numRequiredSignatures: 1, + numReadonlySignedAccounts: 0, + numReadonlyUnsignedAccounts: 0, + }, + recentBlockhash: 'test', + staticAccountKeys, + compiledInstructions: [], + addressTableLookups: [ + { + accountKey: lookupTable.key, + writableIndexes: [0], + readonlyIndexes: [1], + }, + ], + }); + + expect(() => message.getAccountKeys()).to.throw( + 'Failed to get account keys because address table lookups were not resolved', + ); + expect(() => + message.getAccountKeys({ + accountKeysFromLookups: {writable: [PublicKey.unique()], readonly: []}, + }), + ).to.throw( + 'Failed to get account keys because of a mismatch in the number of account keys from lookups', + ); + + const accountKeysFromLookups = message.resolveAddressTableLookups([ + lookupTable, + ]); + const expectedAccountKeys = new MessageAccountKeys( + staticAccountKeys, + accountKeysFromLookups, + ); + + expect( + message.getAccountKeys({ + accountKeysFromLookups, + }), + ).to.eql(expectedAccountKeys); + + expect( + message.getAccountKeys({ + addressLookupTableAccounts: [lookupTable], + }), + ).to.eql(expectedAccountKeys); + }); + + it('resolveAddressTableLookups', () => { + const keys = createTestKeys(7); + const lookupTable = createTestLookupTable(keys); + const createTestMessage = ( + addressTableLookups: MessageAddressTableLookup[], + ): MessageV0 => { + return new MessageV0({ + header: { + numRequiredSignatures: 1, + numReadonlySignedAccounts: 0, + numReadonlyUnsignedAccounts: 0, + }, + recentBlockhash: 'test', + staticAccountKeys: [], + compiledInstructions: [], + addressTableLookups, + }); + }; + + expect( + createTestMessage([]).resolveAddressTableLookups([lookupTable]), + ).to.eql({ + writable: [], + readonly: [], + }); + + expect(() => + createTestMessage([ + { + accountKey: PublicKey.unique(), + writableIndexes: [1, 3, 5], + readonlyIndexes: [0, 2, 4], + }, + ]).resolveAddressTableLookups([lookupTable]), + ).to.throw('Failed to find address lookup table account for table key'); + + expect(() => + createTestMessage([ + { + accountKey: lookupTable.key, + writableIndexes: [10], + readonlyIndexes: [], + }, + ]).resolveAddressTableLookups([lookupTable]), + ).to.throw('Failed to find address for index'); + + expect( + createTestMessage([ + { + accountKey: lookupTable.key, + writableIndexes: [1, 3, 5], + readonlyIndexes: [0, 2, 4], + }, + ]).resolveAddressTableLookups([lookupTable]), + ).to.eql({ + writable: [keys[1], keys[3], keys[5]], + readonly: [keys[0], keys[2], keys[4]], + }); + }); + it('compile', () => { const keys = createTestKeys(7); const recentBlockhash = bs58.encode(sha256('test')); diff --git a/web3.js/test/transaction-tests/message.test.ts b/web3.js/test/transaction-tests/message.test.ts new file mode 100644 index 00000000000000..b9faa7b3314757 --- /dev/null +++ b/web3.js/test/transaction-tests/message.test.ts @@ -0,0 +1,89 @@ +import bs58 from 'bs58'; +import {expect} from 'chai'; +import {sha256} from '@noble/hashes/sha256'; + +import { + TransactionInstruction, + TransactionMessage, +} from '../../src/transaction'; +import {PublicKey} from '../../src/publickey'; +import {AddressLookupTableAccount} from '../../src/programs'; +import {MessageV0} from '../../src/message'; + +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('TransactionMessage', () => { + it('decompile', () => { + 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: true, isWritable: false}, + {pubkey: keys[3], isSigner: false, isWritable: true}, + {pubkey: keys[5], isSigner: false, isWritable: true}, + {pubkey: keys[6], isSigner: false, isWritable: false}, + ], + data: Buffer.alloc(1), + }), + new TransactionInstruction({ + programId: keys[1], + keys: [], + data: Buffer.alloc(2), + }), + new TransactionInstruction({ + programId: keys[3], + keys: [], + data: Buffer.alloc(3), + }), + ]; + + const addressLookupTableAccounts = [createTestLookupTable(keys)]; + const message = MessageV0.compile({ + payerKey, + recentBlockhash, + instructions, + addressLookupTableAccounts, + }); + + expect(() => TransactionMessage.decompile(message)).to.throw( + 'Failed to find key', + ); + + const accountKeys = message.getAccountKeys({addressLookupTableAccounts}); + const decompiledMessage = TransactionMessage.decompile(message, { + addressLookupTableAccounts, + }); + + expect(decompiledMessage.accountKeys).to.eql(accountKeys); + expect(decompiledMessage.recentBlockhash).to.eq(recentBlockhash); + expect(decompiledMessage.instructions).to.eql(instructions); + + expect(decompiledMessage).to.eql( + TransactionMessage.decompile(message, { + accountKeysFromLookups: accountKeys.accountKeysFromLookups!, + }), + ); + }); +});