Introduction

Starting September 29th, 2025, zkSecurity conducted a security audit of new serialization, hash and signature instructions added to the SnarkVM. A single consultant reviewed the code for a week. We reviewed the snarkVM repository on the feat/bytes branch at commit 41d32c5f. The audit focused on the addition of new instructions for serialization (to bits) of types, hashing of serializable types, and ECDSA signature operations. The new hash and ECDSA signature instructions are usable only out-of-circuit in the finalize; a snippet of code run after the in-circuit part of a function which can read account state and apply effects (beyond consuming/creating records).

Motivation

The primary motivation for the addition of these instructions is to interoperate with systems outside of Aleo, for instance, enabling the verification of Ethereum-style ECDSA signatures produced by bridges. Since the complexity and constraint count of writing circuits for ECDSA verification is substantial, requiring the implementation of elliptic curve operations over a foreign curve (not defined over the field of the SNARK) and arithmetizing the hash functions, Aleo has opted to only support these operations in the out-of-circuit component of the VM, namely in the finalize. This substantially reduces the scope and complexity of this effort.

Native & Raw Encoding

The changes introduce a new form of encoding: raw.

Prior to this, Aleo types were serialized using a custom Type-Length-Value (TLV) encoding scheme during signature verification / hashing etc. This encoding is referred to as native encoding and is the “default” encoding for instructions not having the .raw postfix. This encoding ensures that all values of distinct types are serialized as distinct sequences of bits: any sequence of bits corresponds to a distinct value of a distinct type within the system.

This is done to avoid confusion about the “semantics” of signed/hashed sequences of bits/field elements to be signed: by signing a sequence of encoded bits, the signer is signing a unique message of a distinct type within the overall system. As a side-effect the particular encoding is prefix-free, meaning that a sequence of bits can be padded with a constant to e.g. a multiple of 8 bits, and still correspond uniquely to a single type/value pair.

The problem with this encoding is that it prevents interoperability (and is verbose): using the existing hashing/signing interfaces it is not possible to verify a signature on a value encoded in another format, e.g. an ASN.1 DER encoded message or one encoded using RLP (used in Ethereum). To enable this, Provable introduced a raw encoding, which serializes Aleo/SnarkVM types without the type and length prefix, for instance a struct:

struct example:
  v0 as u32;
  v1 as u32;

Is serialized simply as 64 bits: the bits of v0 in little endian followed by the bits of v1 in little endian. The result is an encoding which is much more efficient, and allows “parsing” signed sequences of bits into Aleo structs; by “deserializing” (effectively casting) the bits into a struct. One obvious thing to observe is that this format does not uniquely describe the type of a value, for instance, the struct above, once encoded, has the same bit representation as this:

struct example2:
  vx as u64;

Which is also serialized 64 bits: as the bits of vx in little endian. Therefore, implementing / ensuring adequate domain separation is left to the application by design.

New Instructions

For context, let’s provide a reference of every new instruction added to the SnarkVM. A total of 37 new instructions were added in the reviewed pull request:

Serialization Instructions

Instructions for converting types to bit arrays:

  • serialize.bits — converts types to bit arrays in the native encoding.
  • serialize.bits.raw — converts types to raw bit arrays without metadata

Deserialization Instructions

Instructions for converting bit arrays back to typed values:

  • deserialize.bits — converts bit arrays with metadata back to typed values
  • deserialize.bits.raw — converts raw bit arrays back to typed values

Hash Instructions

Instructions for hashing SnarkVM types:

  • Keccak hash of “native” TLV encoded values:

    • hash.keccak256.native
    • hash.keccak384.native
    • hash.keccak512.native
  • Sha3 hash of “native” TLV encoded values:

    • hash.sha3_256.native
    • hash.sha3_384.native
    • hash.sha3_512.native
  • Keccak hash of “raw” encoded values:

    • hash.keccak256.raw
    • hash.keccak384.raw
    • hash.keccak512.raw
  • Sha3 hash of “raw” encoded values:

    • hash.sha3_256.raw
    • hash.sha3_384.raw
    • hash.sha3_512.raw

ECDSA Signature Verification Instructions

Adds support for ECDSA signatures over the Secp256k1 (“Bitcoin”/”Ethereum”) curve.

Verification of an ECDSA signature against message digest:
  • ecdsa.verify.digest
  • ecdsa.verify.digest.eth

ECDSA verification/signing starts by computing Hash(m), the rest of the signing/verification is independent of the hash function (except that the output may be truncated). This allows to potentially support hash functions besides Keccak* and Sha3* as well as interop with other systems where the hash is computed separately.

Verification (Native Encoded Messages)

Verification of ECDSA signatures with various hash functions on messages encoded using the native encoding scheme:

  • ecdsa.verify.keccak256
  • ecdsa.verify.keccak384
  • ecdsa.verify.keccak512
  • ecdsa.verify.sha3_256
  • ecdsa.verify.sha3_384
  • ecdsa.verify.sha3_512
Verification (Raw Encoded Messages)

Verification of signatures, with various hash functions on messages encoded using the raw encoding scheme:

  • ecdsa.verify.keccak256.raw
  • ecdsa.verify.keccak384.raw
  • ecdsa.verify.keccak512.raw
  • ecdsa.verify.sha3_256.raw
  • ecdsa.verify.sha3_384.raw
  • ecdsa.verify.sha3_512.raw
Verification (Raw Encoded Messages, Ethereum Addresses)

Verification of signatures, with various hash functions on messages against “Ethereum addresses” with raw encoding:

  • ecdsa.verify.keccak256.eth
  • ecdsa.verify.keccak384.eth
  • ecdsa.verify.keccak512.eth
  • ecdsa.verify.sha3_256.eth
  • ecdsa.verify.sha3_384.eth
  • ecdsa.verify.sha3_512.eth

Native Signatures with Raw Encoding

Verification of native Aleo signatures is extended to support raw encoded messages:

  • sign.verify.raw

Which works by:

  • Serializing the type using raw encoding.
  • Packing the bits into field elements.
  • Signing the sequence of field elements.

Primary Considerations

The implementation uses the well-known k259 crate, which has previously undergone audit and is out of scope for this report. As indicated by findings, the primary source of “subtle behavior” (both bugs and questions about intended behavior), is in the way that values are encoded/packed/interpreted when fed to the hash functions, subsequently used directly or as part of the ECDSA signature verification.