From 805b122e764bbf18de8729d9be844dff21f444dc Mon Sep 17 00:00:00 2001 From: Harold <41681731+yewman@users.noreply.github.com> Date: Thu, 13 Feb 2025 02:35:51 +1100 Subject: [PATCH] refactor(svm): refactor transaction implementation (#513) * Transaction refactor * replace transaction builder * Removed CheckedReader * remove nullability from address lookups * Address PR comments * implement serialize * restore transaction unit tests * missed test case * address comments * Address PR comments * fix type issue * Extract signable component into message * add clone test --- docs/docusaurus/docs/usage/rpc-api-client.mdx | 40 +- src/bincode/benchmarks.zig | 2 +- src/core/entry.zig | 28 +- src/core/lib.zig | 8 +- src/core/transaction.zig | 1152 +++++++---------- src/gossip/data.zig | 2 +- src/ledger/reader.zig | 25 +- src/ledger/tests.zig | 2 +- src/rpc/client.zig | 2 +- .../mock_transfer_generator.zig | 67 +- src/transaction_sender/transaction_info.zig | 2 +- 11 files changed, 566 insertions(+), 764 deletions(-) diff --git a/docs/docusaurus/docs/usage/rpc-api-client.mdx b/docs/docusaurus/docs/usage/rpc-api-client.mdx index 0661037a7..6460a78b3 100644 --- a/docs/docusaurus/docs/usage/rpc-api-client.mdx +++ b/docs/docusaurus/docs/usage/rpc-api-client.mdx @@ -12,7 +12,9 @@ From a string: const Pubkey = @import("sig").core.Pubkey; fn main() !void { - const pubkey = try Pubkey.fromString("4rL4RCWHz3iNCdCaveD8KcHfV9YWGsqSHFPo7X2zBNwa"); + + const pubkey = try Pubkey.parseBase58String("4rL4RCWHz3iNCdCaveD8KcHfV9YWGsqSHFPo7X2zBNwa"); + } ``` @@ -89,7 +91,7 @@ pub fn main() !void { var client = try rpc.Client.init(allocator, .{ .http_endpoint = HTTP_ENDPOINT }); defer client.deinit(); - const pubkey = try Pubkey.fromString("4rL4RCWHz3iNCdCaveD8KcHfV9YWGsqSHFPo7X2zBNwa"); + const pubkey = try Pubkey.parseBase58String("4rL4RCWHz3iNCdCaveD8KcHfV9YWGsqSHFPo7X2zBNwa"); var resp = try client.getAccountInfo(pubkey, .{ .encoding = .Base64 }); defer resp.deinit(); @@ -128,7 +130,7 @@ const Pubkey = sig.core.Pubkey; const allocator = std.heap.page_allocator; pub fn main() !void { - const pubkey = try Pubkey.fromString("4rL4RCWHz3iNCdCaveD8KcHfV9YWGsqSHFPo7X2zBNwa"); + const pubkey = try Pubkey.parseBase58String("4rL4RCWHz3iNCdCaveD8KcHfV9YWGsqSHFPo7X2zBNwa"); var resp = try client.getBalance(pubkey); defer resp.deinit(); @@ -958,10 +960,10 @@ pub fn main() !void { defer client.deinit(); var accounts = [2]Pubkey{ - try Pubkey.fromString( + try Pubkey.parseBase58String( "6dmNQ5jwLeLk5REvio1JcMshcbvkYMwy26sJ8pbkvStu", ) , - try Pubkey.fromString( + try Pubkey.parseBase58String( "BGsqMegLpV6n6Ve146sSX2dTjUMj3M92HnU8BbNRMhF2", ), }; @@ -1280,10 +1282,10 @@ pub fn main() !void { defer client.deinit(); var accounts2 = [2]Pubkey{ - try Pubkey.fromString( + try Pubkey.parseBase58String( "4rL4RCWHz3iNCdCaveD8KcHfV9YWGsqSHFPo7X2zBNwa", ), - try Pubkey.fromString( + try Pubkey.parseBase58String( "BGsqMegLpV6n6Ve146sSX2dTjUMj3M92HnU8BbNRMhF2", ), }; @@ -1344,7 +1346,7 @@ pub fn main() !void { var filters = [1]Filter{.{ .memcmp = .{ .offset = 0, .bytes = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" } }}; var resp = try client.getProgramAccounts( - try Pubkey.fromString("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"), + try Pubkey.parseBase58String("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"), .{ .filters = &filters }, ); defer resp.deinit(); @@ -1480,7 +1482,7 @@ pub fn main() !void { var client = try rpc.Client.init(allocator, .{ .http_endpoint = HTTP_ENDPOINT }); defer client.deinit(); - var resp = try client.getSignaturesForAddress(try Pubkey.fromString("4rL4RCWHz3iNCdCaveD8KcHfV9YWGsqSHFPo7X2zBNwa"), .{ .limit = 10 }); + var resp = try client.getSignaturesForAddress(try Pubkey.parseBase58String("4rL4RCWHz3iNCdCaveD8KcHfV9YWGsqSHFPo7X2zBNwa"), .{ .limit = 10 }); defer resp.deinit(); if (resp.err()) |err| { @@ -1670,7 +1672,7 @@ pub fn main() !void { var client = try rpc.Client.init(allocator, .{ .http_endpoint = HTTP_ENDPOINT }); defer client.deinit(); - var resp = try client.getStakeActivation(try Pubkey.fromString( + var resp = try client.getStakeActivation(try Pubkey.parseBase58String( "CWrKSEDYhj6VHGocZowq2BUncKESqD7rdLTSrsoasTjU", ), .{}); defer resp.deinit(); @@ -1817,7 +1819,7 @@ pub fn main() !void { var client = try rpc.Client.init(allocator, .{ .http_endpoint = HTTP_ENDPOINT }); defer client.deinit(); - var pubkey = try Pubkey.fromString( + var pubkey = try Pubkey.parseBase58String( "6A5NHCj1yF6urc9wZNe6Bcjj4LVszQNj5DwAWG97yzMu", ); var resp = try client.getTokenAccountBalance(pubkey, .{}); @@ -1876,10 +1878,10 @@ pub fn main() !void { var client = try rpc.Client.init(allocator, .{ .http_endpoint = HTTP_ENDPOINT }); defer client.deinit(); - var programPubkey = try Pubkey.fromString( + var programPubkey = try Pubkey.parseBase58String( "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", ); - var pubkey = try Pubkey.fromString( + var pubkey = try Pubkey.parseBase58String( "CTz5UMLQm2SRWHzQnU62Pi4yJqbNGjgRBHqqp6oDHfF7", ); var resp = try client.getTokenAccountsByDelegate(pubkey, .{ .programId = programPubkey }, .{}); @@ -1938,10 +1940,10 @@ pub fn main() !void { var client = try rpc.Client.init(allocator, .{ .http_endpoint = HTTP_ENDPOINT }); defer client.deinit(); - var mintPubkey = try Pubkey.fromString( + var mintPubkey = try Pubkey.parseBase58String( "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", ); - var pubkey = try Pubkey.fromString( + var pubkey = try Pubkey.parseBase58String( "CTz5UMLQm2SRWHzQnU62Pi4yJqbNGjgRBHqqp6oDHfF7", ); var resp = try client.getTokenAccountsByOwner(pubkey, .{ .mint = mintPubkey }, .{}); @@ -1992,7 +1994,7 @@ pub fn main() !void { var client = try rpc.Client.init(allocator, .{ .http_endpoint = HTTP_ENDPOINT }); defer client.deinit(); - var mintPubkey = try Pubkey.fromString( + var mintPubkey = try Pubkey.parseBase58String( "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", ); var resp = try client.getTokenLargestAccounts(mintPubkey, .{}); @@ -2043,7 +2045,7 @@ pub fn main() !void { var client = try rpc.Client.init(allocator, .{ .http_endpoint = HTTP_ENDPOINT }); defer client.deinit(); - var mintPubkey = try Pubkey.fromString( + var mintPubkey = try Pubkey.parseBase58String( "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", ); var resp = try client.getTokenSupply(mintPubkey, .{}); @@ -2237,7 +2239,7 @@ pub fn main() !void { var client = try rpc.Client.init(allocator, .{ .http_endpoint = HTTP_ENDPOINT }); defer client.deinit(); - var votePubkey = try Pubkey.fromString( + var votePubkey = try Pubkey.parseBase58String( "CertusDeBmqN8ZawdkxK5kFGMwBXdudvWHYwtNgNhvLu", ); var resp = try client.getVoteAccounts(.{ .votePubkey = votePubkey }); @@ -2376,7 +2378,7 @@ pub fn main() !void { var client = try rpc.Client.init(allocator, .{ .http_endpoint = HTTP_ENDPOINT }); defer client.deinit(); - var pubkey = try Pubkey.fromString( + var pubkey = try Pubkey.parseBase58String( "Bvg7GuhqwNmV2JVyeZjhAcTPFqPktfmq25VBaZipozda", ); var resp = try client.requestAirdrop(pubkey, 10000, .{}); diff --git a/src/bincode/benchmarks.zig b/src/bincode/benchmarks.zig index 9e29db542..b469826b2 100644 --- a/src/bincode/benchmarks.zig +++ b/src/bincode/benchmarks.zig @@ -24,7 +24,7 @@ pub const BenchmarkEntry = struct { const actual_struct = try sig.bincode.readFromSlice( allocator, Entry, - &test_entry.bincode_serialized_bytes, + &test_entry.as_bytes, .{}, ); defer actual_struct.deinit(allocator); diff --git a/src/core/entry.zig b/src/core/entry.zig index 2fe140812..a7bba8820 100644 --- a/src/core/entry.zig +++ b/src/core/entry.zig @@ -1,40 +1,45 @@ pub const std = @import("std"); pub const sig = @import("../sig.zig"); -pub const core = @import("lib.zig"); + +const Hash = sig.core.hash.Hash; +const Transaction = sig.core.transaction.Transaction; pub const Entry = struct { /// The number of hashes since the previous Entry ID. num_hashes: u64, /// The SHA-256 hash `num_hashes` after the previous Entry ID. - hash: core.Hash, + hash: Hash, /// An unordered list of transactions that were observed before the Entry ID was /// generated. They may have been observed before a previous Entry ID but were /// pushed back into this list to ensure deterministic interpretation of the ledger. - transactions: std.ArrayListUnmanaged(core.VersionedTransaction), + transactions: std.ArrayListUnmanaged(Transaction), pub fn isTick(self: Entry) bool { return self.transactions.items.len == 0; } pub fn deinit(self: Entry, allocator: std.mem.Allocator) void { - for (self.transactions.items) |tx| { - tx.deinit(allocator); - } + for (self.transactions.items) |tx| tx.deinit(allocator); allocator.free(self.transactions.allocatedSlice()); } }; test "Entry serialization and deserialization" { const entry = test_entry.as_struct; - try sig.bincode.testRoundTrip(entry, &test_entry.bincode_serialized_bytes); + try sig.bincode.testRoundTrip(entry, &test_entry.as_bytes); } pub const test_entry = struct { + var txns = [_]Transaction{ + sig.core.transaction.transaction_v0_example.as_struct, + sig.core.transaction.transaction_v0_example.as_struct, + }; + pub const as_struct = Entry{ .num_hashes = 149218308, - .hash = core.Hash + .hash = sig.core.Hash .parseBase58String("G8T3smgLc4XavAtxScD3u4FTAqPtwbFCEJKwJbfoECcd") catch unreachable, .transactions = .{ .items = txns[0..2], @@ -42,12 +47,7 @@ pub const test_entry = struct { }, }; - var txns = [_]core.VersionedTransaction{ - core.transaction.test_v0_transaction.as_struct, - core.transaction.test_v0_transaction.as_struct, - }; - - pub const bincode_serialized_bytes = [_]u8{ + pub const as_bytes = [_]u8{ 4, 228, 228, 8, 0, 0, 0, 0, 224, 199, 210, 235, 148, 143, 98, 241, 248, 45, 140, 115, 214, 164, 132, 17, 95, 89, 221, 166, 5, 158, 5, 121, 181, 80, 48, 103, 173, 21, 40, 70, 2, 0, 0, 0, 0, 0, 0, 0, 2, 81, 7, 106, 50, 99, diff --git a/src/core/lib.zig b/src/core/lib.zig index 7f0e6ad3b..b84227f61 100644 --- a/src/core/lib.zig +++ b/src/core/lib.zig @@ -22,15 +22,9 @@ pub const Nonce = shred.Nonce; pub const Pubkey = pubkey.Pubkey; pub const ShredVersion = shred.ShredVersion; pub const Signature = signature.Signature; +pub const Transaction = transaction.Transaction; pub const Epoch = time.Epoch; pub const Slot = time.Slot; -pub const CompiledInstruction = transaction.CompiledInstruction; -pub const Message = transaction.Message; -pub const MessageHeader = transaction.MessageHeader; -pub const Transaction = transaction.Transaction; -pub const VersionedTransaction = transaction.VersionedTransaction; -pub const V0Message = transaction.V0Message; - pub const Cluster = enum { mainnet, testnet, devnet, localnet }; diff --git a/src/core/transaction.zig b/src/core/transaction.zig index be3e204e6..7ee3560d5 100644 --- a/src/core/transaction.zig +++ b/src/core/transaction.zig @@ -1,105 +1,136 @@ const std = @import("std"); const sig = @import("../sig.zig"); -const Ed25519 = std.crypto.sign.Ed25519; -const KeyPair = Ed25519.KeyPair; + +const leb = std.leb; const Hash = sig.core.Hash; const Pubkey = sig.core.Pubkey; const Signature = sig.core.Signature; -const indexOf = sig.utils.slice.indexOf; -const peekableReader = sig.utils.io.peekableReader; const shortVecConfig = sig.bincode.shortvec.sliceConfig; -pub const VersionedTransaction = struct { - signatures: []const Signature, - message: VersionedMessage, +pub const Transaction = struct { + /// Signatures + signatures: []Signature, - pub const @"!bincode-config:signatures" = shortVecConfig([]const Signature); + /// The version, either legacy or v0. + version: TransactionVersion, - pub fn deinit(self: VersionedTransaction, allocator: std.mem.Allocator) void { - allocator.free(self.signatures); - self.message.deinit(allocator); - } + /// The signable data of a transaction + msg: TransactionMessage, - pub fn sanitize(self: VersionedTransaction) !void { - switch (self.message) { - inline .legacy, .v0 => |m| try m.sanitize(), - } + /// MAX_BYTES is the maximum size of a transaction. + pub const MAX_BYTES: u32 = 1232; + + /// MAX_SIGNATURES is the maximum number of signatures that can be applied to a transaction. + pub const MAX_SIGNATURES: u8 = 127; + + /// MAX_ACCOUNTS is the maximum number of accounts that can be loaded by a transaction. + pub const MAX_ACCOUNTS: u16 = 128; + + /// MAX_INSTRUCTIONS is the maximum number of instructions that can be executed by a transaction. + pub const MAX_INSTRUCTIONS: u8 = 64; + + /// MAX_ADDRESS_LOOKUP_TABLES is the maximum number of address lookup tables that can be used by a transaction. + pub const MAX_ADDRESS_LOOKUP_TABLES: u16 = 127; + + /// VERSION_PREFIX is used to differentiate between legacy and versioned transactions. If the first byte after the + /// signatures has its high bit set, then the transaction is versioned and the remaining bits represent the version. + /// Otherwise, the transaction is legacy and the first byte after the signatures is the first byte of the message. + pub const VERSION_PREFIX: u8 = 0x80; + + pub const @"!bincode-config": sig.bincode.FieldConfig(Transaction) = .{ + .deserializer = deserialize, + .serializer = serialize, + }; + + pub const EMPTY = Transaction{ + .signatures = &.{}, + .version = .legacy, + .msg = .{ + .signature_count = 0, + .readonly_signed_count = 0, + .readonly_unsigned_count = 0, + .account_keys = &.{}, + .recent_blockhash = .{ .data = [_]u8{0x00} ** Hash.SIZE }, + .instructions = &.{}, + .address_lookups = &.{}, + }, + }; + + pub fn deinit(self: Transaction, allocator: std.mem.Allocator) void { + sig.bincode.free(allocator, self); } -}; -const VersionedMessage = union(enum) { - legacy: Message, - v0: V0Message, + pub fn clone(self: Transaction, allocator: std.mem.Allocator) !Transaction { + const signatures = try allocator.dupe(Signature, self.signatures); + errdefer allocator.free(signatures); + return .{ + .signatures = signatures, + .version = self.version, + .msg = try self.msg.clone(allocator), + }; + } - pub fn deinit(self: VersionedMessage, allocator: std.mem.Allocator) void { - switch (self) { - inline .legacy, .v0 => |m| m.deinit(allocator), - } + pub fn serialize(writer: anytype, data: anytype, _: sig.bincode.Params) !void { + try leb.writeULEB128(writer, @as(u16, @truncate(data.signatures.len))); + for (data.signatures) |sgn| try writer.writeAll(&sgn.data); + try data.version.serialize(writer); + try data.msg.serialize(writer, data.version); } - pub fn accountKeys(self: VersionedMessage) []const Pubkey { - return switch (self) { - inline .legacy, .v0 => |m| m.account_keys, + pub fn deserialize(allocator: std.mem.Allocator, reader: anytype, _: sig.bincode.Params) !Transaction { + const signatures = try allocator.alloc(Signature, try leb.readULEB128(u16, reader)); + for (signatures) |*sgn| sgn.* = .{ .data = try reader.readBytesNoEof(Signature.SIZE) }; + var peekable = sig.utils.io.peekableReader(reader); + const version = try TransactionVersion.deserialize(&peekable); + return .{ + .signatures = signatures, + .version = version, + .msg = try TransactionMessage.deserialize(allocator, peekable.reader(), version), }; } - pub const @"!bincode-config" = sig.bincode.FieldConfig(VersionedMessage){ - .serializer = bincode_config.serialize, - .deserializer = bincode_config.deserialize, - .free = bincode_config.free, - }; + pub fn validate(self: Transaction) !void { + try self.msg.validate(); + } +}; - const bincode_config = struct { - /// Bit mask that indicates whether a serialized message is versioned. - const MESSAGE_VERSION_PREFIX: u8 = 0x80; +pub const TransactionVersion = enum(u8) { + /// Legacy transaction without address lookups. + legacy = 0xFF, + /// Transaction with address lookups. + v0 = 0x00, - fn serialize(writer: anytype, data: anytype, params: sig.bincode.Params) !void { - const self: VersionedMessage = data; - switch (self) { - .legacy => |msg| { - try sig.bincode.write(writer, msg, params); - }, - .v0 => |msg| { - try writer.writeByte(0x80); - try sig.bincode.write(writer, msg, params); - }, - } - } + pub fn serialize(self: TransactionVersion, writer: anytype) !void { + if (self != .legacy) + try writer.writeByte(Transaction.VERSION_PREFIX | @intFromEnum(self)); + } - fn deserialize( - allocator: std.mem.Allocator, - original_reader: anytype, - params: sig.bincode.Params, - ) !VersionedMessage { - var peekable = peekableReader(original_reader); - const reader = peekable.reader(); - - if (try peekable.peekByte() & MESSAGE_VERSION_PREFIX != 0) { - return switch (try reader.readByte() & ~MESSAGE_VERSION_PREFIX) { - 0 => .{ .v0 = try sig.bincode.read(allocator, V0Message, reader, params) }, - 127 => error.OffChainMessage, - else => error.InvalidMessageTag, - }; - } else { - return .{ .legacy = try sig.bincode.read(allocator, Message, reader, params) }; - } - } + pub fn deserialize(peekable: anytype) !TransactionVersion { + if (try peekable.peekByte() & Transaction.VERSION_PREFIX == 0) + return TransactionVersion.legacy; - fn free(allocator: std.mem.Allocator, data: anytype) void { - VersionedMessage.deinit(data, allocator); - } - }; + const version = try peekable.reader().readByte(); + return switch (version & ~Transaction.VERSION_PREFIX) { + 0 => .v0, + 127 => error.OffChain, + else => error.InvalidVersion, + }; + } }; -pub const V0Message = struct { - /// The message header, identifying signed and read-only `account_keys`. - /// Header values only describe static `account_keys`, they do not describe - /// any additional account keys loaded via address table lookups. - header: MessageHeader, - - /// List of accounts loaded by this transaction. +pub const TransactionMessage = struct { + /// The number of signatures required for this transaction to be considered + /// valid. The signers of those signatures must match the first + /// `signature_count` of `account_keys`. + signature_count: u8, + /// The last `readonly_signed_count` of the signed account keys are read-only accounts. + readonly_signed_count: u8, + /// The last `readonly_unsigned_count` of the unsigned account keys are read-only accounts. + readonly_unsigned_count: u8, + + /// Addresses of accounts loaded by this transaction. account_keys: []const Pubkey, /// The blockhash of a recent block. @@ -110,521 +141,388 @@ pub const V0Message = struct { /// /// # Notes /// - /// Program indexes must index into the list of message `account_keys` because - /// program id's cannot be dynamically loaded from a lookup table. + /// Program indexes must index into the list of `account_keys` because + /// program addresses cannot be dynamically loaded from a lookup table. /// - /// Account indexes must index into the list of addresses - /// constructed from the concatenation of three key lists: - /// 1) message `account_keys` - /// 2) ordered list of keys loaded from `writable` lookup table indexes - /// 3) ordered list of keys loaded from `readable` lookup table indexes - instructions: []const CompiledInstruction, - - /// List of address table lookups used to load additional accounts - /// for this transaction. - address_table_lookups: []const MessageAddressTableLookup, - - pub const @"!bincode-config:account_keys" = shortVecConfig([]const Pubkey); - pub const @"!bincode-config:instructions" = shortVecConfig([]const CompiledInstruction); - pub const @"!bincode-config:address_table_lookups" = shortVecConfig([]const MessageAddressTableLookup); - - pub fn deinit(self: V0Message, allocator: std.mem.Allocator) void { - inline for (.{ self.instructions, self.address_table_lookups }) |slice| { - for (slice) |item| { - item.deinit(allocator); - } - } - allocator.free(self.account_keys); - allocator.free(self.instructions); - allocator.free(self.address_table_lookups); - } - - pub fn sanitize(_: V0Message) !void { - // TODO - std.debug.print("V0Message.sanitize not implemented", .{}); + /// Account indexes must index into the list of account keys + /// constructed from the concatenation of three address lists: + /// 1) `account_keys` + /// 2) ordered list of account_keys loaded from `writable` lookup table indexes + /// 3) ordered list of account_keys loaded from `readable` lookup table indexes + instructions: []const TransactionInstruction, + + /// `AddressLookup`'s are used to load account addresses from lookup tables. + address_lookups: []const TransactionAddressLookup = &.{}, + + pub fn deinit(self: TransactionMessage, allocator: std.mem.Allocator) void { + sig.bincode.free(allocator, self); } - pub fn addressTableLookups(self: V0Message) ?[]MessageAddressTableLookup { - switch (self) { - .legacy => null, - .v0 => |m| m.address_table_lookups, - } - } -}; - -pub const MessageAddressTableLookup = struct { - /// Address lookup table account key - account_key: Pubkey, - /// List of indexes used to load writable account addresses - writable_indexes: []const u8, - /// List of indexes used to load readonly account addresses - readonly_indexes: []const u8, - - pub const @"!bincode-config:writable_indexes" = shortVecConfig([]const u8); - pub const @"!bincode-config:readonly_indexes" = shortVecConfig([]const u8); - - pub fn deinit(self: MessageAddressTableLookup, allocator: std.mem.Allocator) void { - allocator.free(self.writable_indexes); - allocator.free(self.readonly_indexes); - } -}; - -pub const Transaction = struct { - signatures: []const Signature, - message: Message, + pub fn clone(self: TransactionMessage, allocator: std.mem.Allocator) !TransactionMessage { + const account_keys = try allocator.dupe(Pubkey, self.account_keys); + errdefer sig.bincode.free(allocator, account_keys); - pub const @"!bincode-config:signatures" = shortVecConfig([]const Signature); + var instructions = try allocator.alloc(TransactionInstruction, self.instructions.len); + errdefer sig.bincode.free(allocator, instructions); + for (self.instructions, 0..) |instr, i| + instructions[i] = try instr.clone(allocator); - pub const MAX_BYTES: usize = 1232; + const address_lookups = try allocator.alloc(TransactionAddressLookup, self.address_lookups.len); + errdefer sig.bincode.free(allocator, address_lookups); + for (address_lookups, 0..) |*alt, i| + alt.* = try self.address_lookups[i].clone(allocator); - pub const EMPTY: Transaction = .{ - .signatures = &.{}, - .message = Message.EMPTY, - }; - - pub fn newUnsigned(allocator: std.mem.Allocator, message: Message) error{OutOfMemory}!Transaction { - return Transaction{ - .signatures = try allocator.alloc(Signature, message.header.num_required_signatures), - .message = message, - }; - } - - pub fn clone(self: *const Transaction, allocator: std.mem.Allocator) error{OutOfMemory}!Transaction { return .{ - .signatures = try allocator.dupe(Signature, self.signatures), - .message = try self.message.clone(allocator), + .signature_count = self.signature_count, + .readonly_signed_count = self.readonly_signed_count, + .readonly_unsigned_count = self.readonly_unsigned_count, + .account_keys = account_keys, + .recent_blockhash = self.recent_blockhash, + .instructions = instructions, + .address_lookups = address_lookups, }; } - pub fn deinit(self: *const Transaction, allocator: std.mem.Allocator) void { - allocator.free(self.signatures); - self.message.deinit(allocator); - } + pub fn serialize(self: TransactionMessage, writer: anytype, version: TransactionVersion) !void { + try writer.writeByte(self.signature_count); + try writer.writeByte(self.readonly_signed_count); + try writer.writeByte(self.readonly_unsigned_count); - pub fn sanitize(self: *const Transaction) !void { - const num_required_sigs = self.message.header.num_required_signatures; - const num_signatures = self.signatures.len; - if (num_required_sigs > num_signatures) { - return error.InsufficientSignatures; - } + // WARN: Truncate okay if transaction is valid + try leb.writeULEB128(writer, @as(u16, @truncate(self.account_keys.len))); + for (self.account_keys) |id| try writer.writeAll(&id.data); - const num_account_keys = self.message.account_keys.len; - if (num_signatures > num_account_keys) { - return error.TooManySignatures; - } - try self.message.sanitize(); - } -}; + try writer.writeAll(&self.recent_blockhash.data); -pub const Message = struct { - header: MessageHeader, - account_keys: []const Pubkey, - recent_blockhash: Hash, - instructions: []const CompiledInstruction, + // WARN: Truncate okay if transaction is valid + try leb.writeULEB128(writer, @as(u16, @truncate(self.instructions.len))); + for (self.instructions) |instr| try sig.bincode.write(writer, instr, .{}); - pub const @"!bincode-config:account_keys" = shortVecConfig([]const Pubkey); - pub const @"!bincode-config:instructions" = shortVecConfig([]const CompiledInstruction); + // WARN: Truncate okay if transaction is valid + if (version != TransactionVersion.legacy) { + try leb.writeULEB128(writer, @as(u16, @truncate(self.address_lookups.len))); + for (self.address_lookups) |alt| try sig.bincode.write(writer, alt, .{}); + } + } - pub const EMPTY: Message = .{ - .header = .{ - .num_required_signatures = 0, - .num_readonly_signed_accounts = 0, - .num_readonly_unsigned_accounts = 0, - }, - .account_keys = &.{}, - .recent_blockhash = blk: { - @setEvalBranchQuota(1962); - break :blk Hash.generateSha256Hash(&.{0}); - }, - .instructions = &.{}, - }; + pub fn deserialize(allocator: std.mem.Allocator, reader: anytype, version: TransactionVersion) !TransactionMessage { + const signature_count = try reader.readByte(); + const readonly_signed_count = try reader.readByte(); + const readonly_unsigned_count = try reader.readByte(); - pub fn init( - allocator: std.mem.Allocator, - instructions: []const Instruction, - payer: Pubkey, - recent_blockhash: Hash, - ) !Message { - var compiled_keys = try CompiledKeys.init(allocator, instructions, payer); - defer compiled_keys.deinit(); - const header, const account_keys = try compiled_keys.intoMessageHeaderAndAccountKeys(allocator); - const compiled_instructions = try compileInstructions(allocator, instructions, account_keys); - return .{ - .header = header, - .account_keys = account_keys, - .recent_blockhash = recent_blockhash, - .instructions = compiled_instructions, - }; - } + const account_keys = try allocator.alloc(Pubkey, try leb.readULEB128(u16, reader)); + errdefer sig.bincode.free(allocator, account_keys); + for (account_keys) |*id| id.* = .{ .data = try reader.readBytesNoEof(Pubkey.SIZE) }; - pub fn clone(self: *const Message, allocator: std.mem.Allocator) error{OutOfMemory}!Message { - const account_keys = try allocator.dupe(Pubkey, self.account_keys); - errdefer allocator.free(account_keys); + const recent_blockhash: Hash = .{ .data = try reader.readBytesNoEof(Hash.SIZE) }; - const instructions = try allocator.alloc(CompiledInstruction, self.instructions.len); - errdefer allocator.free(instructions); + const instructions = try allocator.alloc(TransactionInstruction, try leb.readULEB128(u16, reader)); + errdefer sig.bincode.free(allocator, instructions); + for (instructions) |*instr| instr.* = try sig.bincode.read(allocator, TransactionInstruction, reader, .{}); - for (instructions, self.instructions, 0..) |*ci, original_ci, i| { - errdefer for (instructions[0..i]) |prev_ci| prev_ci.deinit(allocator); - ci.* = try original_ci.clone(allocator); - } - errdefer comptime unreachable; // otherwise we have to remember to free each instruction + const address_lookups_len = if (version == .legacy) 0 else try leb.readULEB128(u16, reader); + const address_lookups = try allocator.alloc(TransactionAddressLookup, address_lookups_len); + errdefer sig.bincode.free(allocator, address_lookups); + for (address_lookups) |*alt| alt.* = try sig.bincode.read(allocator, TransactionAddressLookup, reader, .{}); return .{ - .header = self.header, + .signature_count = signature_count, + .readonly_signed_count = readonly_signed_count, + .readonly_unsigned_count = readonly_unsigned_count, .account_keys = account_keys, - .recent_blockhash = self.recent_blockhash, + .recent_blockhash = recent_blockhash, .instructions = instructions, + .address_lookups = address_lookups, }; } - pub fn deinit(self: *const Message, allocator: std.mem.Allocator) void { - allocator.free(self.account_keys); - for (self.instructions) |*ci| ci.deinit(allocator); - allocator.free(self.instructions); - } - - pub const MessageSanitizeError = error{ - NotEnoughAccounts, - MissingWritableFeePayer, - ProgramIdAccountMissing, - ProgramIdCannotBePayer, - AccountIndexOutOfBounds, - }; - - pub fn sanitize(self: *const Message) MessageSanitizeError!void { + pub fn validate(self: TransactionMessage) !void { // number of accounts should match spec in header. signed and unsigned should not overlap. - if (self.header.num_required_signatures +| self.header.num_readonly_unsigned_accounts > self.account_keys.len) { + if (self.signature_count +| self.readonly_unsigned_count > self.account_keys.len) return error.NotEnoughAccounts; - } + // there should be at least 1 RW fee-payer account. - if (self.header.num_readonly_signed_accounts >= self.header.num_required_signatures) { + if (self.readonly_signed_count >= self.signature_count) return error.MissingWritableFeePayer; - } - for (self.instructions) |ci| { - if (ci.program_id_index >= self.account_keys.len) { + for (self.instructions) |ti| { + if (ti.program_index >= self.account_keys.len) return error.ProgramIdAccountMissing; - } + // A program cannot be a payer. - if (ci.program_id_index == 0) { + if (ti.program_index == 0) return error.ProgramIdCannotBePayer; - } - for (ci.accounts) |ai| { - if (ai >= self.account_keys.len) { + + for (ti.account_indexes) |ai| { + if (ai >= self.account_keys.len) return error.AccountIndexOutOfBounds; - } } } } }; -pub const MessageHeader = struct { - /// The number of signatures required for this message to be considered - /// valid. The signers of those signatures must match the first - /// `num_required_signatures` of [`Message::account_keys`]. - // NOTE: Serialization-related changes must be paired with the direct read at sigverify. - num_required_signatures: u8, - - /// The last `num_readonly_signed_accounts` of the signed keys are read-only - /// accounts. - num_readonly_signed_accounts: u8, - - /// The last `num_readonly_unsigned_accounts` of the unsigned keys are - /// read-only accounts. - num_readonly_unsigned_accounts: u8, -}; - -pub const Instruction = struct { - program_id: Pubkey, - accounts: []AccountMeta, - data: []u8, - - pub fn initSystemInstruction( - allocator: std.mem.Allocator, - data: SystemInstruction, - accounts: []AccountMeta, - ) !Instruction { - return .{ - .program_id = SYSTEM_PROGRAM_ID, - .accounts = accounts, - .data = try sig.bincode.writeAlloc(allocator, data, .{}), - }; - } - - pub fn deinit(self: *const Instruction, allocator: std.mem.Allocator) void { - allocator.free(self.accounts); - allocator.free(self.data); - } -}; - -pub const CompiledInstruction = struct { - /// Index into the transaction keys array indicating the program account that executes this instruction. - program_id_index: u8, - /// Ordered indices into the transaction keys array indicating which accounts to pass to the program. - accounts: []const u8, - /// The program input data. +pub const TransactionInstruction = struct { + /// Index into the transactions account_keys array + program_index: u8, + /// Index into the concatenation of the transactions account_keys array, + /// writable lookup results, and readable lookup results + account_indexes: []const u8, + /// Serialized program instruction. data: []const u8, - pub const @"!bincode-config:accounts" = shortVecConfig([]const u8); + pub const @"!bincode-config:account_indexes" = shortVecConfig([]const u8); pub const @"!bincode-config:data" = shortVecConfig([]const u8); - pub fn clone(self: *const CompiledInstruction, allocator: std.mem.Allocator) error{OutOfMemory}!CompiledInstruction { - return .{ - .program_id_index = self.program_id_index, - .accounts = try allocator.dupe(u8, self.accounts), - .data = try allocator.dupe(u8, self.data), - }; - } - - pub fn deinit(self: *const CompiledInstruction, allocator: std.mem.Allocator) void { - allocator.free(self.accounts); - allocator.free(self.data); - } -}; - -pub const AccountMeta = struct { - pubkey: Pubkey, - is_signer: bool, - is_writable: bool, - - pub fn newMutable(pubkey: Pubkey, is_signer: bool) AccountMeta { - return .{ - .pubkey = pubkey, - .is_signer = is_signer, - .is_writable = true, - }; + pub fn deinit(self: TransactionInstruction, allocator: std.mem.Allocator) void { + sig.bincode.free(allocator, self); } - pub fn newImmutable(pubkey: Pubkey, is_signer: bool) AccountMeta { + pub fn clone(self: *const TransactionInstruction, allocator: std.mem.Allocator) !TransactionInstruction { + const account_indexes = try allocator.dupe(u8, self.account_indexes); + errdefer allocator.free(account_indexes); return .{ - .pubkey = pubkey, - .is_signer = is_signer, - .is_writable = false, + .program_index = self.program_index, + .account_indexes = account_indexes, + .data = try allocator.dupe(u8, self.data), }; } }; -pub const CompiledKeys = struct { - maybe_payer: ?Pubkey, - key_meta_map: std.AutoArrayHashMap(Pubkey, CompiledKeyMeta), - - pub fn init(allocator: std.mem.Allocator, instructions: []const Instruction, maybe_payer: ?Pubkey) !CompiledKeys { - var key_meta_map = std.AutoArrayHashMap(Pubkey, CompiledKeyMeta).init(allocator); - for (instructions) |instruction| { - const instruction_meta_gopr = try key_meta_map.getOrPut(instruction.program_id); - if (!instruction_meta_gopr.found_existing) { - instruction_meta_gopr.value_ptr.* = CompiledKeyMeta.ALL_FALSE; - } - instruction_meta_gopr.value_ptr.*.is_invoked = true; - - for (instruction.accounts) |account_meta| { - const account_meta_gopr = try key_meta_map.getOrPut(account_meta.pubkey); - if (!account_meta_gopr.found_existing) { - account_meta_gopr.value_ptr.* = CompiledKeyMeta.ALL_FALSE; - } - account_meta_gopr.value_ptr.is_signer = account_meta_gopr.value_ptr.is_signer or - account_meta.is_signer; - account_meta_gopr.value_ptr.is_writable = account_meta_gopr.value_ptr.is_writable or - account_meta.is_writable; - } +pub const TransactionAddressLookup = struct { + /// Address of the lookup table + table_address: Pubkey, + /// List of indexes used to load writable account ids + writable_indexes: []const u8, + /// List of indexes used to load readonly account ids + readonly_indexes: []const u8, - if (maybe_payer) |payer| { - const payer_meta_gopr = try key_meta_map.getOrPut(payer); - if (!payer_meta_gopr.found_existing) { - payer_meta_gopr.value_ptr.* = CompiledKeyMeta.ALL_FALSE; - } - payer_meta_gopr.value_ptr.is_signer = true; - payer_meta_gopr.value_ptr.is_writable = true; - } - } - return .{ .maybe_payer = maybe_payer, .key_meta_map = key_meta_map }; - } + pub const @"!bincode-config:writable_indexes" = shortVecConfig([]const u8); + pub const @"!bincode-config:readonly_indexes" = shortVecConfig([]const u8); - pub fn deinit(self: *CompiledKeys) void { - self.key_meta_map.deinit(); + pub fn deinit(self: TransactionAddressLookup, allocator: std.mem.Allocator) void { + sig.bincode.free(allocator, self); } - /// Creates message header and account keys from the compiled keys. - /// Account keys memory is allocated and owned by the caller. - pub fn intoMessageHeaderAndAccountKeys( - self: *CompiledKeys, - allocator: std.mem.Allocator, - ) !struct { MessageHeader, []Pubkey } { - const account_keys_buf = try allocator.alloc(Pubkey, self.key_meta_map.count() - - @intFromBool(self.maybe_payer == null)); - errdefer allocator.free(account_keys_buf); - var account_keys = std.ArrayListUnmanaged(Pubkey).initBuffer(account_keys_buf); - - var writable_signers_end: usize = 0; - var readonly_signers_end: usize = 0; - var writable_non_signers_end: usize = 0; - - if (self.maybe_payer) |payer| { - _ = self.key_meta_map.swapRemove(payer); - account_keys.insertAssumeCapacity(writable_signers_end, payer); - writable_signers_end += 1; - readonly_signers_end += 1; - writable_non_signers_end += 1; - } - - for (self.key_meta_map.keys(), self.key_meta_map.values()) |key, meta| { - if (meta.is_signer and meta.is_writable) { - account_keys.insertAssumeCapacity(writable_signers_end, key); - writable_signers_end += 1; - readonly_signers_end += 1; - writable_non_signers_end += 1; - } else if (meta.is_signer and !meta.is_writable) { - account_keys.insertAssumeCapacity(readonly_signers_end, key); - readonly_signers_end += 1; - writable_non_signers_end += 1; - } else if (!meta.is_signer and meta.is_writable) { - account_keys.insertAssumeCapacity(writable_non_signers_end, key); - writable_non_signers_end += 1; - } else if (!meta.is_signer and !meta.is_writable) { - account_keys.appendAssumeCapacity(key); - } else unreachable; - } - - std.debug.assert(account_keys.items.len == account_keys_buf.len); - - const header = MessageHeader{ - .num_required_signatures = @intCast(readonly_signers_end), - .num_readonly_signed_accounts = @intCast(readonly_signers_end - writable_signers_end), - .num_readonly_unsigned_accounts = @intCast(account_keys.items.len - writable_non_signers_end), + pub fn clone(self: *const TransactionAddressLookup, allocator: std.mem.Allocator) !TransactionAddressLookup { + const writable_indexes = try allocator.dupe(u8, self.writable_indexes); + errdefer allocator.free(writable_indexes); + return .{ + .table_address = self.table_address, + .writable_indexes = writable_indexes, + .readonly_indexes = try allocator.dupe(u8, self.readonly_indexes), }; - - return .{ header, account_keys_buf }; } }; -pub const CompiledKeyMeta = packed struct { - is_signer: bool, - is_writable: bool, - is_invoked: bool, - - pub const ALL_FALSE: CompiledKeyMeta = .{ - .is_signer = false, - .is_writable = false, - .is_invoked = false, +test "clone transaction" { + const allocator = std.testing.allocator; + const transaction = Transaction{ + .signatures = &.{}, + .version = .legacy, + .msg = .{ + .signature_count = 1, + .readonly_signed_count = 0, + .readonly_unsigned_count = 0, + .account_keys = &.{Pubkey.ZEROES}, + .recent_blockhash = Hash.ZEROES, + .instructions = &.{}, + .address_lookups = &.{}, + }, }; -}; - -pub const CompileError = error{ - AccountIndexOverflow, - AddressTableLookupIndexOverflow, - UnknownInstructionKey, -}; -const SYSTEM_PROGRAM_ID = Pubkey.ZEROES; + const clone = try transaction.clone(allocator); + defer clone.deinit(allocator); + + try std.testing.expectEqual(transaction.signatures.len, clone.signatures.len); + try std.testing.expectEqual(transaction.version, clone.version); + try std.testing.expectEqual(transaction.msg.signature_count, clone.msg.signature_count); + try std.testing.expectEqual(transaction.msg.readonly_signed_count, clone.msg.readonly_signed_count); + try std.testing.expectEqual(transaction.msg.readonly_unsigned_count, clone.msg.readonly_unsigned_count); + try std.testing.expectEqual(transaction.msg.account_keys.len, clone.msg.account_keys.len); + try std.testing.expectEqual(transaction.msg.recent_blockhash, clone.msg.recent_blockhash); + try std.testing.expectEqual(transaction.msg.instructions.len, clone.msg.instructions.len); + try std.testing.expectEqual(transaction.msg.address_lookups.len, clone.msg.address_lookups.len); +} -const SystemInstruction = union(enum(u8)) { - CreateAccount, - Assign, - Transfer: struct { - lamports: u64, - }, -}; +test "sanitize succeeds minimal valid transaction" { + const transaction = Transaction{ + .signatures = &.{}, + .version = .legacy, + .msg = .{ + .signature_count = 1, + .readonly_signed_count = 0, + .readonly_unsigned_count = 0, + .account_keys = &.{Pubkey.ZEROES}, + .recent_blockhash = Hash.ZEROES, + .instructions = &.{}, + .address_lookups = &.{}, + }, + }; + try std.testing.expectEqual({}, transaction.validate()); +} -pub fn buildTransferTansaction( - allocator: std.mem.Allocator, - random: std.Random, - from_keypair: KeyPair, - to_pubkey: Pubkey, - lamports: u64, - recent_blockhash: Hash, -) !Transaction { - const from_pubkey = Pubkey.fromPublicKey(&from_keypair.public_key); - const transfer_instruction = try transfer( - allocator, - from_pubkey, - to_pubkey, - lamports, - ); - defer transfer_instruction.deinit(allocator); - const instructions = [_]Instruction{transfer_instruction}; +test "sanitize fails empty transaction" { + try std.testing.expectError(error.MissingWritableFeePayer, Transaction.EMPTY.validate()); +} - const message = try Message.init(allocator, &instructions, from_pubkey, recent_blockhash); - const message_bytes = try sig.bincode.writeAlloc(allocator, message, .{}); - defer allocator.free(message_bytes); +test "sanitize fails missing signers" { + const transaction = Transaction{ + .signatures = &.{}, + .version = .legacy, + .msg = .{ + .signature_count = 2, + .readonly_signed_count = 0, + .readonly_unsigned_count = 0, + .account_keys = &.{Pubkey.ZEROES}, + .recent_blockhash = Hash.ZEROES, + .instructions = &.{}, + .address_lookups = &.{}, + }, + }; + try std.testing.expectEqual(error.NotEnoughAccounts, transaction.validate()); +} - var signatures = try allocator.alloc(Signature, 1); - var noise: [KeyPair.seed_length]u8 = undefined; - random.bytes(noise[0..]); - signatures[0] = .{ .data = (try from_keypair.sign(message_bytes, noise)).toBytes() }; +test "sanitize fails missing unsigned" { + const transaction = Transaction{ + .signatures = &.{}, + .version = .legacy, + .msg = .{ + .signature_count = 1, + .readonly_signed_count = 0, + .readonly_unsigned_count = 1, + .account_keys = &.{Pubkey.ZEROES}, + .recent_blockhash = Hash.ZEROES, + .instructions = &.{}, + .address_lookups = &.{}, + }, + }; + try std.testing.expectEqual(error.NotEnoughAccounts, transaction.validate()); +} - return .{ - .signatures = signatures, - .message = message, +test "sanitize fails no writable signed" { + const transaction = Transaction{ + .signatures = &.{}, + .version = .legacy, + .msg = .{ + .signature_count = 1, + .readonly_signed_count = 1, + .readonly_unsigned_count = 0, + .account_keys = &.{ Pubkey.ZEROES, Pubkey.ZEROES }, + .recent_blockhash = Hash.ZEROES, + .instructions = &.{}, + .address_lookups = &.{}, + }, }; + try std.testing.expectEqual(error.MissingWritableFeePayer, transaction.validate()); } -pub fn transfer( - allocator: std.mem.Allocator, - from_pubkey: Pubkey, - to_pubkey: Pubkey, - lamports: u64, -) !Instruction { - var account_metas = try allocator.alloc(AccountMeta, 2); - account_metas[0] = AccountMeta.newMutable(from_pubkey, true); - account_metas[1] = AccountMeta.newMutable(to_pubkey, false); - return try Instruction.initSystemInstruction( - allocator, - .{ .Transfer = .{ .lamports = lamports } }, - account_metas, - ); +test "sanitize fails missing program id" { + const transaction = Transaction{ + .signatures = &.{}, + .version = .legacy, + .msg = .{ + .signature_count = 1, + .readonly_signed_count = 0, + .readonly_unsigned_count = 0, + .account_keys = &.{Pubkey.ZEROES}, + .recent_blockhash = Hash.ZEROES, + .instructions = &.{.{ + .program_index = 1, + .account_indexes = &.{}, + .data = &.{}, + }}, + .address_lookups = &.{}, + }, + }; + try std.testing.expectEqual(error.ProgramIdAccountMissing, transaction.validate()); } -pub fn compileInstruction( - allocator: std.mem.Allocator, - instruction: Instruction, - account_keys: []const Pubkey, -) !CompiledInstruction { - const program_id_index = indexOf(Pubkey, account_keys, instruction.program_id).?; - var accounts = try allocator.alloc(u8, instruction.accounts.len); - for (instruction.accounts, 0..) |account, i| { - accounts[i] = @truncate(indexOf(Pubkey, account_keys, account.pubkey).?); - } - return .{ - .program_id_index = @truncate(program_id_index), - .data = try allocator.dupe(u8, instruction.data), - .accounts = accounts, +test "sanitize fails if program id has index 0" { + const transaction = Transaction{ + .signatures = &.{}, + .version = .legacy, + .msg = .{ + .signature_count = 1, + .readonly_signed_count = 0, + .readonly_unsigned_count = 0, + .account_keys = &.{Pubkey.ZEROES}, + .recent_blockhash = Hash.ZEROES, + .instructions = &.{.{ + .program_index = 0, + .account_indexes = &.{}, + .data = &.{}, + }}, + .address_lookups = &.{}, + }, }; + try std.testing.expectEqual(error.ProgramIdCannotBePayer, transaction.validate()); } -pub fn compileInstructions( - allocator: std.mem.Allocator, - instructions: []const Instruction, - account_keys: []const Pubkey, -) ![]CompiledInstruction { - var compiled_instructions = try allocator.alloc(CompiledInstruction, instructions.len); - for (instructions, 0..) |instruction, i| { - compiled_instructions[i] = try compileInstruction(allocator, instruction, account_keys); - } - return compiled_instructions; +test "satinize fails account index out of bounds" { + const transaction = Transaction{ + .signatures = &.{}, + .version = .legacy, + .msg = .{ + .signature_count = 1, + .readonly_signed_count = 0, + .readonly_unsigned_count = 1, + .account_keys = &.{ Pubkey.ZEROES, Pubkey.ZEROES }, + .recent_blockhash = Hash.ZEROES, + .instructions = &.{.{ + .program_index = 1, + .account_indexes = &.{2}, + .data = &.{}, + }}, + .address_lookups = &.{}, + }, + }; + try std.testing.expectEqual(error.AccountIndexOutOfBounds, transaction.validate()); } -test "create transfer transaction" { - const allocator = std.testing.allocator; +test "parse legacy" { + try sig.bincode.testRoundTrip( + transaction_legacy_example.as_struct, + &transaction_legacy_example.as_bytes, + ); +} - var prng = std.Random.DefaultPrng.init(19); - const random = prng.random(); - - const from_keypair = try KeyPair.create([_]u8{0} ** KeyPair.seed_length); - const to_pubkey: Pubkey = .{ .data = .{1} ** Pubkey.SIZE }; - const recent_blockhash = Hash.generateSha256Hash(&.{0}); - const tx = try buildTransferTansaction( - allocator, - random, - from_keypair, - to_pubkey, - 100, - recent_blockhash, +test "parse v0" { + try sig.bincode.testRoundTrip( + transaction_v0_example.as_struct, + &transaction_v0_example.as_bytes, ); - defer tx.deinit(allocator); - const actual_bytes = try sig.bincode.writeAlloc(allocator, tx, .{}); - defer allocator.free(actual_bytes); - const expected_bytes = [_]u8{ +} + +pub const transaction_legacy_example = struct { + var signatures = [_]Signature{ + Signature.parseBase58String( + "Z2hT7E85gqWWVKEsZXxJ184u7rXdRnB6EKz2PHAUajx6jHrUZhN5WkE7tPw6PrUA3XzeZRjoE7xJDtQzshZm1Pk", + ) catch unreachable, + }; + + const as_struct = Transaction{ + .signatures = &signatures, + .version = .legacy, + .msg = .{ + .signature_count = 1, + .readonly_signed_count = 0, + .readonly_unsigned_count = 1, + .account_keys = &.{ + Pubkey.parseBase58String("4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS") catch unreachable, + Pubkey.parseBase58String("4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi") catch unreachable, + Pubkey.parseBase58String("11111111111111111111111111111111") catch unreachable, + }, + .recent_blockhash = Hash.parseBase58String("8RBsoeyoRwajj86MZfZE6gMDJQVYGYcdSfx1zxqxNHbr") catch unreachable, + .instructions = &.{.{ + .program_index = 2, + .account_indexes = &.{ 0, 1 }, + .data = &.{ 2, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0 }, + }}, + .address_lookups = &.{}, + }, + }; + + const as_bytes = [_]u8{ 1, 27, 158, 238, 65, 248, 46, 208, 15, 65, 178, 83, 163, 117, 224, 86, 163, 91, 67, 228, 176, 117, 246, 111, 69, 133, 194, 78, 89, 205, 86, 166, 98, 22, 27, 163, 250, 167, 208, 146, 201, 53, 24, 212, 97, 230, 100, 176, 26, 194, 121, @@ -639,145 +537,47 @@ test "create transfer transaction" { 56, 118, 133, 17, 163, 6, 23, 175, 160, 29, 1, 2, 2, 0, 1, 12, 2, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, }; - try std.testing.expectEqualSlices(u8, &expected_bytes, actual_bytes); -} - -test "blank Message fails to sanitize" { - try std.testing.expectError(error.MissingWritableFeePayer, Message.EMPTY.sanitize()); -} - -test "minimal valid Message sanitizes" { - try std.testing.expectEqual({}, Message.sanitize(&.{ - .header = .{ - .num_required_signatures = 1, - .num_readonly_signed_accounts = 0, - .num_readonly_unsigned_accounts = 0, - }, - .account_keys = &.{Pubkey.ZEROES}, - .recent_blockhash = Hash.generateSha256Hash(&.{0}), - .instructions = &.{}, - })); -} - -test "Message sanitize fails if missing signers" { - try std.testing.expectError(error.NotEnoughAccounts, Message.sanitize(&.{ - .header = .{ - .num_required_signatures = 2, - .num_readonly_signed_accounts = 0, - .num_readonly_unsigned_accounts = 0, - }, - .account_keys = &.{Pubkey.ZEROES}, - .recent_blockhash = Hash.generateSha256Hash(&.{0}), - .instructions = &.{}, - })); -} - -test "Message sanitize fails if missing unsigned" { - try std.testing.expectError(error.NotEnoughAccounts, Message.sanitize(&.{ - .header = .{ - .num_required_signatures = 1, - .num_readonly_signed_accounts = 0, - .num_readonly_unsigned_accounts = 1, - }, - .account_keys = &.{Pubkey.ZEROES}, - .recent_blockhash = Hash.generateSha256Hash(&.{0}), - .instructions = &.{}, - })); -} - -test "Message sanitize fails if no writable signed" { - try std.testing.expectError(error.MissingWritableFeePayer, Message.sanitize(&.{ - .header = .{ - .num_required_signatures = 1, - .num_readonly_signed_accounts = 1, - .num_readonly_unsigned_accounts = 0, - }, - .account_keys = &.{ Pubkey.ZEROES, Pubkey.ZEROES }, - .recent_blockhash = Hash.generateSha256Hash(&[_]u8{0}), - .instructions = &.{}, - })); -} - -test "Message sanitize fails if missing program id" { - try std.testing.expectError(error.ProgramIdAccountMissing, Message.sanitize(&.{ - .header = .{ - .num_required_signatures = 1, - .num_readonly_signed_accounts = 0, - .num_readonly_unsigned_accounts = 0, - }, - .account_keys = &.{Pubkey.ZEROES}, - .recent_blockhash = Hash.generateSha256Hash(&[_]u8{0}), - .instructions = &.{.{ - .program_id_index = 1, - .accounts = &.{}, - .data = &.{}, - }}, - })); -} - -test "Message sanitize fails if program id has index 0" { - try std.testing.expectError(error.ProgramIdCannotBePayer, Message.sanitize(&.{ - .header = .{ - .num_required_signatures = 1, - .num_readonly_signed_accounts = 0, - .num_readonly_unsigned_accounts = 0, - }, - .account_keys = &.{Pubkey.ZEROES}, - .recent_blockhash = Hash.generateSha256Hash(&[_]u8{0}), - .instructions = &.{.{ - .program_id_index = 0, - .accounts = &.{}, - .data = &.{}, - }}, - })); -} - -test "Message sanitize fails if account index is out of bounds" { - try std.testing.expectError(error.AccountIndexOutOfBounds, Message.sanitize(&.{ - .header = .{ - .num_required_signatures = 1, - .num_readonly_signed_accounts = 0, - .num_readonly_unsigned_accounts = 1, - }, - .account_keys = &.{ Pubkey.ZEROES, Pubkey.ZEROES }, - .recent_blockhash = Hash.generateSha256Hash(&[_]u8{0}), - .instructions = &.{.{ - .program_id_index = 1, - .accounts = &.{2}, - .data = &.{}, - }}, - })); -} - -test "V0Message serialization and deserialization" { - const message = test_v0_message.as_struct; - try sig.bincode.testRoundTrip(message, &test_v0_message.bincode_serialized_bytes); -} - -test "VersionedTransaction v0 serialization and deserialization" { - const transaction = test_v0_transaction.as_struct; - try sig.bincode.testRoundTrip(transaction, &test_v0_transaction.bincode_serialized_bytes); -} +}; -test "VersionedMessage v0 serialization and deserialization" { - const versioned_message = test_v0_versioned_message.as_struct; - try sig.bincode.testRoundTrip(versioned_message, &test_v0_versioned_message.bincode_serialized_bytes); -} +pub const transaction_v0_example = struct { + var signatures = [_]Signature{ + Signature.parseBase58String( + "2cxn1LdtB7GcpeLEnHe5eA7LymTXKkqGF6UvmBM2EtttZEeqBREDaAD7LCagDFHyuc3xXxyDkMPiy3CpK5m6Uskw", + ) catch unreachable, + Signature.parseBase58String( + "4gr9L7K3bALKjPRiRSk4JDB3jYmNaauf6rewNV3XFubX5EHxBn98gqBGhbwmZAB9DJ2pv8GWE1sLoYqhhLbTZcLj", + ) catch unreachable, + }; -pub const test_v0_transaction = struct { - pub const as_struct: VersionedTransaction = .{ - .signatures = &.{ - Signature.parseBase58String( - "2cxn1LdtB7GcpeLEnHe5eA7LymTXKkqGF6UvmBM2EtttZEeqBREDaAD7LCagDFHyuc3xXxyDkMPiy3CpK5m6Uskw", - ) catch unreachable, - Signature.parseBase58String( - "4gr9L7K3bALKjPRiRSk4JDB3jYmNaauf6rewNV3XFubX5EHxBn98gqBGhbwmZAB9DJ2pv8GWE1sLoYqhhLbTZcLj", - ) catch unreachable, + pub const as_struct: Transaction = .{ + .signatures = signatures[0..], + .version = .v0, + .msg = .{ + .signature_count = 39, + .readonly_signed_count = 12, + .readonly_unsigned_count = 102, + .account_keys = &.{ + Pubkey.parseBase58String("GubTBrbgk9JwkwX1FkXvsrF1UC2AP7iTgg8SGtgH14QE") catch unreachable, + Pubkey.parseBase58String("5yCD7QeAk5uAduhLZGxePv21RLsVEktPqJG5pbmZx4J4") catch unreachable, + }, + .recent_blockhash = Hash.parseBase58String("4xzjBNLkRqhBVmZ7JKcX2UEP8wzYKYWpXk7CPXzgrEZW") catch unreachable, + .instructions = &.{.{ + .program_index = 100, + .account_indexes = &.{ 1, 3 }, + .data = &.{ + 104, 232, 42, 254, 46, 48, 104, 89, 101, 211, 253, 161, 65, 155, 204, 89, + 126, 187, 180, 191, 60, 59, 88, 119, 106, 20, 194, 80, 11, 200, 76, 0, + }, + }}, + .address_lookups = &.{.{ + .table_address = Pubkey.parseBase58String("ZETAxsqBRek56DhiGXrn75yj2NHU3aYUnxvHXpkf3aD") catch unreachable, + .writable_indexes = &.{ 1, 3, 5, 7, 90 }, + .readonly_indexes = &.{}, + }}, }, - .message = .{ .v0 = test_v0_message.as_struct }, }; - pub const bincode_serialized_bytes = [_]u8{ + pub const as_bytes = [_]u8{ 2, 81, 7, 106, 50, 99, 54, 99, 92, 187, 47, 10, 170, 102, 132, 42, 25, 4, 26, 67, 106, 76, 132, 119, 57, 38, 159, 7, 243, 132, 127, 236, 31, 83, 124, 56, 140, 54, 239, 100, 65, 111, 8, 246, 103, 155, 246, 108, 196, 95, 231, 253, 121, 109, @@ -798,61 +598,3 @@ pub const test_v0_transaction = struct { 90, 0, }; }; - -pub const test_v0_versioned_message = struct { - pub const as_struct = VersionedMessage{ .v0 = test_v0_message.as_struct }; - - pub const bincode_serialized_bytes = [_]u8{ - 128, 39, 12, 102, 2, 236, 88, 117, 221, 34, 125, 55, 183, 193, 174, 21, 99, 70, - 167, 52, 227, 254, 241, 14, 239, 13, 172, 158, 81, 254, 134, 30, 78, 35, 15, 168, - 79, 73, 211, 242, 100, 122, 21, 163, 216, 62, 58, 230, 205, 163, 112, 95, 100, 134, - 113, 98, 129, 164, 240, 184, 157, 4, 34, 55, 72, 89, 113, 179, 97, 58, 235, 71, - 20, 83, 42, 196, 46, 189, 136, 194, 90, 249, 14, 154, 144, 141, 234, 253, 148, 146, - 168, 110, 10, 237, 82, 157, 190, 248, 20, 215, 105, 1, 100, 2, 1, 3, 32, 104, - 232, 42, 254, 46, 48, 104, 89, 101, 211, 253, 161, 65, 155, 204, 89, 126, 187, 180, - 191, 60, 59, 88, 119, 106, 20, 194, 80, 11, 200, 76, 0, 1, 8, 65, 203, 149, - 184, 2, 85, 213, 101, 44, 13, 181, 13, 65, 128, 17, 94, 229, 31, 215, 47, 49, - 72, 57, 158, 144, 193, 224, 205, 241, 120, 78, 5, 1, 3, 5, 7, 90, 0, - }; -}; - -pub const test_v0_message = struct { - pub const as_struct = V0Message{ - .header = .{ - .num_required_signatures = 39, - .num_readonly_signed_accounts = 12, - .num_readonly_unsigned_accounts = 102, - }, - .account_keys = &.{ - Pubkey.parseBase58String("GubTBrbgk9JwkwX1FkXvsrF1UC2AP7iTgg8SGtgH14QE") catch unreachable, - Pubkey.parseBase58String("5yCD7QeAk5uAduhLZGxePv21RLsVEktPqJG5pbmZx4J4") catch unreachable, - }, - .recent_blockhash = Hash.parseBase58String("4xzjBNLkRqhBVmZ7JKcX2UEP8wzYKYWpXk7CPXzgrEZW") catch unreachable, - .instructions = &.{.{ - .program_id_index = 100, - .accounts = &.{ 1, 3 }, - .data = &.{ - 104, 232, 42, 254, 46, 48, 104, 89, 101, 211, 253, 161, 65, 155, 204, 89, - 126, 187, 180, 191, 60, 59, 88, 119, 106, 20, 194, 80, 11, 200, 76, 0, - }, - }}, - .address_table_lookups = &.{.{ - .account_key = Pubkey.parseBase58String("ZETAxsqBRek56DhiGXrn75yj2NHU3aYUnxvHXpkf3aD") catch unreachable, - .writable_indexes = &.{ 1, 3, 5, 7, 90 }, - .readonly_indexes = &.{}, - }}, - }; - - pub const bincode_serialized_bytes = [_]u8{ - 39, 12, 102, 2, 236, 88, 117, 221, 34, 125, 55, 183, 193, 174, 21, 99, 70, 167, - 52, 227, 254, 241, 14, 239, 13, 172, 158, 81, 254, 134, 30, 78, 35, 15, 168, 79, - 73, 211, 242, 100, 122, 21, 163, 216, 62, 58, 230, 205, 163, 112, 95, 100, 134, 113, - 98, 129, 164, 240, 184, 157, 4, 34, 55, 72, 89, 113, 179, 97, 58, 235, 71, 20, - 83, 42, 196, 46, 189, 136, 194, 90, 249, 14, 154, 144, 141, 234, 253, 148, 146, 168, - 110, 10, 237, 82, 157, 190, 248, 20, 215, 105, 1, 100, 2, 1, 3, 32, 104, 232, - 42, 254, 46, 48, 104, 89, 101, 211, 253, 161, 65, 155, 204, 89, 126, 187, 180, 191, - 60, 59, 88, 119, 106, 20, 194, 80, 11, 200, 76, 0, 1, 8, 65, 203, 149, 184, - 2, 85, 213, 101, 44, 13, 181, 13, 65, 128, 17, 94, 229, 31, 215, 47, 49, 72, - 57, 158, 144, 193, 224, 205, 241, 120, 78, 5, 1, 3, 5, 7, 90, 0, - }; -}; diff --git a/src/gossip/data.zig b/src/gossip/data.zig index f9d94441d..adcd2174f 100644 --- a/src/gossip/data.zig +++ b/src/gossip/data.zig @@ -593,7 +593,7 @@ pub const Vote = struct { pub fn sanitize(self: *const Vote) !void { try sanitizeWallclock(self.wallclock); - try self.transaction.sanitize(); + try self.transaction.validate(); } }; diff --git a/src/ledger/reader.zig b/src/ledger/reader.zig index 39f568467..26708d5b4 100644 --- a/src/ledger/reader.zig +++ b/src/ledger/reader.zig @@ -22,7 +22,6 @@ const Slot = sig.core.Slot; const SortedSet = sig.utils.collections.SortedSet; const Timer = sig.time.Timer; const Transaction = sig.core.Transaction; -const VersionedTransaction = sig.core.VersionedTransaction; // shred const Shred = sig.ledger.shred.Shred; @@ -479,7 +478,7 @@ pub const BlockstoreReader = struct { ArrayList(EntrySummary).init(self.allocator); errdefer entries.deinit(); - var slot_transactions = ArrayList(VersionedTransaction).init(self.allocator); + var slot_transactions = ArrayList(Transaction).init(self.allocator); var num_moved_slot_transactions: usize = 0; defer { for (slot_transactions.items[num_moved_slot_transactions..]) |tx| { @@ -511,9 +510,9 @@ pub const BlockstoreReader = struct { txns_with_statuses.deinit(); } for (slot_transactions.items) |transaction| { - transaction.sanitize() catch |err| { + transaction.validate() catch |err| { self.logger.warn().logf( - "getCompleteeBlockWithEntries sanitize failed: {any}, slot: {any}, {any}", + "getCompleteeBlockWithEntries validate failed: {any}, slot: {any}, {any}", .{ err, slot, transaction }, ); }; @@ -717,15 +716,15 @@ pub const BlockstoreReader = struct { self: *Self, slot: Slot, signature: Signature, - ) !?VersionedTransaction { + ) !?Transaction { const slot_entries = try self.getSlotEntries(slot, 0); // NOTE perf: linear search runs from scratch every time this is called for (slot_entries.items) |entry| { for (entry.transactions.items) |transaction| { - // NOTE perf: redundant calls to sanitize every time this is called - if (transaction.sanitize()) |_| {} else |err| { + // NOTE perf: redundant calls to validate every time this is called + if (transaction.validate()) |_| {} else |err| { self.logger.warn().logf( - "BlockstoreReader.findTransactionInSlot sanitize failed: {any}, slot: {}, {any}", + "BlockstoreReader.findTransactionInSlot validate failed: {any}, slot: {}, {any}", .{ err, slot, transaction }, ); } @@ -1185,8 +1184,12 @@ pub const BlockstoreReader = struct { return e; }; defer bytes.deinit(); - const these_entries = sig.bincode - .readFromSlice(allocator, []Entry, bytes.items, .{}) catch |e| { + const these_entries = bincode.readFromSlice( + allocator, + []Entry, + bytes.items, + .{}, + ) catch |e| { self.logger.err().logf("failed to deserialize entries from shreds: {}", .{e}); return e; }; @@ -1427,7 +1430,7 @@ const TransactionWithStatusMeta = union(enum) { }; pub const VersionedTransactionWithStatusMeta = struct { - transaction: VersionedTransaction, + transaction: Transaction, meta: TransactionStatusMeta, pub fn deinit(self: @This(), allocator: Allocator) void { diff --git a/src/ledger/tests.zig b/src/ledger/tests.zig index d212da90a..25925e207 100644 --- a/src/ledger/tests.zig +++ b/src/ledger/tests.zig @@ -415,7 +415,7 @@ pub fn insertDataForBlockTest(state: *TestState) !InsertDataForBlockResult { for (entry.transactions.items) |transaction| { var pre_balances = std.ArrayList(u64).init(allocator); var post_balances = std.ArrayList(u64).init(allocator); - const num_accounts = transaction.message.accountKeys().len; + const num_accounts = transaction.msg.account_keys.len; for (0..num_accounts) |i| { try pre_balances.append(i * 10); try post_balances.append(i * 11); diff --git a/src/rpc/client.zig b/src/rpc/client.zig index 22efe3817..d5a38467f 100644 --- a/src/rpc/client.zig +++ b/src/rpc/client.zig @@ -11,7 +11,7 @@ const Request = sig.rpc.Request; const Response = sig.rpc.Response; const Logger = sig.trace.log.Logger; const ScopedLogger = sig.trace.log.ScopedLogger; -const Transaction = sig.core.transaction.Transaction; +const Transaction = sig.core.Transaction; const ClusterType = sig.accounts_db.genesis_config.ClusterType; pub const Client = struct { diff --git a/src/transaction_sender/mock_transfer_generator.zig b/src/transaction_sender/mock_transfer_generator.zig index 74467df5d..34faf4019 100644 --- a/src/transaction_sender/mock_transfer_generator.zig +++ b/src/transaction_sender/mock_transfer_generator.zig @@ -182,7 +182,7 @@ pub const MockTransferService = struct { }; }; - const transaction = try sig.core.transaction.buildTransferTansaction( + const transaction = try buildTransferTansaction( self.allocator, random, from_keypair, @@ -238,7 +238,7 @@ pub const MockTransferService = struct { }; }; - const transaction = try sig.core.transaction.buildTransferTansaction( + const transaction = try buildTransferTansaction( self.allocator, random, from_keypair, @@ -283,7 +283,7 @@ pub const MockTransferService = struct { }; }; - const transaction = try sig.core.transaction.buildTransferTansaction( + const transaction = try buildTransferTansaction( self.allocator, random, from_keypair, @@ -374,4 +374,65 @@ pub const MockTransferService = struct { } return error.AirdropFailed; } + + pub fn buildTransferTansaction( + allocator: std.mem.Allocator, + random: std.Random, + from_keypair: KeyPair, + to_pubkey: Pubkey, + lamports: u64, + recent_blockhash: Hash, + ) !sig.core.Transaction { + const from_pubkey = Pubkey.fromPublicKey(&from_keypair.public_key); + + const signatures = try allocator.alloc(Signature, 1); + errdefer allocator.free(signatures); + + const addresses = try allocator.alloc(Pubkey, 3); + errdefer allocator.free(addresses); + addresses[0] = from_pubkey; + addresses[1] = to_pubkey; + // TODO: replace with system_program_id once it's available + addresses[2] = try Pubkey.parseBase58String("11111111111111111111111111111111"); + + const account_indexes = try allocator.alloc(u8, 2); + errdefer allocator.free(account_indexes); + account_indexes[0] = 0; + account_indexes[1] = 1; + + var data = [_]u8{0} ** 12; + var fbs = std.io.fixedBufferStream(&data); + const writer = fbs.writer(); + try writer.writeInt(u32, 2, .little); + try writer.writeInt(u64, lamports, .little); + + const instructions = try allocator.alloc(sig.core.transaction.TransactionInstruction, 1); + errdefer allocator.free(instructions); + instructions[0] = .{ + .program_index = 2, + .account_indexes = account_indexes, + .data = try allocator.dupe(u8, &data), + }; + + const transaction = sig.core.Transaction{ + .signatures = signatures, + .version = .legacy, + .msg = .{ + .signature_count = @intCast(signatures.len), + .readonly_signed_count = 0, + .readonly_unsigned_count = 1, + .account_keys = addresses, + .recent_blockhash = recent_blockhash, + .instructions = instructions, + }, + }; + + const buffer = [_]u8{0} ** sig.core.Transaction.MAX_BYTES; + const signable = &buffer; //try transaction.msgwriteSignableToSlice(&buffer); + var noise: [KeyPair.seed_length]u8 = undefined; + random.bytes(noise[0..]); + transaction.signatures[0] = .{ .data = (try from_keypair.sign(signable, noise)).toBytes() }; + + return transaction; + } }; diff --git a/src/transaction_sender/transaction_info.zig b/src/transaction_sender/transaction_info.zig index dceeaf09d..3f0e29b45 100644 --- a/src/transaction_sender/transaction_info.zig +++ b/src/transaction_sender/transaction_info.zig @@ -3,7 +3,7 @@ const sig = @import("../sig.zig"); const Hash = sig.core.Hash; const Pubkey = sig.core.Pubkey; const Signature = sig.core.Signature; -const Transaction = sig.core.transaction.Transaction; +const Transaction = sig.core.Transaction; const Instant = sig.time.Instant; const Duration = sig.time.Duration;