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.
@@ -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