nanocurrency 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|