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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +58 -0
- data/LICENCE +86 -0
- data/README.md +155 -0
- data/lib/bsv/attest/configuration.rb +9 -0
- data/lib/bsv/attest/response.rb +19 -0
- data/lib/bsv/attest/verification_error.rb +7 -0
- data/lib/bsv/attest/version.rb +7 -0
- data/lib/bsv/attest.rb +71 -0
- data/lib/bsv/network/arc.rb +113 -0
- data/lib/bsv/network/broadcast_error.rb +15 -0
- data/lib/bsv/network/broadcast_response.rb +29 -0
- data/lib/bsv/network/chain_provider_error.rb +14 -0
- data/lib/bsv/network/utxo.rb +28 -0
- data/lib/bsv/network/whats_on_chain.rb +82 -0
- data/lib/bsv/network.rb +12 -0
- data/lib/bsv/primitives/base58.rb +117 -0
- data/lib/bsv/primitives/bsm.rb +131 -0
- data/lib/bsv/primitives/curve.rb +115 -0
- data/lib/bsv/primitives/digest.rb +99 -0
- data/lib/bsv/primitives/ecdsa.rb +224 -0
- data/lib/bsv/primitives/ecies.rb +128 -0
- data/lib/bsv/primitives/extended_key.rb +315 -0
- data/lib/bsv/primitives/mnemonic/wordlist.rb +270 -0
- data/lib/bsv/primitives/mnemonic.rb +192 -0
- data/lib/bsv/primitives/private_key.rb +139 -0
- data/lib/bsv/primitives/public_key.rb +118 -0
- data/lib/bsv/primitives/schnorr.rb +108 -0
- data/lib/bsv/primitives/signature.rb +136 -0
- data/lib/bsv/primitives.rb +23 -0
- data/lib/bsv/script/builder.rb +73 -0
- data/lib/bsv/script/chunk.rb +77 -0
- data/lib/bsv/script/interpreter/error.rb +54 -0
- data/lib/bsv/script/interpreter/interpreter.rb +281 -0
- data/lib/bsv/script/interpreter/operations/arithmetic.rb +243 -0
- data/lib/bsv/script/interpreter/operations/bitwise.rb +68 -0
- data/lib/bsv/script/interpreter/operations/crypto.rb +209 -0
- data/lib/bsv/script/interpreter/operations/data_push.rb +34 -0
- data/lib/bsv/script/interpreter/operations/flow_control.rb +94 -0
- data/lib/bsv/script/interpreter/operations/splice.rb +89 -0
- data/lib/bsv/script/interpreter/operations/stack_ops.rb +112 -0
- data/lib/bsv/script/interpreter/script_number.rb +218 -0
- data/lib/bsv/script/interpreter/stack.rb +203 -0
- data/lib/bsv/script/opcodes.rb +165 -0
- data/lib/bsv/script/script.rb +424 -0
- data/lib/bsv/script.rb +20 -0
- data/lib/bsv/transaction/beef.rb +323 -0
- data/lib/bsv/transaction/merkle_path.rb +250 -0
- data/lib/bsv/transaction/p2pkh.rb +44 -0
- data/lib/bsv/transaction/sighash.rb +48 -0
- data/lib/bsv/transaction/transaction.rb +380 -0
- data/lib/bsv/transaction/transaction_input.rb +109 -0
- data/lib/bsv/transaction/transaction_output.rb +51 -0
- data/lib/bsv/transaction/unlocking_script_template.rb +36 -0
- data/lib/bsv/transaction/var_int.rb +50 -0
- data/lib/bsv/transaction.rb +21 -0
- data/lib/bsv/version.rb +5 -0
- data/lib/bsv/wallet/insufficient_funds_error.rb +15 -0
- data/lib/bsv/wallet/wallet.rb +119 -0
- data/lib/bsv/wallet.rb +8 -0
- data/lib/bsv-attest.rb +4 -0
- data/lib/bsv-sdk.rb +11 -0
- 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
|
data/lib/bsv/network.rb
ADDED
|
@@ -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
|