Skip to content

Instantly share code, notes, and snippets.

@Rexagon
Last active November 17, 2025 11:50
Show Gist options
  • Select an option

  • Save Rexagon/3457abdd8aa1011bd70e166532188b0e to your computer and use it in GitHub Desktop.

Select an option

Save Rexagon/3457abdd8aa1011bd70e166532188b0e to your computer and use it in GitHub Desktop.
CHKSIG proposal

Draft

Improved signature verification for TON L2 networks

The appearance of new networks in which a user can reuse the same key for the same contracts carries new risks. The proposed solution allows using same keys and contracts across different L2 networks without worrying about new replay attacks, as the signatures themselves will be different everywhere and will depend on a unique network ID (later referred to as global_id).

Most contracts verify the signature of external message with CHKSIGNU or CHKSIGNS. Therefore, to minimize the number of any changes, we propose adding new behavior for these opcodes (enabled/disabled via capability). Fow now, this new behavior can only be implemented on the L2 side, however, it requires at least the reservation of three additional opcodes, which are needed for explicit verification of signatures with other ids.

VM state changes

VM state now includes an additional stack for signature domains. Its current top item is referred to as "the current signature domain" and is used as a behaviour modifier for signature verification opcodes (CHKSIGNU and CHKSIGNS for now). For L2 networks their signature domain is implicitly added to the stack before contract execution, while in the main networks execution starts with an empty stack. An empty stack is equivalent to any stack with no_signature_domain item on top.

Signature domain scheme

// Non-empty variant. A root hash of its Cell representation
// is used as a prefix for the verified data.
signature_domain#2d72df7d global_id:int32 = SignatureDomain;

// Special variant to NOT add any prefix for the verified data.
// Can be used to verify mainnet signatures from L2 networks.
no_signature_domain#b65d972d = SignatureDomain;

Changed opcodes

  • f910 CHKSIGNU and f911 CHKSIGNS - when SignatureWithId capability is enabled the resulting data for verification is prefixed with a byte slice computed from the current signature domain. For signature_domain variant this is a 32-byte slice with a root hash of the cell representation. For no_signature_domain this is an empty slice (so the verification result is identical to the original implementation).

    Example for CHKSIGNU:

    global_id = 1000 # 0x000003e8
    prefix = CellBuilder().store_u32(0x2d72df7d).store_i32(global_id).build().repr_hash
    # prefix = bytes.fromhex("9f78986f23148053358938611edcbcfdcd1e7844c5522a87ccefcd7ff55bc4e5")
    data = 0xdeadbeef
    signature = ...
    public_key = ...
    
    data_to_check = prefix + data.to_bytes(32, byteorder='big')
    # data_to_check = [0x9f, 0x78, ..., 0xc4, 0xe5, 0x00, 0x00, 0x00, .., 0xbe, 0xef]
    ed25519_verify(data_to_check, signature, public_key)

    Example for CHKSIGNS:

    global_id = 1000 # 0x000003e8
    prefix = CellBuilder().store_u32(0x2d72df7d).store_i32(global_id).build().repr_hash
    # prefix = bytes.fromhex("9f78986f23148053358938611edcbcfdcd1e7844c5522a87ccefcd7ff55bc4e5")
    slice = CellSlice("deadbeef")
    signature = ...
    public_key = ...
    
    data_hash = sha256(slice.data) 
    # data_hash = [0x5f, 0x78, 0xc3, ..]
    data_to_check = prefix + data_hash 
    # data_to_check = [0x9f, 0x78, ..., 0xc4, 0xe5, 0x5f, 0x78, 0xc3, ..]
    ed25519_verify(data_to_check, signature, public_key)

New opcodes

  • f91800 SIGNDOMAIN (- x or ⊥) - pushes the current signature domain where x is a global_id or Null if there were no signature domain.
  • f91801 SIGNDOMAIN_POP (- x or ⊥) - same as SIGNDOMAIN, but removes the fetched value from the signature domain stack.
  • f91802 SIGNDOMAIN_PUSH (x or ⊥ -) - pushes an optional global_id to the signature domain stack making it the current signature domain. -2^31 ≤ x < 2^31

Scope of changes

  • L2 side - must support these opcodes at the VM level;

    Example of the implemented changes in Tycho VM.

  • Wallets and libraries - must have a context with a known global_id of the selected network when working with signatures;

    Example of the usage of such context with JS SDK.

  • Ledger - must use the provided signature id (if any) when forming a signed external message;

Unresolved questions

  • [x] Do behavior need to be changed for other opcodes (P256_CHKSIGNU, BLS_VERIFY or others)?

    Resolved: there is no need to support this behavior for anything other than basic signatures. Alternative cryptography is mostly used in specialized cases where no one will just deploy the same contracts across multiple networks as is.

    NOTE: Signature verification for algorithms other than Ed25519 is yet to be implemented in tycho-vm

  • Should we really compute a prefix as a cell hash?

    Pros:

    • Can potentially store byte-unaligned data.

    Cons:

    • Adds a strange dependency on cells for crypto modules in SDK's.
    • Increases the amount of data being checked (4 bytes vs 32 bytes).
  • Should signature domain contain workchain or address?

    Pros:

    • Adds an additional guard against replaying messages.

    Cons:

    • It's unclear how to verify signatures that are passed between workchains or those not tied to any address (e.g. election data signatures). Could potentially make contracts much more complicated since you'd need to know some variable context everywhere.
    • If for some reason some contract needs to be deployed with signature in its StateInit data, it won't be possible to compute its address and keep the signature valid at the same time.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment