laksa 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/.gitignore +8 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +50 -0
- data/LICENSE.txt +674 -0
- data/README.md +94 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/laksa.gemspec +46 -0
- data/lib/laksa.rb +21 -0
- data/lib/laksa/account/account.rb +34 -0
- data/lib/laksa/account/transaction.rb +184 -0
- data/lib/laksa/account/transaction_factory.rb +16 -0
- data/lib/laksa/account/wallet.rb +129 -0
- data/lib/laksa/contract/contract.rb +148 -0
- data/lib/laksa/contract/contract_factory.rb +47 -0
- data/lib/laksa/crypto/key_store.rb +113 -0
- data/lib/laksa/crypto/key_tool.rb +61 -0
- data/lib/laksa/crypto/schnorr.rb +146 -0
- data/lib/laksa/jsonrpc/provider.rb +23 -0
- data/lib/laksa/proto/message.proto +44 -0
- data/lib/laksa/proto/message_pb.rb +46 -0
- data/lib/laksa/util/bech32.rb +28 -0
- data/lib/laksa/util/unit.rb +37 -0
- data/lib/laksa/util/util.rb +17 -0
- data/lib/laksa/util/validator.rb +40 -0
- data/lib/laksa/version.rb +3 -0
- metadata +201 -0
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Laksa
|
4
|
+
module Contract
|
5
|
+
class Contract
|
6
|
+
include Account
|
7
|
+
|
8
|
+
NIL_ADDRESS = "0000000000000000000000000000000000000000";
|
9
|
+
|
10
|
+
attr_reader :factory, :provider, :signer, :code, :abi, :init, :state, :address, :status
|
11
|
+
|
12
|
+
def initialize(factory, code, abi, address, init, state)
|
13
|
+
@factory = factory
|
14
|
+
@provider = factory.provider
|
15
|
+
@signer = factory.signer
|
16
|
+
|
17
|
+
@code = code
|
18
|
+
@abi = abi
|
19
|
+
@init = init
|
20
|
+
@state = state
|
21
|
+
|
22
|
+
if address && !address.empty?
|
23
|
+
@address = address
|
24
|
+
@status = ContractStatus::DEPLOYED
|
25
|
+
else
|
26
|
+
@status = ContractStatus::INITIALISED
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialised?
|
31
|
+
return @status === ContractStatus::INITIALISED
|
32
|
+
end
|
33
|
+
|
34
|
+
def deployed?
|
35
|
+
return @status === ContractStatus::DEPLOYED
|
36
|
+
end
|
37
|
+
|
38
|
+
def rejected?
|
39
|
+
return @status === ContractStatus::REJECTED
|
40
|
+
end
|
41
|
+
|
42
|
+
def deploy(deploy_params, attempts = 33, interval = 1000, to_ds = false)
|
43
|
+
raise 'Cannot deploy without code or initialisation parameters.' if @code == nil || @code == ''
|
44
|
+
raise 'Cannot deploy without code or initialisation parameters.' if @init == nil || @init.length == 0
|
45
|
+
|
46
|
+
tx_params = TxParams.new
|
47
|
+
tx_params.id = deploy_params.id
|
48
|
+
tx_params.version = deploy_params.version
|
49
|
+
tx_params.nonce = deploy_params.nonce
|
50
|
+
tx_params.sender_pub_key = deploy_params.sender_pub_key
|
51
|
+
tx_params.gas_price = deploy_params.gas_price
|
52
|
+
tx_params.gas_limit = deploy_params.gas_limit
|
53
|
+
|
54
|
+
tx_params.to_addr = NIL_ADDRESS
|
55
|
+
tx_params.amount = '0'
|
56
|
+
tx_params.code = @code.gsub("/\\", "")
|
57
|
+
tx_params.data = @init.to_json.gsub('\\"', '"')
|
58
|
+
|
59
|
+
tx = Transaction.new(tx_params, @provider)
|
60
|
+
|
61
|
+
tx = self.prepare_tx(tx, attempts, interval);
|
62
|
+
|
63
|
+
if tx.rejected?
|
64
|
+
@status = ContractStatus::REJECTED
|
65
|
+
|
66
|
+
return [tx, self]
|
67
|
+
end
|
68
|
+
|
69
|
+
@status = ContractStatus::DEPLOYED
|
70
|
+
@address = ContractFactory.get_address_for_contract(tx)
|
71
|
+
|
72
|
+
[tx, self]
|
73
|
+
end
|
74
|
+
|
75
|
+
def call(transition, args, params, attempts = 33, interval = 1000, to_ds = false)
|
76
|
+
data = {
|
77
|
+
_tag: transition,
|
78
|
+
params: args,
|
79
|
+
};
|
80
|
+
|
81
|
+
return 'Contract has not been deployed!' unless @address
|
82
|
+
|
83
|
+
tx_params = TxParams.new
|
84
|
+
tx_params.id = params['id'] if params.has_key?('id')
|
85
|
+
tx_params.version = params['version'] if params.has_key?('version')
|
86
|
+
tx_params.nonce = params['nonce'] if params.has_key?('nonce')
|
87
|
+
tx_params.sender_pub_key = params['sender_pub_key'] if params.has_key?('sender_pub_key')
|
88
|
+
tx_params.gas_price = params['gas_price'] if params.has_key?('gas_price')
|
89
|
+
tx_params.gas_limit = params['gas_limit'] if params.has_key?('gas_limit')
|
90
|
+
|
91
|
+
tx_params.to_addr = @address
|
92
|
+
tx_params.data = JSON.generate(data)
|
93
|
+
|
94
|
+
tx = Transaction.new(tx_params, @provider, TxStatus::INITIALIZED, to_ds)
|
95
|
+
|
96
|
+
tx = self.prepare_tx(tx, attempts, interval)
|
97
|
+
end
|
98
|
+
|
99
|
+
def state
|
100
|
+
return [] unless self.deployed
|
101
|
+
|
102
|
+
response = @provider.GetSmartContractState(@address)
|
103
|
+
return response.result
|
104
|
+
end
|
105
|
+
|
106
|
+
def prepare_tx(tx, attempts, interval)
|
107
|
+
tx = @signer.sign(tx);
|
108
|
+
|
109
|
+
response = @provider.CreateTransaction(tx.to_payload)
|
110
|
+
|
111
|
+
if response['error']
|
112
|
+
tx.status = TxStatus::REJECTED
|
113
|
+
else
|
114
|
+
tx.confirm(response['result']['TranID'], attempts, interval)
|
115
|
+
end
|
116
|
+
|
117
|
+
tx
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class ContractStatus
|
122
|
+
DEPLOYED = 0
|
123
|
+
REJECTED = 1
|
124
|
+
INITIALISED = 2
|
125
|
+
end
|
126
|
+
|
127
|
+
class Value
|
128
|
+
attr_reader :vname, :type, :value
|
129
|
+
def initialize(vname, type, value)
|
130
|
+
@vname = vname
|
131
|
+
@type = type
|
132
|
+
@value = value
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class DeployParams
|
137
|
+
attr_reader :id, :version, :nonce, :gas_price, :gas_limit, :sender_pub_key
|
138
|
+
def initialize(id, version, nonce, gas_price, gas_limit, sender_pub_key)
|
139
|
+
@id = id
|
140
|
+
@version = version
|
141
|
+
@nonce = nonce
|
142
|
+
@gas_price = gas_price
|
143
|
+
@gas_limit = gas_limit
|
144
|
+
@sender_pub_key = sender_pub_key
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
module Laksa
|
4
|
+
module Contract
|
5
|
+
# ContractFactory
|
6
|
+
#
|
7
|
+
# individual `Contract` instances are instead obtained by
|
8
|
+
# calling `ContractFactory.at` (for an already-deployed contract) and
|
9
|
+
# `ContractFactory.new` (to deploy a new contract).
|
10
|
+
class ContractFactory
|
11
|
+
|
12
|
+
attr_reader :provider, :signer
|
13
|
+
|
14
|
+
def initialize(provider, signer)
|
15
|
+
@provider = provider
|
16
|
+
@signer = signer
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.get_address_for_contract(tx)
|
20
|
+
sha256 = Digest::SHA256.new
|
21
|
+
|
22
|
+
sender_address = Laksa::Crypto::KeyTool.get_address_from_public_key(tx.sender_pub_key)
|
23
|
+
|
24
|
+
sha256 << Util.decode_hex(sender_address)
|
25
|
+
|
26
|
+
nonce = 0;
|
27
|
+
if tx.nonce && !tx.nonce.empty?
|
28
|
+
nonce = tx.nonce.to_i - 1
|
29
|
+
end
|
30
|
+
|
31
|
+
nonce_hex = [nonce].pack('Q>*')
|
32
|
+
|
33
|
+
sha256 << nonce_hex
|
34
|
+
|
35
|
+
sha256.hexdigest[24..-1]
|
36
|
+
end
|
37
|
+
|
38
|
+
def new_contract(code, init, abi)
|
39
|
+
Contract.new(self, code, abi, nil, init, nil)
|
40
|
+
end
|
41
|
+
|
42
|
+
def at_contract(address, code, init, abi)
|
43
|
+
Contract.new(self, code, abi, address, init, nil)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'pbkdf2'
|
2
|
+
require 'scrypt'
|
3
|
+
require 'openssl'
|
4
|
+
require 'digest'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module Laksa
|
8
|
+
module Crypto
|
9
|
+
class KeyStore
|
10
|
+
T_PBKDF2 = 'pbkdf2'
|
11
|
+
T_SCRYPT = 'scrypt'
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
end
|
15
|
+
|
16
|
+
# encryptPrivateKey
|
17
|
+
#
|
18
|
+
# Encodes and encrypts an account in the format specified by
|
19
|
+
# https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition.
|
20
|
+
# However, note that, in keeping with the hash function used by Zilliqa's
|
21
|
+
# core protocol, the MAC is generated using sha256 instead of keccak.
|
22
|
+
#
|
23
|
+
# NOTE: only scrypt and pbkdf2 are supported.
|
24
|
+
#
|
25
|
+
# @param {string} private_key - hex-encoded private key
|
26
|
+
# @param {string} password - a password used for encryption
|
27
|
+
# @param {KDF} kdf_type - the key derivation function to be used
|
28
|
+
def encrypt_private_key(private_key, password, kdf_type)
|
29
|
+
address = KeyTool.get_address_from_private_key(private_key)
|
30
|
+
|
31
|
+
iv = KeyTool.generate_random_bytes(16)
|
32
|
+
salt = KeyTool.generate_random_bytes(32)
|
33
|
+
|
34
|
+
case kdf_type
|
35
|
+
when T_PBKDF2
|
36
|
+
derived_key = PBKDF2.new(password: password, salt: salt, key_length: 32, iterations: 262144).value
|
37
|
+
when T_SCRYPT
|
38
|
+
derived_key = SCrypt::Engine.scrypt(password, salt, 8192, 8, 1, 32)
|
39
|
+
end
|
40
|
+
|
41
|
+
encrypt_key = derived_key[0..15]
|
42
|
+
|
43
|
+
cipher = OpenSSL::Cipher.new('aes-128-ctr')
|
44
|
+
cipher.encrypt
|
45
|
+
cipher.iv = iv
|
46
|
+
cipher.key = encrypt_key
|
47
|
+
cipher.padding = 0
|
48
|
+
|
49
|
+
ciphertext = cipher.update(Util.decode_hex(private_key)) + cipher.final
|
50
|
+
|
51
|
+
mac = generate_mac(derived_key, ciphertext)
|
52
|
+
|
53
|
+
datas = {address: address,
|
54
|
+
crypto: {
|
55
|
+
cipher: 'aes-128-ctr',
|
56
|
+
cipherparams: {'iv': Util.encode_hex(iv)},
|
57
|
+
ciphertext: Util.encode_hex(ciphertext),
|
58
|
+
kdf: kdf_type,
|
59
|
+
kdfparams: {n: 8192, c:262144, r:8, p:1, dklen: 32, salt: salt.bytes},
|
60
|
+
mac: mac
|
61
|
+
},
|
62
|
+
id: SecureRandom.uuid,
|
63
|
+
version: 3
|
64
|
+
}
|
65
|
+
|
66
|
+
datas.to_json
|
67
|
+
end
|
68
|
+
|
69
|
+
# decrypt_private_key
|
70
|
+
#
|
71
|
+
# Recovers the private key from a keystore file using the given passphrase.
|
72
|
+
#
|
73
|
+
# @param {KeystoreV3} encrypt_json
|
74
|
+
# @param {string} password
|
75
|
+
def decrypt_private_key(encrypt_json, password)
|
76
|
+
datas = JSON.parse(encrypt_json)
|
77
|
+
|
78
|
+
ciphertext = Util.decode_hex(datas['crypto']['ciphertext'])
|
79
|
+
iv = Util.decode_hex(datas['crypto']['cipherparams']['iv'])
|
80
|
+
kdfparams = datas['crypto']['kdfparams']
|
81
|
+
kdf_type = datas['crypto']['kdf']
|
82
|
+
|
83
|
+
case kdf_type
|
84
|
+
when T_PBKDF2
|
85
|
+
derived_key = PBKDF2.new(password: password, salt: kdfparams['salt'].pack('c*'), key_length: kdfparams['dklen'], iterations: kdfparams['c']).value
|
86
|
+
when T_SCRYPT
|
87
|
+
derived_key = SCrypt::Engine.scrypt(password, kdfparams['salt'].pack('c*'), kdfparams['n'], kdfparams['r'], kdfparams['p'], kdfparams['dklen'])
|
88
|
+
end
|
89
|
+
|
90
|
+
encrypt_key = derived_key[0..15]
|
91
|
+
|
92
|
+
mac = generate_mac(derived_key, ciphertext)
|
93
|
+
|
94
|
+
raise 'Failed to decrypt.' if mac.casecmp(datas['crypto']['mac']) != 0
|
95
|
+
|
96
|
+
cipher = OpenSSL::Cipher.new(datas['crypto']['cipher'])
|
97
|
+
cipher.decrypt
|
98
|
+
cipher.iv = iv
|
99
|
+
cipher.key = encrypt_key
|
100
|
+
cipher.padding = 0
|
101
|
+
|
102
|
+
private_key = cipher.update(ciphertext) + cipher.final
|
103
|
+
|
104
|
+
return Util.encode_hex private_key
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
def generate_mac(derived_key, ciphertext)
|
109
|
+
Digest::SHA256.hexdigest(derived_key[16..32] + ciphertext)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'secp256k1'
|
2
|
+
require 'digest'
|
3
|
+
|
4
|
+
module Laksa
|
5
|
+
module Crypto
|
6
|
+
class KeyTool
|
7
|
+
include Secp256k1
|
8
|
+
def initialize(private_key)
|
9
|
+
is_raw = private_key.length == 32 ? true : false
|
10
|
+
|
11
|
+
@pk = PrivateKey.new(privkey: private_key, raw: is_raw)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.generate_private_key
|
15
|
+
Util.encode_hex KeyTool.generate_random_bytes(32)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.generate_random_bytes(size)
|
19
|
+
SecureRandom.random_bytes(size)
|
20
|
+
end
|
21
|
+
|
22
|
+
# getPubKeyFromPrivateKey
|
23
|
+
#
|
24
|
+
# takes a hex-encoded string (private key) and returns its corresponding
|
25
|
+
# hex-encoded 33-byte public key.
|
26
|
+
#
|
27
|
+
# @param {string} privateKey
|
28
|
+
# @returns {string}
|
29
|
+
def self.get_public_key_from_private_key(private_key, is_compressed = true)
|
30
|
+
is_raw = private_key.length == 32 ? true : false
|
31
|
+
|
32
|
+
pk = PrivateKey.new(privkey: private_key, raw: is_raw)
|
33
|
+
|
34
|
+
(Util.encode_hex pk.pubkey.serialize(compressed: is_compressed)).downcase
|
35
|
+
end
|
36
|
+
|
37
|
+
# getAddressFromPrivateKey
|
38
|
+
#
|
39
|
+
# takes a hex-encoded string (private key) and returns its corresponding
|
40
|
+
# 20-byte hex-encoded address.
|
41
|
+
#
|
42
|
+
# @param {string} privateKey
|
43
|
+
# @returns {string}
|
44
|
+
def self.get_address_from_private_key(private_key)
|
45
|
+
public_key = KeyTool.get_public_key_from_private_key(private_key)
|
46
|
+
KeyTool.get_address_from_public_key(public_key)
|
47
|
+
end
|
48
|
+
|
49
|
+
# getAddressFromPublicKey
|
50
|
+
#
|
51
|
+
# takes hex-encoded string and returns the corresponding address
|
52
|
+
#
|
53
|
+
# @param {string} public_key
|
54
|
+
# @returns {string}
|
55
|
+
def self.get_address_from_public_key(public_key)
|
56
|
+
orig_address = Digest::SHA256.hexdigest Util.decode_hex public_key
|
57
|
+
orig_address[24..-1].downcase
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'secp256k1'
|
2
|
+
require 'digest'
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
module Laksa
|
6
|
+
module Crypto
|
7
|
+
class Schnorr
|
8
|
+
include Secp256k1
|
9
|
+
|
10
|
+
N = OpenSSL::BN.new('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', 16)
|
11
|
+
G = OpenSSL::BN.new('79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798', 16)
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
end
|
15
|
+
|
16
|
+
# sign
|
17
|
+
#
|
18
|
+
# @param {String} msg
|
19
|
+
# @param {String} key
|
20
|
+
def self.sign(message, private_key, public_key)
|
21
|
+
sig = nil
|
22
|
+
while !sig
|
23
|
+
k = Util.encode_hex SecureRandom.random_bytes(32)
|
24
|
+
k_bn = OpenSSL::BN.new(k, 16)
|
25
|
+
|
26
|
+
sig = self.try_sign(message, private_key, k_bn, public_key)
|
27
|
+
end
|
28
|
+
|
29
|
+
sig
|
30
|
+
end
|
31
|
+
|
32
|
+
# trySign
|
33
|
+
#
|
34
|
+
# @param {String} message - the message to sign over
|
35
|
+
# @param {String} privateKey - the private key
|
36
|
+
# @param {BN} k_bn - output of the HMAC-DRBG
|
37
|
+
#
|
38
|
+
# @returns {Signature | null =>}
|
39
|
+
def self.try_sign(message, private_key, k_bn, public_key)
|
40
|
+
group = OpenSSL::PKey::EC::Group.new('secp256k1')
|
41
|
+
|
42
|
+
prikey_bn = OpenSSL::BN.new(private_key, 16)
|
43
|
+
|
44
|
+
pubkey_bn = OpenSSL::BN.new(public_key, 16)
|
45
|
+
pubkey_point = OpenSSL::PKey::EC::Point.new(group, pubkey_bn)
|
46
|
+
|
47
|
+
throw 'Bad private key.' if prikey_bn.zero? || prikey_bn >= N
|
48
|
+
|
49
|
+
# 1a. check that k is not 0
|
50
|
+
return nil if k_bn.zero?
|
51
|
+
|
52
|
+
# 1b. check that k is < the order of the group
|
53
|
+
return nil if k_bn >= N
|
54
|
+
|
55
|
+
# 2. Compute commitment Q = kG, where g is the base point
|
56
|
+
q_point = pubkey_point.mul(0, k_bn)
|
57
|
+
|
58
|
+
# 3. Compute the challenge r = H(Q || pubKey || msg)
|
59
|
+
# mod reduce the r value by the order of secp256k1, n
|
60
|
+
r_bn = hash(q_point, pubkey_point, message) % N
|
61
|
+
|
62
|
+
return nil if r_bn.zero?
|
63
|
+
|
64
|
+
# 4. Compute s = k - r * prv
|
65
|
+
# 4a. Compute r * prv
|
66
|
+
s_bn = r_bn * prikey_bn % N
|
67
|
+
# 4b. Compute s = k - r * prv mod n
|
68
|
+
s_bn = k_bn.mod_sub(s_bn, N)
|
69
|
+
|
70
|
+
return nil if s_bn.zero?
|
71
|
+
|
72
|
+
Signature.new(r_bn.to_s(16), s_bn.to_s(16))
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
# Verify signature.
|
77
|
+
#
|
78
|
+
# @param {Buffer} message
|
79
|
+
# @param {Buffer} sig
|
80
|
+
# @param {Buffer} public_key
|
81
|
+
#
|
82
|
+
# @returns {boolean}
|
83
|
+
#
|
84
|
+
# 1. Check if r,s is in [1, ..., order-1]
|
85
|
+
# 2. Compute Q = sG + r*kpub
|
86
|
+
# 3. If Q = O (the neutral point), return 0;
|
87
|
+
# 4. r' = H(Q, kpub, m)
|
88
|
+
# 5. return r' == r
|
89
|
+
def self.verify(message, sig, public_key)
|
90
|
+
pubkey = PublicKey.new
|
91
|
+
pubkey.deserialize Util.decode_hex(public_key)
|
92
|
+
|
93
|
+
r = sig.r
|
94
|
+
r_bn = OpenSSL::BN.new(r, 16)
|
95
|
+
|
96
|
+
s = sig.s
|
97
|
+
s_bn = OpenSSL::BN.new(s, 16)
|
98
|
+
|
99
|
+
throw 'Invalid signature' if (s_bn.zero? || r_bn.zero?)
|
100
|
+
|
101
|
+
throw 'Invalid signature' if (s_bn.negative? || r_bn.negative?)
|
102
|
+
|
103
|
+
throw 'Invalid signature' if (s_bn >= N || r_bn >= N)
|
104
|
+
|
105
|
+
group = OpenSSL::PKey::EC::Group.new('secp256k1')
|
106
|
+
pubkey_bn = OpenSSL::BN.new(public_key, 16)
|
107
|
+
pubkey_point = OpenSSL::PKey::EC::Point.new(group, pubkey_bn)
|
108
|
+
|
109
|
+
throw 'Invalid public key' unless pubkey_point.on_curve?
|
110
|
+
|
111
|
+
q_point = pubkey_point.mul(r_bn, s_bn)
|
112
|
+
|
113
|
+
throw 'Invalid intermediate point.' if q_point.infinity?
|
114
|
+
|
115
|
+
h_bn = self.hash(q_point, pubkey_point, message) % N
|
116
|
+
|
117
|
+
throw 'Invalid hash.' if (h_bn.zero?)
|
118
|
+
|
119
|
+
h_bn.eql?(r_bn)
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
# Hash (r | M).
|
124
|
+
def self.hash(q_point, pubkey_point, message)
|
125
|
+
sha256 = Digest::SHA256.new
|
126
|
+
sha256 << q_point.to_octet_string(:compressed)
|
127
|
+
sha256 << pubkey_point.to_octet_string(:compressed)
|
128
|
+
sha256 << Util.decode_hex(message)
|
129
|
+
|
130
|
+
OpenSSL::BN.new(sha256.hexdigest, 16)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class Signature
|
135
|
+
attr_reader :r, :s
|
136
|
+
def initialize(r, s)
|
137
|
+
@r = r
|
138
|
+
@s = s
|
139
|
+
end
|
140
|
+
|
141
|
+
def to_s
|
142
|
+
"#{@r}#{@s}"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|