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
SIGNATURE_PREFIX_SIMPLE =

Prefix

'smp'
SIGNATURE_PREFIX_FULL =
'ful'
SIGNATURE_PREFIX_POF =
'pof'
BIP322_VERIFY_FLAGS =

BIP-322 Required rules

[
  SCRIPT_VERIFY_P2SH, SCRIPT_VERIFY_DERSIG, SCRIPT_VERIFY_STRICTENC,
  SCRIPT_VERIFY_LOW_S, SCRIPT_VERIFY_NULLFAIL, SCRIPT_VERIFY_MINIMALDATA,
  SCRIPT_VERIFY_CLEANSTACK, SCRIPT_VERIFY_MINIMALIF,
  SCRIPT_VERIFY_CONST_SCRIPTCODE,
  SCRIPT_VERIFY_WITNESS, SCRIPT_VERIFY_TAPROOT,
  SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_NOPS,
  SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM
].inject(:|)

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.



119
120
121
122
123
124
125
# File 'lib/bitcoin/message_sign.rb', line 119

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.



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/bitcoin/message_sign.rb', line 36

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)
  prefix_marker = ''
  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? || addr.p2wsh?
                            [:witness_v0, :ecdsa]
                          elsif addr.p2tr?
                            [:taproot, :schnorr]
                          else

                          end
          tx = to_sign_tx(digest, address)
          prev_out = Bitcoin::TxOut.new(script_pubkey: addr, value: 0)
          if addr.p2tr?
            sighash = tx.sighash_for_input(0, addr, sig_version: :taproot, prevouts: [prev_out], hash_type: Bitcoin::SIGHASH_TYPE[:default])
            tweaked = Bitcoin::Taproot.tweak_private_key(key, '')
            tx.in[0].script_witness.stack << tweaked.sign(sighash, algo: :schnorr)
          elsif addr.p2wpkh? || addr.p2wsh?
            sighash = tx.sighash_for_input(0, addr, sig_version: :witness_v0, amount: 0, prevouts: [prev_out])
            ecdsa  = key.sign(sighash, algo: :ecdsa) + [Bitcoin::SIGHASH_TYPE[:all]].pack('C')
            tx.in[0].script_witness.stack << ecdsa
            tx.in[0].script_witness.stack << key.pubkey.htb
          else
            raise ArgumentError, "#{address} dose not supported."
          end
          prefix_marker = format == FORMAT_SIMPLE ? SIGNATURE_PREFIX_SIMPLE : SIGNATURE_PREFIX_FULL
          format == FORMAT_SIMPLE ? tx.in[0].script_witness.to_payload : tx.to_payload
        end
  prefix_marker + Base64.strict_encode64(sig)
end

.to_sign_tx(digest, addr) ⇒ Object



159
160
161
162
163
164
165
166
167
# File 'lib/bitcoin/message_sign.rb', line 159

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



146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/bitcoin/message_sign.rb', line 146

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.



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/bitcoin/message_sign.rb', line 78

def verify_message(address, signature, message, prefix: Bitcoin.chain_params.message_magic)
  addr_script = Bitcoin::Script.parse_from_addr(address)
  variant, body = case signature
                  when ''
                    raise ArgumentError, 'signature too short'
                  when /\A#{SIGNATURE_PREFIX_SIMPLE}/
                    [:simple, signature[3..]]
                  when /\A#{SIGNATURE_PREFIX_FULL}/
                    [:full, signature[3..]]
                  when /\A#{SIGNATURE_PREFIX_POF}/
                    [:pof, signature[3..]]
                  else
                    [:fallback, signature]
                  end
  begin
    payload = Base64.strict_decode64(body)
  rescue ArgumentError
    raise ArgumentError, 'Invalid signature'
  end

  case variant
  when :simple, :fallback
    return verify_legacy(address, payload, message, prefix: prefix) if addr_script.p2pkh?
    raise ArgumentError, 'simple format not supported for this address' unless
      addr_script.p2wpkh? || addr_script.p2wsh? || addr_script.p2tr?
    verify_simple(addr_script, payload, message, prefix: prefix)
  when :full
    begin
      tx = Bitcoin::Tx.parse_from_payload(payload)
    rescue StandardError
      raise ArgumentError, 'error parsing signature as full variant'
    end
    verify_full(addr_script, tx, message, prefix: prefix)
  when :pof
    raise NotImplementedError, 'proof of funds variant is not supported'
  else
    raise ArgumentError, "unknown signature variant: #{variant.inspect}"
  end
end