tapyrus 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.
Files changed (128) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +12 -0
  7. data/CODE_OF_CONDUCT.md +49 -0
  8. data/Gemfile +6 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +100 -0
  11. data/Rakefile +6 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/exe/tapyrusrb-cli +5 -0
  15. data/exe/tapyrusrbd +41 -0
  16. data/lib/openassets/marker_output.rb +20 -0
  17. data/lib/openassets/payload.rb +54 -0
  18. data/lib/openassets/util.rb +28 -0
  19. data/lib/openassets.rb +9 -0
  20. data/lib/tapyrus/base58.rb +38 -0
  21. data/lib/tapyrus/block.rb +77 -0
  22. data/lib/tapyrus/block_header.rb +88 -0
  23. data/lib/tapyrus/bloom_filter.rb +78 -0
  24. data/lib/tapyrus/chain_params.rb +90 -0
  25. data/lib/tapyrus/chainparams/mainnet.yml +41 -0
  26. data/lib/tapyrus/chainparams/regtest.yml +38 -0
  27. data/lib/tapyrus/chainparams/testnet.yml +41 -0
  28. data/lib/tapyrus/constants.rb +195 -0
  29. data/lib/tapyrus/descriptor.rb +147 -0
  30. data/lib/tapyrus/ext_key.rb +337 -0
  31. data/lib/tapyrus/key.rb +296 -0
  32. data/lib/tapyrus/key_path.rb +26 -0
  33. data/lib/tapyrus/logger.rb +42 -0
  34. data/lib/tapyrus/merkle_tree.rb +149 -0
  35. data/lib/tapyrus/message/addr.rb +35 -0
  36. data/lib/tapyrus/message/base.rb +28 -0
  37. data/lib/tapyrus/message/block.rb +46 -0
  38. data/lib/tapyrus/message/block_transaction_request.rb +45 -0
  39. data/lib/tapyrus/message/block_transactions.rb +31 -0
  40. data/lib/tapyrus/message/block_txn.rb +27 -0
  41. data/lib/tapyrus/message/cmpct_block.rb +42 -0
  42. data/lib/tapyrus/message/error.rb +10 -0
  43. data/lib/tapyrus/message/fee_filter.rb +27 -0
  44. data/lib/tapyrus/message/filter_add.rb +28 -0
  45. data/lib/tapyrus/message/filter_clear.rb +17 -0
  46. data/lib/tapyrus/message/filter_load.rb +39 -0
  47. data/lib/tapyrus/message/get_addr.rb +17 -0
  48. data/lib/tapyrus/message/get_block_txn.rb +27 -0
  49. data/lib/tapyrus/message/get_blocks.rb +29 -0
  50. data/lib/tapyrus/message/get_data.rb +21 -0
  51. data/lib/tapyrus/message/get_headers.rb +28 -0
  52. data/lib/tapyrus/message/header_and_short_ids.rb +57 -0
  53. data/lib/tapyrus/message/headers.rb +35 -0
  54. data/lib/tapyrus/message/headers_parser.rb +24 -0
  55. data/lib/tapyrus/message/inv.rb +21 -0
  56. data/lib/tapyrus/message/inventories_parser.rb +23 -0
  57. data/lib/tapyrus/message/inventory.rb +51 -0
  58. data/lib/tapyrus/message/mem_pool.rb +17 -0
  59. data/lib/tapyrus/message/merkle_block.rb +42 -0
  60. data/lib/tapyrus/message/network_addr.rb +63 -0
  61. data/lib/tapyrus/message/not_found.rb +21 -0
  62. data/lib/tapyrus/message/ping.rb +30 -0
  63. data/lib/tapyrus/message/pong.rb +26 -0
  64. data/lib/tapyrus/message/prefilled_tx.rb +29 -0
  65. data/lib/tapyrus/message/reject.rb +46 -0
  66. data/lib/tapyrus/message/send_cmpct.rb +43 -0
  67. data/lib/tapyrus/message/send_headers.rb +16 -0
  68. data/lib/tapyrus/message/tx.rb +30 -0
  69. data/lib/tapyrus/message/ver_ack.rb +17 -0
  70. data/lib/tapyrus/message/version.rb +69 -0
  71. data/lib/tapyrus/message.rb +70 -0
  72. data/lib/tapyrus/mnemonic/wordlist/chinese_simplified.txt +2048 -0
  73. data/lib/tapyrus/mnemonic/wordlist/chinese_traditional.txt +2048 -0
  74. data/lib/tapyrus/mnemonic/wordlist/english.txt +2048 -0
  75. data/lib/tapyrus/mnemonic/wordlist/french.txt +2048 -0
  76. data/lib/tapyrus/mnemonic/wordlist/italian.txt +2048 -0
  77. data/lib/tapyrus/mnemonic/wordlist/japanese.txt +2048 -0
  78. data/lib/tapyrus/mnemonic/wordlist/spanish.txt +2048 -0
  79. data/lib/tapyrus/mnemonic.rb +77 -0
  80. data/lib/tapyrus/network/connection.rb +73 -0
  81. data/lib/tapyrus/network/message_handler.rb +241 -0
  82. data/lib/tapyrus/network/peer.rb +223 -0
  83. data/lib/tapyrus/network/peer_discovery.rb +42 -0
  84. data/lib/tapyrus/network/pool.rb +135 -0
  85. data/lib/tapyrus/network.rb +13 -0
  86. data/lib/tapyrus/node/cli.rb +112 -0
  87. data/lib/tapyrus/node/configuration.rb +38 -0
  88. data/lib/tapyrus/node/spv.rb +79 -0
  89. data/lib/tapyrus/node.rb +7 -0
  90. data/lib/tapyrus/opcodes.rb +178 -0
  91. data/lib/tapyrus/out_point.rb +44 -0
  92. data/lib/tapyrus/rpc/http_server.rb +65 -0
  93. data/lib/tapyrus/rpc/request_handler.rb +150 -0
  94. data/lib/tapyrus/rpc/tapyrus_core_client.rb +72 -0
  95. data/lib/tapyrus/rpc.rb +7 -0
  96. data/lib/tapyrus/script/multisig.rb +92 -0
  97. data/lib/tapyrus/script/script.rb +551 -0
  98. data/lib/tapyrus/script/script_error.rb +111 -0
  99. data/lib/tapyrus/script/script_interpreter.rb +668 -0
  100. data/lib/tapyrus/script/tx_checker.rb +81 -0
  101. data/lib/tapyrus/script_witness.rb +38 -0
  102. data/lib/tapyrus/secp256k1/native.rb +174 -0
  103. data/lib/tapyrus/secp256k1/ruby.rb +123 -0
  104. data/lib/tapyrus/secp256k1.rb +12 -0
  105. data/lib/tapyrus/slip39/share.rb +122 -0
  106. data/lib/tapyrus/slip39/sss.rb +245 -0
  107. data/lib/tapyrus/slip39/wordlist/english.txt +1024 -0
  108. data/lib/tapyrus/slip39.rb +93 -0
  109. data/lib/tapyrus/store/chain_entry.rb +67 -0
  110. data/lib/tapyrus/store/db/level_db.rb +98 -0
  111. data/lib/tapyrus/store/db.rb +9 -0
  112. data/lib/tapyrus/store/spv_chain.rb +101 -0
  113. data/lib/tapyrus/store.rb +9 -0
  114. data/lib/tapyrus/tx.rb +347 -0
  115. data/lib/tapyrus/tx_in.rb +89 -0
  116. data/lib/tapyrus/tx_out.rb +74 -0
  117. data/lib/tapyrus/util.rb +133 -0
  118. data/lib/tapyrus/validation.rb +115 -0
  119. data/lib/tapyrus/version.rb +3 -0
  120. data/lib/tapyrus/wallet/account.rb +151 -0
  121. data/lib/tapyrus/wallet/base.rb +162 -0
  122. data/lib/tapyrus/wallet/db.rb +81 -0
  123. data/lib/tapyrus/wallet/master_key.rb +110 -0
  124. data/lib/tapyrus/wallet.rb +8 -0
  125. data/lib/tapyrus.rb +219 -0
  126. data/tapyrusrb.conf.sample +0 -0
  127. data/tapyrusrb.gemspec +47 -0
  128. metadata +451 -0
@@ -0,0 +1,93 @@
1
+ module Tapyrus
2
+ module SLIP39
3
+
4
+ WORDS = File.readlines("#{__dir__}/slip39/wordlist/english.txt").map(&:strip)
5
+
6
+ module_function
7
+
8
+ def bits_to_bytes(n)
9
+ (n + 7) / 8
10
+ end
11
+
12
+ def bits_to_words(n)
13
+ (n + RADIX_BITS - 1) / RADIX_BITS
14
+ end
15
+
16
+ # The length of the radix in bits.
17
+ RADIX_BITS = 10
18
+ # The number of words in the wordlist.
19
+ RADIX = 2 ** RADIX_BITS
20
+ # The length of the random identifier in bits.
21
+ ID_LENGTH_BITS = 15
22
+ # The length of the iteration exponent in bits.
23
+ ITERATION_EXP_LENGTH_BITS = 5
24
+ # The length of the random identifier and iteration exponent in words.
25
+ ID_EXP_LENGTH_WORDS = bits_to_words(ID_LENGTH_BITS + ITERATION_EXP_LENGTH_BITS)
26
+ # The maximum number of shares that can be created.
27
+ MAX_SHARE_COUNT = 16
28
+ # The length of the RS1024 checksum in words.
29
+ CHECKSUM_LENGTH_WORDS = 3
30
+ # The length of the digest of the shared secret in bytes.
31
+ DIGEST_LENGTH_BYTES = 4
32
+ # The customization string used in the RS1024 checksum and in the PBKDF2 salt.
33
+ CUSTOMIZATION_STRING = 'shamir'.bytes
34
+ # The length of the mnemonic in words without the share value.
35
+ METADATA_LENGTH_WORDS = ID_EXP_LENGTH_WORDS + 2 + CHECKSUM_LENGTH_WORDS
36
+ # The minimum allowed entropy of the master secret.
37
+ MIN_STRENGTH_BITS = 128
38
+ # The minimum allowed length of the mnemonic in words.
39
+ MIN_MNEMONIC_LENGTH_WORDS = METADATA_LENGTH_WORDS + bits_to_words(MIN_STRENGTH_BITS)
40
+ # The minimum number of iterations to use in PBKDF2.
41
+ BASE_ITERATION_COUNT = 10000
42
+ # The number of rounds to use in the Feistel cipher.
43
+ ROUND_COUNT = 4
44
+ # The index of the share containing the shared secret.
45
+ SECRET_INDEX = 255
46
+ # The index of the share containing the digest of the shared secret.
47
+ DIGEST_INDEX = 254
48
+
49
+ EXP_TABLE = [
50
+ 1, 3, 5, 15, 17, 51, 85, 255, 26, 46, 114, 150, 161, 248, 19,
51
+ 53, 95, 225, 56, 72, 216, 115, 149, 164, 247, 2, 6, 10, 30, 34,
52
+ 102, 170, 229, 52, 92, 228, 55, 89, 235, 38, 106, 190, 217, 112, 144,
53
+ 171, 230, 49, 83, 245, 4, 12, 20, 60, 68, 204, 79, 209, 104, 184,
54
+ 211, 110, 178, 205, 76, 212, 103, 169, 224, 59, 77, 215, 98, 166, 241,
55
+ 8, 24, 40, 120, 136, 131, 158, 185, 208, 107, 189, 220, 127, 129, 152,
56
+ 179, 206, 73, 219, 118, 154, 181, 196, 87, 249, 16, 48, 80, 240, 11,
57
+ 29, 39, 105, 187, 214, 97, 163, 254, 25, 43, 125, 135, 146, 173, 236,
58
+ 47, 113, 147, 174, 233, 32, 96, 160, 251, 22, 58, 78, 210, 109, 183,
59
+ 194, 93, 231, 50, 86, 250, 21, 63, 65, 195, 94, 226, 61, 71, 201,
60
+ 64, 192, 91, 237, 44, 116, 156, 191, 218, 117, 159, 186, 213, 100, 172,
61
+ 239, 42, 126, 130, 157, 188, 223, 122, 142, 137, 128, 155, 182, 193, 88,
62
+ 232, 35, 101, 175, 234, 37, 111, 177, 200, 67, 197, 84, 252, 31, 33,
63
+ 99, 165, 244, 7, 9, 27, 45, 119, 153, 176, 203, 70, 202, 69, 207,
64
+ 74, 222, 121, 139, 134, 145, 168, 227, 62, 66, 198, 81, 243, 14, 18,
65
+ 54, 90, 238, 41, 123, 141, 140, 143, 138, 133, 148, 167, 242, 13, 23,
66
+ 57, 75, 221, 124, 132, 151, 162, 253, 28, 36, 108, 180, 199, 82, 246
67
+ ]
68
+
69
+ LOG_TABLE = [
70
+ 0, 0, 25, 1, 50, 2, 26, 198, 75, 199, 27, 104, 51, 238, 223, 3,
71
+ 100, 4, 224, 14, 52, 141, 129, 239, 76, 113, 8, 200, 248, 105, 28,
72
+ 193, 125, 194, 29, 181, 249, 185, 39, 106, 77, 228, 166, 114, 154, 201,
73
+ 9, 120, 101, 47, 138, 5, 33, 15, 225, 36, 18, 240, 130, 69, 53,
74
+ 147, 218, 142, 150, 143, 219, 189, 54, 208, 206, 148, 19, 92, 210, 241,
75
+ 64, 70, 131, 56, 102, 221, 253, 48, 191, 6, 139, 98, 179, 37, 226,
76
+ 152, 34, 136, 145, 16, 126, 110, 72, 195, 163, 182, 30, 66, 58, 107,
77
+ 40, 84, 250, 133, 61, 186, 43, 121, 10, 21, 155, 159, 94, 202, 78,
78
+ 212, 172, 229, 243, 115, 167, 87, 175, 88, 168, 80, 244, 234, 214, 116,
79
+ 79, 174, 233, 213, 231, 230, 173, 232, 44, 215, 117, 122, 235, 22, 11,
80
+ 245, 89, 203, 95, 176, 156, 169, 81, 160, 127, 12, 246, 111, 23, 196,
81
+ 73, 236, 216, 67, 31, 45, 164, 118, 123, 183, 204, 187, 62, 90, 251,
82
+ 96, 177, 134, 59, 82, 161, 108, 170, 85, 41, 157, 151, 178, 135, 144,
83
+ 97, 190, 220, 252, 188, 149, 207, 205, 55, 63, 91, 209, 83, 57, 132,
84
+ 60, 65, 162, 109, 71, 20, 42, 158, 93, 86, 242, 211, 171, 68, 17, 146,
85
+ 217, 35, 32, 46, 137, 180, 124, 184, 38, 119, 153, 227, 165, 103, 74, 237,
86
+ 222, 197, 49, 254, 24, 13, 99, 140, 128, 192, 247, 112, 7
87
+ ]
88
+
89
+ autoload :SSS, 'tapyrus/slip39/sss'
90
+ autoload :Share, 'tapyrus/slip39/share'
91
+
92
+ end
93
+ end
@@ -0,0 +1,67 @@
1
+ module Tapyrus
2
+ module Store
3
+
4
+ # wrap a block header object with extra data.
5
+ class ChainEntry
6
+
7
+ attr_reader :header
8
+ attr_reader :height
9
+
10
+ # @param [Tapyrus::BlockHeader] header a block header.
11
+ # @param [Integer] height a block height.
12
+ def initialize(header, height)
13
+ @header = header
14
+ @height = height
15
+ end
16
+
17
+ # get database key
18
+ def key
19
+ Tapyrus::Store::KEY_PREFIX[:entry] + header.block_hash
20
+ end
21
+
22
+ def hash
23
+ header.hash
24
+ end
25
+
26
+ # block hash
27
+ def block_hash
28
+ header.block_hash
29
+ end
30
+
31
+ # previous block hash
32
+ def prev_hash
33
+ header.prev_hash
34
+ end
35
+
36
+ # whether genesis block
37
+ def genesis?
38
+ Tapyrus.chain_params.genesis_block.header == header
39
+ end
40
+
41
+ # @param [String] payload a payload with binary format.
42
+ def self.parse_from_payload(payload)
43
+ buf = StringIO.new(payload)
44
+ len = Tapyrus.unpack_var_int_from_io(buf)
45
+ height = buf.read(len).reverse.bth.to_i(16)
46
+ new(Tapyrus::BlockHeader.parse_from_payload(buf.read(80)), height)
47
+ end
48
+
49
+ # build next block +StoredBlock+ instance.
50
+ # @param [Tapyrus::BlockHeader] next_block a next block candidate header.
51
+ # @return [Tapyrus::Store::ChainEntry] a next stored block (not saved).
52
+ def build_next_block(next_block)
53
+ ChainEntry.new(next_block, height + 1)
54
+ end
55
+
56
+ # generate payload
57
+ def to_payload
58
+ height_value = height.to_even_length_hex
59
+ height_value = height_value.htb.reverse
60
+ Tapyrus.pack_var_int(height_value.bytesize) + height_value + header.to_payload
61
+ end
62
+
63
+ end
64
+
65
+ end
66
+
67
+ end
@@ -0,0 +1,98 @@
1
+ require 'leveldb-native'
2
+
3
+ module Tapyrus
4
+ module Store
5
+ module DB
6
+
7
+ class LevelDB
8
+
9
+ attr_reader :db
10
+ attr_reader :logger
11
+
12
+ def initialize(path = "#{Tapyrus.base_dir}/db/spv")
13
+ # @logger = Tapyrus::Logger.create(:debug)
14
+ FileUtils.mkdir_p(path)
15
+ @db = ::LevelDBNative::DB.new(path)
16
+ # logger.debug 'Opened LevelDB successfully.'
17
+ end
18
+
19
+ # put data into LevelDB.
20
+ # @param [Object] key a key.
21
+ # @param [Object] value a value.
22
+ def put(key, value)
23
+ # logger.debug "put #{key} data"
24
+ db.put(key, value)
25
+ end
26
+
27
+ # get value from specified key.
28
+ # @param [Object] key a key.
29
+ # @return[Object] the stored value.
30
+ def get(key)
31
+ db.get(key)
32
+ end
33
+
34
+ # get best block hash.
35
+ def best_hash
36
+ db.get(KEY_PREFIX[:best])
37
+ end
38
+
39
+ # delete specified key data.
40
+ def delete(key)
41
+ db.delete(key)
42
+ end
43
+
44
+ # get block hash specified +height+
45
+ def get_hash_from_height(height)
46
+ db.get(height_key(height))
47
+ end
48
+
49
+ # get next block hash specified +hash+
50
+ def next_hash(hash)
51
+ db.get(KEY_PREFIX[:next] + hash)
52
+ end
53
+
54
+ # get entry payload
55
+ # @param [String] hash the hash with hex format.
56
+ # @return [String] the ChainEntry payload.
57
+ def get_entry_payload_from_hash(hash)
58
+ db.get(KEY_PREFIX[:entry] + hash)
59
+ end
60
+
61
+ def save_entry(entry)
62
+ db.batch do
63
+ db.put(entry.key ,entry.to_payload)
64
+ db.put(height_key(entry.height), entry.block_hash)
65
+ connect_entry(entry)
66
+ end
67
+ end
68
+
69
+ def close
70
+ db.close
71
+ end
72
+
73
+ private
74
+
75
+ # generate height key
76
+ def height_key(height)
77
+ height = height.to_even_length_hex
78
+ KEY_PREFIX[:height] + height.rhex
79
+ end
80
+
81
+ def connect_entry(entry)
82
+ unless entry.genesis?
83
+ tip_block = Tapyrus::Store::ChainEntry.parse_from_payload(get_entry_payload_from_hash(best_hash))
84
+ unless tip_block.block_hash == entry.prev_hash
85
+ raise "entry(#{entry.block_hash}) does not reference current best block hash(#{tip_block.block_hash})"
86
+ end
87
+ unless tip_block.height + 1 == entry.height
88
+ raise "block height is small than current best block."
89
+ end
90
+ end
91
+ db.put(KEY_PREFIX[:best], entry.block_hash)
92
+ db.put(KEY_PREFIX[:next] + entry.prev_hash, entry.block_hash)
93
+ end
94
+ end
95
+
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,9 @@
1
+ module Tapyrus
2
+ module Store
3
+
4
+ module DB
5
+ autoload :LevelDB, 'tapyrus/store/db/level_db'
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,101 @@
1
+ module Tapyrus
2
+
3
+ module Store
4
+
5
+ KEY_PREFIX = {
6
+ entry: 'e', # key: block hash, value: Tapyrus::Store::ChainEntry payload
7
+ height: 'h', # key: block height, value: block hash.
8
+ best: 'B', # value: best block hash.
9
+ next: 'n' # key: block hash, value: A hash of the next block of the specified hash
10
+ }
11
+
12
+ class SPVChain
13
+
14
+ attr_reader :db
15
+ attr_reader :logger
16
+
17
+ def initialize(db = Tapyrus::Store::DB::LevelDB.new)
18
+ @db = db # TODO multiple db switch
19
+ @logger = Tapyrus::Logger.create(:debug)
20
+ initialize_block
21
+ end
22
+
23
+ # get latest block in the store.
24
+ # @return[Tapyrus::Store::ChainEntry]
25
+ def latest_block
26
+ hash = db.best_hash
27
+ return nil unless hash
28
+ find_entry_by_hash(hash)
29
+ end
30
+
31
+ # find block entry with the specified height.
32
+ def find_entry_by_height(height)
33
+ find_entry_by_hash(db.get_hash_from_height(height))
34
+ end
35
+
36
+ # find block entry with the specified hash
37
+ def find_entry_by_hash(hash)
38
+ payload = db.get_entry_payload_from_hash(hash)
39
+ return nil unless payload
40
+ ChainEntry.parse_from_payload(payload)
41
+ end
42
+
43
+ # append block header to chain.
44
+ # @param [Tapyrus::BlockHeader] header a block header.
45
+ # @return [Tapyrus::Store::ChainEntry] appended block header entry.
46
+ def append_header(header)
47
+ logger.info("append header #{header.block_id}")
48
+ raise "this header is invalid. #{header.block_hash}" unless header.valid?
49
+ best_block = latest_block
50
+ current_height = best_block.height
51
+ if best_block.block_hash == header.prev_hash
52
+ entry = Tapyrus::Store::ChainEntry.new(header, current_height + 1)
53
+ db.save_entry(entry)
54
+ entry
55
+ else
56
+ unless find_entry_by_hash(header.block_hash)
57
+ # TODO implements recovery process
58
+ raise "header's previous hash(#{header.prev_hash}) does not match current best block's(#{best_block.block_hash})."
59
+ end
60
+ end
61
+ end
62
+
63
+ # get next block hash for specified +hash+
64
+ # @param [String] hash the block hash(little endian)
65
+ # @return [String] the next block hash. If it does not exist yet, return nil.
66
+ def next_hash(hash)
67
+ db.next_hash(hash)
68
+ end
69
+
70
+ # get median time past for specified block +hash+
71
+ # @param [String] hash the block hash.
72
+ # @return [Integer] the median time past value.
73
+ def mtp(hash)
74
+ time = []
75
+ Tapyrus::MEDIAN_TIME_SPAN.times do
76
+ entry = find_entry_by_hash(hash)
77
+ break unless entry
78
+
79
+ time << entry.header.time
80
+ hash = entry.header.prev_hash
81
+ end
82
+ time.sort!
83
+ time[time.size / 2]
84
+ end
85
+
86
+ private
87
+
88
+ # if database is empty, put genesis block.
89
+ def initialize_block
90
+ unless latest_block
91
+ block = Tapyrus.chain_params.genesis_block
92
+ genesis = ChainEntry.new(block.header, 0)
93
+ db.save_entry(genesis)
94
+ end
95
+ end
96
+
97
+ end
98
+
99
+ end
100
+
101
+ end
@@ -0,0 +1,9 @@
1
+ module Tapyrus
2
+ module Store
3
+
4
+ autoload :DB, 'tapyrus/store/db'
5
+ autoload :SPVChain, 'tapyrus/store/spv_chain'
6
+ autoload :ChainEntry, 'tapyrus/store/chain_entry'
7
+
8
+ end
9
+ end