Skip to content

Commit

Permalink
Support bech32m spec
Browse files Browse the repository at this point in the history
  • Loading branch information
azuchi committed Feb 5, 2021
1 parent 935391e commit 8713f1a
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 49 deletions.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# Bech32 [![Build Status](https://travis-ci.org/azuchi/bech32rb.svg?branch=master)](https://travis-ci.org/azuchi/bech32rb) [![Gem Version](https://badge.fury.io/rb/bech32.svg)](https://badge.fury.io/rb/bech32) [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) <img src="http://segwit.co/static/public/images/logo.png" width="100">

The implementation of the Bech32 encoder and decoder for Ruby.
The implementation of the Bech32/Bech32m encoder and decoder for Ruby.

Bech32 is checksummed base32 format that is used in following Bitcoin address format.

https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki

Bech32m is checksummed base32m format that is used in following Bitcoin address format.

https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki

## Installation

Add this line to your application's Gemfile:
Expand Down Expand Up @@ -35,13 +39,15 @@ require 'bech32'
Decode Bech32-encoded data into hrp part and data part.

```ruby
hrp, data = Bech32.decode('BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4')
hrp, data, spec = Bech32.decode('BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4')

# hrp is human-readable part of Bech32 format
'bc'

# data is data part of Bech32 format
[0, 14, 20, 15, 7, 13, 26, 0, 25, 18, 6, 11, 13, 8, 21, 4, 20, 3, 17, 2, 29, 3, 12, 29, 3, 4, 15, 24, 20, 6, 14, 30, 22]

# spec is whether Bech32::Encoding::BECH32 or Bech32::Encoding::BECH32M
```

Decode Bech32-encoded Segwit address into `Bech32::SegwitAddr` instance.
Expand Down Expand Up @@ -77,7 +83,7 @@ Encode Bech32 human-readable part and data part into Bech32 string.
hrp = 'bc'
data = [0, 14, 20, 15, 7, 13, 26, 0, 25, 18, 6, 11, 13, 8, 21, 4, 20, 3, 17, 2, 29, 3, 12, 29, 3, 4, 15, 24, 20, 6, 14, 30, 22]

bech = Bech32.encode(hrp, data)
bech = Bech32.encode(hrp, data, Bech32::Encoding::BECH32)
=> bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
```

Expand Down
36 changes: 27 additions & 9 deletions lib/bech32.rb
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
module Bech32

module Encoding
BECH32 = 1
BECH32M = 2
end

autoload :SegwitAddr, 'bech32/segwit_addr'

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)

BECH32M_CONST = 0x2bc830a3

module_function

# Returns the encoded Bech32 string.
#
# require 'bech32'
#
# bech = Bech32.encode('bc', [])
# bech = Bech32.encode('bc', [], Bech32::Encoding::BECH32)
#
# <i>Generates:</i>
# 'BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4' # bech
#
def encode(hrp, data)
checksummed = data + create_checksum(hrp, data)
def encode(hrp, data, spec)
checksummed = data + create_checksum(hrp, data, spec)
hrp + SEPARATOR + checksummed.map{|i|CHARSET[i]}.join
end

Expand All @@ -27,11 +34,12 @@ def encode(hrp, data)
# require 'bech32'
#
# addr = 'BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4'
# hrp, data = Bech32.decode(addr)
# hrp, data, spec = Bech32.decode(addr)
#
# <i>Generates:</i>
# 'bc' # hrp
# [0, 14, 20, 15, 7, 13, 26, 0, 25, 18, 6, 11, 13, 8, 21, 4, 20, 3, 17, 2, 29, 3, 12, 29, 3, 4, 15, 24, 20, 6, 14, 30, 22] # data
# 1 # spec see Bech32::Encoding
#
def decode(bech, max_length = 90)
# check uppercase/lowercase
Expand All @@ -47,20 +55,30 @@ def decode(bech, max_length = 90)
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, data[0..-7]]
spec = verify_checksum(hrp, data)
spec ? [hrp, data[0..-7], spec] : nil
end

# Returns computed checksum values of +hrp+ and +data+
def create_checksum(hrp, data)
def create_checksum(hrp, data, spec)
values = expand_hrp(hrp) + data
polymod = polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1
const = (spec == Bech32::Encoding::BECH32M ? Bech32::BECH32M_CONST : 1)
polymod = polymod(values + [0, 0, 0, 0, 0, 0]) ^ const
(0..5).map{|i|(polymod >> 5 * (5 - i)) & 31}
end

# Verify a checksum given Bech32 string
# @param [String] hrp hrp part.
# @param [Array[Integer]] data data array.
# @return [Integer] spec
def verify_checksum(hrp, data)
polymod(expand_hrp(hrp) + data) == 1
const = polymod(expand_hrp(hrp) + data)
case const
when 1
Encoding::BECH32
when BECH32M_CONST
Encoding::BECH32M
end
end

# Expand the hrp into values for checksum computation.
Expand Down
6 changes: 4 additions & 2 deletions lib/bech32/segwit_addr.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,21 @@ def script_pubkey=(script_pubkey)

# Returns segwit address string which generated from hrp, witness version and witness program.
def addr
Bech32.encode(hrp, [ver] + convert_bits(prog, 8, 5))
spec = (ver == 0 ? Bech32::Encoding::BECH32 : Bech32::Encoding::BECH32M)
Bech32.encode(hrp, [ver] + convert_bits(prog, 8, 5), spec)
end

private

def parse_addr(addr)
@hrp, data = Bech32.decode(addr)
@hrp, data, spec = Bech32.decode(addr)
raise 'Invalid address.' if hrp.nil? || data[0].nil? || ![HRP_MAINNET, HRP_TESTNET, HRP_REGTEST].include?(hrp)
@ver = data[0]
raise 'Invalid witness version' if @ver > 16
@prog = convert_bits(data[1..-1], 5, 8, false)
raise 'Invalid witness program' if @prog.nil? || @prog.length < 2 || @prog.length > 40
raise 'Invalid witness program with version 0' if @ver == 0 && (@prog.length != 20 && @prog.length != 32)
raise 'Witness version and encoding spec do not match' if (@ver == 0 && spec != Bech32::Encoding::BECH32) || (@ver != 0 && spec != Bech32::Encoding::BECH32M)
end

def convert_bits(data, from, to, padding=true)
Expand Down
131 changes: 96 additions & 35 deletions spec/bech32_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

describe Bech32 do

VALID_CHECKSUM = [
VALID_BECH32 = [
"A12UEL5L",
"a12uel5l",
"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs",
Expand All @@ -12,7 +12,17 @@
"?1ezyfcl"
]

INVALID_CHECKSUM = [
VALID_BECH32M = [
"A1LQFN3A",
"a1lqfn3a",
"an83characterlonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11sg7hg6",
"abcdef1l7aum6echk45nj3s0wdvt2fg8x9yrzpqzd3ryx",
"11llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllludsr8",
"split1checkupstagehandshakeupstreamerranterredcaperredlc445v",
"?1v759aa",
]

INVALID_BECH32 = [
" 1nwldj5", # HRP character out of range
"\x7F" + "1axkwrx", # HRP character out of range
"\x80" + "1eym55h", # HRP character out of range
Expand All @@ -22,54 +32,105 @@
"1pzry9x0s0muk", # Empty HRP
"x1b4n0q5v", # Invalid data character
"li1dgmt3", # Too short checksum
"de1lg7wt\xff", # Invalid character in checksum
"de1lg7wt" + "\xff",# Invalid character in checksum
"A1G7SGD8", # checksum calculated with uppercase form of HRP
"10a06t8", # empty HRP
"1qzzfhee", # empty HRP
]

INVALID_BECH32M = [
" 1xj0phk", # HRP character out of range
"\x7F" + "1g6xzxy", # HRP character out of range
"\x80" + "1vctc34", # HRP character out of range
# overall max length exceeded
"an84characterslonghumanreadablepartthatcontainsthetheexcludedcharactersbioandnumber11d6pts4",
"qyrz8wqd2c9m", # No separator character
"1qyrz8wqd2c9m", # Empty HRP
"y1b0jsk6g", # Invalid data character
"lt1igcx5c0", # Invalid data character
"in1muywd", # Too short checksum
"mm1crxm3i", # Invalid character in checksum
"au1s5cgom", # Invalid character in checksum
"M1VUXWEZ", # Checksum calculated with uppercase form of HRP
"16plkw9", # Empty HRP
"1p2gdwpf", # Empty HRP
]

VALID_ADDRESS = [
["BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", "0014751e76e8199196d454941c45d1b3a323f1433bd6"],
["tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
"00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"],
["bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx",
"5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6"],
["BC1SW50QA3JX3S", "6002751e"],
["bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", "5210751e76e8199196d454941c45d1b3a323"],
["tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy",
"0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"],
["bcrt1q8w9txuafw0q3lvhqrdsl49fea53h7rfwulh46h", "00143b8ab373a973c11fb2e01b61fa9539ed237f0d2e"]
["BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", "0014751e76e8199196d454941c45d1b3a323f1433bd6"],
["tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7",
"00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262"],
["bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kt5nd6y",
"5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6"],
["BC1SW50QGDZ25J", "6002751e"],
["bc1zw508d6qejxtdg4y5r3zarvaryvaxxpcs", "5210751e76e8199196d454941c45d1b3a323"],
["tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy",
"0020000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"],
["tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c",
"5120000000c4a5cad46221b2a187905e5266362b99d5e91c6ce24d165dab93e86433"],
["bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0",
"512079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"]
]

INVALID_ADDRESS = [
"tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty",
"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5",
"BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2",
"bc1rw5uspcuh",
"bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90",
"bca0w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90234567789035",
"BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P",
"tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7",
"bc1zw508d6qejxtdg4y5r3zarvaryvqyzf3du",
"tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv",
"bc1gmk9yu",
# Invalid HRP
"tc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq5zuyut",
# Invalid checksum algorithm (bech32 instead of bech32m)
"bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqh2y7hd",
# Invalid checksum algorithm (bech32 instead of bech32m)
"tb1z0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqglt7rf",
# Invalid checksum algorithm (bech32 instead of bech32m)
"BC1S0XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ54WELL",
# Invalid checksum algorithm (bech32m instead of bech32)
"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kemeawh",
# Invalid checksum algorithm (bech32m instead of bech32)
"tb1q0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq24jc47",
# Invalid character in checksum
"bc1p38j9r5y49hruaue7wxjce0updqjuyyx0kh56v8s25huc6995vvpql3jow4",
# Invalid witness version
"BC130XLXVLHEMJA6C4DQV22UAPCTQUPFHLXM9H8Z3K2E72Q4K9HCZ7VQ7ZWS8R",
# Invalid program length (1 byte)
"bc1pw5dgrnzv",
# Invalid program length (41 bytes)
"bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v8n0nx0muaewav253zgeav",
# Invalid program length for witness version 0 (per BIP141)
"BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P",
# Mixed case
"tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq47Zagq",
# More than 4 padding bits
"bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7v07qwwzcrf",
# Non-zero padding in 8-to-5 conversion
"tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vpggkg4j",
# Empty data section
"bc1gmk9yu"
]

it 'valid checksum' do
VALID_CHECKSUM.each do |bech|
hrp, _ = Bech32.decode(bech)
expect(hrp).to be_truthy
pos = bech.rindex('1')
bech = bech[0..pos] + (bech[pos + 1].ord ^ 1).chr + bech[pos+2..-1]
hrp, _ = Bech32.decode(bech)
expect(hrp).to be_nil
specs = {}
specs[Bech32::Encoding::BECH32] = VALID_BECH32
specs[Bech32::Encoding::BECH32M] = VALID_BECH32M
specs.each do |k, v|
v.each do |bech|
hrp, _, spec = Bech32.decode(bech)
expect(hrp).to be_truthy
expect(spec).to eq(k)
pos = bech.rindex('1')
bech = bech[0..pos] + (bech[pos + 1].ord ^ 1).chr + bech[pos+2..-1]
hrp, _ = Bech32.decode(bech)
expect(hrp).to be_nil
end
end
end

def test_invalid_checksum
INVALID_CHECKSUM.each do |bech|
hrp, _ = Bech32.decode(bech)
assert_nil (hrp)
it 'test_invalid_checksum' do
specs = {}
specs[Bech32::Encoding::BECH32] = INVALID_BECH32
specs[Bech32::Encoding::BECH32M] = INVALID_BECH32M
specs.each do |k, v|
v.each do |bech|
hrp, _, spec = Bech32.decode(bech)
expect(hrp.nil? || spec != k).to be true
end
end
end

Expand Down

0 comments on commit 8713f1a

Please sign in to comment.