Skip to content

Commit 4923edd

Browse files
committed
Implement Silent Payment PSBT Signing
1 parent 0c637e5 commit 4923edd

5 files changed

Lines changed: 340 additions & 7 deletions

File tree

shared/auth.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,12 @@ async def interact(self):
369369

370370
ccc_c_xfp = CCCFeature.get_xfp() # can be None
371371
args = self.psbt.consider_inputs(cosign_xfp=ccc_c_xfp)
372+
373+
# BIP-375: Preview Silent Payment outputs if possible
374+
# This computes real addresses for single-signer scenarios
375+
if self.psbt.has_silent_payment_outputs():
376+
self.psbt.preview_silent_payment_outputs()
377+
372378
self.psbt.consider_outputs(*args, cosign_xfp=ccc_c_xfp)
373379
del args # not needed anymore
374380
# we can properly assess sighash only after we know
@@ -588,7 +594,12 @@ def make_msg(offset, count):
588594
for i, (idx, out) in enumerate(self.psbt.output_iter(offset, end)):
589595
outp = self.psbt.outputs[idx]
590596
item = "Output %d%s:\n\n" % (idx, " (change)" if outp.is_change else "")
591-
msg, addr_or_script = self.render_output(out)
597+
# render silent payment address
598+
if outp.sp_v0_info:
599+
msg = self.psbt.render_silent_payment_output_string(outp, out)
600+
addr_or_script = self.psbt.encode_silent_payment_address(outp)
601+
else:
602+
msg, addr_or_script = self.render_output(out)
592603
item += msg
593604
addrs.append(addr_or_script)
594605
if outp.is_change:
@@ -688,6 +699,8 @@ def output_summary_text(self, msg):
688699
total_change = 0
689700
has_change = False
690701

702+
# TODO: Test pay to silent payment change address
703+
691704
for idx, tx_out in self.psbt.output_iter():
692705
outp = self.psbt.outputs[idx]
693706
if outp.is_change:
@@ -701,7 +714,11 @@ def output_summary_text(self, msg):
701714

702715
else:
703716
if len(largest_outs) < MAX_VISIBLE_OUTPUTS:
704-
rendered, _ = self.render_output(tx_out)
717+
# render the silent payment address
718+
if outp.sp_v0_info:
719+
rendered = self.psbt.render_silent_payment_output_string(outp, tx_out)
720+
else:
721+
rendered, _ = self.render_output(tx_out)
705722
largest_outs.append((tx_out.nValue, rendered))
706723
if len(largest_outs) == MAX_VISIBLE_OUTPUTS:
707724
# descending sort from the biggest value to lowest (sort on out.nValue)

shared/bech32.py

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

shared/chains.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class ChainsBase:
5858
curve = 'secp256k1'
5959
menu_name = None # use 'name' if this isn't defined
6060
ccc_min_block = 0
61+
sp_hrp = 'sp' # BIP-352 Silent Payment address HRP (default mainnet)
6162

6263
# b44_cointype comes from
6364
# <https://github.com/satoshilabs/slips/blob/master/slip-0044.md>
@@ -341,6 +342,7 @@ class BitcoinTestnet(ChainsBase):
341342
}
342343

343344
bech32_hrp = 'tb'
345+
sp_hrp = 'tsp' # BIP-352 Silent Payment testnet HRP
344346

345347
b58_addr = bytes([111])
346348
b58_script = bytes([196])
@@ -363,6 +365,7 @@ class BitcoinRegtest(ChainsBase):
363365
}
364366

365367
bech32_hrp = 'bcrt'
368+
sp_hrp = 'tsp' # BIP-352 Silent Payment regtest HRP
366369

367370
b58_addr = bytes([111])
368371
b58_script = bytes([196])

0 commit comments

Comments
 (0)