From 4c543bfa048b962227c7e407967eabad9e5dee54 Mon Sep 17 00:00:00 2001 From: Xuejie Xiao Date: Wed, 3 Apr 2019 06:29:44 +0000 Subject: [PATCH] feat: Address format implementation --- lib/bech32.rb | 114 ++++++++++++++++++++++++++++++++++++++++++++++ lib/ckb.rb | 1 + lib/ckb/api.rb | 22 ++++++++- lib/ckb/wallet.rb | 42 +++++++++++------ 4 files changed, 162 insertions(+), 17 deletions(-) create mode 100644 lib/bech32.rb diff --git a/lib/bech32.rb b/lib/bech32.rb new file mode 100644 index 00000000..d44d44a0 --- /dev/null +++ b/lib/bech32.rb @@ -0,0 +1,114 @@ +# Taken from https://github.com/sipa/bech32/blob/bdc264f84014c234e908d72026b7b780122be11f/ref/ruby/bech32.rb +# Modified to add this function: https://github.com/sipa/bech32/blob/bdc264f84014c234e908d72026b7b780122be11f/ref/ruby/segwit_addr.rb#L64-L85 + +# Copyright (c) 2017 Shigeyuki Azuchi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +module Bech32 + + SEPARATOR = '1' + + CHARSET = %w(q p z r y 9 x 8 g f 2 t v d w 0 s 3 j n 5 4 k h c e 6 m u a 7 l) + + module_function + + def convert_bits(data, from, to, padding=true) + acc = 0 + bits = 0 + ret = [] + maxv = (1 << to) - 1 + max_acc = (1 << (from + to - 1)) - 1 + data.each do |v| + return nil if v < 0 || (v >> from) != 0 + acc = ((acc << from) | v) & max_acc + bits += from + while bits >= to + bits -= to + ret << ((acc >> bits) & maxv) + end + end + if padding + ret << ((acc << (to - bits)) & maxv) unless bits == 0 + elsif bits >= from || ((acc << (to - bits)) & maxv) != 0 + return nil + end + ret + end + + # Encode Bech32 string + def encode(hrp, data) + data = convert_bits(data.bytes, 8, 5) + checksummed = data + create_checksum(hrp, data) + hrp + SEPARATOR + checksummed.map{|i|CHARSET[i]}.join + end + + # Decode a Bech32 string and determine hrp and data + def decode(bech) + # check invalid bytes + return nil if bech.scrub('?').include?('?') + # check uppercase/lowercase + return nil if (bech.downcase != bech && bech.upcase != bech) + bech.each_char{|c|return nil if c.ord < 33 || c.ord > 126} + bech = bech.downcase + # check data length + pos = bech.rindex(SEPARATOR) + return nil if pos.nil? || pos < 1 || pos + 7 > bech.length || bech.length > 90 + # check valid charset + bech[pos+1..-1].each_char{|c|return nil unless CHARSET.include?(c)} + # split hrp and data + hrp = bech[0..pos-1] + data = bech[pos+1..-1].each_char.map{|c|CHARSET.index(c)} + # check checksum + return nil unless verify_checksum(hrp, data) + [hrp, convert_bits(data[0..-7], 5, 8, false).map(&:chr).join] + end + + # Compute the checksum values given hrp and data. + def create_checksum(hrp, data) + values = expand_hrp(hrp) + data + polymod = polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1 + (0..5).map{|i|(polymod >> 5 * (5 - i)) & 31} + end + + # Verify a checksum given Bech32 string + def verify_checksum(hrp, data) + polymod(expand_hrp(hrp) + data) == 1 + end + + # Expand the hrp into values for checksum computation. + def expand_hrp(hrp) + hrp.each_char.map{|c|c.ord >> 5} + [0] + hrp.each_char.map{|c|c.ord & 31} + end + + # Compute Bech32 checksum + def polymod(values) + generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + chk = 1 + values.each do |v| + top = chk >> 25 + chk = (chk & 0x1ffffff) << 5 ^ v + (0..4).each{|i|chk ^= ((top >> i) & 1) == 0 ? 0 : generator[i]} + end + chk + end + + private_class_method :polymod, :expand_hrp + +end diff --git a/lib/ckb.rb b/lib/ckb.rb index 145f799d..85b6a48e 100644 --- a/lib/ckb.rb +++ b/lib/ckb.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "bech32" require "ckb/version" require "ckb/api" require "ckb/blake2b" diff --git a/lib/ckb/api.rb b/lib/ckb/api.rb index cea334ee..725ba879 100644 --- a/lib/ckb/api.rb +++ b/lib/ckb/api.rb @@ -13,12 +13,16 @@ class API attr_reader :uri attr_reader :system_script_out_point attr_reader :system_script_cell_hash + attr_reader :prefix DEFAULT_URL = "http://localhost:8114" MODE_TESTNET = "testnet" MODE_CUSTOM = "custom" + PREFIX_MAINNET = "ckb" + PREFIX_TESTNET = "ckt" + def initialize(host: DEFAULT_URL, mode: MODE_TESTNET) @uri = URI(host) if mode == MODE_TESTNET @@ -31,15 +35,29 @@ def initialize(host: DEFAULT_URL, mode: MODE_TESTNET) } cell_data = CKB::Utils.hex_to_bin(system_cell_transaction[:outputs][0][:data]) cell_hash = CKB::Utils.bin_to_prefix_hex(CKB::Blake2b.digest(cell_data)) - self.set_system_script_cell(out_point, cell_hash) + self.set_system_script_cell(out_point, cell_hash, prefix: PREFIX_TESTNET) end end # @param out_point [Hash] { hash: "0x...", index: 0 } # @param cell_hash [String] "0x..." - def set_system_script_cell(out_point, cell_hash) + def set_system_script_cell(out_point, cell_hash, prefix: PREFIX_MAINNET) @system_script_out_point = out_point @system_script_cell_hash = cell_hash + @prefix = prefix + end + + # Generates address assuming default lock script is used + def generate_address(pubkey_hash) + Bech32.encode(prefix, "\x00\x00\x00\x00\x00\x02" + pubkey_hash) + end + + # Parse address into lock assuming default lock script is used + def parse_address(address) + prefix, data = Bech32.decode(address) + raise "Invalid prefix" if prefix != @prefix + raise "Invalid version/type/script" if data.slice(0..5) != "\x00\x00\x00\x00\x00\x02" + data.slice(6..-1) end def system_script_cell diff --git a/lib/ckb/wallet.rb b/lib/ckb/wallet.rb index c3a6c253..8a441f70 100644 --- a/lib/ckb/wallet.rb +++ b/lib/ckb/wallet.rb @@ -46,7 +46,7 @@ def get_balance get_unspent_cells.map { |cell| cell[:capacity] }.reduce(0, &:+) end - def generate_tx(target_lock, capacity) + def generate_tx(target_address, capacity) i = gather_inputs(capacity, MIN_CELL_CAPACITY) input_capacities = i.capacities @@ -54,7 +54,7 @@ def generate_tx(target_lock, capacity) { capacity: capacity, data: "", - lock: target_lock + lock: generate_lock(api.parse_address(target_address)) } ] if input_capacities > capacity @@ -72,10 +72,10 @@ def generate_tx(target_lock, capacity) } end - # @param target_lock [Hash] + # @param target_address [String] # @param capacity [Integer] - def send_capacity(target_lock, capacity) - tx = generate_tx(target_lock, capacity) + def send_capacity(target_address, capacity) + tx = generate_tx(target_address, capacity) send_transaction_bin(tx) end @@ -84,16 +84,6 @@ def get_transaction(hash_hex) api.get_transaction(hash_hex) end - def lock - @lock ||= { - version: 0, - binary_hash: api.system_script_cell_hash, - args: [ - CKB::Utils.bin_to_hex(CKB::Blake2b.digest(CKB::Blake2b.digest(pubkey_bin))) - ] - } - end - def block_assembler_config args = lock[:args].map do |arg| "[#{arg.bytes.map(&:to_s).join(", ")}]" @@ -105,6 +95,10 @@ def block_assembler_config ).strip end + def address + api.generate_address(pubkey_hash_bin) + end + private def send_transaction_bin(transaction) @@ -141,9 +135,27 @@ def pubkey_bin CKB::Utils.extract_pubkey_bin(privkey) end + def pubkey_hash_bin + CKB::Blake2b.digest(CKB::Blake2b.digest(pubkey_bin)) + end + def lock_hash @lock_hash ||= CKB::Utils.json_script_to_type_hash(lock) end + + def lock + @lock ||= generate_lock(pubkey_hash_bin) + end + + def generate_lock(target_pubkey_hash_bin) + { + version: 0, + binary_hash: api.system_script_cell_hash, + args: [ + CKB::Utils.bin_to_hex(target_pubkey_hash_bin) + ] + } + end end end