Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QA: Add proper bech32 encoding/decoding utilities to python framework #2747

Merged
merged 1 commit into from
Mar 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion test/functional/p2p_quorum_connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@

from random import getrandbits
from test_framework.test_framework import PivxDMNTestFramework
from test_framework.bech32 import bech32_str_to_bytes
from test_framework.mininode import P2PInterface
from test_framework.messages import msg_version
from test_framework.util import (
assert_equal,
bytes_to_hex_str,
connect_nodes,
hash256,
bech32_str_to_bytes,
wait_until,
)

Expand Down
113 changes: 113 additions & 0 deletions test/functional/test_framework/bech32.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
# Copyright (c) 2017 Pieter Wuille
# Copyright (c) 2022 The PIVX developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Reference implementation for Bech32 encoding/decoding"""

CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
BECH32_CONST = 1

def bech32_polymod(values):
"""Internal function that computes the Bech32 checksum."""
generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
chk = 1
for value in values:
top = chk >> 25
chk = (chk & 0x1ffffff) << 5 ^ value
for i in range(5):
chk ^= generator[i] if ((top >> i) & 1) else 0
return chk

def bech32_hrp_expand(hrp):
"""Expand the HRP into values for checksum computation."""
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]

def bech32_verify_checksum(hrp, data):
"""Verify a checksum given HRP and converted data characters."""
return bech32_polymod(bech32_hrp_expand(hrp) + data) == BECH32_CONST

def bech32_create_checksum(hrp, data):
"""Compute the checksum values given HRP and data."""
values = bech32_hrp_expand(hrp) + data
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ BECH32_CONST
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]

def bech32_encode(hrp, data):
"""Compute a Bech32 string given HRP and data values."""
combined = data + bech32_create_checksum(hrp, data)
return hrp + '1' + ''.join([CHARSET[d] for d in combined])

def bech32_decode(bech):
"""Validate a Bech32 string, and determine HRP and data."""
if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
(bech.lower() != bech and bech.upper() != bech)):
return None, None
bech = bech.lower()
pos = bech.rfind('1')
if pos < 1 or pos + 7 > len(bech) or len(bech) > 95:
return None, None
if not all(x in CHARSET for x in bech[pos+1:]):
return None, None
hrp = bech[:pos]
data = [CHARSET.find(x) for x in bech[pos+1:]]
if not bech32_verify_checksum(hrp, data):
return None, None
return hrp, data[:-6]

def convertbits(data, frombits, tobits, pad=True):
"""General power-of-2 base conversion."""
acc = 0
bits = 0
ret = []
maxv = (1 << tobits) - 1
max_acc = (1 << (frombits + tobits - 1)) - 1
for value in data:
if value < 0 or (value >> frombits):
return None
acc = ((acc << frombits) | value) & max_acc
bits += frombits
while bits >= tobits:
bits -= tobits
ret.append((acc >> bits) & maxv)
if pad:
if bits:
ret.append((acc << (tobits - bits)) & maxv)
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
return None
return ret

def bech32_str_to_bytes(bech_str):
_, data = bech32_decode(bech_str)
if data is None:
return None
decoded = convertbits(data, 5, 8, False)
if decoded is None:
return None
return bytes(decoded)



if __name__ == '__main__':
def test_bls_bech32(bech32_str, hrp, bytes_data):
# test Bech32 encoding/decoding for BLS secret keys
h, d = bech32_decode(bech32_str)
assert h == hrp
assert bech32_str == bech32_encode(h, d)
assert bech32_str_to_bytes(bech32_str) == bytes_data
assert bech32_decode(bech32_str + CHARSET[0]) == (None, None) # add 1 char
assert bech32_decode(bech32_str[:-1]) == (None, None) # remove 1 char
bech32_str = bech32_str[:-1] + CHARSET[(CHARSET.index(bech32_str[-1]) + 1) % len(CHARSET)]
assert bech32_decode(bech32_str) == (None, None) # change 1 char

# BLS secret key
sk_bls = "bls-sk-test1y8z22ksxta8hwqh9r6jsyc42frqft43t2uav6s0vqrjjf67metnq2mwkm0"
sk = b'!\xc4\xa5Z\x06_Ow\x02\xe5\x1e\xa5\x02b\xaaH\xc0\x95\xd6+W:\xcdA\xec\x00\xe5$\xeb\xdb\xca\xe6'
test_bls_bech32(sk_bls, "bls-sk-test", sk)

# BLS public key
pk_bls = "bls-pk-test132ngslan8s5ha4k74wynyez3drzkw3fa2d60676ch3ac6gzx6ttd0xg8pqj0x5n5tgajvuycqmgtgnsep90"
pk = b'\x8a\xa6\x88\x7f\xb3<)~\xd6\xde\xab\x892dQh\xc5gE=St\xfd{X\xbc{\x8d F\xd2\xd6\xd7\x99\x07\x08$\xf3RtZ;&p\x98\x06\xd0\xb4'
test_bls_bech32(pk_bls, "bls-pk-test", pk)

print("All good.")
37 changes: 0 additions & 37 deletions test/functional/test_framework/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,43 +201,6 @@ def hash256(byte_str):
def hex_str_to_bytes(hex_str):
return unhexlify(hex_str.encode('ascii'))

def convertbits(data, frombits, tobits, pad=True):
"""General power-of-2 base conversion."""
acc = 0
bits = 0
ret = []
maxv = (1 << tobits) - 1
max_acc = (1 << (frombits + tobits - 1)) - 1
for value in data:
if value < 0 or (value >> frombits):
return None
acc = ((acc << frombits) | value) & max_acc
bits += frombits
while bits >= tobits:
bits -= tobits
ret.append((acc >> bits) & maxv)
if pad:
if bits:
ret.append((acc << (tobits - bits)) & maxv)
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
return None
return ret

def bech32_str_to_bytes(bech_str):
BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
bech_str = bech_str.lower()
pos = bech_str.rfind('1')
if pos < 1 or pos + 7 > len(bech_str):
return None
if not all(x in BECH32_CHARSET for x in bech_str[pos+1:]):
return None
# !TODO: verify checksum
data = [BECH32_CHARSET.find(x) for x in bech_str[pos+1:]]
decoded = convertbits(data[:-6], 5, 8, False)
if decoded is None:
return None
return bytes(decoded)

def str_to_b64str(string):
return b64encode(string.encode('utf-8')).decode('ascii')

Expand Down