bsv-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +58 -0
  3. data/LICENCE +86 -0
  4. data/README.md +155 -0
  5. data/lib/bsv/attest/configuration.rb +9 -0
  6. data/lib/bsv/attest/response.rb +19 -0
  7. data/lib/bsv/attest/verification_error.rb +7 -0
  8. data/lib/bsv/attest/version.rb +7 -0
  9. data/lib/bsv/attest.rb +71 -0
  10. data/lib/bsv/network/arc.rb +113 -0
  11. data/lib/bsv/network/broadcast_error.rb +15 -0
  12. data/lib/bsv/network/broadcast_response.rb +29 -0
  13. data/lib/bsv/network/chain_provider_error.rb +14 -0
  14. data/lib/bsv/network/utxo.rb +28 -0
  15. data/lib/bsv/network/whats_on_chain.rb +82 -0
  16. data/lib/bsv/network.rb +12 -0
  17. data/lib/bsv/primitives/base58.rb +117 -0
  18. data/lib/bsv/primitives/bsm.rb +131 -0
  19. data/lib/bsv/primitives/curve.rb +115 -0
  20. data/lib/bsv/primitives/digest.rb +99 -0
  21. data/lib/bsv/primitives/ecdsa.rb +224 -0
  22. data/lib/bsv/primitives/ecies.rb +128 -0
  23. data/lib/bsv/primitives/extended_key.rb +315 -0
  24. data/lib/bsv/primitives/mnemonic/wordlist.rb +270 -0
  25. data/lib/bsv/primitives/mnemonic.rb +192 -0
  26. data/lib/bsv/primitives/private_key.rb +139 -0
  27. data/lib/bsv/primitives/public_key.rb +118 -0
  28. data/lib/bsv/primitives/schnorr.rb +108 -0
  29. data/lib/bsv/primitives/signature.rb +136 -0
  30. data/lib/bsv/primitives.rb +23 -0
  31. data/lib/bsv/script/builder.rb +73 -0
  32. data/lib/bsv/script/chunk.rb +77 -0
  33. data/lib/bsv/script/interpreter/error.rb +54 -0
  34. data/lib/bsv/script/interpreter/interpreter.rb +281 -0
  35. data/lib/bsv/script/interpreter/operations/arithmetic.rb +243 -0
  36. data/lib/bsv/script/interpreter/operations/bitwise.rb +68 -0
  37. data/lib/bsv/script/interpreter/operations/crypto.rb +209 -0
  38. data/lib/bsv/script/interpreter/operations/data_push.rb +34 -0
  39. data/lib/bsv/script/interpreter/operations/flow_control.rb +94 -0
  40. data/lib/bsv/script/interpreter/operations/splice.rb +89 -0
  41. data/lib/bsv/script/interpreter/operations/stack_ops.rb +112 -0
  42. data/lib/bsv/script/interpreter/script_number.rb +218 -0
  43. data/lib/bsv/script/interpreter/stack.rb +203 -0
  44. data/lib/bsv/script/opcodes.rb +165 -0
  45. data/lib/bsv/script/script.rb +424 -0
  46. data/lib/bsv/script.rb +20 -0
  47. data/lib/bsv/transaction/beef.rb +323 -0
  48. data/lib/bsv/transaction/merkle_path.rb +250 -0
  49. data/lib/bsv/transaction/p2pkh.rb +44 -0
  50. data/lib/bsv/transaction/sighash.rb +48 -0
  51. data/lib/bsv/transaction/transaction.rb +380 -0
  52. data/lib/bsv/transaction/transaction_input.rb +109 -0
  53. data/lib/bsv/transaction/transaction_output.rb +51 -0
  54. data/lib/bsv/transaction/unlocking_script_template.rb +36 -0
  55. data/lib/bsv/transaction/var_int.rb +50 -0
  56. data/lib/bsv/transaction.rb +21 -0
  57. data/lib/bsv/version.rb +5 -0
  58. data/lib/bsv/wallet/insufficient_funds_error.rb +15 -0
  59. data/lib/bsv/wallet/wallet.rb +119 -0
  60. data/lib/bsv/wallet.rb +8 -0
  61. data/lib/bsv-attest.rb +4 -0
  62. data/lib/bsv-sdk.rb +11 -0
  63. metadata +104 -0
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+
7
+ module BSV
8
+ module Network
9
+ # WhatsOnChain chain data provider for reading transactions and UTXOs
10
+ # from the BSV network.
11
+ #
12
+ # Any object responding to #fetch_utxos(address) and
13
+ # #fetch_transaction(txid) can serve as a chain data provider;
14
+ # this class implements that contract using the WhatsOnChain API.
15
+ #
16
+ # The HTTP client is injectable for testability. It must respond to
17
+ # #request(uri, request) and return an object with #code and #body.
18
+ class WhatsOnChain
19
+ BASE_URL = 'https://api.whatsonchain.com'
20
+
21
+ def initialize(network: :mainnet, http_client: nil)
22
+ @network = network == :mainnet ? 'main' : 'test'
23
+ @http_client = http_client
24
+ end
25
+
26
+ # Fetch unspent transaction outputs for an address.
27
+ # @param address [String] BSV address
28
+ # @return [Array<UTXO>]
29
+ def fetch_utxos(address)
30
+ response = get("/v1/bsv/#{@network}/address/#{address}/unspent")
31
+ body = JSON.parse(response.body)
32
+
33
+ body.map do |entry|
34
+ UTXO.new(
35
+ tx_hash: entry['tx_hash'],
36
+ tx_pos: entry['tx_pos'],
37
+ satoshis: entry['value'],
38
+ height: entry['height']
39
+ )
40
+ end
41
+ end
42
+
43
+ # Fetch a raw transaction by its txid and parse it.
44
+ # @param txid [String] transaction ID (hex)
45
+ # @return [BSV::Transaction::Transaction]
46
+ def fetch_transaction(txid)
47
+ response = get("/v1/bsv/#{@network}/tx/#{txid}/hex")
48
+ BSV::Transaction::Transaction.from_hex(response.body)
49
+ end
50
+
51
+ private
52
+
53
+ def get(path)
54
+ uri = URI("#{BASE_URL}#{path}")
55
+ request = Net::HTTP::Get.new(uri)
56
+ response = execute(uri, request)
57
+ handle_response(response)
58
+ response
59
+ end
60
+
61
+ def execute(uri, request)
62
+ if @http_client
63
+ @http_client.request(uri, request)
64
+ else
65
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
66
+ http.request(request)
67
+ end
68
+ end
69
+ end
70
+
71
+ def handle_response(response)
72
+ code = response.code.to_i
73
+ return if (200..299).cover?(code)
74
+
75
+ raise ChainProviderError.new(
76
+ response.body || "HTTP #{code}",
77
+ status_code: code
78
+ )
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Network
5
+ autoload :BroadcastError, 'bsv/network/broadcast_error'
6
+ autoload :BroadcastResponse, 'bsv/network/broadcast_response'
7
+ autoload :ChainProviderError, 'bsv/network/chain_provider_error'
8
+ autoload :UTXO, 'bsv/network/utxo'
9
+ autoload :ARC, 'bsv/network/arc'
10
+ autoload :WhatsOnChain, 'bsv/network/whats_on_chain'
11
+ end
12
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Primitives
5
+ # Base58 and Base58Check encoding/decoding.
6
+ #
7
+ # Implements the Base58 alphabet used throughout Bitcoin for addresses,
8
+ # WIF keys, and extended keys. Base58Check adds a 4-byte double-SHA-256
9
+ # checksum for error detection.
10
+ #
11
+ # @example Encode and decode an address payload
12
+ # encoded = BSV::Primitives::Base58.check_encode(payload)
13
+ # decoded = BSV::Primitives::Base58.check_decode(encoded)
14
+ module Base58
15
+ # The Base58 alphabet (no 0, O, I, l to avoid visual ambiguity).
16
+ ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
17
+
18
+ # The base (58).
19
+ BASE = ALPHABET.length # 58
20
+
21
+ # Reverse lookup table mapping ASCII byte values to Base58 digit indices.
22
+ DECODE_MAP = Array.new(256, -1).tap do |map|
23
+ ALPHABET.each_char.with_index { |c, i| map[c.ord] = i }
24
+ end.freeze
25
+
26
+ # Raised when a Base58Check checksum does not match.
27
+ class ChecksumError < StandardError; end
28
+
29
+ module_function
30
+
31
+ # Encode binary data as a Base58 string.
32
+ #
33
+ # Leading zero bytes are preserved as +'1'+ characters.
34
+ #
35
+ # @param bytes [String] binary data to encode
36
+ # @return [String] Base58-encoded string
37
+ def encode(bytes)
38
+ return '' if bytes.empty?
39
+
40
+ # Count leading zero bytes
41
+ leading_zeros = 0
42
+ bytes.each_byte { |b| b.zero? ? leading_zeros += 1 : break }
43
+
44
+ # Convert to big integer and repeatedly divide by 58
45
+ n = bytes.unpack1('H*').to_i(16)
46
+ result = +''
47
+ while n.positive?
48
+ n, remainder = n.divmod(BASE)
49
+ result << ALPHABET[remainder]
50
+ end
51
+
52
+ # Preserve leading zeros as '1' characters
53
+ result << (ALPHABET[0] * leading_zeros)
54
+ result.reverse!
55
+ result
56
+ end
57
+
58
+ # Decode a Base58 string to binary data.
59
+ #
60
+ # Leading +'1'+ characters are decoded as zero bytes.
61
+ #
62
+ # @param string [String] Base58-encoded string
63
+ # @return [String] decoded binary data
64
+ # @raise [ArgumentError] if the string contains invalid Base58 characters
65
+ def decode(string)
66
+ return ''.b if string.empty?
67
+
68
+ # Count leading '1' characters (representing zero bytes)
69
+ leading_ones = 0
70
+ string.each_char { |c| c == ALPHABET[0] ? leading_ones += 1 : break }
71
+
72
+ # Convert from base58 to integer
73
+ n = 0
74
+ string.each_char do |c|
75
+ digit = DECODE_MAP[c.ord]
76
+ raise ArgumentError, "invalid Base58 character: #{c.inspect}" if digit == -1
77
+
78
+ n = (n * BASE) + digit
79
+ end
80
+
81
+ # Convert integer to bytes
82
+ hex = n.zero? ? '' : n.to_s(16)
83
+ hex = "0#{hex}" if hex.length.odd?
84
+ result = [hex].pack('H*')
85
+
86
+ # Prepend zero bytes for leading '1' characters
87
+ (("\x00" * leading_ones) + result).b
88
+ end
89
+
90
+ # Encode binary data with a 4-byte double-SHA-256 checksum appended.
91
+ #
92
+ # @param payload [String] binary data to encode
93
+ # @return [String] Base58Check-encoded string
94
+ def check_encode(payload)
95
+ checksum = Digest.sha256d(payload)[0, 4]
96
+ encode(payload + checksum)
97
+ end
98
+
99
+ # Decode a Base58Check string and verify its checksum.
100
+ #
101
+ # @param string [String] Base58Check-encoded string
102
+ # @return [String] decoded payload (without checksum)
103
+ # @raise [ChecksumError] if the checksum does not match or input is too short
104
+ def check_decode(string)
105
+ data = decode(string)
106
+ raise ChecksumError, 'input too short for checksum' if data.length < 4
107
+
108
+ payload = data[0...-4]
109
+ checksum = data[-4..]
110
+ expected = Digest.sha256d(payload)[0, 4]
111
+ raise ChecksumError, 'checksum mismatch' unless checksum == expected
112
+
113
+ payload
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Primitives
5
+ # Bitcoin Signed Messages (BSM).
6
+ #
7
+ # Signs and verifies messages using the standard Bitcoin message signing
8
+ # protocol. Messages are prefixed with +"Bitcoin Signed Message:\n"+,
9
+ # length-prefixed, and double-SHA-256 hashed before signing with
10
+ # recoverable ECDSA. Signatures are 65-byte compact format, base64-encoded.
11
+ #
12
+ # @example Sign and verify a message
13
+ # key = BSV::Primitives::PrivateKey.generate
14
+ # sig = BSV::Primitives::BSM.sign('hello', key)
15
+ # BSV::Primitives::BSM.verify('hello', sig, key.public_key.address) #=> true
16
+ module BSM
17
+ # The standard Bitcoin message signing prefix.
18
+ MAGIC_PREFIX = "Bitcoin Signed Message:\n".b.freeze
19
+
20
+ module_function
21
+
22
+ # Sign a message with a private key.
23
+ #
24
+ # Produces a 65-byte compact recoverable signature encoded as base64.
25
+ # The flag byte (31-34) indicates compressed P2PKH recovery per BIP-137.
26
+ #
27
+ # @param message [String] the message to sign
28
+ # @param private_key [PrivateKey] the signing key
29
+ # @return [String] base64-encoded compact signature
30
+ def sign(message, private_key)
31
+ hash = magic_hash(message)
32
+ sig, recovery_id = ECDSA.sign_recoverable(hash, private_key.bn)
33
+
34
+ # Flag byte: 31-34 = compressed P2PKH (BIP-137)
35
+ flag = 31 + recovery_id
36
+ compact = [flag].pack('C') + bn_to_bytes(sig.r) + bn_to_bytes(sig.s)
37
+ [compact].pack('m0')
38
+ end
39
+
40
+ # Verify a signed message against a Bitcoin address.
41
+ #
42
+ # Recovers the public key from the compact signature and checks
43
+ # whether the derived address matches the expected address.
44
+ #
45
+ # @param message [String] the original message
46
+ # @param signature [String] base64-encoded compact signature
47
+ # @param address [String] the expected Bitcoin address
48
+ # @return [Boolean] +true+ if the signature is valid for the given address
49
+ # @raise [ArgumentError] if the signature encoding or flag byte is invalid
50
+ def verify(message, signature, address)
51
+ compact = decode_compact(signature)
52
+ flag = compact.getbyte(0)
53
+ validate_flag!(flag)
54
+
55
+ recovery_id = (flag - 27) & 3
56
+ compressed = flag >= 31
57
+
58
+ r_bn = OpenSSL::BN.new(compact[1, 32], 2)
59
+ s_bn = OpenSSL::BN.new(compact[33, 32], 2)
60
+ sig = Signature.new(r_bn, s_bn)
61
+
62
+ hash = magic_hash(message)
63
+ pub = ECDSA.recover_public_key(hash, sig, recovery_id)
64
+
65
+ derived = if compressed
66
+ pub.address
67
+ else
68
+ h160 = Digest.hash160(pub.uncompressed)
69
+ Base58.check_encode(PublicKey::MAINNET_PUBKEY_HASH + h160)
70
+ end
71
+
72
+ derived == address
73
+ rescue OpenSSL::PKey::EC::Point::Error
74
+ false
75
+ end
76
+
77
+ # Compute the double-SHA-256 hash of a Bitcoin-prefixed message.
78
+ #
79
+ # @param message [String] the message to hash
80
+ # @return [String] 32-byte double-SHA-256 digest
81
+ def magic_hash(message)
82
+ message = message.encode('UTF-8') if message.encoding != Encoding::UTF_8
83
+ msg_bytes = message.b
84
+ buf = encode_varint(MAGIC_PREFIX.bytesize) + MAGIC_PREFIX +
85
+ encode_varint(msg_bytes.bytesize) + msg_bytes
86
+ Digest.sha256d(buf)
87
+ end
88
+
89
+ class << self
90
+ private
91
+
92
+ def bn_to_bytes(bn)
93
+ bytes = bn.to_s(2)
94
+ bytes = ("\x00".b * (32 - bytes.length)) + bytes if bytes.length < 32
95
+ bytes
96
+ end
97
+
98
+ def decode_compact(signature)
99
+ compact = signature.unpack1('m0')
100
+ unless compact.bytesize == 65
101
+ raise ArgumentError,
102
+ "invalid signature length: #{compact.bytesize} (expected 65)"
103
+ end
104
+
105
+ compact
106
+ rescue ArgumentError => e
107
+ raise e if e.message.include?('invalid signature length')
108
+
109
+ raise ArgumentError, "invalid base64 encoding: #{e.message}"
110
+ end
111
+
112
+ def validate_flag!(flag)
113
+ return if flag.between?(27, 34)
114
+
115
+ raise ArgumentError,
116
+ "flag byte #{flag} out of range (expected 27-34)"
117
+ end
118
+
119
+ def encode_varint(len)
120
+ if len < 0xFD
121
+ [len].pack('C')
122
+ elsif len <= 0xFFFF
123
+ "\xFD".b + [len].pack('v')
124
+ else
125
+ "\xFE".b + [len].pack('V')
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module BSV
6
+ module Primitives
7
+ # Low-level secp256k1 elliptic curve operations.
8
+ #
9
+ # Wraps +OpenSSL::PKey::EC+ to provide point arithmetic, scalar
10
+ # multiplication, and key construction helpers used throughout the SDK.
11
+ # All constants and methods operate on the secp256k1 curve.
12
+ module Curve
13
+ # The secp256k1 curve group.
14
+ GROUP = OpenSSL::PKey::EC::Group.new('secp256k1')
15
+
16
+ # The curve order (number of points on the curve).
17
+ N = GROUP.order
18
+
19
+ # The generator point (base point).
20
+ G = GROUP.generator
21
+
22
+ # Half the curve order, used for low-S normalisation.
23
+ HALF_N = (N >> 1)
24
+
25
+ module_function
26
+
27
+ # Multiply the generator point by a scalar.
28
+ #
29
+ # @param scalar_bn [OpenSSL::BN] the scalar multiplier
30
+ # @return [OpenSSL::PKey::EC::Point] the resulting curve point
31
+ def multiply_generator(scalar_bn)
32
+ G.mul(scalar_bn)
33
+ end
34
+
35
+ # Multiply an arbitrary curve point by a scalar.
36
+ #
37
+ # @param point [OpenSSL::PKey::EC::Point] the point to multiply
38
+ # @param scalar_bn [OpenSSL::BN] the scalar multiplier
39
+ # @return [OpenSSL::PKey::EC::Point] the resulting curve point
40
+ def multiply_point(point, scalar_bn)
41
+ point.mul(scalar_bn)
42
+ end
43
+
44
+ # Add two curve points together.
45
+ #
46
+ # Uses +Point#add+ where available (Ruby 3.0+ / OpenSSL 3), falling
47
+ # back to multi-scalar multiplication for Ruby 2.7 compatibility.
48
+ #
49
+ # @param point_a [OpenSSL::PKey::EC::Point] first point
50
+ # @param point_b [OpenSSL::PKey::EC::Point] second point
51
+ # @return [OpenSSL::PKey::EC::Point] the sum of the two points
52
+ def add_points(point_a, point_b)
53
+ if point_a.respond_to?(:add)
54
+ point_a.add(point_b)
55
+ else
56
+ # Ruby 2.7 / OpenSSL < 3: use multi-scalar mul
57
+ # point_a.mul(bns, points) = bns[0]*point_a + bns[1]*points[0] + ...
58
+ one = OpenSSL::BN.new('1')
59
+ point_a.mul([one, one], [point_b])
60
+ end
61
+ end
62
+
63
+ # Extract the x-coordinate from a curve point as a big number.
64
+ #
65
+ # @param point [OpenSSL::PKey::EC::Point] the curve point
66
+ # @return [OpenSSL::BN] the x-coordinate
67
+ def point_x(point)
68
+ x_hex = point.to_bn(:uncompressed).to_s(16)
69
+ # Uncompressed format: 04 || X (64 hex) || Y (64 hex)
70
+ OpenSSL::BN.new(x_hex[2, 64], 16)
71
+ end
72
+
73
+ # Reconstruct a curve point from its byte representation.
74
+ #
75
+ # @param bytes [String] compressed (33 bytes) or uncompressed (65 bytes) point encoding
76
+ # @return [OpenSSL::PKey::EC::Point] the decoded curve point
77
+ def point_from_bytes(bytes)
78
+ OpenSSL::PKey::EC::Point.new(GROUP, OpenSSL::BN.new(bytes, 2))
79
+ end
80
+
81
+ # Build an +OpenSSL::PKey::EC+ key object from raw private key bytes.
82
+ #
83
+ # @param private_bytes [String] 32-byte big-endian private key
84
+ # @return [OpenSSL::PKey::EC] an EC key with both private and public components
85
+ def ec_key_from_private_bytes(private_bytes)
86
+ priv_bn = OpenSSL::BN.new(private_bytes, 2)
87
+ pub_point = multiply_generator(priv_bn)
88
+
89
+ asn1 = OpenSSL::ASN1::Sequence.new([
90
+ OpenSSL::ASN1::Integer.new(1),
91
+ OpenSSL::ASN1::OctetString.new(private_bytes),
92
+ OpenSSL::ASN1::ObjectId.new('secp256k1', 0, :EXPLICIT),
93
+ OpenSSL::ASN1::BitString.new(pub_point.to_octet_string(:compressed), 1,
94
+ :EXPLICIT)
95
+ ])
96
+ OpenSSL::PKey::EC.new(asn1.to_der)
97
+ end
98
+
99
+ # Build an +OpenSSL::PKey::EC+ key object from raw public key bytes.
100
+ #
101
+ # @param public_bytes [String] compressed (33) or uncompressed (65) public key bytes
102
+ # @return [OpenSSL::PKey::EC] an EC key with only the public component
103
+ def ec_key_from_public_bytes(public_bytes)
104
+ asn1 = OpenSSL::ASN1::Sequence.new([
105
+ OpenSSL::ASN1::Sequence.new([
106
+ OpenSSL::ASN1::ObjectId.new('id-ecPublicKey'),
107
+ OpenSSL::ASN1::ObjectId.new('secp256k1')
108
+ ]),
109
+ OpenSSL::ASN1::BitString.new(public_bytes)
110
+ ])
111
+ OpenSSL::PKey::EC.new(asn1.to_der)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module BSV
6
+ module Primitives
7
+ # Cryptographic hash functions and HMAC operations.
8
+ #
9
+ # Thin wrappers around +OpenSSL::Digest+ and +OpenSSL::HMAC+ providing
10
+ # the hash algorithms used throughout the BSV protocol: SHA-1, SHA-256,
11
+ # double-SHA-256, SHA-512, RIPEMD-160, Hash160, HMAC, and PBKDF2.
12
+ module Digest
13
+ module_function
14
+
15
+ # Compute SHA-1 digest.
16
+ #
17
+ # @param data [String] binary data to hash
18
+ # @return [String] 20-byte digest
19
+ def sha1(data)
20
+ OpenSSL::Digest::SHA1.digest(data)
21
+ end
22
+
23
+ # Compute SHA-256 digest.
24
+ #
25
+ # @param data [String] binary data to hash
26
+ # @return [String] 32-byte digest
27
+ def sha256(data)
28
+ OpenSSL::Digest::SHA256.digest(data)
29
+ end
30
+
31
+ # Compute double-SHA-256 (SHA-256d) digest.
32
+ #
33
+ # Used extensively in Bitcoin for transaction and block hashing.
34
+ #
35
+ # @param data [String] binary data to hash
36
+ # @return [String] 32-byte digest
37
+ def sha256d(data)
38
+ sha256(sha256(data))
39
+ end
40
+
41
+ # Compute SHA-512 digest.
42
+ #
43
+ # @param data [String] binary data to hash
44
+ # @return [String] 64-byte digest
45
+ def sha512(data)
46
+ OpenSSL::Digest::SHA512.digest(data)
47
+ end
48
+
49
+ # Compute RIPEMD-160 digest.
50
+ #
51
+ # @param data [String] binary data to hash
52
+ # @return [String] 20-byte digest
53
+ def ripemd160(data)
54
+ OpenSSL::Digest::RIPEMD160.digest(data)
55
+ end
56
+
57
+ # Compute Hash160: RIPEMD-160(SHA-256(data)).
58
+ #
59
+ # Standard Bitcoin hash used for addresses and P2PKH script matching.
60
+ #
61
+ # @param data [String] binary data to hash
62
+ # @return [String] 20-byte digest
63
+ def hash160(data)
64
+ ripemd160(sha256(data))
65
+ end
66
+
67
+ # Compute HMAC-SHA-256.
68
+ #
69
+ # @param key [String] HMAC key
70
+ # @param data [String] data to authenticate
71
+ # @return [String] 32-byte MAC
72
+ def hmac_sha256(key, data)
73
+ OpenSSL::HMAC.digest('SHA256', key, data)
74
+ end
75
+
76
+ # Compute HMAC-SHA-512.
77
+ #
78
+ # @param key [String] HMAC key
79
+ # @param data [String] data to authenticate
80
+ # @return [String] 64-byte MAC
81
+ def hmac_sha512(key, data)
82
+ OpenSSL::HMAC.digest('SHA512', key, data)
83
+ end
84
+
85
+ # Derive a key using PBKDF2-HMAC-SHA-512.
86
+ #
87
+ # Used by BIP-39 to convert mnemonic phrases into seeds.
88
+ #
89
+ # @param password [String] the password (mnemonic phrase)
90
+ # @param salt [String] the salt (+"mnemonic"+ + passphrase)
91
+ # @param iterations [Integer] iteration count (default: 2048 per BIP-39)
92
+ # @param key_length [Integer] desired output length in bytes (default: 64)
93
+ # @return [String] derived key bytes
94
+ def pbkdf2_hmac_sha512(password, salt, iterations: 2048, key_length: 64)
95
+ OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, key_length, 'sha512')
96
+ end
97
+ end
98
+ end
99
+ end