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.



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

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



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

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



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

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
# 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)
    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