I'm trying to write a sign function in Ruby for Ethereum using secp256k1.

I have access to this sign_compact function which seems to be for Bitcoin:


I'm trying to use this to sign for Ethereum but there seems to be some steps missing. Specifically, something to do with the head, r-value and s-value. Someone seems to be doing this properly using OpenSSL instead of secp256k1 here:


The lines that seem to be different are:

if signature.get_array_of_pointer(0, 2).all?{|i| BN_num_bits(i) <= 256 }
    head = [ Eth.v_base + i ].pack("C")
    if public_key_hex == recover_public_key_from_signature(hash, [head, r, s].join, i, pubkey_compressed)
      rec_id = i; break

How would I go about using the secp256k1 sign_compact function to sign Ethereum messages? Is there something I need to change for the signature? Or is there somewhere to find the protocol for this?


2 Answers 2


I have never programmed Ruby but I know a little bit about Bitcoin and Ethereum signatures. I hope I can help you.

A bitcoin signature consists of two parts: (r,s). An Ethereum signature consists of three parts: (v,r,s).

The extra value v which is only one byte allows for the derivation of the public key from the signature. There are only four possible values of v so the code that you are pointing to goes through all four possible values to check which is the correct one. If the recovered public key matches the provided one (input to the sign_compact function) we have found the correct rec_id value and this one is then returned along with the signature. So the function ends up returning (v,r,s) instead of just (r,s),

Perhaps the Yellow Paper may provide some illumination to this problem as well: enter image description here

It looks to me like the header value in the sign_compact for Bitcoin actually is set to this recovery bit (assuming that the function call secp256k1_ecdsa_recoverable_signature_serialize_compact sets this value). So I believe the function sign_compact actually returns what you need.


Check out the Ruby gem rbsecp256k1/examples (lines 40-51):

# Signs a personal message with the given private key.
# @param private_key [Secp256k1::PrivateKey] key to use for signing.
# @param chain_id [Integer] unique identifier for chain.
# @return [String] binary signature data including recovery id v at end.
def sign(private_key, chain_id)
  ctx = Secp256k1::Context.new
  signature, recovery_id = ctx.sign_recoverable(private_key, hash).compact
  result = signature.bytes
  result = result.append(Chains.to_v(recovery_id, chain_id))

This is a fairly involved example that explicitly addresses your question.

And since you asked about the eth gem specifically, I very recently removed the OpenSSL code completely and implemented the signatures and recovery exclusively in Secp256k1 (lines 65-83):

# Signs arbitrary data without validation. Should not be used unless really
# desired. See also: {Key.personal_sign}, {Key.sign_typed_data}, and
# {Signature.recover}.
# @param blob [Object] that arbitrary data to be signed.
# @param chain_id [Integer] the chain id the signature should be generated on.
# @return [String] a hexa-decimal signature.
def sign(blob, chain_id = nil)
  context = Secp256k1::Context.new
  compact, recovery_id = context.sign_recoverable(@private_key, blob).compact
  signature = compact.bytes
  v = Chain.to_v recovery_id, chain_id
  is_leading_zero = true
  [v].pack("N").unpack("C*").each do |byte|
    is_leading_zero = false if byte > 0 and is_leading_zero
    signature.append byte unless is_leading_zero and byte === 0
  Util.bin_to_hex signature.pack "c*"

It's in the Eth::Key class and also contains a personal_sign and sign_typed_data.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.