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.


93
94
95
96
97
98
99
# File 'lib/bitcoin/message_sign.rb', line 93

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


133
134
135
136
137
138
139
140
141
# File 'lib/bitcoin/message_sign.rb', line 133

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


120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/bitcoin/message_sign.rb', line 120

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
80
81
82
83
84
85
86
87
88
89
90
# 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 Exception
      false
    end
  elsif addr_script.witness_program?
    # BIP322 verification
    digest = message_hash(message, prefix: prefix, legacy: false)
    begin
      # Full
      tx = Bitcoin::Tx.parse_from_payload(sig)
      validate_to_sign_tx!(tx)
      to_spend = to_spend_tx(digest, address)
      return false unless tx.in[0].out_point.tx_hash == to_spend.tx_hash
    rescue Exception
      # Simple
      tx = to_sign_tx(digest, address)
      tx.in[0].script_witness = Bitcoin::ScriptWitness.parse_from_payload(sig)
    end
    script_pubkey = Bitcoin::Script.parse_from_addr(address)
    tx_out = Bitcoin::TxOut.new(script_pubkey: script_pubkey)
    flags = Bitcoin::STANDARD_SCRIPT_VERIFY_FLAGS
    interpreter = Bitcoin::ScriptInterpreter.new(flags: flags, 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