From 65c79a132a889ee1613890e1f4169ef243b70af8 Mon Sep 17 00:00:00 2001 From: Sobeston <15335529+Sobeston@users.noreply.github.com> Date: Thu, 13 Feb 2025 05:47:13 +0000 Subject: [PATCH 01/16] wip --- src/runtime/program/precompile_program.zig | 24 +++ .../program/precompile_program_execute.zig | 182 ++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 src/runtime/program/precompile_program.zig create mode 100644 src/runtime/program/precompile_program_execute.zig diff --git a/src/runtime/program/precompile_program.zig b/src/runtime/program/precompile_program.zig new file mode 100644 index 000000000..9b3e7b802 --- /dev/null +++ b/src/runtime/program/precompile_program.zig @@ -0,0 +1,24 @@ +const std = @import("std"); +const sig = @import("../../sig.zig"); + +const Pubkey = sig.core.Pubkey; + +/// Re-export instruction execute method in the system_program namespace +pub const execute = @import("precompile_program_execute.zig").precompileProgramExecute; + +pub const COMPUTE_UNITS = 1; // TODO: what is the actual value? + +pub const ID = + Pubkey.parseBase58String("11111111111111111111111111111111") catch unreachable; + +// parsed internally +pub const PrecompileProgramInstruction = []const u8; + +// https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/precompile-error/src/lib.rs#L6 +pub const PrecompileProgramError = error{ + InvalidPublicKey, + InvalidRecoveryId, + InvalidSignature, + InvalidDataOffsets, + InvalidInstructionDataSize, +}; diff --git a/src/runtime/program/precompile_program_execute.zig b/src/runtime/program/precompile_program_execute.zig new file mode 100644 index 000000000..5ee8dbbdf --- /dev/null +++ b/src/runtime/program/precompile_program_execute.zig @@ -0,0 +1,182 @@ +const std = @import("std"); +const sig = @import("../../sig.zig"); + +const Ed25519 = std.crypto.sign.Ed25519; +const Keccak256 = std.crypto.hash.sha3.Keccak256; +const Secp256k1 = std.crypto.ecc.Secp256k1; + +const nonce = sig.runtime.nonce; +const pubkey_utils = sig.runtime.pubkey_utils; +const precompile_program = sig.runtime.program.precompile_program; + +const Pubkey = sig.core.Pubkey; +const InstructionError = sig.core.instruction.InstructionError; + +const InstructionContext = sig.runtime.InstructionContext; +const BorrowedAccount = sig.runtime.BorrowedAccount; + +const PrecompileProgramInstruction = precompile_program.PrecompileProgramInstruction; +const PrecompileProgramError = precompile_program.PrecompileProgramError; + +const ED25519_SIGNATURE_SERIALIZED_SIZE = 64; +const ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE = 14; +const ED25519_SIGNATURE_OFFSETS_START = 2; +const ED25519_DATA_START = (ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE + ED25519_SIGNATURE_OFFSETS_START); +const ED25519_PUBKEY_SERIALIZED_SIZE = 32; + +const SECP256K1_PUBKEY_SERIALIZED_SIZE = 20; +const SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE = 11; +const SECP256K1_SIGNATURE_OFFSETS_START = 1; +const SECP256K1_DATA_START = (SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE + SECP256K1_SIGNATURE_OFFSETS_START); + +const Ed25519SignatureOffsets = packed struct { + /// Offset to ed25519 signature of 64 bytes. + signature_offset: u16, + /// Instruction index to find signature. + signature_instruction_idx: u16, + /// Offset to public key of 32 bytes. + pubkey_offset: u16, + /// Instruction index to find public key. + pubkey_instruction_idx: u16, + /// Offset to start of message data. + message_data_offset: u16, + /// Size of message data. + message_data_size: u16, + /// Index of instruction data to get message data. + message_instruction_idx: u16, +}; + +comptime { + if (@sizeOf(Ed25519SignatureOffsets) != ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE) { + @compileError("bad size"); + } +} + +const Secp256k1SignatureOffsets = packed struct { + /// Offset to 64-byte signature plus 1-byte recovery ID. + signature_offset: u16, + /// Within the transaction, the index of the instruction whose instruction data contains the signature. + signature_instruction_idx: u8, + /// Offset to 20-byte Ethereum address. + eth_address_offset: u16, + /// Within the transaction, the index of the instruction whose instruction data contains the address. + eth_address_instruction_idx: u8, + /// Offset to start of message data. + message_data_offset: u16, + /// Size of message data in bytes. + message_data_size: u16, + /// Within the transaction, the index of the instruction whose instruction data contains the message. + message_instruction_idx: u8, +}; +comptime { + if (@sizeOf(Secp256k1SignatureOffsets) != SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE) { + @compileError("bad size"); + } +} + +pub fn precompileProgramExecute(ic: *const InstructionContext, mode: enum { ed25519_verify, secp256k1_verify }) InstructionError!void { + try ic.tc.consumeCompute(precompile_program.COMPUTE_UNITS); + + // not sure if we even need a switch like this, seems they're invoked as separate programs? + (switch (mode) { + .ed25519_verify => ed25519Verify(ic), + .secp256k1_verify => secp256k1Verify(ic), + }) catch |err| { + ic.tc.custom_error = @intFromError(err); + return err.Custom; + }; +} + +fn ed25519Verify(ic: *const InstructionContext) PrecompileProgramError!void { + const data = ic.instruction; + const n_signatures = data[0]; + if (data.len < ED25519_DATA_START) { + if (data.len == 2 and n_signatures == 0) return; + return error.InvalidInstructionDataSize; + } + if (n_signatures == 0) return error.InvalidInstructionDataSize; + + const expected_data_size = n_signatures * ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE + ED25519_SIGNATURE_OFFSETS_START; + if (data.len < expected_data_size) return error.InvalidInstructionDataSize; + + // firedancer seems to assume natural alignment in this loop? Need to prove it to myself. + for (0..n_signatures) |i| { + const offset = ED25519_SIGNATURE_OFFSETS_START + i * ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE; + const sig_offsets: *const Ed25519SignatureOffsets = @ptrCast(data.ptr + offset); + + const signature = try getInstructionValue( + Ed25519.Signature, + data, + sig_offsets.signature_instruction_idx, + sig_offsets.signature_offset, + ); + const pubkey = try getInstructionValue( + Ed25519.PublicKey, + data, + sig_offsets.pubkey_instruction_idx, + sig_offsets.pubkey_offset, + ); + const msg = try getInstructionData( + sig_offsets.message_data_size, + data, + sig_offsets.message_instruction_idx, + sig_offsets.message_data_offset, + ); + signature.verify(msg, pubkey) catch return error.InvalidSignature; + } +} + +// https://github.com/firedancer-io/firedancer/blob/af74882ffb2c24783a82718dbc5111a94e1b5f6f/src/flamenco/runtime/program/fd_precompiles.c#L227 +fn secp256k1Verify(ic: *const InstructionContext) PrecompileProgramError!void { + const data = ic.instruction; + const n_signatures = data[0]; + if (data.len < SECP256K1_DATA_START) { + if (data.len == 1 and n_signatures == 0) return; + return error.InvalidInstructionDataSize; + } + if (n_signatures == 0 and data.len > 1) return error.InvalidInstructionDataSize; + + const expected_data_size = SECP256K1_SIGNATURE_OFFSETS_START + + n_signatures * SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE; + if (data.len < expected_data_size) return error.InvalidInstructionDataSize; + + // firedancer seems to assume natural alignment in this loop? Need to prove it to myself. + for (0..n_signatures) |i| { + const offset = SECP256K1_SIGNATURE_OFFSETS_START + + i * SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE; + const sig_offsets: *const Secp256k1SignatureOffsets = @ptrCast(data.ptr + offset); + + sig_offsets = @panic("TODO"); + } +} + +fn getInstructionValue( + T: type, + current_instruction_data: []const u8, + instruction_idx: u16, + offset: usize, +) error{InvalidSignature}!*const T { + // aligncast potentially dangerous? + return @alignCast(@ptrCast(try getInstructionData( + instruction_idx, + current_instruction_data, + offset, + @sizeOf(T), + ))); +} + +// https://github.com/firedancer-io/firedancer/blob/af74882ffb2c24783a82718dbc5111a94e1b5f6f/src/flamenco/runtime/program/fd_precompiles.c#L74 +fn getInstructionData( + len: usize, + current_instruction_data: []const u8, + instruction_idx: u16, + offset: usize, +) error{InvalidSignature}![]const u8 { + const data: []const u8 = if (instruction_idx == std.math.maxInt(u16)) + current_instruction_data + else + @panic("todo: handle multiple instructions"); + + if (offset + len > data.len) return error.InvalidSignature; + return data[offset..][0..len]; +} From 5ff2c46166fd936590befa580de87537d882e105 Mon Sep 17 00:00:00 2001 From: Sobeston <15335529+Sobeston@users.noreply.github.com> Date: Thu, 13 Feb 2025 16:48:42 +0000 Subject: [PATCH 02/16] fixup --- src/runtime/program/precompile_program.zig | 10 ++++++++-- src/runtime/program/precompile_program_execute.zig | 7 +++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/runtime/program/precompile_program.zig b/src/runtime/program/precompile_program.zig index 9b3e7b802..34f287ee8 100644 --- a/src/runtime/program/precompile_program.zig +++ b/src/runtime/program/precompile_program.zig @@ -8,8 +8,14 @@ pub const execute = @import("precompile_program_execute.zig").precompileProgramE pub const COMPUTE_UNITS = 1; // TODO: what is the actual value? -pub const ID = - Pubkey.parseBase58String("11111111111111111111111111111111") catch unreachable; +pub const ID_ED25519_VERIFY = + Pubkey.parseBase58String("Ed25519SigVerify111111111111111111111111111") catch unreachable; + +pub const ID_SECP256K1 = + Pubkey.parseBase58String("KeccakSecp256k11111111111111111111111111111") catch unreachable; + +pub const ID_SECP256R1_VERIFY = + Pubkey.parseBase58String("Secp256r1SigVerify1111111111111111111111111") catch unreachable; // parsed internally pub const PrecompileProgramInstruction = []const u8; diff --git a/src/runtime/program/precompile_program_execute.zig b/src/runtime/program/precompile_program_execute.zig index 5ee8dbbdf..5f61ed14a 100644 --- a/src/runtime/program/precompile_program_execute.zig +++ b/src/runtime/program/precompile_program_execute.zig @@ -96,12 +96,15 @@ fn ed25519Verify(ic: *const InstructionContext) PrecompileProgramError!void { } if (n_signatures == 0) return error.InvalidInstructionDataSize; - const expected_data_size = n_signatures * ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE + ED25519_SIGNATURE_OFFSETS_START; + const expected_data_size = ED25519_SIGNATURE_OFFSETS_START + + n_signatures * ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE; if (data.len < expected_data_size) return error.InvalidInstructionDataSize; // firedancer seems to assume natural alignment in this loop? Need to prove it to myself. for (0..n_signatures) |i| { - const offset = ED25519_SIGNATURE_OFFSETS_START + i * ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE; + const offset = ED25519_SIGNATURE_OFFSETS_START + + i * ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE; + const sig_offsets: *const Ed25519SignatureOffsets = @ptrCast(data.ptr + offset); const signature = try getInstructionValue( From b6a62bd28d6557369bb5018a15feaa0d142f1e9d Mon Sep 17 00:00:00 2001 From: Sobeston <15335529+Sobeston@users.noreply.github.com> Date: Fri, 14 Feb 2025 06:18:48 +0000 Subject: [PATCH 03/16] wip / restructure / ed25519 test --- src/runtime/program/lib.zig | 2 + src/runtime/program/precompile_program.zig | 30 --- .../program/precompile_program_execute.zig | 185 ---------------- .../program/precompile_programs/ed25519.zig | 204 ++++++++++++++++++ .../program/precompile_programs/lib.zig | 128 +++++++++++ .../program/precompile_programs/secp256k1.zig | 75 +++++++ .../program/precompile_programs/secp256r1.zig | 17 ++ src/tests.zig | 1 + 8 files changed, 427 insertions(+), 215 deletions(-) delete mode 100644 src/runtime/program/precompile_program.zig delete mode 100644 src/runtime/program/precompile_program_execute.zig create mode 100644 src/runtime/program/precompile_programs/ed25519.zig create mode 100644 src/runtime/program/precompile_programs/lib.zig create mode 100644 src/runtime/program/precompile_programs/secp256k1.zig create mode 100644 src/runtime/program/precompile_programs/secp256r1.zig diff --git a/src/runtime/program/lib.zig b/src/runtime/program/lib.zig index 7ca06c82b..b2681add0 100644 --- a/src/runtime/program/lib.zig +++ b/src/runtime/program/lib.zig @@ -1,3 +1,5 @@ pub const system_program = @import("system_program/lib.zig"); pub const test_program_execute = @import("test_program_execute.zig"); + +pub const precompile_programs = @import("precompile_programs/lib.zig"); diff --git a/src/runtime/program/precompile_program.zig b/src/runtime/program/precompile_program.zig deleted file mode 100644 index 34f287ee8..000000000 --- a/src/runtime/program/precompile_program.zig +++ /dev/null @@ -1,30 +0,0 @@ -const std = @import("std"); -const sig = @import("../../sig.zig"); - -const Pubkey = sig.core.Pubkey; - -/// Re-export instruction execute method in the system_program namespace -pub const execute = @import("precompile_program_execute.zig").precompileProgramExecute; - -pub const COMPUTE_UNITS = 1; // TODO: what is the actual value? - -pub const ID_ED25519_VERIFY = - Pubkey.parseBase58String("Ed25519SigVerify111111111111111111111111111") catch unreachable; - -pub const ID_SECP256K1 = - Pubkey.parseBase58String("KeccakSecp256k11111111111111111111111111111") catch unreachable; - -pub const ID_SECP256R1_VERIFY = - Pubkey.parseBase58String("Secp256r1SigVerify1111111111111111111111111") catch unreachable; - -// parsed internally -pub const PrecompileProgramInstruction = []const u8; - -// https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/precompile-error/src/lib.rs#L6 -pub const PrecompileProgramError = error{ - InvalidPublicKey, - InvalidRecoveryId, - InvalidSignature, - InvalidDataOffsets, - InvalidInstructionDataSize, -}; diff --git a/src/runtime/program/precompile_program_execute.zig b/src/runtime/program/precompile_program_execute.zig deleted file mode 100644 index 5f61ed14a..000000000 --- a/src/runtime/program/precompile_program_execute.zig +++ /dev/null @@ -1,185 +0,0 @@ -const std = @import("std"); -const sig = @import("../../sig.zig"); - -const Ed25519 = std.crypto.sign.Ed25519; -const Keccak256 = std.crypto.hash.sha3.Keccak256; -const Secp256k1 = std.crypto.ecc.Secp256k1; - -const nonce = sig.runtime.nonce; -const pubkey_utils = sig.runtime.pubkey_utils; -const precompile_program = sig.runtime.program.precompile_program; - -const Pubkey = sig.core.Pubkey; -const InstructionError = sig.core.instruction.InstructionError; - -const InstructionContext = sig.runtime.InstructionContext; -const BorrowedAccount = sig.runtime.BorrowedAccount; - -const PrecompileProgramInstruction = precompile_program.PrecompileProgramInstruction; -const PrecompileProgramError = precompile_program.PrecompileProgramError; - -const ED25519_SIGNATURE_SERIALIZED_SIZE = 64; -const ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE = 14; -const ED25519_SIGNATURE_OFFSETS_START = 2; -const ED25519_DATA_START = (ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE + ED25519_SIGNATURE_OFFSETS_START); -const ED25519_PUBKEY_SERIALIZED_SIZE = 32; - -const SECP256K1_PUBKEY_SERIALIZED_SIZE = 20; -const SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE = 11; -const SECP256K1_SIGNATURE_OFFSETS_START = 1; -const SECP256K1_DATA_START = (SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE + SECP256K1_SIGNATURE_OFFSETS_START); - -const Ed25519SignatureOffsets = packed struct { - /// Offset to ed25519 signature of 64 bytes. - signature_offset: u16, - /// Instruction index to find signature. - signature_instruction_idx: u16, - /// Offset to public key of 32 bytes. - pubkey_offset: u16, - /// Instruction index to find public key. - pubkey_instruction_idx: u16, - /// Offset to start of message data. - message_data_offset: u16, - /// Size of message data. - message_data_size: u16, - /// Index of instruction data to get message data. - message_instruction_idx: u16, -}; - -comptime { - if (@sizeOf(Ed25519SignatureOffsets) != ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE) { - @compileError("bad size"); - } -} - -const Secp256k1SignatureOffsets = packed struct { - /// Offset to 64-byte signature plus 1-byte recovery ID. - signature_offset: u16, - /// Within the transaction, the index of the instruction whose instruction data contains the signature. - signature_instruction_idx: u8, - /// Offset to 20-byte Ethereum address. - eth_address_offset: u16, - /// Within the transaction, the index of the instruction whose instruction data contains the address. - eth_address_instruction_idx: u8, - /// Offset to start of message data. - message_data_offset: u16, - /// Size of message data in bytes. - message_data_size: u16, - /// Within the transaction, the index of the instruction whose instruction data contains the message. - message_instruction_idx: u8, -}; -comptime { - if (@sizeOf(Secp256k1SignatureOffsets) != SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE) { - @compileError("bad size"); - } -} - -pub fn precompileProgramExecute(ic: *const InstructionContext, mode: enum { ed25519_verify, secp256k1_verify }) InstructionError!void { - try ic.tc.consumeCompute(precompile_program.COMPUTE_UNITS); - - // not sure if we even need a switch like this, seems they're invoked as separate programs? - (switch (mode) { - .ed25519_verify => ed25519Verify(ic), - .secp256k1_verify => secp256k1Verify(ic), - }) catch |err| { - ic.tc.custom_error = @intFromError(err); - return err.Custom; - }; -} - -fn ed25519Verify(ic: *const InstructionContext) PrecompileProgramError!void { - const data = ic.instruction; - const n_signatures = data[0]; - if (data.len < ED25519_DATA_START) { - if (data.len == 2 and n_signatures == 0) return; - return error.InvalidInstructionDataSize; - } - if (n_signatures == 0) return error.InvalidInstructionDataSize; - - const expected_data_size = ED25519_SIGNATURE_OFFSETS_START + - n_signatures * ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE; - if (data.len < expected_data_size) return error.InvalidInstructionDataSize; - - // firedancer seems to assume natural alignment in this loop? Need to prove it to myself. - for (0..n_signatures) |i| { - const offset = ED25519_SIGNATURE_OFFSETS_START + - i * ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE; - - const sig_offsets: *const Ed25519SignatureOffsets = @ptrCast(data.ptr + offset); - - const signature = try getInstructionValue( - Ed25519.Signature, - data, - sig_offsets.signature_instruction_idx, - sig_offsets.signature_offset, - ); - const pubkey = try getInstructionValue( - Ed25519.PublicKey, - data, - sig_offsets.pubkey_instruction_idx, - sig_offsets.pubkey_offset, - ); - const msg = try getInstructionData( - sig_offsets.message_data_size, - data, - sig_offsets.message_instruction_idx, - sig_offsets.message_data_offset, - ); - signature.verify(msg, pubkey) catch return error.InvalidSignature; - } -} - -// https://github.com/firedancer-io/firedancer/blob/af74882ffb2c24783a82718dbc5111a94e1b5f6f/src/flamenco/runtime/program/fd_precompiles.c#L227 -fn secp256k1Verify(ic: *const InstructionContext) PrecompileProgramError!void { - const data = ic.instruction; - const n_signatures = data[0]; - if (data.len < SECP256K1_DATA_START) { - if (data.len == 1 and n_signatures == 0) return; - return error.InvalidInstructionDataSize; - } - if (n_signatures == 0 and data.len > 1) return error.InvalidInstructionDataSize; - - const expected_data_size = SECP256K1_SIGNATURE_OFFSETS_START + - n_signatures * SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE; - if (data.len < expected_data_size) return error.InvalidInstructionDataSize; - - // firedancer seems to assume natural alignment in this loop? Need to prove it to myself. - for (0..n_signatures) |i| { - const offset = SECP256K1_SIGNATURE_OFFSETS_START + - i * SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE; - const sig_offsets: *const Secp256k1SignatureOffsets = @ptrCast(data.ptr + offset); - - sig_offsets = @panic("TODO"); - } -} - -fn getInstructionValue( - T: type, - current_instruction_data: []const u8, - instruction_idx: u16, - offset: usize, -) error{InvalidSignature}!*const T { - // aligncast potentially dangerous? - return @alignCast(@ptrCast(try getInstructionData( - instruction_idx, - current_instruction_data, - offset, - @sizeOf(T), - ))); -} - -// https://github.com/firedancer-io/firedancer/blob/af74882ffb2c24783a82718dbc5111a94e1b5f6f/src/flamenco/runtime/program/fd_precompiles.c#L74 -fn getInstructionData( - len: usize, - current_instruction_data: []const u8, - instruction_idx: u16, - offset: usize, -) error{InvalidSignature}![]const u8 { - const data: []const u8 = if (instruction_idx == std.math.maxInt(u16)) - current_instruction_data - else - @panic("todo: handle multiple instructions"); - - if (offset + len > data.len) return error.InvalidSignature; - return data[offset..][0..len]; -} diff --git a/src/runtime/program/precompile_programs/ed25519.zig b/src/runtime/program/precompile_programs/ed25519.zig new file mode 100644 index 000000000..6c5f6a54f --- /dev/null +++ b/src/runtime/program/precompile_programs/ed25519.zig @@ -0,0 +1,204 @@ +const std = @import("std"); +const sig = @import("../../../sig.zig"); +const precompile_programs = sig.runtime.program.precompile_programs; + +const PrecompileProgramError = precompile_programs.PrecompileProgramError; +const getInstructionValue = precompile_programs.getInstructionValue; +const getInstructionData = precompile_programs.getInstructionData; + +const Ed25519 = std.crypto.sign.Ed25519; + +const Pubkey = sig.core.Pubkey; +const InstructionContext = sig.runtime.InstructionContext; + +pub const ED25519_DATA_START = (ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE + + ED25519_SIGNATURE_OFFSETS_START); +pub const ED25519_PUBKEY_SERIALIZED_SIZE = 32; +pub const ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE = 14; +pub const ED25519_SIGNATURE_OFFSETS_START = 2; +pub const ED25519_SIGNATURE_SERIALIZED_SIZE = 64; + +comptime { + std.debug.assert(ED25519_PUBKEY_SERIALIZED_SIZE == Ed25519.PublicKey.encoded_length); + std.debug.assert(ED25519_SIGNATURE_SERIALIZED_SIZE == Ed25519.Signature.encoded_length); + std.debug.assert(ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE == @sizeOf(Ed25519SignatureOffsets)); +} + +pub const Ed25519SignatureOffsets = extern struct { + /// Offset to ed25519 signature of 64 bytes. + signature_offset: u16 = 0, + /// Instruction index to find signature. + signature_instruction_idx: u16 = 0, + /// Offset to public key of 32 bytes. + pubkey_offset: u16 = 0, + /// Instruction index to find public key. + pubkey_instruction_idx: u16 = 0, + /// Offset to start of message data. + message_data_offset: u16 = 0, + /// Size of message data. + message_data_size: u16 = 0, + /// Index of instruction data to get message data. + message_instruction_idx: u16 = 0, +}; + +// https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/ed25519_instruction.rs#L88 +// https://github.com/firedancer-io/firedancer/blob/af74882ffb2c24783a82718dbc5111a94e1b5f6f/src/flamenco/runtime/program/fd_precompiles.c#L118 +pub fn verify( + current_instruction_data: []const u8, + all_instruction_datas: []const []const u8, +) PrecompileProgramError!void { + const data = current_instruction_data; + const n_signatures = data[0]; + if (data.len < ED25519_DATA_START) { + if (data.len == 2 and n_signatures == 0) return; + return error.InvalidInstructionDataSize; + } + if (n_signatures == 0) return error.InvalidInstructionDataSize; + + const expected_data_size = ED25519_SIGNATURE_OFFSETS_START + + n_signatures * ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE; + if (data.len < expected_data_size) return error.InvalidInstructionDataSize; + + // firedancer seems to assume natural alignment in this loop? Our data should be aligned. + for (0..n_signatures) |i| { + const offset = ED25519_SIGNATURE_OFFSETS_START + + i * ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE; + + const sig_offsets: *const Ed25519SignatureOffsets = @alignCast(@ptrCast(data.ptr + offset)); + + const signature = try getInstructionValue( + Ed25519.Signature, + data, + all_instruction_datas, + sig_offsets.signature_instruction_idx, + sig_offsets.signature_offset, + ); + const pubkey = try getInstructionValue( + Ed25519.PublicKey, + data, + all_instruction_datas, + sig_offsets.pubkey_instruction_idx, + sig_offsets.pubkey_offset, + ); + const msg = try getInstructionData( + sig_offsets.message_data_size, + data, + all_instruction_datas, + sig_offsets.message_instruction_idx, + sig_offsets.message_data_offset, + ); + signature.verify(msg, pubkey.*) catch return error.InvalidSignature; + } +} + +pub fn newInstruction( + allocator: std.mem.Allocator, + keypair: Ed25519.KeyPair, + message: []const u8, +) !sig.core.Instruction { + const signature = try keypair.sign(message, null); + + const num_signatures: u8 = 1; + const pubkey_offset = ED25519_DATA_START; + const signature_offset = pubkey_offset + ED25519_PUBKEY_SERIALIZED_SIZE; + const message_data_offset = signature_offset + ED25519_SIGNATURE_SERIALIZED_SIZE; + + const offsets = Ed25519SignatureOffsets{ + .signature_offset = signature_offset, + .signature_instruction_idx = std.math.maxInt(u16), + .pubkey_offset = pubkey_offset, + .pubkey_instruction_idx = std.math.maxInt(u16), + .message_data_offset = message_data_offset, + .message_data_size = message.len, + .message_instruction_idx = std.math.maxInt(u16), + }; + + const instruction_data = try std.ArrayListAligned(u8, 2).initCapacity( + allocator, + message_data_offset + message.len, + ); + errdefer instruction_data.deinit(); + + // add 2nd byte for padding, so that offset structure is aligned + instruction_data.appendSliceAssumeCapacity(.{ num_signatures, 0 }); + instruction_data.appendSliceAssumeCapacity(std.mem.asBytes(&offsets)); + std.debug.assert(instruction_data.len == pubkey_offset); + instruction_data.appendSliceAssumeCapacity(keypair.public_key.toBytes()); + std.debug.assert(instruction_data.len == signature_offset); + instruction_data.appendSliceAssumeCapacity(signature.toBytes()); + std.debug.assert(instruction_data.len == message_data_offset); + instruction_data.appendSliceAssumeCapacity(message); + + return .{ + .program_id = sig.runtime.ids.PRECOMPILE_ED25519_PROGRAM_ID, + .accounts = &.{}, + .data = try instruction_data.toOwnedSlice(), + }; +} + +fn test_case( + allocator: std.mem.Allocator, + num_signatures: u16, + offsets: Ed25519SignatureOffsets, +) (PrecompileProgramError || error{OutOfMemory})!void { + var instruction_data = try std.ArrayListAligned(u8, 2).initCapacity(allocator, ED25519_DATA_START); + defer instruction_data.deinit(); + + instruction_data.appendSliceAssumeCapacity(std.mem.asBytes(&num_signatures)); + instruction_data.appendSliceAssumeCapacity(std.mem.asBytes(&offsets)); + + return try verify(instruction_data.items, &.{}); +} + +test "ed25519 invalid offsets" { + const allocator = std.testing.allocator; + var instruction_data = try std.ArrayListAligned(u8, 2).initCapacity(allocator, ED25519_DATA_START); + defer instruction_data.deinit(); + + const offsets = Ed25519SignatureOffsets{ + .signature_offset = 0, + .signature_instruction_idx = 0, + .pubkey_offset = 0, + .pubkey_instruction_idx = 0, + .message_data_offset = 0, + .message_data_size = 0, + .message_instruction_idx = 0, + }; + + // Set up instruction data with invalid size + instruction_data.appendSliceAssumeCapacity(std.mem.asBytes(&1)); + instruction_data.appendSliceAssumeCapacity(std.mem.asBytes(&offsets)); + try instruction_data.resize(instruction_data.items.len - 1); + + try std.testing.expectEqual( + verify(instruction_data.items, &.{}), + error.InvalidInstructionDataSize, + ); + + // invalid signature instruction index + const invalid_signature_offsets = Ed25519SignatureOffsets{ + .signature_instruction_idx = 1, + }; + try std.testing.expectEqual( + test_case(allocator, 1, invalid_signature_offsets), + error.InvalidDataOffsets, + ); + + // invalid message instruction index + const invalid_message_offsets = Ed25519SignatureOffsets{ + .message_instruction_idx = 1, + }; + try std.testing.expectEqual( + test_case(allocator, 1, invalid_message_offsets), + error.InvalidDataOffsets, + ); + + // invalid public key instruction index + const invalid_pubkey_offsets = Ed25519SignatureOffsets{ + .pubkey_instruction_idx = 1, + }; + try std.testing.expectEqual( + test_case(allocator, 1, invalid_pubkey_offsets), + error.InvalidDataOffsets, + ); +} diff --git a/src/runtime/program/precompile_programs/lib.zig b/src/runtime/program/precompile_programs/lib.zig new file mode 100644 index 000000000..10b5eb251 --- /dev/null +++ b/src/runtime/program/precompile_programs/lib.zig @@ -0,0 +1,128 @@ +const std = @import("std"); +const sig = @import("../../../sig.zig"); + +pub const ed25519 = @import("ed25519.zig"); +pub const secp256k1 = @import("secp256k1.zig"); +pub const secp256r1 = @import("secp256r1.zig"); + +const Ed25519 = std.crypto.sign.Ed25519; +const Keccak256 = std.crypto.hash.sha3.Keccak256; +const Secp256k1 = std.crypto.ecc.Secp256k1; + +const Pubkey = sig.core.Pubkey; + +pub const ed25519Verify = ed25519.verify; +pub const secp256k1Verify = secp256k1.verify; +pub const secp256r1Verify = secp256r1.verify; + +pub const COMPUTE_UNITS = 1; // does this consume compute units? + +// TODO: should be moved to global features file +pub const SECP256R1_FEATURE_ID = + Pubkey.parseBase58String("sr11RdZWgbHTHxSroPALe6zgaT5A1K9LcE4nfsZS4gi") catch unreachable; + +pub const PRECOMPILES = [_]Precompile{ + .{ + .program_id = sig.runtime.ids.PRECOMPILE_ED25519_PROGRAM_ID, + .function = ed25519Verify, + .required_feature = null, + }, + .{ + .program_id = sig.runtime.ids.PRECOMPILE_SECP256K1_PROGRAM_ID, + .function = secp256k1Verify, + .required_feature = null, + }, + .{ + .program_id = sig.runtime.ids.PRECOMPILE_SECP256R1_PROGRAM_ID, + .function = secp256r1Verify, + .required_feature = SECP256R1_FEATURE_ID, + }, +}; + +pub fn verifyPrecompiles( + allocator: std.mem.Allocator, + transaction: sig.core.Transaction, + feature_set: sig.runtime.FeatureSet, +) (PrecompileProgramError || error{OutOfMemory})!void { + // could remove this alloc by passing in the transaction in directly, but maybe less clean + var instruction_datas: ?[]const []const u8 = null; + defer if (instruction_datas) |instr_datas| allocator.free(instr_datas); + + for (transaction.msg.instructions) |instruction| { + const program_id = transaction.msg.account_keys[instruction.program_index]; + for (PRECOMPILES) |precompile| { + if (!precompile.program_id.equals(&program_id)) continue; + + const precompile_feature_enabled = precompile.required_feature == null or + feature_set.active.contains(precompile.required_feature.?); + if (!precompile_feature_enabled) continue; + + const datas = instruction_datas orelse blk: { + const buf = try allocator.alloc([]const u8, transaction.msg.instructions.len); + for (0.., transaction.msg.instructions) |i, instr| buf[i] = instr.data; + instruction_datas = buf; + break :blk buf; + }; + try precompile.function(instruction.data, datas); + } + } +} + +pub const PrecompileFn = fn ( + current_instruction_data: []const u8, + all_instruction_datas: []const []const u8, +) PrecompileProgramError!void; + +pub const Precompile = struct { + program_id: Pubkey, + function: *const PrecompileFn, + required_feature: ?Pubkey, +}; + +// parsed internally +pub const PrecompileProgramInstruction = []const u8; + +// https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/precompile-error/src/lib.rs#L6 +pub const PrecompileProgramError = error{ + InvalidPublicKey, + InvalidRecoveryId, + InvalidSignature, + InvalidDataOffsets, + InvalidInstructionDataSize, +}; + +pub fn getInstructionValue( + T: type, + current_instruction_data: []const u8, + all_instruction_datas: []const []const u8, + instruction_idx: u16, + offset: usize, +) error{ InvalidSignature, InvalidDataOffsets }!*const T { + // aligncast potentially dangerous? + return @alignCast(@ptrCast(try getInstructionData( + @sizeOf(T), + current_instruction_data, + all_instruction_datas, + instruction_idx, + offset, + ))); +} + +// https://github.com/firedancer-io/firedancer/blob/af74882ffb2c24783a82718dbc5111a94e1b5f6f/src/flamenco/runtime/program/fd_precompiles.c#L74 +pub fn getInstructionData( + len: usize, + current_instruction_data: []const u8, + all_instruction_datas: []const []const u8, + instruction_idx: u16, + offset: usize, +) error{ InvalidSignature, InvalidDataOffsets }![]const u8 { + const data: []const u8 = if (instruction_idx == std.math.maxInt(u16)) + current_instruction_data + else data: { + if (instruction_idx >= all_instruction_datas.len) return error.InvalidDataOffsets; + break :data all_instruction_datas[instruction_idx]; + }; + + if (offset + len > data.len) return error.InvalidSignature; + return data[offset..][0..len]; +} diff --git a/src/runtime/program/precompile_programs/secp256k1.zig b/src/runtime/program/precompile_programs/secp256k1.zig new file mode 100644 index 000000000..96fde8650 --- /dev/null +++ b/src/runtime/program/precompile_programs/secp256k1.zig @@ -0,0 +1,75 @@ +const std = @import("std"); +const sig = @import("../../../sig.zig"); +const precompile_programs = sig.runtime.program.precompile_programs; + +const PrecompileProgramError = precompile_programs.PrecompileProgramError; +const getInstructionValue = precompile_programs.getInstructionValue; +const getInstructionData = precompile_programs.getInstructionData; + +const Keccak256 = std.crypto.hash.sha3.Keccak256; +const Secp256k1 = std.crypto.ecc.Secp256k1; + +pub const SECP256K1_DATA_START = (SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE + SECP256K1_SIGNATURE_OFFSETS_START); +pub const SECP256K1_PUBKEY_SERIALIZED_SIZE = 20; +pub const SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE = 11; +pub const SECP256K1_SIGNATURE_OFFSETS_START = 1; +pub const SECP256K1_SIGNATURE_SERIALIZED_SIZE = 64; + +pub const Secp256k1SignatureOffsets = extern struct { + /// Offset to 64-byte signature plus 1-byte recovery ID. + signature_offset: u16, + /// Within the transaction, the index of the instruction whose instruction data contains the signature. + signature_instruction_idx: u8, + /// Offset to 20-byte Ethereum address. + eth_address_offset: u16, + /// Within the transaction, the index of the instruction whose instruction data contains the address. + eth_address_instruction_idx: u8, + /// Offset to start of message data. + message_data_offset: u16, + /// Size of message data in bytes. + message_data_size: u16, + /// Within the transaction, the index of the instruction whose instruction data contains the message. + message_instruction_idx: u8, +}; + +comptime { + // std.debug.assert(SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE == @sizeOf(Secp256k1SignatureOffsets)); +} + +// https://github.com/firedancer-io/firedancer/blob/af74882ffb2c24783a82718dbc5111a94e1b5f6f/src/flamenco/runtime/program/fd_precompiles.c#L227 +// https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/secp256k1_instruction.rs#L925 +pub fn verify( + current_instruction_data: []const u8, + all_instruction_datas: []const []const u8, +) PrecompileProgramError!void { + _ = all_instruction_datas; + + const data = current_instruction_data; + const n_signatures = data[0]; + if (data.len < SECP256K1_DATA_START) { + if (data.len == 1 and n_signatures == 0) return; + return error.InvalidInstructionDataSize; + } + if (n_signatures == 0 and data.len > 1) return error.InvalidInstructionDataSize; + + const expected_data_size = SECP256K1_SIGNATURE_OFFSETS_START + + n_signatures * SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE; + if (data.len < expected_data_size) return error.InvalidInstructionDataSize; + + // firedancer seems to assume natural alignment in this loop? Need to prove it to myself. + for (0..n_signatures) |i| { + const offset = SECP256K1_SIGNATURE_OFFSETS_START + + i * SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE; + const sig_offsets: *const Secp256k1SignatureOffsets = @alignCast(@ptrCast(data.ptr + offset)); + + // const signature = try getInstructionValue( + // SECP256K1_SIGNATURE_SERIALIZED_SIZE, // + recovery id + // data, + // all_instruction_datas, + // sig_offsets.signature_instruction_idx, + // sig_offsets.signature_offset, + // ); + _ = sig_offsets; + @panic("TODO"); + } +} diff --git a/src/runtime/program/precompile_programs/secp256r1.zig b/src/runtime/program/precompile_programs/secp256r1.zig new file mode 100644 index 000000000..265b2e44e --- /dev/null +++ b/src/runtime/program/precompile_programs/secp256r1.zig @@ -0,0 +1,17 @@ +const std = @import("std"); +const sig = @import("../../../sig.zig"); +const precompile_programs = sig.runtime.program.precompile_programs; + +const PrecompileProgramError = precompile_programs.PrecompileProgramError; +const getInstructionValue = precompile_programs.getInstructionValue; +const getInstructionData = precompile_programs.getInstructionData; + +// I don't see where this function is in firedancer? +pub fn verify( + current_instruction_data: []const u8, + all_instruction_datas: []const []const u8, +) PrecompileProgramError!void { + _ = current_instruction_data; + _ = all_instruction_datas; + @panic("TODO"); +} diff --git a/src/tests.zig b/src/tests.zig index 6655a374c..b51fc6e86 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -4,6 +4,7 @@ const sig = @import("sig.zig"); comptime { refAllDeclsRecursive(sig, 2); refAllDeclsRecursive(sig.ledger, 2); + refAllDeclsRecursive(sig.runtime.program, 2); } /// Like std.testing.refAllDeclsRecursive, except: From 3be8261360f3c2388bc116db4f14ed1c576bef78 Mon Sep 17 00:00:00 2001 From: Sobeston <15335529+Sobeston@users.noreply.github.com> Date: Mon, 24 Feb 2025 18:53:42 +0000 Subject: [PATCH 04/16] wip / expand testing --- .../program/precompile_programs/ed25519.zig | 123 ++++++++++++++++-- .../program/precompile_programs/lib.zig | 4 +- .../program/precompile_programs/secp256r1.zig | 3 +- 3 files changed, 117 insertions(+), 13 deletions(-) diff --git a/src/runtime/program/precompile_programs/ed25519.zig b/src/runtime/program/precompile_programs/ed25519.zig index 6c5f6a54f..d24b4eef1 100644 --- a/src/runtime/program/precompile_programs/ed25519.zig +++ b/src/runtime/program/precompile_programs/ed25519.zig @@ -91,6 +91,7 @@ pub fn verify( } } +// https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/ed25519_instruction.rs#L35 pub fn newInstruction( allocator: std.mem.Allocator, keypair: Ed25519.KeyPair, @@ -136,6 +137,7 @@ pub fn newInstruction( }; } +// https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/ed25519_instruction.rs#L258 fn test_case( allocator: std.mem.Allocator, num_signatures: u16, @@ -147,7 +149,7 @@ fn test_case( instruction_data.appendSliceAssumeCapacity(std.mem.asBytes(&num_signatures)); instruction_data.appendSliceAssumeCapacity(std.mem.asBytes(&offsets)); - return try verify(instruction_data.items, &.{}); + return try verify(instruction_data.items, &.{&(.{0} ** 100)}); } test "ed25519 invalid offsets" { @@ -155,15 +157,7 @@ test "ed25519 invalid offsets" { var instruction_data = try std.ArrayListAligned(u8, 2).initCapacity(allocator, ED25519_DATA_START); defer instruction_data.deinit(); - const offsets = Ed25519SignatureOffsets{ - .signature_offset = 0, - .signature_instruction_idx = 0, - .pubkey_offset = 0, - .pubkey_instruction_idx = 0, - .message_data_offset = 0, - .message_data_size = 0, - .message_instruction_idx = 0, - }; + const offsets = Ed25519SignatureOffsets{}; // Set up instruction data with invalid size instruction_data.appendSliceAssumeCapacity(std.mem.asBytes(&1)); @@ -202,3 +196,112 @@ test "ed25519 invalid offsets" { error.InvalidDataOffsets, ); } + +// https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/ed25519_instruction.rs#L326 +test "ed25519 message data offsets" { + { + const offsets = Ed25519SignatureOffsets{ + .message_data_offset = 99, + .message_data_size = 1, + }; + try std.testing.expectEqual( + test_case(std.testing.allocator, 1, offsets), + error.InvalidSignature, + ); + } + + { + const offsets = Ed25519SignatureOffsets{ + .message_data_offset = 100, + .message_data_size = 1, + }; + try std.testing.expectEqual( + test_case(std.testing.allocator, 1, offsets), + error.InvalidDataOffsets, + ); + } + + { + const offsets = Ed25519SignatureOffsets{ + .message_data_offset = 100, + .message_data_size = 1000, + }; + try std.testing.expectEqual( + test_case(std.testing.allocator, 1, offsets), + error.InvalidDataOffsets, + ); + } + + { + const offsets = Ed25519SignatureOffsets{ + .message_data_offset = std.math.maxInt(u16), + .message_data_size = std.math.maxInt(u16), + }; + try std.testing.expectEqual( + test_case(std.testing.allocator, 1, offsets), + error.InvalidDataOffsets, + ); + } +} + +// https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/ed25519_instruction.rs#L369 +test "ed25519 pubkey offset" { + { + const offsets = Ed25519SignatureOffsets{ + .pubkey_offset = std.math.maxInt(u16), + }; + try std.testing.expectEqual( + test_case(std.testing.allocator, 1, offsets), + error.InvalidDataOffsets, + ); + } + + { + const offsets = Ed25519SignatureOffsets{ + .pubkey_offset = 100 - ED25519_PUBKEY_SERIALIZED_SIZE + 1, + }; + try std.testing.expectEqual( + test_case(std.testing.allocator, 1, offsets), + error.InvalidDataOffsets, + ); + } +} + +// https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/ed25519_instruction.rs#L389-L390 +test "ed25519 signature offset" { + { + const offsets = Ed25519SignatureOffsets{ + .signature_offset = std.math.maxInt(u16), + }; + try std.testing.expectEqual( + test_case(std.testing.allocator, 1, offsets), + error.InvalidDataOffsets, + ); + } + + { + const offsets = Ed25519SignatureOffsets{ + .signature_offset = 100 - ED25519_SIGNATURE_SERIALIZED_SIZE + 1, + }; + try std.testing.expectEqual( + test_case(std.testing.allocator, 1, offsets), + error.InvalidDataOffsets, + ); + } +} +// // https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/ed25519_instruction.rs#L411 +// test "ed25519" { +// const keypair = try Ed25519.KeyPair.create(null); + +// var instruction = try newInstruction(std.testing.allocator, keypair, "hello"); + +// const mint_keypair = try Ed25519.KeyPair.create(null); + +// const feature_set = sig.runtime.FeatureSet.EMPTY; // should be all enabled to match agave, but doesn't mattter +// const Transaction = sig.core.transaction.Transaction; + +// var tx = Transaction { +// .signatures = +// } + +// } diff --git a/src/runtime/program/precompile_programs/lib.zig b/src/runtime/program/precompile_programs/lib.zig index 10b5eb251..42dca1926 100644 --- a/src/runtime/program/precompile_programs/lib.zig +++ b/src/runtime/program/precompile_programs/lib.zig @@ -115,7 +115,7 @@ pub fn getInstructionData( all_instruction_datas: []const []const u8, instruction_idx: u16, offset: usize, -) error{ InvalidSignature, InvalidDataOffsets }![]const u8 { +) error{InvalidDataOffsets}![]const u8 { const data: []const u8 = if (instruction_idx == std.math.maxInt(u16)) current_instruction_data else data: { @@ -123,6 +123,6 @@ pub fn getInstructionData( break :data all_instruction_datas[instruction_idx]; }; - if (offset + len > data.len) return error.InvalidSignature; + if (offset + len > data.len) return error.InvalidDataOffsets; return data[offset..][0..len]; } diff --git a/src/runtime/program/precompile_programs/secp256r1.zig b/src/runtime/program/precompile_programs/secp256r1.zig index 265b2e44e..36e7263d6 100644 --- a/src/runtime/program/precompile_programs/secp256r1.zig +++ b/src/runtime/program/precompile_programs/secp256r1.zig @@ -6,7 +6,8 @@ const PrecompileProgramError = precompile_programs.PrecompileProgramError; const getInstructionValue = precompile_programs.getInstructionValue; const getInstructionData = precompile_programs.getInstructionData; -// I don't see where this function is in firedancer? +// firedancer puts this one behind an ifdef. Maybe we don't need it? +//https://github.com/firedancer-io/firedancer/blob/49056135a4c7ba024cb75a45925439239904238b/src/flamenco/runtime/program/fd_precompiles.c#L376pub fn verify( pub fn verify( current_instruction_data: []const u8, all_instruction_datas: []const []const u8, From f675808fad017cba2abd6459c0a28a82cafdefdb Mon Sep 17 00:00:00 2001 From: Sobeston <15335529+Sobeston@users.noreply.github.com> Date: Mon, 24 Feb 2025 22:58:13 +0000 Subject: [PATCH 05/16] flesh out secp256k1 --- .../program/precompile_programs/ed25519.zig | 21 +--- .../program/precompile_programs/lib.zig | 5 +- .../program/precompile_programs/secp256k1.zig | 109 +++++++++++++++--- 3 files changed, 97 insertions(+), 38 deletions(-) diff --git a/src/runtime/program/precompile_programs/ed25519.zig b/src/runtime/program/precompile_programs/ed25519.zig index d24b4eef1..5ddf58765 100644 --- a/src/runtime/program/precompile_programs/ed25519.zig +++ b/src/runtime/program/precompile_programs/ed25519.zig @@ -11,8 +11,8 @@ const Ed25519 = std.crypto.sign.Ed25519; const Pubkey = sig.core.Pubkey; const InstructionContext = sig.runtime.InstructionContext; -pub const ED25519_DATA_START = (ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE + - ED25519_SIGNATURE_OFFSETS_START); +pub const ED25519_DATA_START = ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE + + ED25519_SIGNATURE_OFFSETS_START; pub const ED25519_PUBKEY_SERIALIZED_SIZE = 32; pub const ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE = 14; pub const ED25519_SIGNATURE_OFFSETS_START = 2; @@ -152,6 +152,7 @@ fn test_case( return try verify(instruction_data.items, &.{&(.{0} ** 100)}); } +// https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/ed25519_instruction.rs#L279 test "ed25519 invalid offsets" { const allocator = std.testing.allocator; var instruction_data = try std.ArrayListAligned(u8, 2).initCapacity(allocator, ED25519_DATA_START); @@ -289,19 +290,5 @@ test "ed25519 signature offset" { ); } } -// // https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/ed25519_instruction.rs#L411 -// test "ed25519" { -// const keypair = try Ed25519.KeyPair.create(null); -// var instruction = try newInstruction(std.testing.allocator, keypair, "hello"); - -// const mint_keypair = try Ed25519.KeyPair.create(null); - -// const feature_set = sig.runtime.FeatureSet.EMPTY; // should be all enabled to match agave, but doesn't mattter -// const Transaction = sig.core.transaction.Transaction; - -// var tx = Transaction { -// .signatures = -// } - -// } +// TODO: should we implement https://github.com/anza-xyz/agave/pull/1876/ ? diff --git a/src/runtime/program/precompile_programs/lib.zig b/src/runtime/program/precompile_programs/lib.zig index 42dca1926..ba776a77d 100644 --- a/src/runtime/program/precompile_programs/lib.zig +++ b/src/runtime/program/precompile_programs/lib.zig @@ -15,7 +15,7 @@ pub const ed25519Verify = ed25519.verify; pub const secp256k1Verify = secp256k1.verify; pub const secp256r1Verify = secp256r1.verify; -pub const COMPUTE_UNITS = 1; // does this consume compute units? +pub const COMPUTE_UNITS = 0; // does this consume compute units? // TODO: should be moved to global features file pub const SECP256R1_FEATURE_ID = @@ -82,6 +82,7 @@ pub const Precompile = struct { // parsed internally pub const PrecompileProgramInstruction = []const u8; +// custom errors // https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/precompile-error/src/lib.rs#L6 pub const PrecompileProgramError = error{ InvalidPublicKey, @@ -97,7 +98,7 @@ pub fn getInstructionValue( all_instruction_datas: []const []const u8, instruction_idx: u16, offset: usize, -) error{ InvalidSignature, InvalidDataOffsets }!*const T { +) error{InvalidDataOffsets}!*const T { // aligncast potentially dangerous? return @alignCast(@ptrCast(try getInstructionData( @sizeOf(T), diff --git a/src/runtime/program/precompile_programs/secp256k1.zig b/src/runtime/program/precompile_programs/secp256k1.zig index 96fde8650..c421e5519 100644 --- a/src/runtime/program/precompile_programs/secp256k1.zig +++ b/src/runtime/program/precompile_programs/secp256k1.zig @@ -8,14 +8,23 @@ const getInstructionData = precompile_programs.getInstructionData; const Keccak256 = std.crypto.hash.sha3.Keccak256; const Secp256k1 = std.crypto.ecc.Secp256k1; +const Ecdsa = std.crypto.sign.ecdsa.Ecdsa(Secp256k1, Keccak256); +const Scalar = Secp256k1.scalar.Scalar; +const Message = Scalar; +const Field = Secp256k1.Fe; -pub const SECP256K1_DATA_START = (SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE + SECP256K1_SIGNATURE_OFFSETS_START); +pub const SECP256K1_DATA_START = SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE + + SECP256K1_SIGNATURE_OFFSETS_START; pub const SECP256K1_PUBKEY_SERIALIZED_SIZE = 20; pub const SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE = 11; pub const SECP256K1_SIGNATURE_OFFSETS_START = 1; pub const SECP256K1_SIGNATURE_SERIALIZED_SIZE = 64; -pub const Secp256k1SignatureOffsets = extern struct { +comptime { + std.debug.assert(SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE == @bitSizeOf(Secp256k1SignatureOffsets) / 8); +} + +pub const Secp256k1SignatureOffsets = packed struct { /// Offset to 64-byte signature plus 1-byte recovery ID. signature_offset: u16, /// Within the transaction, the index of the instruction whose instruction data contains the signature. @@ -32,24 +41,20 @@ pub const Secp256k1SignatureOffsets = extern struct { message_instruction_idx: u8, }; -comptime { - // std.debug.assert(SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE == @sizeOf(Secp256k1SignatureOffsets)); -} - // https://github.com/firedancer-io/firedancer/blob/af74882ffb2c24783a82718dbc5111a94e1b5f6f/src/flamenco/runtime/program/fd_precompiles.c#L227 // https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/secp256k1_instruction.rs#L925 pub fn verify( current_instruction_data: []const u8, all_instruction_datas: []const []const u8, ) PrecompileProgramError!void { - _ = all_instruction_datas; - const data = current_instruction_data; - const n_signatures = data[0]; + if (data.len < SECP256K1_DATA_START) { - if (data.len == 1 and n_signatures == 0) return; + if (data.len == 1 and data[0] == 0) return; // success return error.InvalidInstructionDataSize; } + + const n_signatures = data[0]; if (n_signatures == 0 and data.len > 1) return error.InvalidInstructionDataSize; const expected_data_size = SECP256K1_SIGNATURE_OFFSETS_START + @@ -62,14 +67,80 @@ pub fn verify( i * SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE; const sig_offsets: *const Secp256k1SignatureOffsets = @alignCast(@ptrCast(data.ptr + offset)); - // const signature = try getInstructionValue( - // SECP256K1_SIGNATURE_SERIALIZED_SIZE, // + recovery id - // data, - // all_instruction_datas, - // sig_offsets.signature_instruction_idx, - // sig_offsets.signature_offset, - // ); - _ = sig_offsets; - @panic("TODO"); + const signature_slice = try getInstructionData( + SECP256K1_SIGNATURE_SERIALIZED_SIZE + 1, // + recovery_id + data, + all_instruction_datas, + sig_offsets.signature_instruction_idx, + sig_offsets.signature_offset, + ); + + const recovery_id = signature_slice[SECP256K1_SIGNATURE_SERIALIZED_SIZE]; + // https://docs.rs/libsecp256k1/0.6.0/src/libsecp256k1/lib.rs.html#674-680 + if (recovery_id > 4) return error.InvalidRecoveryId; + const signature: *const Ecdsa.Signature = @ptrCast(signature_slice[0..SECP256K1_SIGNATURE_SERIALIZED_SIZE]); + + const eth_address = try getInstructionData( + SECP256K1_PUBKEY_SERIALIZED_SIZE, + data, + all_instruction_datas, + sig_offsets.eth_address_instruction_idx, + sig_offsets.eth_address_offset, + ); + + const msg = try getInstructionData( + sig_offsets.message_data_size, + data, + all_instruction_datas, + sig_offsets.message_instruction_idx, + sig_offsets.message_data_offset, + ); + + var msg_hash: [Keccak256.digest_length]u8 = undefined; + Keccak256.hash(msg, &msg_hash, .{}); + + comptime { + std.debug.assert(Keccak256.digest_length == 32); + } + + const msg_scalar = Secp256k1.scalar.Scalar.fromBytes(msg_hash, .little) catch @panic("handle this"); + + const pubkey = try recoverPubkey(&msg_scalar, signature, recovery_id); + const recovered_eth_address = constructEthPubkey(pubkey); + + if (!std.mem.eql(u8, eth_address, &recovered_eth_address)) { + return error.InvalidSignature; + } } } +// https://docs.rs/libsecp256k1/0.6.0/src/libsecp256k1/lib.rs.html#764-770 +fn recoverPubkey( + message: *const Message, + signature: *const Ecdsa.Signature, + recovery_id: u8, +) error{InvalidSignature}!Ecdsa.PublicKey { + std.debug.assert(recovery_id < 4); + _ = message; + + if (std.mem.allEqual(u8, &signature.r, 0)) return error.InvalidSignature; + if (std.mem.allEqual(u8, &signature.s, 0)) return error.InvalidSignature; + + // I think zig std doesn't quite support the necessary operations + @panic("TODO"); +} + +/// https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/secp256k1_instruction.rs#L903 +/// https://ethereum.org/en/developers/docs/accounts/#keyfiles +/// > The public key is generated from the private key using +/// the Elliptic Curve Digital Signature Algorithm(opens in a +/// new tab). You get a public address for your account by +/// taking the last 20 bytes of the Keccak-256 hash of the +/// public key +fn constructEthPubkey( + pubkey: Ecdsa.PublicKey, +) [SECP256K1_PUBKEY_SERIALIZED_SIZE]u8 { + var pubkey_hash: [Keccak256.digest_length]u8 = undefined; + const full_pubkey_bytes = pubkey.toUncompressedSec1(); + Keccak256.hash(full_pubkey_bytes[1..], &pubkey_hash, .{}); + return pubkey_hash[12..32].*; +} From 9b7bd90a61670e03fcedfdde1389925698c516fd Mon Sep 17 00:00:00 2001 From: Sobeston <15335529+Sobeston@users.noreply.github.com> Date: Mon, 24 Feb 2025 23:06:06 +0000 Subject: [PATCH 06/16] fix style --- .../program/precompile_programs/ed25519.zig | 13 ++++++++----- src/runtime/program/precompile_programs/lib.zig | 4 ---- .../program/precompile_programs/secp256k1.zig | 16 +++++++++++----- .../program/precompile_programs/secp256r1.zig | 2 -- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/runtime/program/precompile_programs/ed25519.zig b/src/runtime/program/precompile_programs/ed25519.zig index 5ddf58765..7863bac9f 100644 --- a/src/runtime/program/precompile_programs/ed25519.zig +++ b/src/runtime/program/precompile_programs/ed25519.zig @@ -8,9 +8,6 @@ const getInstructionData = precompile_programs.getInstructionData; const Ed25519 = std.crypto.sign.Ed25519; -const Pubkey = sig.core.Pubkey; -const InstructionContext = sig.runtime.InstructionContext; - pub const ED25519_DATA_START = ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE + ED25519_SIGNATURE_OFFSETS_START; pub const ED25519_PUBKEY_SERIALIZED_SIZE = 32; @@ -143,7 +140,10 @@ fn test_case( num_signatures: u16, offsets: Ed25519SignatureOffsets, ) (PrecompileProgramError || error{OutOfMemory})!void { - var instruction_data = try std.ArrayListAligned(u8, 2).initCapacity(allocator, ED25519_DATA_START); + var instruction_data = try std.ArrayListAligned(u8, 2).initCapacity( + allocator, + ED25519_DATA_START, + ); defer instruction_data.deinit(); instruction_data.appendSliceAssumeCapacity(std.mem.asBytes(&num_signatures)); @@ -155,7 +155,10 @@ fn test_case( // https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/ed25519_instruction.rs#L279 test "ed25519 invalid offsets" { const allocator = std.testing.allocator; - var instruction_data = try std.ArrayListAligned(u8, 2).initCapacity(allocator, ED25519_DATA_START); + var instruction_data = try std.ArrayListAligned(u8, 2).initCapacity( + allocator, + ED25519_DATA_START, + ); defer instruction_data.deinit(); const offsets = Ed25519SignatureOffsets{}; diff --git a/src/runtime/program/precompile_programs/lib.zig b/src/runtime/program/precompile_programs/lib.zig index ba776a77d..0a381f07f 100644 --- a/src/runtime/program/precompile_programs/lib.zig +++ b/src/runtime/program/precompile_programs/lib.zig @@ -5,10 +5,6 @@ pub const ed25519 = @import("ed25519.zig"); pub const secp256k1 = @import("secp256k1.zig"); pub const secp256r1 = @import("secp256r1.zig"); -const Ed25519 = std.crypto.sign.Ed25519; -const Keccak256 = std.crypto.hash.sha3.Keccak256; -const Secp256k1 = std.crypto.ecc.Secp256k1; - const Pubkey = sig.core.Pubkey; pub const ed25519Verify = ed25519.verify; diff --git a/src/runtime/program/precompile_programs/secp256k1.zig b/src/runtime/program/precompile_programs/secp256k1.zig index c421e5519..50bed26cb 100644 --- a/src/runtime/program/precompile_programs/secp256k1.zig +++ b/src/runtime/program/precompile_programs/secp256k1.zig @@ -3,7 +3,6 @@ const sig = @import("../../../sig.zig"); const precompile_programs = sig.runtime.program.precompile_programs; const PrecompileProgramError = precompile_programs.PrecompileProgramError; -const getInstructionValue = precompile_programs.getInstructionValue; const getInstructionData = precompile_programs.getInstructionData; const Keccak256 = std.crypto.hash.sha3.Keccak256; @@ -21,7 +20,8 @@ pub const SECP256K1_SIGNATURE_OFFSETS_START = 1; pub const SECP256K1_SIGNATURE_SERIALIZED_SIZE = 64; comptime { - std.debug.assert(SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE == @bitSizeOf(Secp256k1SignatureOffsets) / 8); + std.debug.assert(SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE == + @bitSizeOf(Secp256k1SignatureOffsets) / 8); } pub const Secp256k1SignatureOffsets = packed struct { @@ -65,7 +65,9 @@ pub fn verify( for (0..n_signatures) |i| { const offset = SECP256K1_SIGNATURE_OFFSETS_START + i * SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE; - const sig_offsets: *const Secp256k1SignatureOffsets = @alignCast(@ptrCast(data.ptr + offset)); + const sig_offsets: *const Secp256k1SignatureOffsets = @alignCast( + @ptrCast(data.ptr + offset), + ); const signature_slice = try getInstructionData( SECP256K1_SIGNATURE_SERIALIZED_SIZE + 1, // + recovery_id @@ -78,7 +80,9 @@ pub fn verify( const recovery_id = signature_slice[SECP256K1_SIGNATURE_SERIALIZED_SIZE]; // https://docs.rs/libsecp256k1/0.6.0/src/libsecp256k1/lib.rs.html#674-680 if (recovery_id > 4) return error.InvalidRecoveryId; - const signature: *const Ecdsa.Signature = @ptrCast(signature_slice[0..SECP256K1_SIGNATURE_SERIALIZED_SIZE]); + const signature: *const Ecdsa.Signature = @ptrCast( + signature_slice[0..SECP256K1_SIGNATURE_SERIALIZED_SIZE], + ); const eth_address = try getInstructionData( SECP256K1_PUBKEY_SERIALIZED_SIZE, @@ -103,7 +107,7 @@ pub fn verify( std.debug.assert(Keccak256.digest_length == 32); } - const msg_scalar = Secp256k1.scalar.Scalar.fromBytes(msg_hash, .little) catch @panic("handle this"); + const msg_scalar = Secp256k1.scalar.Scalar.fromBytes(msg_hash, .little) catch unreachable; const pubkey = try recoverPubkey(&msg_scalar, signature, recovery_id); const recovered_eth_address = constructEthPubkey(pubkey); @@ -125,6 +129,8 @@ fn recoverPubkey( if (std.mem.allEqual(u8, &signature.r, 0)) return error.InvalidSignature; if (std.mem.allEqual(u8, &signature.s, 0)) return error.InvalidSignature; + _ = Field; + // I think zig std doesn't quite support the necessary operations @panic("TODO"); } diff --git a/src/runtime/program/precompile_programs/secp256r1.zig b/src/runtime/program/precompile_programs/secp256r1.zig index 36e7263d6..5c9e26eb0 100644 --- a/src/runtime/program/precompile_programs/secp256r1.zig +++ b/src/runtime/program/precompile_programs/secp256r1.zig @@ -3,8 +3,6 @@ const sig = @import("../../../sig.zig"); const precompile_programs = sig.runtime.program.precompile_programs; const PrecompileProgramError = precompile_programs.PrecompileProgramError; -const getInstructionValue = precompile_programs.getInstructionValue; -const getInstructionData = precompile_programs.getInstructionData; // firedancer puts this one behind an ifdef. Maybe we don't need it? //https://github.com/firedancer-io/firedancer/blob/49056135a4c7ba024cb75a45925439239904238b/src/flamenco/runtime/program/fd_precompiles.c#L376pub fn verify( From 25561b0742df03d9867dc1c36bd14319de69e0c0 Mon Sep 17 00:00:00 2001 From: Sobeston <15335529+Sobeston@users.noreply.github.com> Date: Tue, 25 Feb 2025 00:24:20 +0000 Subject: [PATCH 07/16] add note on secpr1 --- src/runtime/program/precompile_programs/secp256k1.zig | 5 +++-- src/runtime/program/precompile_programs/secp256r1.zig | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/runtime/program/precompile_programs/secp256k1.zig b/src/runtime/program/precompile_programs/secp256k1.zig index 50bed26cb..951dfd17e 100644 --- a/src/runtime/program/precompile_programs/secp256k1.zig +++ b/src/runtime/program/precompile_programs/secp256k1.zig @@ -118,6 +118,7 @@ pub fn verify( } } // https://docs.rs/libsecp256k1/0.6.0/src/libsecp256k1/lib.rs.html#764-770 +// https://github.com/firedancer-io/firedancer/blob/341bba05a3a7ca18d3d550d6b58c1b6a9207184f/src/ballet/secp256k1/fd_secp256k1.c#L7 fn recoverPubkey( message: *const Message, signature: *const Ecdsa.Signature, @@ -146,7 +147,7 @@ fn constructEthPubkey( pubkey: Ecdsa.PublicKey, ) [SECP256K1_PUBKEY_SERIALIZED_SIZE]u8 { var pubkey_hash: [Keccak256.digest_length]u8 = undefined; - const full_pubkey_bytes = pubkey.toUncompressedSec1(); - Keccak256.hash(full_pubkey_bytes[1..], &pubkey_hash, .{}); + const serialised_pubkey = pubkey.toUncompressedSec1(); + Keccak256.hash(serialised_pubkey[1..], &pubkey_hash, .{}); return pubkey_hash[12..32].*; } diff --git a/src/runtime/program/precompile_programs/secp256r1.zig b/src/runtime/program/precompile_programs/secp256r1.zig index 5c9e26eb0..1d4a817ce 100644 --- a/src/runtime/program/precompile_programs/secp256r1.zig +++ b/src/runtime/program/precompile_programs/secp256r1.zig @@ -4,8 +4,10 @@ const precompile_programs = sig.runtime.program.precompile_programs; const PrecompileProgramError = precompile_programs.PrecompileProgramError; -// firedancer puts this one behind an ifdef. Maybe we don't need it? -//https://github.com/firedancer-io/firedancer/blob/49056135a4c7ba024cb75a45925439239904238b/src/flamenco/runtime/program/fd_precompiles.c#L376pub fn verify( +// Part of SIMD-0075, which is accepted. +// Firedancer puts this one behind an ifdef. Maybe we don't need it yet? +// https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0075-precompile-for-secp256r1-sigverify.md +// https://github.com/firedancer-io/firedancer/blob/49056135a4c7ba024cb75a45925439239904238b/src/flamenco/runtime/program/fd_precompiles.c#L376pub fn verify( pub fn verify( current_instruction_data: []const u8, all_instruction_datas: []const []const u8, From 477ec716a46aeb27666977e92a8369c66eaa7b1e Mon Sep 17 00:00:00 2001 From: Sobeston <15335529+Sobeston@users.noreply.github.com> Date: Tue, 25 Feb 2025 00:29:45 +0000 Subject: [PATCH 08/16] remove secp256k1 implementation - moving it to followup PR --- .../program/precompile_programs/secp256k1.zig | 107 +----------------- 1 file changed, 2 insertions(+), 105 deletions(-) diff --git a/src/runtime/program/precompile_programs/secp256k1.zig b/src/runtime/program/precompile_programs/secp256k1.zig index 951dfd17e..2990f428d 100644 --- a/src/runtime/program/precompile_programs/secp256k1.zig +++ b/src/runtime/program/precompile_programs/secp256k1.zig @@ -3,14 +3,10 @@ const sig = @import("../../../sig.zig"); const precompile_programs = sig.runtime.program.precompile_programs; const PrecompileProgramError = precompile_programs.PrecompileProgramError; -const getInstructionData = precompile_programs.getInstructionData; const Keccak256 = std.crypto.hash.sha3.Keccak256; const Secp256k1 = std.crypto.ecc.Secp256k1; const Ecdsa = std.crypto.sign.ecdsa.Ecdsa(Secp256k1, Keccak256); -const Scalar = Secp256k1.scalar.Scalar; -const Message = Scalar; -const Field = Secp256k1.Fe; pub const SECP256K1_DATA_START = SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE + SECP256K1_SIGNATURE_OFFSETS_START; @@ -47,107 +43,8 @@ pub fn verify( current_instruction_data: []const u8, all_instruction_datas: []const []const u8, ) PrecompileProgramError!void { - const data = current_instruction_data; + _ = current_instruction_data; + _ = all_instruction_datas; - if (data.len < SECP256K1_DATA_START) { - if (data.len == 1 and data[0] == 0) return; // success - return error.InvalidInstructionDataSize; - } - - const n_signatures = data[0]; - if (n_signatures == 0 and data.len > 1) return error.InvalidInstructionDataSize; - - const expected_data_size = SECP256K1_SIGNATURE_OFFSETS_START + - n_signatures * SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE; - if (data.len < expected_data_size) return error.InvalidInstructionDataSize; - - // firedancer seems to assume natural alignment in this loop? Need to prove it to myself. - for (0..n_signatures) |i| { - const offset = SECP256K1_SIGNATURE_OFFSETS_START + - i * SECP256K1_SIGNATURE_OFFSETS_SERIALIZED_SIZE; - const sig_offsets: *const Secp256k1SignatureOffsets = @alignCast( - @ptrCast(data.ptr + offset), - ); - - const signature_slice = try getInstructionData( - SECP256K1_SIGNATURE_SERIALIZED_SIZE + 1, // + recovery_id - data, - all_instruction_datas, - sig_offsets.signature_instruction_idx, - sig_offsets.signature_offset, - ); - - const recovery_id = signature_slice[SECP256K1_SIGNATURE_SERIALIZED_SIZE]; - // https://docs.rs/libsecp256k1/0.6.0/src/libsecp256k1/lib.rs.html#674-680 - if (recovery_id > 4) return error.InvalidRecoveryId; - const signature: *const Ecdsa.Signature = @ptrCast( - signature_slice[0..SECP256K1_SIGNATURE_SERIALIZED_SIZE], - ); - - const eth_address = try getInstructionData( - SECP256K1_PUBKEY_SERIALIZED_SIZE, - data, - all_instruction_datas, - sig_offsets.eth_address_instruction_idx, - sig_offsets.eth_address_offset, - ); - - const msg = try getInstructionData( - sig_offsets.message_data_size, - data, - all_instruction_datas, - sig_offsets.message_instruction_idx, - sig_offsets.message_data_offset, - ); - - var msg_hash: [Keccak256.digest_length]u8 = undefined; - Keccak256.hash(msg, &msg_hash, .{}); - - comptime { - std.debug.assert(Keccak256.digest_length == 32); - } - - const msg_scalar = Secp256k1.scalar.Scalar.fromBytes(msg_hash, .little) catch unreachable; - - const pubkey = try recoverPubkey(&msg_scalar, signature, recovery_id); - const recovered_eth_address = constructEthPubkey(pubkey); - - if (!std.mem.eql(u8, eth_address, &recovered_eth_address)) { - return error.InvalidSignature; - } - } -} -// https://docs.rs/libsecp256k1/0.6.0/src/libsecp256k1/lib.rs.html#764-770 -// https://github.com/firedancer-io/firedancer/blob/341bba05a3a7ca18d3d550d6b58c1b6a9207184f/src/ballet/secp256k1/fd_secp256k1.c#L7 -fn recoverPubkey( - message: *const Message, - signature: *const Ecdsa.Signature, - recovery_id: u8, -) error{InvalidSignature}!Ecdsa.PublicKey { - std.debug.assert(recovery_id < 4); - _ = message; - - if (std.mem.allEqual(u8, &signature.r, 0)) return error.InvalidSignature; - if (std.mem.allEqual(u8, &signature.s, 0)) return error.InvalidSignature; - - _ = Field; - - // I think zig std doesn't quite support the necessary operations @panic("TODO"); } - -/// https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/secp256k1_instruction.rs#L903 -/// https://ethereum.org/en/developers/docs/accounts/#keyfiles -/// > The public key is generated from the private key using -/// the Elliptic Curve Digital Signature Algorithm(opens in a -/// new tab). You get a public address for your account by -/// taking the last 20 bytes of the Keccak-256 hash of the -/// public key -fn constructEthPubkey( - pubkey: Ecdsa.PublicKey, -) [SECP256K1_PUBKEY_SERIALIZED_SIZE]u8 { - var pubkey_hash: [Keccak256.digest_length]u8 = undefined; - const serialised_pubkey = pubkey.toUncompressedSec1(); - Keccak256.hash(serialised_pubkey[1..], &pubkey_hash, .{}); - return pubkey_hash[12..32].*; -} From f63be6d90df6e34b59fb2b49295952a8c6356adc Mon Sep 17 00:00:00 2001 From: Sobeston <15335529+Sobeston@users.noreply.github.com> Date: Tue, 25 Feb 2025 01:30:27 +0000 Subject: [PATCH 09/16] improve coverage --- .../program/precompile_programs/ed25519.zig | 18 +++---- .../program/precompile_programs/lib.zig | 48 +++++++++++++++++++ 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/runtime/program/precompile_programs/ed25519.zig b/src/runtime/program/precompile_programs/ed25519.zig index 7863bac9f..8023a9586 100644 --- a/src/runtime/program/precompile_programs/ed25519.zig +++ b/src/runtime/program/precompile_programs/ed25519.zig @@ -94,6 +94,8 @@ pub fn newInstruction( keypair: Ed25519.KeyPair, message: []const u8, ) !sig.core.Instruction { + std.debug.assert(message.len < std.math.maxInt(u16)); + const signature = try keypair.sign(message, null); const num_signatures: u8 = 1; @@ -107,24 +109,24 @@ pub fn newInstruction( .pubkey_offset = pubkey_offset, .pubkey_instruction_idx = std.math.maxInt(u16), .message_data_offset = message_data_offset, - .message_data_size = message.len, + .message_data_size = @intCast(message.len), .message_instruction_idx = std.math.maxInt(u16), }; - const instruction_data = try std.ArrayListAligned(u8, 2).initCapacity( + var instruction_data = try std.ArrayList(u8).initCapacity( allocator, message_data_offset + message.len, ); errdefer instruction_data.deinit(); // add 2nd byte for padding, so that offset structure is aligned - instruction_data.appendSliceAssumeCapacity(.{ num_signatures, 0 }); + instruction_data.appendSliceAssumeCapacity(&.{ num_signatures, 0 }); instruction_data.appendSliceAssumeCapacity(std.mem.asBytes(&offsets)); - std.debug.assert(instruction_data.len == pubkey_offset); - instruction_data.appendSliceAssumeCapacity(keypair.public_key.toBytes()); - std.debug.assert(instruction_data.len == signature_offset); - instruction_data.appendSliceAssumeCapacity(signature.toBytes()); - std.debug.assert(instruction_data.len == message_data_offset); + std.debug.assert(instruction_data.items.len == pubkey_offset); + instruction_data.appendSliceAssumeCapacity(&keypair.public_key.toBytes()); + std.debug.assert(instruction_data.items.len == signature_offset); + instruction_data.appendSliceAssumeCapacity(&signature.toBytes()); + std.debug.assert(instruction_data.items.len == message_data_offset); instruction_data.appendSliceAssumeCapacity(message); return .{ diff --git a/src/runtime/program/precompile_programs/lib.zig b/src/runtime/program/precompile_programs/lib.zig index 0a381f07f..0a9600ae7 100644 --- a/src/runtime/program/precompile_programs/lib.zig +++ b/src/runtime/program/precompile_programs/lib.zig @@ -6,6 +6,7 @@ pub const secp256k1 = @import("secp256k1.zig"); pub const secp256r1 = @import("secp256r1.zig"); const Pubkey = sig.core.Pubkey; +const Ed25519 = std.crypto.sign.Ed25519; pub const ed25519Verify = ed25519.verify; pub const secp256k1Verify = secp256k1.verify; @@ -123,3 +124,50 @@ pub fn getInstructionData( if (offset + len > data.len) return error.InvalidDataOffsets; return data[offset..][0..len]; } + +test "verify ed25519" { + try verifyPrecompiles( + std.testing.allocator, + sig.core.Transaction.EMPTY, + sig.runtime.FeatureSet.EMPTY, + ); + + const bad_ed25519_tx = std.mem.zeroInit(sig.core.Transaction, .{ + .msg = .{ + .account_keys = &.{sig.runtime.ids.PRECOMPILE_ED25519_PROGRAM_ID}, + .instructions = &.{ + .{ + .program_index = 0, + .account_indexes = &.{0}, + .data = "hello", + }, + }, + }, + .version = .legacy, + }); + + try std.testing.expectError( + error.InvalidInstructionDataSize, + verifyPrecompiles(std.testing.allocator, bad_ed25519_tx, sig.runtime.FeatureSet.EMPTY), + ); + + const keypair = try Ed25519.KeyPair.create(null); + const ed25519_instruction = try ed25519.newInstruction( + std.testing.allocator, + keypair, + "hello!", + ); + defer std.testing.allocator.free(ed25519_instruction.data); + + const ed25519_tx = std.mem.zeroInit(sig.core.Transaction, .{ + .msg = .{ + .account_keys = &.{sig.runtime.ids.PRECOMPILE_ED25519_PROGRAM_ID}, + .instructions = &.{ + .{ .program_index = 0, .account_indexes = &.{0}, .data = ed25519_instruction.data }, + }, + }, + .version = .legacy, + }); + + try verifyPrecompiles(std.testing.allocator, ed25519_tx, sig.runtime.FeatureSet.EMPTY); +} From e422d97e22f1554768032fb424a3e3d23861bef5 Mon Sep 17 00:00:00 2001 From: Sobeston <15335529+Sobeston@users.noreply.github.com> Date: Tue, 25 Feb 2025 02:13:53 +0000 Subject: [PATCH 10/16] remove overflow issue --- src/runtime/program/precompile_programs/ed25519.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/runtime/program/precompile_programs/ed25519.zig b/src/runtime/program/precompile_programs/ed25519.zig index 8023a9586..68c6084be 100644 --- a/src/runtime/program/precompile_programs/ed25519.zig +++ b/src/runtime/program/precompile_programs/ed25519.zig @@ -52,8 +52,8 @@ pub fn verify( } if (n_signatures == 0) return error.InvalidInstructionDataSize; - const expected_data_size = ED25519_SIGNATURE_OFFSETS_START + - n_signatures * ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE; + const expected_data_size: u64 = ED25519_SIGNATURE_OFFSETS_START + + @as(u64, n_signatures) * ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE; if (data.len < expected_data_size) return error.InvalidInstructionDataSize; // firedancer seems to assume natural alignment in this loop? Our data should be aligned. From a9fecfd28675d399c2f36230ba29135aa6383f23 Mon Sep 17 00:00:00 2001 From: Sobeston <15335529+Sobeston@users.noreply.github.com> Date: Tue, 25 Feb 2025 04:11:50 +0000 Subject: [PATCH 11/16] fixup --- src/runtime/program/precompile_programs/ed25519.zig | 5 +++-- src/runtime/program/precompile_programs/lib.zig | 10 ++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/runtime/program/precompile_programs/ed25519.zig b/src/runtime/program/precompile_programs/ed25519.zig index 68c6084be..b66c9cc19 100644 --- a/src/runtime/program/precompile_programs/ed25519.zig +++ b/src/runtime/program/precompile_programs/ed25519.zig @@ -45,11 +45,12 @@ pub fn verify( all_instruction_datas: []const []const u8, ) PrecompileProgramError!void { const data = current_instruction_data; - const n_signatures = data[0]; if (data.len < ED25519_DATA_START) { - if (data.len == 2 and n_signatures == 0) return; + if (data.len == 2 and data[0] == 0) return; return error.InvalidInstructionDataSize; } + + const n_signatures = data[0]; if (n_signatures == 0) return error.InvalidInstructionDataSize; const expected_data_size: u64 = ED25519_SIGNATURE_OFFSETS_START + diff --git a/src/runtime/program/precompile_programs/lib.zig b/src/runtime/program/precompile_programs/lib.zig index 0a9600ae7..901b608d8 100644 --- a/src/runtime/program/precompile_programs/lib.zig +++ b/src/runtime/program/precompile_programs/lib.zig @@ -60,6 +60,7 @@ pub fn verifyPrecompiles( instruction_datas = buf; break :blk buf; }; + try precompile.function(instruction.data, datas); } } @@ -159,15 +160,20 @@ test "verify ed25519" { ); defer std.testing.allocator.free(ed25519_instruction.data); - const ed25519_tx = std.mem.zeroInit(sig.core.Transaction, .{ + const ed25519_tx: sig.core.Transaction = .{ .msg = .{ .account_keys = &.{sig.runtime.ids.PRECOMPILE_ED25519_PROGRAM_ID}, .instructions = &.{ .{ .program_index = 0, .account_indexes = &.{0}, .data = ed25519_instruction.data }, }, + .signature_count = 1, + .readonly_signed_count = 1, + .readonly_unsigned_count = 0, + .recent_blockhash = sig.core.Hash.ZEROES, }, .version = .legacy, - }); + .signatures = &.{}, + }; try verifyPrecompiles(std.testing.allocator, ed25519_tx, sig.runtime.FeatureSet.EMPTY); } From a44f14bd9d357f6393cc3c02f06b724fc29f1896 Mon Sep 17 00:00:00 2001 From: Sobeston <15335529+Sobeston@users.noreply.github.com> Date: Tue, 25 Feb 2025 21:52:06 +0000 Subject: [PATCH 12/16] address comments --- .../program/precompile_programs/ed25519.zig | 71 +++++++++---------- .../program/precompile_programs/lib.zig | 12 ++-- 2 files changed, 38 insertions(+), 45 deletions(-) diff --git a/src/runtime/program/precompile_programs/ed25519.zig b/src/runtime/program/precompile_programs/ed25519.zig index b66c9cc19..37bbaf0e2 100644 --- a/src/runtime/program/precompile_programs/ed25519.zig +++ b/src/runtime/program/precompile_programs/ed25519.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const sig = @import("../../../sig.zig"); const precompile_programs = sig.runtime.program.precompile_programs; @@ -57,12 +58,11 @@ pub fn verify( @as(u64, n_signatures) * ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE; if (data.len < expected_data_size) return error.InvalidInstructionDataSize; - // firedancer seems to assume natural alignment in this loop? Our data should be aligned. for (0..n_signatures) |i| { const offset = ED25519_SIGNATURE_OFFSETS_START + i * ED25519_SIGNATURE_OFFSETS_SERIALIZED_SIZE; - const sig_offsets: *const Ed25519SignatureOffsets = @alignCast(@ptrCast(data.ptr + offset)); + const sig_offsets: *align(1) const Ed25519SignatureOffsets = @ptrCast(data.ptr + offset); const signature = try getInstructionValue( Ed25519.Signature, @@ -95,6 +95,7 @@ pub fn newInstruction( keypair: Ed25519.KeyPair, message: []const u8, ) !sig.core.Instruction { + std.debug.assert(builtin.is_test); std.debug.assert(message.len < std.math.maxInt(u16)); const signature = try keypair.sign(message, null); @@ -104,7 +105,7 @@ pub fn newInstruction( const signature_offset = pubkey_offset + ED25519_PUBKEY_SERIALIZED_SIZE; const message_data_offset = signature_offset + ED25519_SIGNATURE_SERIALIZED_SIZE; - const offsets = Ed25519SignatureOffsets{ + const offsets: Ed25519SignatureOffsets = .{ .signature_offset = signature_offset, .signature_instruction_idx = std.math.maxInt(u16), .pubkey_offset = pubkey_offset, @@ -138,21 +139,17 @@ pub fn newInstruction( } // https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/ed25519_instruction.rs#L258 -fn test_case( - allocator: std.mem.Allocator, +fn testCase( num_signatures: u16, offsets: Ed25519SignatureOffsets, -) (PrecompileProgramError || error{OutOfMemory})!void { - var instruction_data = try std.ArrayListAligned(u8, 2).initCapacity( - allocator, - ED25519_DATA_START, - ); - defer instruction_data.deinit(); +) PrecompileProgramError!void { + std.debug.assert(builtin.is_test); - instruction_data.appendSliceAssumeCapacity(std.mem.asBytes(&num_signatures)); - instruction_data.appendSliceAssumeCapacity(std.mem.asBytes(&offsets)); + var instruction_data: [ED25519_DATA_START]u8 align(2) = undefined; + @memcpy(instruction_data[0..2], std.mem.asBytes(&num_signatures)); + @memcpy(instruction_data[2..], std.mem.asBytes(&offsets)); - return try verify(instruction_data.items, &.{&(.{0} ** 100)}); + return try verify(&instruction_data, &.{&(.{0} ** 100)}); } // https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/ed25519_instruction.rs#L279 @@ -164,7 +161,7 @@ test "ed25519 invalid offsets" { ); defer instruction_data.deinit(); - const offsets = Ed25519SignatureOffsets{}; + const offsets: Ed25519SignatureOffsets = .{}; // Set up instruction data with invalid size instruction_data.appendSliceAssumeCapacity(std.mem.asBytes(&1)); @@ -177,29 +174,29 @@ test "ed25519 invalid offsets" { ); // invalid signature instruction index - const invalid_signature_offsets = Ed25519SignatureOffsets{ + const invalid_signature_offsets: Ed25519SignatureOffsets = .{ .signature_instruction_idx = 1, }; try std.testing.expectEqual( - test_case(allocator, 1, invalid_signature_offsets), + testCase(1, invalid_signature_offsets), error.InvalidDataOffsets, ); // invalid message instruction index - const invalid_message_offsets = Ed25519SignatureOffsets{ + const invalid_message_offsets: Ed25519SignatureOffsets = .{ .message_instruction_idx = 1, }; try std.testing.expectEqual( - test_case(allocator, 1, invalid_message_offsets), + testCase(1, invalid_message_offsets), error.InvalidDataOffsets, ); // invalid public key instruction index - const invalid_pubkey_offsets = Ed25519SignatureOffsets{ + const invalid_pubkey_offsets: Ed25519SignatureOffsets = .{ .pubkey_instruction_idx = 1, }; try std.testing.expectEqual( - test_case(allocator, 1, invalid_pubkey_offsets), + testCase(1, invalid_pubkey_offsets), error.InvalidDataOffsets, ); } @@ -207,45 +204,45 @@ test "ed25519 invalid offsets" { // https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/ed25519_instruction.rs#L326 test "ed25519 message data offsets" { { - const offsets = Ed25519SignatureOffsets{ + const offsets: Ed25519SignatureOffsets = .{ .message_data_offset = 99, .message_data_size = 1, }; try std.testing.expectEqual( - test_case(std.testing.allocator, 1, offsets), + testCase(1, offsets), error.InvalidSignature, ); } { - const offsets = Ed25519SignatureOffsets{ + const offsets: Ed25519SignatureOffsets = .{ .message_data_offset = 100, .message_data_size = 1, }; try std.testing.expectEqual( - test_case(std.testing.allocator, 1, offsets), + testCase(1, offsets), error.InvalidDataOffsets, ); } { - const offsets = Ed25519SignatureOffsets{ + const offsets: Ed25519SignatureOffsets = .{ .message_data_offset = 100, .message_data_size = 1000, }; try std.testing.expectEqual( - test_case(std.testing.allocator, 1, offsets), + testCase(1, offsets), error.InvalidDataOffsets, ); } { - const offsets = Ed25519SignatureOffsets{ + const offsets: Ed25519SignatureOffsets = .{ .message_data_offset = std.math.maxInt(u16), .message_data_size = std.math.maxInt(u16), }; try std.testing.expectEqual( - test_case(std.testing.allocator, 1, offsets), + testCase(1, offsets), error.InvalidDataOffsets, ); } @@ -254,21 +251,21 @@ test "ed25519 message data offsets" { // https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/ed25519_instruction.rs#L369 test "ed25519 pubkey offset" { { - const offsets = Ed25519SignatureOffsets{ + const offsets: Ed25519SignatureOffsets = .{ .pubkey_offset = std.math.maxInt(u16), }; try std.testing.expectEqual( - test_case(std.testing.allocator, 1, offsets), + testCase(1, offsets), error.InvalidDataOffsets, ); } { - const offsets = Ed25519SignatureOffsets{ + const offsets: Ed25519SignatureOffsets = .{ .pubkey_offset = 100 - ED25519_PUBKEY_SERIALIZED_SIZE + 1, }; try std.testing.expectEqual( - test_case(std.testing.allocator, 1, offsets), + testCase(1, offsets), error.InvalidDataOffsets, ); } @@ -277,21 +274,21 @@ test "ed25519 pubkey offset" { // https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/ed25519_instruction.rs#L389-L390 test "ed25519 signature offset" { { - const offsets = Ed25519SignatureOffsets{ + const offsets: Ed25519SignatureOffsets = .{ .signature_offset = std.math.maxInt(u16), }; try std.testing.expectEqual( - test_case(std.testing.allocator, 1, offsets), + testCase(1, offsets), error.InvalidDataOffsets, ); } { - const offsets = Ed25519SignatureOffsets{ + const offsets: Ed25519SignatureOffsets = .{ .signature_offset = 100 - ED25519_SIGNATURE_SERIALIZED_SIZE + 1, }; try std.testing.expectEqual( - test_case(std.testing.allocator, 1, offsets), + testCase(1, offsets), error.InvalidDataOffsets, ); } diff --git a/src/runtime/program/precompile_programs/lib.zig b/src/runtime/program/precompile_programs/lib.zig index 901b608d8..887fe6a89 100644 --- a/src/runtime/program/precompile_programs/lib.zig +++ b/src/runtime/program/precompile_programs/lib.zig @@ -56,7 +56,7 @@ pub fn verifyPrecompiles( const datas = instruction_datas orelse blk: { const buf = try allocator.alloc([]const u8, transaction.msg.instructions.len); - for (0.., transaction.msg.instructions) |i, instr| buf[i] = instr.data; + for (transaction.msg.instructions, 0..) |instr, i| buf[i] = instr.data; instruction_datas = buf; break :blk buf; }; @@ -77,9 +77,6 @@ pub const Precompile = struct { required_feature: ?Pubkey, }; -// parsed internally -pub const PrecompileProgramInstruction = []const u8; - // custom errors // https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/precompile-error/src/lib.rs#L6 pub const PrecompileProgramError = error{ @@ -96,15 +93,14 @@ pub fn getInstructionValue( all_instruction_datas: []const []const u8, instruction_idx: u16, offset: usize, -) error{InvalidDataOffsets}!*const T { - // aligncast potentially dangerous? - return @alignCast(@ptrCast(try getInstructionData( +) error{InvalidDataOffsets}!*align(1) const T { + return @ptrCast(try getInstructionData( @sizeOf(T), current_instruction_data, all_instruction_datas, instruction_idx, offset, - ))); + )); } // https://github.com/firedancer-io/firedancer/blob/af74882ffb2c24783a82718dbc5111a94e1b5f6f/src/flamenco/runtime/program/fd_precompiles.c#L74 From 387a16678fa23184161ecef35422252dccbfacd1 Mon Sep 17 00:00:00 2001 From: Sobeston <15335529+Sobeston@users.noreply.github.com> Date: Wed, 26 Feb 2025 17:39:54 +0000 Subject: [PATCH 13/16] make is_test check comptime --- src/runtime/program/precompile_programs/ed25519.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/runtime/program/precompile_programs/ed25519.zig b/src/runtime/program/precompile_programs/ed25519.zig index 37bbaf0e2..3a221cf0e 100644 --- a/src/runtime/program/precompile_programs/ed25519.zig +++ b/src/runtime/program/precompile_programs/ed25519.zig @@ -95,7 +95,7 @@ pub fn newInstruction( keypair: Ed25519.KeyPair, message: []const u8, ) !sig.core.Instruction { - std.debug.assert(builtin.is_test); + if (!builtin.is_test) @compileError("newInstruction is only for use in tests"); std.debug.assert(message.len < std.math.maxInt(u16)); const signature = try keypair.sign(message, null); @@ -143,7 +143,7 @@ fn testCase( num_signatures: u16, offsets: Ed25519SignatureOffsets, ) PrecompileProgramError!void { - std.debug.assert(builtin.is_test); + if (!builtin.is_test) @compileError("testCase is only for use in tests"); var instruction_data: [ED25519_DATA_START]u8 align(2) = undefined; @memcpy(instruction_data[0..2], std.mem.asBytes(&num_signatures)); From 41dbdff2327181564202bac43005ba2cf3959cfd Mon Sep 17 00:00:00 2001 From: Sobeston <15335529+Sobeston@users.noreply.github.com> Date: Wed, 26 Feb 2025 23:25:04 +0000 Subject: [PATCH 14/16] address comments / calculate compute units consumed --- .../program/precompile_programs/ed25519.zig | 3 +- .../program/precompile_programs/lib.zig | 40 ++++++++++++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/runtime/program/precompile_programs/ed25519.zig b/src/runtime/program/precompile_programs/ed25519.zig index 3a221cf0e..9aa9f9381 100644 --- a/src/runtime/program/precompile_programs/ed25519.zig +++ b/src/runtime/program/precompile_programs/ed25519.zig @@ -39,6 +39,7 @@ pub const Ed25519SignatureOffsets = extern struct { message_instruction_idx: u16 = 0, }; +// TODO: support verify_strict feature https://github.com/anza-xyz/agave/pull/1876/ // https://github.com/anza-xyz/agave/blob/a8aef04122068ec36a7af0721e36ee58efa0bef2/sdk/src/ed25519_instruction.rs#L88 // https://github.com/firedancer-io/firedancer/blob/af74882ffb2c24783a82718dbc5111a94e1b5f6f/src/flamenco/runtime/program/fd_precompiles.c#L118 pub fn verify( @@ -293,5 +294,3 @@ test "ed25519 signature offset" { ); } } - -// TODO: should we implement https://github.com/anza-xyz/agave/pull/1876/ ? diff --git a/src/runtime/program/precompile_programs/lib.zig b/src/runtime/program/precompile_programs/lib.zig index 887fe6a89..119214e3f 100644 --- a/src/runtime/program/precompile_programs/lib.zig +++ b/src/runtime/program/precompile_programs/lib.zig @@ -12,7 +12,15 @@ pub const ed25519Verify = ed25519.verify; pub const secp256k1Verify = secp256k1.verify; pub const secp256r1Verify = secp256r1.verify; -pub const COMPUTE_UNITS = 0; // does this consume compute units? +/// https://github.com/anza-xyz/agave/blob/df063a8c6483ad1d2bbbba50ab0b7fd7290eb7f4/cost-model/src/block_cost_limits.rs#L15 +/// Cluster averaged compute unit to micro-sec conversion rate +pub const COMPUTE_UNIT_TO_US_RATIO: u64 = 30; +/// Number of compute units for one signature verification. +pub const SIGNATURE_COST: u64 = COMPUTE_UNIT_TO_US_RATIO * 24; +/// Number of compute units for one secp256k1 signature verification. +pub const SECP256K1_VERIFY_COST: u64 = COMPUTE_UNIT_TO_US_RATIO * 223; +/// Number of compute units for one ed25519 signature verification. +pub const ED25519_VERIFY_COST: u64 = COMPUTE_UNIT_TO_US_RATIO * 76; // TODO: should be moved to global features file pub const SECP256R1_FEATURE_ID = @@ -36,6 +44,34 @@ pub const PRECOMPILES = [_]Precompile{ }, }; +// https://github.com/anza-xyz/agave/blob/f9d4939d1d6ad2783efc8ec60db058809bb87f55/cost-model/src/cost_model.rs#L115 +pub fn verifyPrecompilesComputeCost( + transaction: sig.core.Transaction, + feature_set: sig.runtime.FeatureSet, +) u64 { + _ = feature_set; // TODO: support verify_strict feature https://github.com/anza-xyz/agave/pull/1876/ + + var n_secp256k1_instruction_signatures: u64 = 0; + var n_ed25519_instruction_signatures: u64 = 0; + + // https://github.com/anza-xyz/agave/blob/6ea38fce866595908486a01c7d6b7182988f3b2d/sdk/program/src/message/sanitized.rs#L385 + for (transaction.msg.instructions) |instruction| { + if (instruction.data.len == 0) continue; + + const program_id = transaction.msg.account_keys[instruction.program_index]; + if (program_id.equals(sig.runtime.ids.PRECOMPILE_SECP256K1_PROGRAM_ID)) { + n_secp256k1_instruction_signatures +|= instruction.data[0]; + } + if (program_id.equals(sig.runtime.ids.PRECOMPILE_ED25519_PROGRAM_ID)) { + n_ed25519_instruction_signatures +|= instruction.data[0]; + } + } + + return transaction.msg.signature_count.len *| SIGNATURE_COST +| + n_secp256k1_instruction_signatures *| SECP256K1_VERIFY_COST +| + n_ed25519_instruction_signatures *| ED25519_VERIFY_COST; +} + pub fn verifyPrecompiles( allocator: std.mem.Allocator, transaction: sig.core.Transaction, @@ -118,7 +154,7 @@ pub fn getInstructionData( break :data all_instruction_datas[instruction_idx]; }; - if (offset + len > data.len) return error.InvalidDataOffsets; + if (offset +| len > data.len) return error.InvalidDataOffsets; return data[offset..][0..len]; } From 4e29665bc690c724c9c1dac0c3f29d5e5d46cff4 Mon Sep 17 00:00:00 2001 From: Sobeston <15335529+Sobeston@users.noreply.github.com> Date: Wed, 26 Feb 2025 23:32:27 +0000 Subject: [PATCH 15/16] fixup / style --- src/runtime/program/precompile_programs/lib.zig | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/runtime/program/precompile_programs/lib.zig b/src/runtime/program/precompile_programs/lib.zig index 119214e3f..88e1f9830 100644 --- a/src/runtime/program/precompile_programs/lib.zig +++ b/src/runtime/program/precompile_programs/lib.zig @@ -49,7 +49,8 @@ pub fn verifyPrecompilesComputeCost( transaction: sig.core.Transaction, feature_set: sig.runtime.FeatureSet, ) u64 { - _ = feature_set; // TODO: support verify_strict feature https://github.com/anza-xyz/agave/pull/1876/ + // TODO: support verify_strict feature https://github.com/anza-xyz/agave/pull/1876/ + _ = feature_set; var n_secp256k1_instruction_signatures: u64 = 0; var n_ed25519_instruction_signatures: u64 = 0; @@ -59,15 +60,15 @@ pub fn verifyPrecompilesComputeCost( if (instruction.data.len == 0) continue; const program_id = transaction.msg.account_keys[instruction.program_index]; - if (program_id.equals(sig.runtime.ids.PRECOMPILE_SECP256K1_PROGRAM_ID)) { + if (program_id.equals(&sig.runtime.ids.PRECOMPILE_SECP256K1_PROGRAM_ID)) { n_secp256k1_instruction_signatures +|= instruction.data[0]; } - if (program_id.equals(sig.runtime.ids.PRECOMPILE_ED25519_PROGRAM_ID)) { + if (program_id.equals(&sig.runtime.ids.PRECOMPILE_ED25519_PROGRAM_ID)) { n_ed25519_instruction_signatures +|= instruction.data[0]; } } - return transaction.msg.signature_count.len *| SIGNATURE_COST +| + return transaction.msg.signature_count *| SIGNATURE_COST +| n_secp256k1_instruction_signatures *| SECP256K1_VERIFY_COST +| n_ed25519_instruction_signatures *| ED25519_VERIFY_COST; } From 2a1b51e751734d316eb26fbac2684e4fdbe20e4e Mon Sep 17 00:00:00 2001 From: Sobeston <15335529+Sobeston@users.noreply.github.com> Date: Fri, 28 Feb 2025 01:13:23 +0000 Subject: [PATCH 16/16] test verify cost --- .../program/precompile_programs/lib.zig | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/runtime/program/precompile_programs/lib.zig b/src/runtime/program/precompile_programs/lib.zig index 88e1f9830..ae3014a97 100644 --- a/src/runtime/program/precompile_programs/lib.zig +++ b/src/runtime/program/precompile_programs/lib.zig @@ -45,6 +45,7 @@ pub const PRECOMPILES = [_]Precompile{ }; // https://github.com/anza-xyz/agave/blob/f9d4939d1d6ad2783efc8ec60db058809bb87f55/cost-model/src/cost_model.rs#L115 +// https://github.com/anza-xyz/agave/blob/6ea38fce866595908486a01c7d6b7182988f3b2d/sdk/program/src/message/sanitized.rs#L378 pub fn verifyPrecompilesComputeCost( transaction: sig.core.Transaction, feature_set: sig.runtime.FeatureSet, @@ -210,3 +211,35 @@ test "verify ed25519" { try verifyPrecompiles(std.testing.allocator, ed25519_tx, sig.runtime.FeatureSet.EMPTY); } + +test "verify cost" { + const keypair = try Ed25519.KeyPair.create(null); + const ed25519_instruction = try ed25519.newInstruction( + std.testing.allocator, + keypair, + "hello!", + ); + defer std.testing.allocator.free(ed25519_instruction.data); + + const ed25519_tx: sig.core.Transaction = .{ + .msg = .{ + .account_keys = &.{sig.runtime.ids.PRECOMPILE_ED25519_PROGRAM_ID}, + .instructions = &.{ + .{ .program_index = 0, .account_indexes = &.{0}, .data = ed25519_instruction.data }, + }, + .signature_count = 1, + .readonly_signed_count = 1, + .readonly_unsigned_count = 0, + .recent_blockhash = sig.core.Hash.ZEROES, + }, + .version = .legacy, + .signatures = &.{}, + }; + + const expected_cost = 1 *| SIGNATURE_COST +| 1 *| ED25519_VERIFY_COST; + // cross-checked with agave (FeatureSet::default()) + try std.testing.expectEqual(3000, expected_cost); + + const compute_units = verifyPrecompilesComputeCost(ed25519_tx, sig.runtime.FeatureSet.EMPTY); + try std.testing.expectEqual(expected_cost, compute_units); +}