Skip to content

Commit

Permalink
Merge pull request #13 from nervosnetwork/address-format-implementation
Browse files Browse the repository at this point in the history
feat: Address format implementation
  • Loading branch information
ashchan authored Apr 10, 2019
2 parents c1a8d96 + 7449ca9 commit e773a24
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 10 deletions.
114 changes: 114 additions & 0 deletions lib/bech32.rb
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions lib/ckb.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require "bech32"
require "ckb/version"
require "ckb/api"
require "ckb/blake2b"
Expand Down
19 changes: 17 additions & 2 deletions lib/ckb/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,15 +35,26 @@ 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_bin)
CKB::Utils.generate_address(prefix, pubkey_hash_bin)
end

# Parse address into lock assuming default lock script is used
def parse_address(address)
CKB::Utils.parse_address(address, prefix)
end

def system_script_cell
Expand Down
21 changes: 18 additions & 3 deletions lib/ckb/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ def self.json_script_to_type_hash(script)

def self.sign_sighash_all_inputs(inputs, outputs, privkey)
blake2b = CKB::Blake2b.new
sighash_type = 0x1.to_s
blake2b.update(sighash_type)
inputs.each do |input|
previous_output = input[:previous_output]
blake2b.update(hex_to_bin(previous_output[:hash]))
Expand All @@ -58,7 +56,7 @@ def self.sign_sighash_all_inputs(inputs, outputs, privkey)
signature_hex = bin_to_hex(signature_bin)

inputs.map do |input|
args = input[:args] + [signature_hex, sighash_type]
args = input[:args] + [signature_hex]
input.merge(args: args)
end
end
Expand All @@ -84,5 +82,22 @@ def self.normalize_tx_for_json!(transaction)

transaction
end

def self.pubkey_hash_bin(pubkey_bin)
CKB::Blake2b.digest(pubkey_bin)
end

def self.generate_address(prefix, pubkey_hash_bin)
Bech32.encode(prefix, "\x00\x00\x00\x00\x00\x02" + pubkey_hash_bin)
end

def self.parse_address(address, prefix)
decoded_prefix, data = Bech32.decode(address)
raise "Invalid prefix" if decoded_prefix != prefix

raise "Invalid version/type/script" if data.slice(0..5) != "\x00\x00\x00\x00\x00\x02"

data.slice(6..-1)
end
end
end
32 changes: 27 additions & 5 deletions lib/ckb/wallet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ 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

outputs = [
{
capacity: capacity,
data: "",
lock: target_lock
lock: generate_lock(api.parse_address(target_address))
}
]
if input_capacities > capacity
Expand All @@ -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

Expand Down Expand Up @@ -105,6 +105,10 @@ def block_assembler_config
).strip
end

def address
api.generate_address(pubkey_hash_bin)
end

private

def send_transaction_bin(transaction)
Expand Down Expand Up @@ -141,9 +145,27 @@ def pubkey_bin
CKB::Utils.extract_pubkey_bin(privkey)
end

def pubkey_hash_bin
CKB::Utils.pubkey_hash_bin(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

Expand Down
23 changes: 23 additions & 0 deletions spec/ckb/utils_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@
let(:address) { "0xbc374983430db3686ab181138bb510cb8f83aa136d833ac18fc3e73a3ad54b8b" }
let(:privkey_bin) { Utils.hex_to_bin(privkey) }
let(:pubkey_bin) { Utils.hex_to_bin(pubkey) }
let(:pubkey_hash) { "0x36c329ed630d6ce750712a477543672adab57f4c6fd36a71496305456bb298db" }
let(:pubkey_hash_bin) { Utils.hex_to_bin(pubkey_hash) }
let(:prefix) { "ckt" }
let(:address) { "ckt1qqqqqqqqqgmvx20dvvxkee6swy4ywa2rvu4d4dtlf3hax6n3f93s23ttk2vdk68gmaq" }

context "address" do
it "pubkey_hash_bin" do
expect(
Utils.pubkey_hash_bin(pubkey_bin)
).to eq pubkey_hash_bin
end

it "generate_address" do
generated_address = Utils.generate_address(prefix, pubkey_hash_bin)
expect(generated_address).to eq address
end

it "parse_address" do
expect(
Utils.parse_address(address, prefix)
).to eq pubkey_hash_bin
end
end

def always_success_json_object
hash_bin = CKB::Blake2b.digest(
Expand Down

0 comments on commit e773a24

Please sign in to comment.