Skip to content

Commit 4c94285

Browse files
authored
Merge pull request #3 from starcoinorg/bech32
Implement ReceiptIdentifier
2 parents 5ebeaca + 74d5c3f commit 4c94285

File tree

4 files changed

+194
-1
lines changed

4 files changed

+194
-1
lines changed

starcoin/sdk/bech32.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Copyright (c) 2017, 2020 Pieter Wuille
2+
#
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy
4+
# of this software and associated documentation files (the "Software"), to deal
5+
# in the Software without restriction, including without limitation the rights
6+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
# copies of the Software, and to permit persons to whom the Software is
8+
# furnished to do so, subject to the following conditions:
9+
#
10+
# The above copyright notice and this permission notice shall be included in
11+
# all copies or substantial portions of the Software.
12+
#
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
# THE SOFTWARE.
20+
21+
"""Reference implementation for Bech32/Bech32m and segwit addresses."""
22+
23+
24+
from enum import Enum
25+
26+
27+
class Encoding(Enum):
28+
"""Enumeration type to list the various supported encodings."""
29+
BECH32 = 1
30+
BECH32M = 2
31+
32+
33+
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
34+
BECH32M_CONST = 0x2bc830a3
35+
36+
37+
def bech32_polymod(values):
38+
"""Internal function that computes the Bech32 checksum."""
39+
generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
40+
chk = 1
41+
for value in values:
42+
top = chk >> 25
43+
chk = (chk & 0x1ffffff) << 5 ^ value
44+
for i in range(5):
45+
chk ^= generator[i] if ((top >> i) & 1) else 0
46+
return chk
47+
48+
49+
def bech32_hrp_expand(hrp):
50+
"""Expand the HRP into values for checksum computation."""
51+
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
52+
53+
54+
def bech32_verify_checksum(hrp, data):
55+
"""Verify a checksum given HRP and converted data characters."""
56+
const = bech32_polymod(bech32_hrp_expand(hrp) + data)
57+
if const == 1:
58+
return Encoding.BECH32
59+
if const == BECH32M_CONST:
60+
return Encoding.BECH32M
61+
return None
62+
63+
64+
def bech32_create_checksum(hrp, data, spec):
65+
"""Compute the checksum values given HRP and data."""
66+
values = bech32_hrp_expand(hrp) + data
67+
const = BECH32M_CONST if spec == Encoding.BECH32M else 1
68+
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const
69+
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
70+
71+
72+
def bech32_encode(hrp, data, spec):
73+
"""Compute a Bech32 string given HRP and data values."""
74+
combined = data + bech32_create_checksum(hrp, data, spec)
75+
return hrp + '1' + ''.join([CHARSET[d] for d in combined])
76+
77+
78+
def bech32_decode(bech):
79+
"""Validate a Bech32/Bech32m string, and determine HRP and data."""
80+
if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
81+
(bech.lower() != bech and bech.upper() != bech)):
82+
return (None, None, None)
83+
bech = bech.lower()
84+
pos = bech.rfind('1')
85+
if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
86+
return (None, None, None)
87+
if not all(x in CHARSET for x in bech[pos+1:]):
88+
return (None, None, None)
89+
hrp = bech[:pos]
90+
data = [CHARSET.find(x) for x in bech[pos+1:]]
91+
spec = bech32_verify_checksum(hrp, data)
92+
if spec is None:
93+
return (None, None, None)
94+
return (hrp, data[:-6], spec)
95+
96+
97+
def convertbits(data, frombits, tobits, pad=True):
98+
"""General power-of-2 base conversion."""
99+
acc = 0
100+
bits = 0
101+
ret = []
102+
maxv = (1 << tobits) - 1
103+
max_acc = (1 << (frombits + tobits - 1)) - 1
104+
for value in data:
105+
if value < 0 or (value >> frombits):
106+
return None
107+
acc = ((acc << frombits) | value) & max_acc
108+
bits += frombits
109+
while bits >= tobits:
110+
bits -= tobits
111+
ret.append((acc >> bits) & maxv)
112+
if pad:
113+
if bits:
114+
ret.append((acc << (tobits - bits)) & maxv)
115+
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
116+
return None
117+
return ret
118+
119+
120+
def decode(hrp, addr):
121+
"""Decode a segwit address."""
122+
hrpgot, data, spec = bech32_decode(addr)
123+
if hrpgot != hrp:
124+
return None
125+
decoded = convertbits(data[1:], 5, 8, False)
126+
return (data[0], decoded)
127+
128+
129+
def encode(hrp, witver, witprog):
130+
"""Encode a segwit address."""
131+
ret = bech32_encode(
132+
hrp, [witver] + convertbits(witprog, 8, 5), Encoding.BECH32)
133+
if decode(hrp, ret) == (None, None):
134+
return None
135+
return ret

starcoin/sdk/receipt_identifier.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from starcoin.sdk.auth_key import AuthKey
2+
from starcoin.starcoin_types import AccountAddress
3+
from typing import Union
4+
from starcoin.sdk import bech32
5+
from starcoin.sdk import utils
6+
7+
8+
class ReceiptIdentifier:
9+
"""
10+
ref: [sip-21](https://github.com/starcoinorg/SIPs/blob/master/sip-21/index.md)
11+
"""
12+
account_address: AccountAddress
13+
auth_key: Union[AuthKey, None]
14+
15+
def __init__(self, account_address: AccountAddress,
16+
auth_key: Union[AuthKey, None]):
17+
self.account_address = account_address
18+
self.auth_key = auth_key
19+
20+
def encode(self) -> "str":
21+
data = bytearray(self.account_address.bcs_serialize())
22+
if isinstance(self.auth_key, AuthKey):
23+
data.extend(bytearray(self.auth_key.data))
24+
return bech32.encode("stc", 1, data)
25+
26+
@staticmethod
27+
def decode(s: str) -> "ReceiptIdentifier":
28+
(version, data) = bech32.decode("stc", s)
29+
if version != 1:
30+
return None
31+
address = AccountAddress.from_hex(
32+
bytearray(data[0:utils.ACCOUNT_ADDRESS_LEN]).hex())
33+
if len(data) == utils.ACCOUNT_ADDRESS_LEN:
34+
auth_key = None
35+
else:
36+
auth_key = AuthKey(bytes(data[utils.ACCOUNT_ADDRESS_LEN:]))
37+
38+
return ReceiptIdentifier(address, auth_key)

starcoin/starcoin_types/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def bcs_deserialize(input: bytes) -> 'AccessPath':
3434
if buffer:
3535
raise st.DeserializationError("Some input bytes were not read")
3636
return v
37-
37+
3838

3939
@dataclass(frozen=True)
4040
class AccountAddress:
@@ -56,6 +56,7 @@ def from_hex(addr: str) -> 'AccountAddress':
5656
"""Create an account address from bytes."""
5757
return AccountAddress(tuple(st.uint8(x) for x in bytes.fromhex(addr)))
5858

59+
5960
@dataclass(frozen=True)
6061
class AccountResource:
6162
authentication_key: bytes

tests/test_receipt_identifier.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from starcoin.sdk.receipt_identifier import ReceiptIdentifier
2+
from starcoin.starcoin_types import AccountAddress
3+
from starcoin.sdk.auth_key import AuthKey
4+
from starcoin.sdk import utils
5+
6+
7+
def test_receipt_identifier():
8+
auth_key_hex = "93dcc435cfca2dcf3bf44e9948f1f6a98e66a1f1b114a4b8a37ea16e12beeb6d"
9+
address_hex = "1603d10ce8649663e4e5a757a8681833"
10+
11+
# encode
12+
receipt = ReceiptIdentifier(
13+
AccountAddress.from_hex(address_hex), AuthKey(bytes.fromhex(auth_key_hex))).encode()
14+
# deocode
15+
decode_receipt = ReceiptIdentifier.decode(receipt)
16+
17+
assert utils.account_address_hex(
18+
decode_receipt.account_address) == address_hex
19+
assert decode_receipt.auth_key.hex() == auth_key_hex

0 commit comments

Comments
 (0)