nanocurrency 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 +11 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +40 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +16 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/ext/.DS_Store +0 -0
- data/ext/nanocurrency_ext/blake2-config.h +72 -0
- data/ext/nanocurrency_ext/blake2-impl.h +160 -0
- data/ext/nanocurrency_ext/blake2.h +195 -0
- data/ext/nanocurrency_ext/blake2b-load-sse2.h +68 -0
- data/ext/nanocurrency_ext/blake2b-load-sse41.h +402 -0
- data/ext/nanocurrency_ext/blake2b-ref.c +373 -0
- data/ext/nanocurrency_ext/blake2b-round.h +157 -0
- data/ext/nanocurrency_ext/curve25519-donna-32bit.h +579 -0
- data/ext/nanocurrency_ext/curve25519-donna-64bit.h +413 -0
- data/ext/nanocurrency_ext/curve25519-donna-helpers.h +67 -0
- data/ext/nanocurrency_ext/curve25519-donna-sse2.h +1112 -0
- data/ext/nanocurrency_ext/ed25519-donna-32bit-sse2.h +513 -0
- data/ext/nanocurrency_ext/ed25519-donna-32bit-tables.h +61 -0
- data/ext/nanocurrency_ext/ed25519-donna-64bit-sse2.h +436 -0
- data/ext/nanocurrency_ext/ed25519-donna-64bit-tables.h +53 -0
- data/ext/nanocurrency_ext/ed25519-donna-64bit-x86-32bit.h +435 -0
- data/ext/nanocurrency_ext/ed25519-donna-64bit-x86.h +351 -0
- data/ext/nanocurrency_ext/ed25519-donna-basepoint-table.h +259 -0
- data/ext/nanocurrency_ext/ed25519-donna-batchverify.h +275 -0
- data/ext/nanocurrency_ext/ed25519-donna-impl-base.h +364 -0
- data/ext/nanocurrency_ext/ed25519-donna-impl-sse2.h +390 -0
- data/ext/nanocurrency_ext/ed25519-donna-portable-identify.h +103 -0
- data/ext/nanocurrency_ext/ed25519-donna-portable.h +135 -0
- data/ext/nanocurrency_ext/ed25519-donna.h +115 -0
- data/ext/nanocurrency_ext/ed25519-hash-custom.c +28 -0
- data/ext/nanocurrency_ext/ed25519-hash-custom.h +30 -0
- data/ext/nanocurrency_ext/ed25519-hash.h +219 -0
- data/ext/nanocurrency_ext/ed25519-randombytes-custom.h +10 -0
- data/ext/nanocurrency_ext/ed25519-randombytes.h +91 -0
- data/ext/nanocurrency_ext/ed25519.c +150 -0
- data/ext/nanocurrency_ext/ed25519.h +30 -0
- data/ext/nanocurrency_ext/extconf.rb +3 -0
- data/ext/nanocurrency_ext/fuzz/README.md +173 -0
- data/ext/nanocurrency_ext/fuzz/build-nix.php +134 -0
- data/ext/nanocurrency_ext/fuzz/curve25519-ref10.c +1272 -0
- data/ext/nanocurrency_ext/fuzz/curve25519-ref10.h +8 -0
- data/ext/nanocurrency_ext/fuzz/ed25519-donna-sse2.c +3 -0
- data/ext/nanocurrency_ext/fuzz/ed25519-donna.c +1 -0
- data/ext/nanocurrency_ext/fuzz/ed25519-donna.h +34 -0
- data/ext/nanocurrency_ext/fuzz/ed25519-ref10.c +4647 -0
- data/ext/nanocurrency_ext/fuzz/ed25519-ref10.h +9 -0
- data/ext/nanocurrency_ext/fuzz/fuzz-curve25519.c +172 -0
- data/ext/nanocurrency_ext/fuzz/fuzz-ed25519.c +219 -0
- data/ext/nanocurrency_ext/modm-donna-32bit.h +469 -0
- data/ext/nanocurrency_ext/modm-donna-64bit.h +361 -0
- data/ext/nanocurrency_ext/rbext.c +164 -0
- data/ext/nanocurrency_ext/regression.h +1024 -0
- data/lib/nano/account.rb +59 -0
- data/lib/nano/base32.rb +87 -0
- data/lib/nano/block.rb +142 -0
- data/lib/nano/check.rb +65 -0
- data/lib/nano/conversion.rb +102 -0
- data/lib/nano/hash.rb +43 -0
- data/lib/nano/key.rb +69 -0
- data/lib/nano/utils.rb +45 -0
- data/lib/nano/work.rb +51 -0
- data/lib/nanocurrency.rb +7 -0
- data/lib/nanocurrency/version.rb +3 -0
- data/lib/nanocurrency_ext.bundle +0 -0
- data/nanocurrency.gemspec +44 -0
- metadata +192 -0
data/lib/nano/account.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
module Nano
|
2
|
+
##
|
3
|
+
# The Account class is used to simplify conversion from account to
|
4
|
+
# public key
|
5
|
+
class Account
|
6
|
+
|
7
|
+
# @return [String] The base32 encoded account address
|
8
|
+
attr_reader :address
|
9
|
+
|
10
|
+
# @return [String] The public key for the account encoded in hexadecimal
|
11
|
+
attr_reader :public_key
|
12
|
+
|
13
|
+
##
|
14
|
+
# The Account intiailizer.
|
15
|
+
# @param value [Hash] This hash can contain the keys `:account` and
|
16
|
+
# `:public_key` which will be stored within the object
|
17
|
+
def initialize(val)
|
18
|
+
if val[:address]
|
19
|
+
@address = val[:address]
|
20
|
+
end
|
21
|
+
|
22
|
+
if val[:public_key]
|
23
|
+
@public_key = val[:public_key]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# A class initializer to create an Account object from the account
|
29
|
+
# base32 address.
|
30
|
+
#
|
31
|
+
# @param input [String] The base32 account address
|
32
|
+
#
|
33
|
+
# @return [Account] Returns a created account given a valid account address
|
34
|
+
def self.from_address(input)
|
35
|
+
return nil unless input.is_a? String
|
36
|
+
|
37
|
+
prefix_length = nil
|
38
|
+
|
39
|
+
if input.start_with? "nano_"
|
40
|
+
prefix_length = 5
|
41
|
+
elsif input.start_with? "xrb_"
|
42
|
+
prefix_length = 4
|
43
|
+
end
|
44
|
+
|
45
|
+
return nil if prefix_length.nil?
|
46
|
+
|
47
|
+
public_key_bytes = Nano::Base32.decode(input[prefix_length, 52])
|
48
|
+
checksum = Nano::Base32.decode(input[(prefix_length + 52)..-1])
|
49
|
+
public_key_bin = Nano::Utils.hex_to_bin public_key_bytes
|
50
|
+
computed_check = Blake2b.hex(
|
51
|
+
public_key_bin, Blake2b::Key.none, 5
|
52
|
+
).reverse.upcase
|
53
|
+
|
54
|
+
return nil if computed_check == checksum
|
55
|
+
|
56
|
+
Account.new(:address => input, :public_key => public_key_bytes)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/nano/base32.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require_relative './utils'
|
2
|
+
|
3
|
+
module Nano
|
4
|
+
##
|
5
|
+
# This module performs encoding and decoding of Base32 as specified by
|
6
|
+
# the Nano protocol.
|
7
|
+
module Base32
|
8
|
+
extend self
|
9
|
+
|
10
|
+
##
|
11
|
+
# The Base32 alphabet of characters.
|
12
|
+
ALPHABET = "13456789abcdefghijkmnopqrstuwxyz".freeze
|
13
|
+
|
14
|
+
##
|
15
|
+
# Encode an array of bytes into Base32 string.
|
16
|
+
#
|
17
|
+
# @param bytes [Array<Int8>] The byte array to encode.
|
18
|
+
#
|
19
|
+
# @return [String] The base32 encoded representation of the bytes
|
20
|
+
def encode(bytes)
|
21
|
+
length = bytes.length
|
22
|
+
leftover = (length * 8) % 5
|
23
|
+
offset = leftover == 0 ? 0 : 5 - leftover
|
24
|
+
value = 0
|
25
|
+
output = ""
|
26
|
+
bits = 0
|
27
|
+
|
28
|
+
length.times do |i|
|
29
|
+
value = (value << 8) | bytes[i]
|
30
|
+
bits += 8
|
31
|
+
|
32
|
+
while bits >= 5
|
33
|
+
output += ALPHABET[(value >> bits + offset - 5) & 31]
|
34
|
+
bits -= 5
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
if bits > 0
|
39
|
+
output += ALPHABET[(value << (5 - (bits + offset))) & 31]
|
40
|
+
end
|
41
|
+
|
42
|
+
output
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Decodes a Base32 encoded string into a hex string
|
47
|
+
#
|
48
|
+
# @param str [String] The base32 encoded string
|
49
|
+
#
|
50
|
+
# @returns [String] The hexadecimal decoded base32 string
|
51
|
+
def decode(str)
|
52
|
+
length = str.length
|
53
|
+
leftover = (length * 5) % 8
|
54
|
+
offset = leftover == 0 ? 0 : 8 - leftover
|
55
|
+
|
56
|
+
bits = 0
|
57
|
+
value = 0
|
58
|
+
|
59
|
+
output = Array.new
|
60
|
+
|
61
|
+
length.times do |i|
|
62
|
+
value = (value << 5) | read_char(str[i])
|
63
|
+
bits += 5
|
64
|
+
|
65
|
+
if bits >= 8
|
66
|
+
output.push((value >> (bits + offset - 8)) & 255)
|
67
|
+
bits -= 8
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
if bits > 0
|
72
|
+
output.push((value << (bits + offset - 8)) & 255)
|
73
|
+
end
|
74
|
+
|
75
|
+
output = output.drop(1) unless leftover == 0
|
76
|
+
Nano::Utils.bytes_to_hex(output)
|
77
|
+
end
|
78
|
+
|
79
|
+
def read_char(chr)
|
80
|
+
idx = ALPHABET.index(chr)
|
81
|
+
raise ArgumentError, "Character #{chr} not base32 compliant" if idx.nil?
|
82
|
+
idx
|
83
|
+
end
|
84
|
+
|
85
|
+
module_function :read_char
|
86
|
+
end
|
87
|
+
end
|
data/lib/nano/block.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
require_relative "./hash"
|
2
|
+
require_relative "./work"
|
3
|
+
require_relative "./check"
|
4
|
+
require "nanocurrency_ext"
|
5
|
+
|
6
|
+
module Nano
|
7
|
+
##
|
8
|
+
# A class representing a state block in the Nano network.
|
9
|
+
# Can be initialized as an existing block, with a work and signature
|
10
|
+
# or as an incomplete block.
|
11
|
+
class Block
|
12
|
+
|
13
|
+
# @return [String] The type of the block, locked to 'state' currently
|
14
|
+
attr_reader :type
|
15
|
+
|
16
|
+
# @return [String] The account associated with the block.
|
17
|
+
attr_reader :account
|
18
|
+
|
19
|
+
# @return [String] The account representative as set by the block
|
20
|
+
attr_reader :representative
|
21
|
+
|
22
|
+
# @return [String] The previous block to this block
|
23
|
+
attr_reader :previous
|
24
|
+
|
25
|
+
# @return [String] The link for this block
|
26
|
+
attr_reader :link
|
27
|
+
|
28
|
+
# @return [String] The balance for the account at this block
|
29
|
+
attr_reader :balance
|
30
|
+
|
31
|
+
# @return [String?] The proof of work computed for this block
|
32
|
+
attr_reader :work
|
33
|
+
|
34
|
+
# @return [String?] The signature for the block
|
35
|
+
attr_reader :signature
|
36
|
+
|
37
|
+
##
|
38
|
+
# The block initializer, requires certain parameters to be valid.
|
39
|
+
# @param params [Hash] The block parameters to construct the block with.
|
40
|
+
# `:previous` - The previous block hash as a string
|
41
|
+
# `:account` - The associated account address as a string
|
42
|
+
# `:representative` - The account representative as a string
|
43
|
+
# `:balance` - The account balance after this block in raw unit
|
44
|
+
# `:link` - The link hash associated with this block.
|
45
|
+
# `:work` - The proof of work for the block (optional)
|
46
|
+
# `:signature` - The signature for this block (optional)
|
47
|
+
def initialize(params)
|
48
|
+
@type = "state"
|
49
|
+
|
50
|
+
@previous = params[:previous]
|
51
|
+
|
52
|
+
raise ArgumentError, "Missing data for previous" if @previous.nil?
|
53
|
+
raise(
|
54
|
+
ArgumentError, "Invalid previous hash #{@previous}"
|
55
|
+
) unless Nano::Check.is_valid_hash? @previous
|
56
|
+
|
57
|
+
@account = params[:account]
|
58
|
+
raise ArgumentError, "Missing data for account" if @account.nil?
|
59
|
+
raise(
|
60
|
+
ArgumentError, "Invalid account #{@account}"
|
61
|
+
) unless Nano::Check.is_valid_account? @account
|
62
|
+
|
63
|
+
@representative = params[:representative]
|
64
|
+
raise(
|
65
|
+
ArgumentError, "Missing data for representative"
|
66
|
+
) if @representative.nil?
|
67
|
+
raise(
|
68
|
+
ArgumentError, "Invalid representative #{@representative}"
|
69
|
+
) unless Nano::Check.is_valid_account? @representative
|
70
|
+
|
71
|
+
@balance = params[:balance]
|
72
|
+
raise(
|
73
|
+
ArgumentError, "Missing data for balance"
|
74
|
+
) if @balance.nil?
|
75
|
+
raise(
|
76
|
+
ArgumentError, "Invalid balance #{@balance}"
|
77
|
+
) unless Nano::Check.is_balance_valid? @balance
|
78
|
+
|
79
|
+
@link = params[:link]
|
80
|
+
raise(
|
81
|
+
ArgumentError, "Missing data for link"
|
82
|
+
) if @link.nil?
|
83
|
+
raise(
|
84
|
+
ArgumentError, "Invalid link #{@link}"
|
85
|
+
) unless Nano::Check.is_hash_valid? @link
|
86
|
+
|
87
|
+
@work = params[:work]
|
88
|
+
@signature = params[:signature]
|
89
|
+
end
|
90
|
+
|
91
|
+
# @return The link for the account converted to an address.
|
92
|
+
# May not be valid in the case the link is a block hash
|
93
|
+
def link_as_account
|
94
|
+
Nano::Key.derive_address(@link)
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# This method signs the block using the secret key given. This method
|
99
|
+
# will fail if the key is incorrect. If this method succeeds, the block
|
100
|
+
# may still be invalid if the key does not belong to the block account.
|
101
|
+
# This method modifies the block's existing signature.
|
102
|
+
#
|
103
|
+
# @param secret_key [String] The hexidecimal representation of the
|
104
|
+
# secret key. Should match the account but does not perform a check,
|
105
|
+
# currently.
|
106
|
+
#
|
107
|
+
# @return [String] Returns the signature for the block, whilst also
|
108
|
+
# setting the signature internally.
|
109
|
+
def sign!(secret_key)
|
110
|
+
throw ArgumentError, "Invalid key" unless Nano::Check.is_key?(secret_key)
|
111
|
+
|
112
|
+
hash_bin = Nano::Utils.hex_to_bin(hash)
|
113
|
+
secret_bin = Nano::Utils.hex_to_bin(secret_key)
|
114
|
+
|
115
|
+
@signature = Nano::Utils.bin_to_hex(
|
116
|
+
NanocurrencyExt.sign(hash_bin, secret_bin)
|
117
|
+
).upcase
|
118
|
+
|
119
|
+
@signature
|
120
|
+
end
|
121
|
+
|
122
|
+
##
|
123
|
+
# Computes the proof of work for the block, setting the internal work
|
124
|
+
# value and overwriting the existing value. The method uses a native
|
125
|
+
# extension to improve the performance.
|
126
|
+
#
|
127
|
+
# @return [String] The computed work value, also setting it internally.
|
128
|
+
def compute_work!
|
129
|
+
base_prev = "".rjust(64, "0")
|
130
|
+
is_first = previous == base_prev
|
131
|
+
hash = is_first ? Nano::Key.derive_public_key(@account) : previous
|
132
|
+
@work = Nano::Work.compute_work(hash)
|
133
|
+
@work
|
134
|
+
end
|
135
|
+
|
136
|
+
##
|
137
|
+
# @return [String] The computed hash for the block.
|
138
|
+
def hash
|
139
|
+
Nano::Hash.hash_block(self)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
data/lib/nano/check.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
module Nano
|
2
|
+
##
|
3
|
+
# The Check module contains some basic sanity and type checks to ensure
|
4
|
+
# robustness throughout the Gem.
|
5
|
+
module Check
|
6
|
+
|
7
|
+
MIN_INDEX = 0
|
8
|
+
MAX_INDEX = 2 ** 32 - 1
|
9
|
+
|
10
|
+
extend self
|
11
|
+
|
12
|
+
def is_valid_hex?(value)
|
13
|
+
value.is_a?(String) && value.match?(/^[0-9a-fA-F]{32}$/)
|
14
|
+
end
|
15
|
+
|
16
|
+
def is_valid_hash?(value)
|
17
|
+
is_hash_valid?(value)
|
18
|
+
end
|
19
|
+
|
20
|
+
def is_hex_valid?(value)
|
21
|
+
is_valid_hex?(value)
|
22
|
+
end
|
23
|
+
|
24
|
+
def is_numerical?(value)
|
25
|
+
return false unless value.is_a?(String)
|
26
|
+
return false if value.start_with?(".")
|
27
|
+
return false if value.end_with?(".")
|
28
|
+
|
29
|
+
number_without_dot = value.sub(".", "")
|
30
|
+
|
31
|
+
# More than one '.' in the number.
|
32
|
+
return false unless number_without_dot.count(".") == 0
|
33
|
+
|
34
|
+
is_balance_valid?(number_without_dot)
|
35
|
+
end
|
36
|
+
|
37
|
+
def is_balance_valid?(value)
|
38
|
+
value.match?(/^[0-9]*$/)
|
39
|
+
end
|
40
|
+
|
41
|
+
def is_seed_valid?(seed)
|
42
|
+
seed.is_a?(String) && seed.match?(/^[0-9a-fA-F]{64}$/)
|
43
|
+
end
|
44
|
+
|
45
|
+
def is_index_valid?(index)
|
46
|
+
index.is_a?(Integer) && index >= MIN_INDEX && index <= MAX_INDEX
|
47
|
+
end
|
48
|
+
|
49
|
+
def is_key?(input)
|
50
|
+
input.is_a?(String) && input.match?(/^[0-9a-fA-F]{64}$/)
|
51
|
+
end
|
52
|
+
|
53
|
+
def is_hash_valid?(hash)
|
54
|
+
is_seed_valid?(hash)
|
55
|
+
end
|
56
|
+
|
57
|
+
def is_work_valid?(input)
|
58
|
+
input.is_a?(String) && input.match?(/^[0-9a-fA-F]{16}$/)
|
59
|
+
end
|
60
|
+
|
61
|
+
def is_valid_account?(input)
|
62
|
+
!Nano::Account.from_address(input).nil?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require "set"
|
2
|
+
require "bigdecimal"
|
3
|
+
require_relative "./check"
|
4
|
+
|
5
|
+
module Nano
|
6
|
+
|
7
|
+
##
|
8
|
+
# The Unit module provides utilities to allow conversion for the different
|
9
|
+
# unit types referenced in the Nano protocol.
|
10
|
+
module Unit
|
11
|
+
extend self
|
12
|
+
|
13
|
+
# A set of unit symbols found in the Nano protocol.
|
14
|
+
UNIT_SET = [
|
15
|
+
:hex, :raw, :nano, :knano, :Nano, :NANO, :KNano, :MNano
|
16
|
+
].to_set.freeze
|
17
|
+
|
18
|
+
# A hash of the unit types and number of zeros the unit is offset by.
|
19
|
+
UNIT_ZEROS = {
|
20
|
+
:hex => 0,
|
21
|
+
:raw => 0,
|
22
|
+
:nano => 20,
|
23
|
+
:knano => 24,
|
24
|
+
:Nano => 30,
|
25
|
+
:NANO => 30,
|
26
|
+
:KNano => 33,
|
27
|
+
:MNano => 36
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
##
|
31
|
+
# @return [Boolean] True if the unit is contained within the unit set.
|
32
|
+
def valid_unit?(unit)
|
33
|
+
UNIT_SET === unit
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Converts the value in a unit type into another representation
|
38
|
+
#
|
39
|
+
# @param value [String] A number string of the value in the base unit
|
40
|
+
# @param from [Symbol] A valid unit which denotes the base unit
|
41
|
+
# @param to [Symbol] The unit type to convert the value into.
|
42
|
+
# @return [String] The converted value as the unit from to
|
43
|
+
def convert(value, from, to)
|
44
|
+
raise ArgumentError, "Invalid from unit type" unless Unit.valid_unit?(from)
|
45
|
+
raise ArgumentError, "Invalid to unit type" unless Unit.valid_unit?(to)
|
46
|
+
|
47
|
+
from_zeros = zeros_for_unit(from)
|
48
|
+
to_zeros = zeros_for_unit(to)
|
49
|
+
|
50
|
+
raise ArgumentError, "Value must be a string" unless value.is_a? String
|
51
|
+
|
52
|
+
if from == :hex
|
53
|
+
is_hex = Nano::Check.is_valid_hex? value
|
54
|
+
raise ArgumentError, "Invalid hex value string" unless is_hex
|
55
|
+
else
|
56
|
+
is_number = Nano::Check.is_numerical? value
|
57
|
+
raise ArgumentError, "Invalid number value string" unless is_number
|
58
|
+
end
|
59
|
+
|
60
|
+
zero_difference = from_zeros - to_zeros
|
61
|
+
|
62
|
+
big_number = 0
|
63
|
+
if from == :hex
|
64
|
+
big_number = BigDecimal.new(value.to_i(16))
|
65
|
+
else
|
66
|
+
big_number = BigDecimal.new(value)
|
67
|
+
end
|
68
|
+
|
69
|
+
is_increase = zero_difference > 0
|
70
|
+
zero_difference.abs.times do |i|
|
71
|
+
if is_increase
|
72
|
+
big_number = big_number * 10
|
73
|
+
else
|
74
|
+
big_number = big_number / 10
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
if to == :hex
|
79
|
+
big_number.to_i.to_s(16).rjust(32, "0")
|
80
|
+
else
|
81
|
+
if big_number.to_i == big_number
|
82
|
+
big_number.to_i.to_s
|
83
|
+
else
|
84
|
+
big_number.to_s("F")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
# @return [Hash] The unit zeros constant
|
91
|
+
def unit_zeros
|
92
|
+
UNIT_ZEROS
|
93
|
+
end
|
94
|
+
|
95
|
+
# @param [Symbol] The unit type
|
96
|
+
# @return [Integer] The number of zeros for a unit type
|
97
|
+
def zeros_for_unit(unit)
|
98
|
+
raise ArgumentError, "Invalid unit" unless valid_unit? unit
|
99
|
+
UNIT_ZEROS[unit]
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|