feat: add support for creating version 0 transactions#27142
feat: add support for creating version 0 transactions#27142jstarry merged 6 commits intosolana-labs:masterfrom
Conversation
3b212ce to
524b51e
Compare
Codecov Report
@@ Coverage Diff @@
## master #27142 +/- ##
=========================================
+ Coverage 76.9% 77.0% +0.1%
=========================================
Files 48 51 +3
Lines 2505 2649 +144
Branches 355 360 +5
=========================================
+ Hits 1927 2041 +114
- Misses 448 476 +28
- Partials 130 132 +2 |
524b51e to
e2206a9
Compare
e2206a9 to
a1250b0
Compare
jordaaash
left a comment
There was a problem hiding this comment.
Looks great, easy to follow, I had only minor feedback.
| /** | ||
| * Layout for a signature | ||
| */ | ||
| export const signature = (property: string = 'signature') => { | ||
| return BufferLayout.blob(64, property); | ||
| }; | ||
|
|
There was a problem hiding this comment.
Should we add this to https://github.com/solana-labs/buffer-layout-utils/blob/master/src/web3.ts and consume it here? Could move most or all of these into it and eliminate some duplication
There was a problem hiding this comment.
I'd be happy with that but I don't feel it's a big priority for simple layouts for signatures and public keys
| accounts: number[]; | ||
| /** The program input data encoded as base 58 */ | ||
| data: string; | ||
| accountKeyIndexes: number[]; |
There was a problem hiding this comment.
Feels like this should be accounts if it represents the same thing as in CompiledInstruction? MessageCompiledInstruction is a bit confusing as a name (it's just a CompiledInstruction with the data raw instead of encoded)
There was a problem hiding this comment.
MessageCompiledInstruction is meant to be the successor to CompiledInstruction. Encoding the data is unnecessary and inefficient so that was the main reason I wanted to move away from CompiledInstruction. The accounts -> accountKeyIndexes isn't as big of a deal but I feel that it's more clear and descriptive.
| ); | ||
| } | ||
|
|
||
| get version(): TransactionVersion { |
There was a problem hiding this comment.
Conceptually feels a little weird that a TransactionVersion is referenced by a Message, instead of say a MessageVersion. While the transaction is versioned, isn't it because the message is versioned rather than the other way around?
There was a problem hiding this comment.
I personally feel that it's valid to talk about the version of a transaction when you really mean the version of a transaction's message. The only thing that makes this a "message version" to me is the need to encode the version in the signed message, but it really refers to the structure of the transaction as a whole. It's likely that in the future, the structure of the signatures portion of the transaction is updated and then versioning won't be specific to the message structure as it appears to be right now since v0 transactions didn't update how signatures are encoded.
| * Common interface for versioned messages and legacy messages | ||
| */ | ||
| export interface VersionedMessage { | ||
| get version(): TransactionVersion; |
There was a problem hiding this comment.
Yeah, MessageVersion makes way more sense to me here. wdyt?
| const transferIxData = encodeData(SYSTEM_INSTRUCTION_LAYOUTS.Transfer, { | ||
| lamports: BigInt(LAMPORTS_PER_SOL), | ||
| }); | ||
| const transaction = new VersionedTransaction( |
There was a problem hiding this comment.
All this looks good. I'm imagining from #27213 that there will later be some higher level interface than using lookup table indexes directly?
There was a problem hiding this comment.
Yup, that higher level interface looks like this:
const txMessage = new TransactionMessage({
instructions,
recentBlockhash,
payerKey,
});
const versionedTx = new VersionedTransaction(
txMessage.compile({
version: 0,
addressLookupTableAccounts,
});
);| writableIndexes: Array<number>; | ||
| readonlyIndexes: Array<number>; |
There was a problem hiding this comment.
I was looking for a way that you could architect this type such that it was impossible to screw it up. As written, you could create malformed data like this:
const matl: MessageAddressTableLookup = {
accountKey: /* ... */,
writableIndexes: [0, 1],
readonlyIndexes: [1, 2],
};If the storage was, instead, implemented as a sparse array, it would be impossible to overlap a readable/writeable index.
type Readable = 0; // or 'r'
type Writeable = 1; // or 'w'
type IndexesWithWriteability = (Readable | Writeable | undefined)[];
const indexes: IndexesWithWriteability = [];
indexes[0] = 1;
indexes[1] = 1;
indexes[1] = 0; // OVERWRITTEN! Note how overlapping index 1 is impossible here.
indexes[2] = 0;Then you'd do stuff like this at the point of serialization:
const readonlyIndexes = [];
const writeableIndexes = [];
indexes.forEach((writeability, index) => {
if (writeability === 0) {
readonlyIndexes.push(index);
} else if (writeability === 1) {
writeableIndexes.push(index);
}
});
const writeableIndexesLength = shortvec.encodeLength(
encodedWriteableIndexesLength,
writeableIndexes.length,
);There was a problem hiding this comment.
Oh yeah great point, it's definitely possible to create invalid transaction messages with the MessageV0 constructor. This MessageAddressTableLookup type directly maps to the serialization format of v0 messages (MessageV0 is just a barebones data class) so it's not meant to be created or handled directly by devs. In the draft PR here you can see that the construction of v0 messages has a high level API MessageV0.compile(..) which will build these lookup data structures internally in a valid way:
solana/web3.js/src/message/v0.ts
Lines 71 to 80 in 25d2566
solana/web3.js/src/message/compiled-keys.ts
Lines 104 to 123 in 25d2566
There was a problem hiding this comment.
Yeah, only thing is that MessageV0 is exported in #27213, so by Murphy's Law someone will definitely eventually create one with hand rolled indices that overlap.
There was a problem hiding this comment.
I think that's fine. The same holds for any other field in MessageV0... staticAccountKeys could have dup keys, instructions could reference invalid account indexes, the header values could be contradictory, etc.
| import {Message} from './legacy'; | ||
| import {MessageV0} from './v0'; |
There was a problem hiding this comment.
It's really stressing me out that we're going to have to hard-require the implementation for every version from here to the end of time, but I don't have anything helpful to suggest at this point :)
I wonder if, at some point, we let people create builds (hard forks? 😅) of web3.js that no longer include old transaction implementations, if their use case is to produce new transactions and not to interpret old ones.
That said, the existence of this module definitely encourages hard-requiring the whole universe. What would you think about getting rid of this, and instead offering a lightweight getSerializedMessageVersion() utility? That way the end user can decide if they want to:
- Hard require every version and switch between them, or
- Fatal/skip if they encounter a version they are not interested in supporting.
There was a problem hiding this comment.
I'm totally in favor of adding a utility to get the serialized message version but I think it's important to provide the VersionedMessage.deserialize utility in addition since anyone deserializing an arbitrary message would need this logic anyways. I think it only makes sense to forgo this utility if you don't need to deserialize messages at all and are only concerned with creating/serializing a particular set of transaction versions. I'm trying to modularize the code that's specific to a particular transaction version so I think we're in decent position for that already.
There was a problem hiding this comment.
Here's a PR to add that utility function you suggested: #27415
Problem
Can't create version 0 transactions with web3
Summary of Changes
VersionedMessagetype which can be used to represent legacy or v0 messagesVersionedTransactionclass which is message version agnostic by usingVersionedMessageMessageV0class to serialize/deserialize v0 messagesMessageCompiledInstructiontype as a successor to theCompiledInstructiontype because it's inefficient and unnecessary to encode instruction data as a base58 stringFixes #