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,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
|
|
5
|
+
module BSV
|
|
6
|
+
module Primitives
|
|
7
|
+
# A secp256k1 public key for address derivation and signature verification.
|
|
8
|
+
#
|
|
9
|
+
# Public keys are points on the secp256k1 curve. They can be serialised
|
|
10
|
+
# in compressed (33-byte) or uncompressed (65-byte) form, converted to
|
|
11
|
+
# Bitcoin addresses, and used to verify ECDSA signatures.
|
|
12
|
+
#
|
|
13
|
+
# @example Derive address from a public key
|
|
14
|
+
# pub = private_key.public_key
|
|
15
|
+
# pub.address #=> "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
|
|
16
|
+
class PublicKey
|
|
17
|
+
# Address version byte for mainnet P2PKH addresses.
|
|
18
|
+
MAINNET_PUBKEY_HASH = "\x00".b
|
|
19
|
+
|
|
20
|
+
# Address version byte for testnet P2PKH addresses.
|
|
21
|
+
TESTNET_PUBKEY_HASH = "\x6f".b
|
|
22
|
+
|
|
23
|
+
# @return [OpenSSL::PKey::EC::Point] the underlying curve point
|
|
24
|
+
attr_reader :point
|
|
25
|
+
|
|
26
|
+
# @param point [OpenSSL::PKey::EC::Point] a point on the secp256k1 curve
|
|
27
|
+
# @raise [ArgumentError] if point is not an EC point or is at infinity
|
|
28
|
+
def initialize(point)
|
|
29
|
+
raise ArgumentError, 'point must be an EC point' unless point.is_a?(OpenSSL::PKey::EC::Point)
|
|
30
|
+
raise ArgumentError, 'point is at infinity' if point.infinity?
|
|
31
|
+
|
|
32
|
+
@point = point
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Create a public key from raw bytes (compressed or uncompressed).
|
|
36
|
+
#
|
|
37
|
+
# @param bytes [String] 33-byte compressed or 65-byte uncompressed encoding
|
|
38
|
+
# @return [PublicKey]
|
|
39
|
+
def self.from_bytes(bytes)
|
|
40
|
+
point = Curve.point_from_bytes(bytes)
|
|
41
|
+
new(point)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Create a public key from a hex string.
|
|
45
|
+
#
|
|
46
|
+
# @param hex [String] hex-encoded compressed or uncompressed public key
|
|
47
|
+
# @return [PublicKey]
|
|
48
|
+
def self.from_hex(hex)
|
|
49
|
+
from_bytes([hex].pack('H*'))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Derive the public key from a {PrivateKey}.
|
|
53
|
+
#
|
|
54
|
+
# @param private_key [PrivateKey] the private key
|
|
55
|
+
# @return [PublicKey]
|
|
56
|
+
def self.from_private_key(private_key)
|
|
57
|
+
private_key.public_key
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Return the compressed (33-byte) encoding.
|
|
61
|
+
#
|
|
62
|
+
# @return [String] compressed public key bytes
|
|
63
|
+
def compressed
|
|
64
|
+
@point.to_octet_string(:compressed)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Return the uncompressed (65-byte) encoding.
|
|
68
|
+
#
|
|
69
|
+
# @return [String] uncompressed public key bytes
|
|
70
|
+
def uncompressed
|
|
71
|
+
@point.to_octet_string(:uncompressed)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Return the public key as a hex string.
|
|
75
|
+
#
|
|
76
|
+
# @param compressed [Boolean] whether to use compressed encoding (default: true)
|
|
77
|
+
# @return [String] hex-encoded public key
|
|
78
|
+
def to_hex(compressed: true)
|
|
79
|
+
if compressed
|
|
80
|
+
self.compressed.unpack1('H*')
|
|
81
|
+
else
|
|
82
|
+
uncompressed.unpack1('H*')
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Compute the Hash160 (RIPEMD-160 of SHA-256) of the compressed public key.
|
|
87
|
+
#
|
|
88
|
+
# @return [String] 20-byte public key hash
|
|
89
|
+
def hash160
|
|
90
|
+
Digest.hash160(compressed)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Derive a Base58Check-encoded Bitcoin address.
|
|
94
|
+
#
|
|
95
|
+
# @param network [Symbol] +:mainnet+ or +:testnet+
|
|
96
|
+
# @return [String] the P2PKH address
|
|
97
|
+
def address(network: :mainnet)
|
|
98
|
+
prefix = network == :mainnet ? MAINNET_PUBKEY_HASH : TESTNET_PUBKEY_HASH
|
|
99
|
+
Base58.check_encode(prefix + hash160)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Verify an ECDSA signature against a message hash.
|
|
103
|
+
#
|
|
104
|
+
# @param hash [String] 32-byte message digest
|
|
105
|
+
# @param signature [Signature] the signature to verify
|
|
106
|
+
# @return [Boolean] +true+ if the signature is valid
|
|
107
|
+
def verify(hash, signature)
|
|
108
|
+
ECDSA.verify(hash, signature, @point)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @param other [Object] the object to compare
|
|
112
|
+
# @return [Boolean] +true+ if both keys represent the same curve point
|
|
113
|
+
def ==(other)
|
|
114
|
+
other.is_a?(PublicKey) && compressed == other.compressed
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
|
|
5
|
+
module BSV
|
|
6
|
+
module Primitives
|
|
7
|
+
# BRC-94 Schnorr zero-knowledge proof protocol.
|
|
8
|
+
#
|
|
9
|
+
# Provides generation and verification of Schnorr proofs for verifiable
|
|
10
|
+
# revelation of ECDH shared secrets. Given two public keys A and B and
|
|
11
|
+
# a shared secret S = a*B (where a is A's private key), the prover can
|
|
12
|
+
# demonstrate knowledge of the discrete log relationship without
|
|
13
|
+
# revealing the private key.
|
|
14
|
+
#
|
|
15
|
+
# @see https://github.com/bitcoin-sv/BRCs/blob/master/peer-to-peer/0094.md BRC-94
|
|
16
|
+
module Schnorr
|
|
17
|
+
# A Schnorr zero-knowledge proof consisting of a commitment point,
|
|
18
|
+
# blinded shared secret, and response scalar.
|
|
19
|
+
class Proof
|
|
20
|
+
# @return [PublicKey] the commitment point R
|
|
21
|
+
attr_reader :r
|
|
22
|
+
|
|
23
|
+
# @return [PublicKey] the blinded shared secret S'
|
|
24
|
+
attr_reader :s_prime
|
|
25
|
+
|
|
26
|
+
# @return [OpenSSL::BN] the response scalar z
|
|
27
|
+
attr_reader :z
|
|
28
|
+
|
|
29
|
+
# @param r [PublicKey] commitment point
|
|
30
|
+
# @param s_prime [PublicKey] blinded shared secret
|
|
31
|
+
# @param z [OpenSSL::BN] response scalar
|
|
32
|
+
def initialize(r, s_prime, z)
|
|
33
|
+
@r = r
|
|
34
|
+
@s_prime = s_prime
|
|
35
|
+
@z = z
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
# Generate a Schnorr proof of knowledge of a shared secret.
|
|
42
|
+
#
|
|
43
|
+
# Proves that the prover knows the private key +a+ such that
|
|
44
|
+
# +shared_secret = a * public_key_b+, without revealing +a+.
|
|
45
|
+
#
|
|
46
|
+
# @param private_key [PrivateKey] the prover's private key (a)
|
|
47
|
+
# @param public_key_a [PublicKey] the prover's public key (A = a*G)
|
|
48
|
+
# @param public_key_b [PublicKey] the counterparty's public key (B)
|
|
49
|
+
# @param shared_secret [PublicKey] the ECDH shared secret (S = a*B)
|
|
50
|
+
# @return [Proof] the Schnorr proof
|
|
51
|
+
def generate_proof(private_key, public_key_a, public_key_b, shared_secret)
|
|
52
|
+
nonce = PrivateKey.generate
|
|
53
|
+
r_pub = nonce.public_key
|
|
54
|
+
s_prime = PublicKey.new(Curve.multiply_point(public_key_b.point, nonce.bn))
|
|
55
|
+
|
|
56
|
+
e = compute_challenge(public_key_a, public_key_b, shared_secret, s_prime, r_pub)
|
|
57
|
+
|
|
58
|
+
z = (nonce.bn + (e * private_key.bn)) % Curve::N
|
|
59
|
+
|
|
60
|
+
Proof.new(r_pub, s_prime, z)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Verify a Schnorr proof of knowledge of a shared secret.
|
|
64
|
+
#
|
|
65
|
+
# Checks the two verification equations:
|
|
66
|
+
# 1. z*G == R + e*A
|
|
67
|
+
# 2. z*B == S' + e*S
|
|
68
|
+
#
|
|
69
|
+
# @param public_key_a [PublicKey] the prover's public key
|
|
70
|
+
# @param public_key_b [PublicKey] the counterparty's public key
|
|
71
|
+
# @param shared_secret [PublicKey] the claimed shared secret
|
|
72
|
+
# @param proof [Proof] the Schnorr proof to verify
|
|
73
|
+
# @return [Boolean] +true+ if the proof is valid
|
|
74
|
+
def verify_proof(public_key_a, public_key_b, shared_secret, proof)
|
|
75
|
+
e = compute_challenge(public_key_a, public_key_b, shared_secret, proof.s_prime, proof.r)
|
|
76
|
+
|
|
77
|
+
# Equation 1: z·G == R + e·A
|
|
78
|
+
z_g = Curve.multiply_generator(proof.z)
|
|
79
|
+
e_a = Curve.multiply_point(public_key_a.point, e)
|
|
80
|
+
r_plus_ea = Curve.add_points(proof.r.point, e_a)
|
|
81
|
+
|
|
82
|
+
return false unless points_equal?(z_g, r_plus_ea)
|
|
83
|
+
|
|
84
|
+
# Equation 2: z·B == S' + e·S
|
|
85
|
+
z_b = Curve.multiply_point(public_key_b.point, proof.z)
|
|
86
|
+
e_s = Curve.multiply_point(shared_secret.point, e)
|
|
87
|
+
sp_plus_es = Curve.add_points(proof.s_prime.point, e_s)
|
|
88
|
+
|
|
89
|
+
points_equal?(z_b, sp_plus_es)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class << self
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def compute_challenge(pub_a, pub_b, s, s_prime, r)
|
|
96
|
+
message = pub_a.compressed + pub_b.compressed +
|
|
97
|
+
s.compressed + s_prime.compressed + r.compressed
|
|
98
|
+
hash = Digest.sha256(message)
|
|
99
|
+
OpenSSL::BN.new(hash, 2) % Curve::N
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def points_equal?(p1, p2)
|
|
103
|
+
p1.to_octet_string(:compressed) == p2.to_octet_string(:compressed)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
|
|
5
|
+
module BSV
|
|
6
|
+
module Primitives
|
|
7
|
+
# An ECDSA signature consisting of (r, s) components.
|
|
8
|
+
#
|
|
9
|
+
# Supports DER encoding/decoding with strict BIP-66 validation,
|
|
10
|
+
# low-S normalisation (BIP-62 rule 5), and hex convenience methods.
|
|
11
|
+
#
|
|
12
|
+
# @example Parse a DER-encoded signature
|
|
13
|
+
# sig = BSV::Primitives::Signature.from_der(der_bytes)
|
|
14
|
+
# sig.low_s? #=> true
|
|
15
|
+
class Signature
|
|
16
|
+
# @return [OpenSSL::BN] the r component
|
|
17
|
+
attr_reader :r
|
|
18
|
+
|
|
19
|
+
# @return [OpenSSL::BN] the s component
|
|
20
|
+
attr_reader :s
|
|
21
|
+
|
|
22
|
+
# @param r [OpenSSL::BN, Integer] the r component
|
|
23
|
+
# @param s [OpenSSL::BN, Integer] the s component
|
|
24
|
+
def initialize(r, s)
|
|
25
|
+
@r = r.is_a?(OpenSSL::BN) ? r : OpenSSL::BN.new(r.to_s, 10)
|
|
26
|
+
@s = s.is_a?(OpenSSL::BN) ? s : OpenSSL::BN.new(s.to_s, 10)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Parse a signature from DER-encoded bytes with strict BIP-66 validation.
|
|
30
|
+
#
|
|
31
|
+
# @param der_bytes [String] DER-encoded signature bytes
|
|
32
|
+
# @return [Signature]
|
|
33
|
+
# @raise [ArgumentError] if the DER encoding is invalid
|
|
34
|
+
def self.from_der(der_bytes)
|
|
35
|
+
der_bytes = der_bytes.b if der_bytes.encoding != Encoding::ASCII_8BIT
|
|
36
|
+
bytes = der_bytes.bytes
|
|
37
|
+
|
|
38
|
+
raise ArgumentError, 'signature too short' if bytes.length < 8
|
|
39
|
+
raise ArgumentError, 'invalid sequence tag' unless bytes[0] == 0x30
|
|
40
|
+
|
|
41
|
+
total_len = bytes[1]
|
|
42
|
+
raise ArgumentError, 'length mismatch' unless total_len == bytes.length - 2
|
|
43
|
+
|
|
44
|
+
# Parse R
|
|
45
|
+
raise ArgumentError, 'invalid integer tag for R' unless bytes[2] == 0x02
|
|
46
|
+
|
|
47
|
+
r_len = bytes[3]
|
|
48
|
+
raise ArgumentError, 'R length overflows' if 4 + r_len > bytes.length
|
|
49
|
+
raise ArgumentError, 'R is zero length' if r_len.zero?
|
|
50
|
+
|
|
51
|
+
r_bytes = bytes[4, r_len]
|
|
52
|
+
raise ArgumentError, 'R has negative flag' if r_bytes[0] & 0x80 != 0
|
|
53
|
+
raise ArgumentError, 'R has excessive padding' if r_len > 1 && r_bytes[0].zero? && r_bytes[1].nobits?(0x80)
|
|
54
|
+
|
|
55
|
+
# Parse S
|
|
56
|
+
s_offset = 4 + r_len
|
|
57
|
+
raise ArgumentError, 'invalid integer tag for S' unless bytes[s_offset] == 0x02
|
|
58
|
+
|
|
59
|
+
s_len = bytes[s_offset + 1]
|
|
60
|
+
raise ArgumentError, 'S length overflows' if s_offset + 2 + s_len > bytes.length
|
|
61
|
+
raise ArgumentError, 'S is zero length' if s_len.zero?
|
|
62
|
+
|
|
63
|
+
s_bytes = bytes[s_offset + 2, s_len]
|
|
64
|
+
raise ArgumentError, 'S has negative flag' if s_bytes[0] & 0x80 != 0
|
|
65
|
+
raise ArgumentError, 'S has excessive padding' if s_len > 1 && s_bytes[0].zero? && s_bytes[1].nobits?(0x80)
|
|
66
|
+
|
|
67
|
+
raise ArgumentError, 'trailing bytes' unless s_offset + 2 + s_len == bytes.length
|
|
68
|
+
|
|
69
|
+
r = OpenSSL::BN.new(r_bytes.pack('C*'), 2)
|
|
70
|
+
s = OpenSSL::BN.new(s_bytes.pack('C*'), 2)
|
|
71
|
+
new(r, s)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Serialise the signature in DER format.
|
|
75
|
+
#
|
|
76
|
+
# @return [String] DER-encoded binary string
|
|
77
|
+
def to_der
|
|
78
|
+
rb = canonicalise_int(@r)
|
|
79
|
+
sb = canonicalise_int(@s)
|
|
80
|
+
|
|
81
|
+
der = [0x30, rb.length + sb.length + 4,
|
|
82
|
+
0x02, rb.length, *rb,
|
|
83
|
+
0x02, sb.length, *sb]
|
|
84
|
+
der.pack('C*')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Parse a signature from a hex-encoded DER string.
|
|
88
|
+
#
|
|
89
|
+
# @param hex [String] hex-encoded DER signature
|
|
90
|
+
# @return [Signature]
|
|
91
|
+
def self.from_hex(hex)
|
|
92
|
+
from_der([hex].pack('H*'))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Serialise the signature as a hex-encoded DER string.
|
|
96
|
+
#
|
|
97
|
+
# @return [String] hex-encoded DER signature
|
|
98
|
+
def to_hex
|
|
99
|
+
to_der.unpack1('H*')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Check whether the S value is in the lower half of the curve order.
|
|
103
|
+
#
|
|
104
|
+
# BIP-62 rule 5 requires S <= N/2 for transaction malleability protection.
|
|
105
|
+
#
|
|
106
|
+
# @return [Boolean] +true+ if S is in the lower half
|
|
107
|
+
def low_s?
|
|
108
|
+
@s <= Curve::HALF_N
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Return a new signature with S normalised to the lower half of the curve order.
|
|
112
|
+
#
|
|
113
|
+
# @return [Signature] a new signature with low-S, or +self+ if already low-S
|
|
114
|
+
def to_low_s
|
|
115
|
+
return self if low_s?
|
|
116
|
+
|
|
117
|
+
self.class.new(@r, Curve::N - @s)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @param other [Object] the object to compare
|
|
121
|
+
# @return [Boolean] +true+ if both signatures have equal r and s values
|
|
122
|
+
def ==(other)
|
|
123
|
+
other.is_a?(Signature) && @r == other.r && @s == other.s
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def canonicalise_int(bn)
|
|
129
|
+
bytes = bn.to_s(2).bytes
|
|
130
|
+
bytes = [0] if bytes.empty?
|
|
131
|
+
bytes.unshift(0) if bytes[0] & 0x80 != 0
|
|
132
|
+
bytes
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
# Cryptographic primitives for the BSV blockchain.
|
|
5
|
+
#
|
|
6
|
+
# Provides keys, curves, hashing, digital signatures, encryption,
|
|
7
|
+
# HD key derivation (BIP-32), and mnemonic phrase generation (BIP-39).
|
|
8
|
+
# All cryptography uses Ruby's stdlib +openssl+ — no external gems.
|
|
9
|
+
module Primitives
|
|
10
|
+
autoload :Curve, 'bsv/primitives/curve'
|
|
11
|
+
autoload :Digest, 'bsv/primitives/digest'
|
|
12
|
+
autoload :Base58, 'bsv/primitives/base58'
|
|
13
|
+
autoload :Signature, 'bsv/primitives/signature'
|
|
14
|
+
autoload :ECDSA, 'bsv/primitives/ecdsa'
|
|
15
|
+
autoload :ECIES, 'bsv/primitives/ecies'
|
|
16
|
+
autoload :BSM, 'bsv/primitives/bsm'
|
|
17
|
+
autoload :Schnorr, 'bsv/primitives/schnorr'
|
|
18
|
+
autoload :PublicKey, 'bsv/primitives/public_key'
|
|
19
|
+
autoload :PrivateKey, 'bsv/primitives/private_key'
|
|
20
|
+
autoload :ExtendedKey, 'bsv/primitives/extended_key'
|
|
21
|
+
autoload :Mnemonic, 'bsv/primitives/mnemonic'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Script
|
|
5
|
+
# Fluent builder for constructing scripts incrementally.
|
|
6
|
+
#
|
|
7
|
+
# Provides a chainable API for building scripts from opcodes and data
|
|
8
|
+
# pushes, as an alternative to the template constructors on {Script}.
|
|
9
|
+
#
|
|
10
|
+
# @example Build a custom script
|
|
11
|
+
# script = BSV::Script::Script.builder
|
|
12
|
+
# .push_op(:OP_DUP)
|
|
13
|
+
# .push_op(:OP_HASH160)
|
|
14
|
+
# .push_data(pubkey_hash)
|
|
15
|
+
# .push_op(:OP_EQUALVERIFY)
|
|
16
|
+
# .push_op(:OP_CHECKSIG)
|
|
17
|
+
# .build
|
|
18
|
+
class Builder
|
|
19
|
+
def initialize
|
|
20
|
+
@chunks = []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Push an opcode onto the script.
|
|
24
|
+
#
|
|
25
|
+
# @param opcode [Symbol, Integer] opcode name (e.g. +:OP_DUP+) or byte value
|
|
26
|
+
# @return [self] for chaining
|
|
27
|
+
def push_op(opcode)
|
|
28
|
+
code = opcode.is_a?(Symbol) ? Opcodes.const_get(opcode) : opcode
|
|
29
|
+
@chunks << Chunk.new(opcode: code)
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Push raw binary data (automatically selects PUSHDATA encoding).
|
|
34
|
+
#
|
|
35
|
+
# @param data [String] binary data to push
|
|
36
|
+
# @return [self] for chaining
|
|
37
|
+
def push_data(data)
|
|
38
|
+
bytes = data.b
|
|
39
|
+
@chunks << Chunk.new(opcode: push_opcode_for(bytes.bytesize), data: bytes)
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Push hex-encoded data.
|
|
44
|
+
#
|
|
45
|
+
# @param hex [String] hex-encoded data to push
|
|
46
|
+
# @return [self] for chaining
|
|
47
|
+
def push_hex(hex)
|
|
48
|
+
push_data([hex].pack('H*'))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Finalise and return the constructed {Script}.
|
|
52
|
+
#
|
|
53
|
+
# @return [Script] the built script
|
|
54
|
+
def build
|
|
55
|
+
Script.from_chunks(@chunks)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def push_opcode_for(len)
|
|
61
|
+
if len <= 0x4b
|
|
62
|
+
len
|
|
63
|
+
elsif len <= 0xff
|
|
64
|
+
Opcodes::OP_PUSHDATA1
|
|
65
|
+
elsif len <= 0xffff
|
|
66
|
+
Opcodes::OP_PUSHDATA2
|
|
67
|
+
else
|
|
68
|
+
Opcodes::OP_PUSHDATA4
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Script
|
|
5
|
+
# A single element of a parsed script — either an opcode or a data push.
|
|
6
|
+
#
|
|
7
|
+
# Scripts are composed of a sequence of chunks. Each chunk is either
|
|
8
|
+
# a bare opcode (e.g. +OP_DUP+) or a data push (opcode + data payload).
|
|
9
|
+
class Chunk
|
|
10
|
+
# @return [Integer] the opcode byte
|
|
11
|
+
attr_reader :opcode
|
|
12
|
+
|
|
13
|
+
# @return [String, nil] the pushed data bytes, or +nil+ for bare opcodes
|
|
14
|
+
attr_reader :data
|
|
15
|
+
|
|
16
|
+
# @param opcode [Integer] the opcode byte value
|
|
17
|
+
# @param data [String, nil] data payload for push operations
|
|
18
|
+
def initialize(opcode:, data: nil)
|
|
19
|
+
@opcode = opcode
|
|
20
|
+
@data = data&.b
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Whether this chunk carries a data payload.
|
|
24
|
+
#
|
|
25
|
+
# @return [Boolean] +true+ if this is a data push chunk
|
|
26
|
+
def data?
|
|
27
|
+
!@data.nil?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Serialise this chunk back to raw script bytes.
|
|
31
|
+
#
|
|
32
|
+
# @return [String] binary script bytes for this chunk
|
|
33
|
+
def to_binary
|
|
34
|
+
if @data
|
|
35
|
+
push_data_binary(@data)
|
|
36
|
+
else
|
|
37
|
+
[@opcode].pack('C')
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Render this chunk as human-readable ASM.
|
|
42
|
+
#
|
|
43
|
+
# Data pushes are shown as hex strings; opcodes are shown by name.
|
|
44
|
+
#
|
|
45
|
+
# @return [String] ASM representation
|
|
46
|
+
def to_asm
|
|
47
|
+
if @data
|
|
48
|
+
@data.unpack1('H*')
|
|
49
|
+
else
|
|
50
|
+
Opcodes.name_for(@opcode) || @opcode.to_s
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param other [Object] the object to compare
|
|
55
|
+
# @return [Boolean] +true+ if both chunks have equal opcode and data
|
|
56
|
+
def ==(other)
|
|
57
|
+
other.is_a?(Chunk) && @opcode == other.opcode && @data == other.data
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def push_data_binary(data)
|
|
63
|
+
len = data.bytesize
|
|
64
|
+
|
|
65
|
+
if len <= 0x4b
|
|
66
|
+
[len].pack('C') + data
|
|
67
|
+
elsif len <= 0xff
|
|
68
|
+
[Opcodes::OP_PUSHDATA1, len].pack('CC') + data
|
|
69
|
+
elsif len <= 0xffff
|
|
70
|
+
[Opcodes::OP_PUSHDATA2, len].pack('Cv') + data
|
|
71
|
+
else
|
|
72
|
+
[Opcodes::OP_PUSHDATA4, len].pack('CV') + data
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Script
|
|
5
|
+
# Error raised during script execution.
|
|
6
|
+
#
|
|
7
|
+
# Carries a machine-readable error code from {ScriptErrorCode} alongside
|
|
8
|
+
# a human-readable message.
|
|
9
|
+
class ScriptError < StandardError
|
|
10
|
+
# @return [Symbol] the error code from {ScriptErrorCode}
|
|
11
|
+
attr_reader :code
|
|
12
|
+
|
|
13
|
+
# @param code [Symbol] error code from {ScriptErrorCode}
|
|
14
|
+
# @param message [String, nil] human-readable description (auto-generated from code if omitted)
|
|
15
|
+
def initialize(code, message = nil)
|
|
16
|
+
@code = code
|
|
17
|
+
super(message || code.to_s.tr('_', ' '))
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Error codes for script execution failures.
|
|
22
|
+
#
|
|
23
|
+
# Each constant corresponds to a specific script validation rule.
|
|
24
|
+
module ScriptErrorCode
|
|
25
|
+
EVAL_FALSE = :eval_false
|
|
26
|
+
EMPTY_STACK = :empty_stack
|
|
27
|
+
VERIFY_FAILED = :verify_failed
|
|
28
|
+
EQUALVERIFY_FAILED = :equalverify_failed
|
|
29
|
+
NUMEQUALVERIFY_FAILED = :numequalverify_failed
|
|
30
|
+
CHECKSIGVERIFY_FAILED = :checksigverify_failed
|
|
31
|
+
CHECKMULTISIGVERIFY_FAILED = :checkmultisigverify_failed
|
|
32
|
+
UNBALANCED_CONDITIONAL = :unbalanced_conditional
|
|
33
|
+
DISABLED_OPCODE = :disabled_opcode
|
|
34
|
+
RESERVED_OPCODE = :reserved_opcode
|
|
35
|
+
INVALID_STACK_OPERATION = :invalid_stack_operation
|
|
36
|
+
MALFORMED_PUSH = :malformed_push
|
|
37
|
+
NUMBER_TOO_BIG = :number_too_big
|
|
38
|
+
DIVIDE_BY_ZERO = :divide_by_zero
|
|
39
|
+
INVALID_INPUT_LENGTH = :invalid_input_length
|
|
40
|
+
INVALID_PUBKEY_COUNT = :invalid_pubkey_count
|
|
41
|
+
INVALID_SIG_COUNT = :invalid_sig_count
|
|
42
|
+
SIG_NULLFAIL = :sig_nullfail
|
|
43
|
+
SIG_NULLDUMMY = :sig_nulldummy
|
|
44
|
+
SIG_DER = :sig_der
|
|
45
|
+
SIG_HIGH_S = :sig_high_s
|
|
46
|
+
PUBKEY_TYPE = :pubkey_type
|
|
47
|
+
INVALID_SIGHASH_TYPE = :invalid_sighash_type
|
|
48
|
+
EARLY_RETURN = :early_return
|
|
49
|
+
IMPOSSIBLE_ENCODING = :impossible_encoding
|
|
50
|
+
INVALID_OPCODE = :invalid_opcode
|
|
51
|
+
MINIMAL_DATA = :minimal_data
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|