Module: Bitcoin::MessageSign

Defined in:
lib/bitcoin/message_sign.rb

Defined Under Namespace

Classes: Error

Constant Summary collapse

FORMAT_LEGACY =
:legacy
FORMAT_SIMPLE =
:simple
FORMAT_FULL =
:full

Class Method Summary collapse

Class Method Details

.message_hash(message, prefix: Bitcoin.chain_params.message_magic, legacy: true) ⇒ Object

Hashes a message for signing and verification.



82
83
84
85
86
87
88
# File 'lib/bitcoin/message_sign.rb', line 82

def message_hash(message, prefix: Bitcoin.chain_params.message_magic, legacy: true)
  if legacy
    Bitcoin.double_sha256(Bitcoin.pack_var_string(prefix) << Bitcoin.pack_var_string(message))
  else
    Bitcoin.tagged_hash('BIP0322-signed-message', message)
  end
end

.sign_message(key, message, prefix: Bitcoin.chain_params.message_magic, format: FORMAT_LEGACY, address: nil) ⇒ String

Sign a message.

Parameters:

  • key (Bitcoin::Key)

    Private key to sign.

  • message (String)

    The message to be signed.

  • address (String) (defaults to: nil)

    An address of the key used for signing (required for full or simple format).

  • format (String) (defaults to: FORMAT_LEGACY)

    Format of signature data. Default is FORMAT_LEGACY.

  • prefix (String) (defaults to: Bitcoin.chain_params.message_magic)

    (Optional) Prefix used in legacy format.

Returns:

  • (String)

    Signature, base64 encoded.



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/bitcoin/message_sign.rb', line 20

def sign_message(key, message, prefix: Bitcoin.chain_params.message_magic, format: FORMAT_LEGACY, address: nil)
  validate_format!(format)
  digest = message_hash(message, prefix: prefix, legacy: format == FORMAT_LEGACY)
  sig = case format
        when FORMAT_LEGACY
          key.sign_compact(digest)
        else
          validate_address!(address)
          addr = Bitcoin::Script.parse_from_addr(address)
          sig_ver, algo = if addr.p2wpkh?
                            [:witness_v0, :ecdsa]
                          elsif addr.p2tr?
                            [:taproot, :schnorr]
                          else
                            raise ArgumentError "#{address} dose not supported."
                          end
          tx = to_sign_tx(digest, address)
          prev_out = Bitcoin::TxOut.new(script_pubkey: addr)
          sighash = tx.sighash_for_input(0, addr, sig_version: sig_ver, amount: 0, prevouts: [prev_out])
          sig = key.sign(sighash, algo: algo) + [Bitcoin::SIGHASH_TYPE[:all]].pack('C')
          tx.in[0].script_witness.stack << sig
          tx.in[0].script_witness.stack << key.pubkey.htb
          format == FORMAT_SIMPLE ? tx.in[0].script_witness.to_payload : tx.to_payload
        end
  Base64.strict_encode64(sig)
end

.to_sign_tx(digest, addr) ⇒ Object



114
115
116
117
118
119
120
121
122
# File 'lib/bitcoin/message_sign.rb', line 114

def to_sign_tx(digest, addr)
  tx = Bitcoin::Tx.new
  tx.version = 0
  tx.lock_time = 0
  prev_out = Bitcoin::OutPoint.from_txid(to_spend_tx(digest, addr).txid, 0)
  tx.in << Bitcoin::TxIn.new(out_point: prev_out, sequence: 0)
  tx.out << Bitcoin::TxOut.new(script_pubkey: Bitcoin::Script.new << Bitcoin::Opcodes::OP_RETURN)
  tx
end

.to_spend_tx(digest, addr) ⇒ Object



101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/bitcoin/message_sign.rb', line 101

def to_spend_tx(digest, addr)
  validate_address!(addr)
  message_challenge = Bitcoin::Script.parse_from_addr(addr)
  tx = Bitcoin::Tx.new
  tx.version = 0
  tx.lock_time = 0
  prev_out = Bitcoin::OutPoint.create_coinbase_outpoint
  script_sig = Bitcoin::Script.new << Bitcoin::Opcodes::OP_0 << digest
  tx.in << Bitcoin::TxIn.new(out_point: prev_out, sequence: 0, script_sig: script_sig)
  tx.out << Bitcoin::TxOut.new(script_pubkey: message_challenge)
  tx
end

.verify_message(address, signature, message, prefix: Bitcoin.chain_params.message_magic) ⇒ Boolean

Verify a signed message.

Parameters:

  • address (String)

    Signer’s bitcoin address, it must refer to a public key.

  • signature (String)

    The signature in base64 format.

  • message (String)

    The message that was signed.

Returns:

  • (Boolean)

    Verification result.



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/bitcoin/message_sign.rb', line 52

def verify_message(address, signature, message, prefix: Bitcoin.chain_params.message_magic)
  addr_script = Bitcoin::Script.parse_from_addr(address)
  begin
    sig = Base64.strict_decode64(signature)
  rescue ArgumentError
    raise ArgumentError, 'Invalid signature'
  end
  if addr_script.p2pkh?
    begin
      # Legacy verification
      pubkey = Bitcoin::Key.recover_compact(message_hash(message, prefix: prefix, legacy: true), sig)
      return false unless pubkey
      pubkey.to_p2pkh == address
    rescue RuntimeError
      return false
    end
  elsif addr_script.witness_program?
    # BIP322 verification
    tx = to_sign_tx(message_hash(message, prefix: prefix, legacy: false), address)
    tx.in[0].script_witness = Bitcoin::ScriptWitness.parse_from_payload(sig)
    script_pubkey = Bitcoin::Script.parse_from_addr(address)
    tx_out = Bitcoin::TxOut.new(script_pubkey: script_pubkey)
    interpreter = Bitcoin::ScriptInterpreter.new(checker: Bitcoin::TxChecker.new(tx: tx, input_index: 0, prevouts: [tx_out]))
    interpreter.verify_script(Bitcoin::Script.new, script_pubkey, tx.in[0].script_witness)
  else
    raise ArgumentError, "This address unsupported."
  end
end