Skip to content

Commit

Permalink
Merge branch 'constants-segregation', fix #27, close #25, close #18
Browse files Browse the repository at this point in the history
  • Loading branch information
SimoneBronzini committed Mar 24, 2018
2 parents 1af629f + d29747c commit 8585ab5
Show file tree
Hide file tree
Showing 12 changed files with 957 additions and 250 deletions.
115 changes: 95 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Table of Contents
=================

* [btcpy](#btcpy)
* [Table of Contents](#table-of-contents)
* [Requirements](#requirements)
* [Installation](#installation)
* [What it does](#what-it-does)
Expand All @@ -27,10 +28,11 @@ Table of Contents
* [Usage examples](#usage-examples)
* [Network setup](#network-setup)
* [Parsing and serialization](#parsing-and-serialization)
* [Keys and addresses](#keys-and-addresses)
* [Keys](#keys)
* [HD keys](#hd-keys)
* [Scripts](#scripts)
* [Low-level scripting functionalities](#low-level-scripting-functionalities)
* [Addresses](#addresses)
* [Transactions](#transactions)
* [Creating transactions](#creating-transactions)
* [Spending a transaction](#spending-a-transaction)
Expand All @@ -42,9 +44,11 @@ Table of Contents
* [Multisig](#multisig)
* [Timelocks, Hashlocks, IfElse](#timelocks-hashlocks-ifelse)
* [Low-level signing](#low-level-signing)
* [Contributing](#contributing-and-running-tests)
* [Contributing and running tests](#contributing-and-running-tests)
* [Roadmp to v1](#roadmp-to-v1)
* [TODO](#todo)


# Requirements
The strict requirements of this library are:

Expand Down Expand Up @@ -152,7 +156,7 @@ In the same way, these structures can be serialized and deserialized by using th
`serialize()` and `deserialize()` methods. These methods respectively return and
expect a `bytearray` type.

## Keys and addresses
## Keys
The `PublicKey` class can handle both compressed and uncompressed public
keys. In any case both the compressed and uncompressed version can be extracted.
However, the structure will remember how it was initialised, so the `hexlify()`,
Expand Down Expand Up @@ -209,19 +213,7 @@ using its `compress()` method:
```

Addresses can be either created from a `PublicKey` or from a script.
In particular this second use case will be documented in the **Script** section.

Another low-level way to build an `Address` or `SegWitAddress` is by using their
constructor and providing the following data:

```python
address = Address(addr_type, hashed_data)
sw_address = SegWitAddress(addr_type, hashed_data, version)
```

where `addr_type` can be either `'p2pkh'` or `'p2sh'` in the case of `Address`
and `'p2wpkh'` or `'p2wsh'` in the case of SegWitAddress. `hashed_data` must be a
160 or 256 bits long `bytearray`.
In particular this second use case will be documented in the **Addresses** section.

### HD keys
The `structs.hd` module provides functionalities to handle BIP32 HD keys.
Expand Down Expand Up @@ -256,8 +248,10 @@ the following hierarchy
* `ScriptPubKey`
* `P2pkhscript`
* `P2wpkhScript`
* `P2wpkhV0Script`
* `P2shScript`
* `P2wshScript`
* `P2wshV0Script`
* `P2pkScript`
* `NulldataScript`
* `MultisigScript`
Expand Down Expand Up @@ -355,6 +349,81 @@ will contain `[error]` where the push takes place. For non-existing opcodes the
the special opcode `OP_INVALIDOPCODE`. These two beahviours match Bitcoin Core's behaviour when
producing script asm.

## Addresses

Supported addresses are: `P2pkhAddress`, `P2shAddress`, `P2wpkhAddress` and `P2wshAddress`.
These constructors can be used to build an address from a hash (plus a SegWit version in the
case of `P2wpkhAddress` or `P2wshAddress`), for example:

```python
from btcpy.structs.crypto import PublicKey
from btcpy.structs.address import P2pkhAddress, P2wpkhAddress
pubk = PublicKey.unhexlify('02ea4e183e8c751a4cc72abb7088cea79351dbfb7981ceb48f286ccfdade4d42c8')
address = P2pkhAddress(pubk.hash())
sw_address = P2wpkhAddress(pubk.hash(), version=0)
print(str(address)) # prints "mkGY1QBotzNCrpJaEsje3BpYJsktksi3gJ"
print(str(sw_address)) # prints "tb1qxs0gs9dzukv863jud3wpldtrjh9edeqqqzahcz"
```

Please note that by default all the address constructors will return an address in the
format of the network type specified in setup (testnet in the case of this example) but
a flag can be passed to them to return an address for another network:

```python
address = P2pkhAddress(pubk.hash(), mainnet=True)
sw_address = P2wpkhAddress(pubk.hash(), version=0, mainnet=True)
print(str(address)) # prints "15kaiM6q5xvx5hpxXJmGDGcDStABoGTzSX"
print(str(sw_address)) # prints "bc1qxs0gs9dzukv863jud3wpldtrjh9edeqq2yxyr3"
```

However, a more common usecase is generating an address for a script, for this the `from_script`
static method of all address classes can be used, in particular:

* `P2pkhAddress.from_script(script, mainnet=None)` will instantiate a `P2pkhAddress` from a
`P2pkhScript`, raising `WrongScriptType` exception in case another type of script is provided.
* `P2shAddress.from_script(script, mainnet=None)` will instantiate a `P2shAddress` representing
the script address if a `P2shscript` is provided, while returning the address of the script
embedded in P2SH format if other script types are provided.
* `P2wpkhAddress.from_script(script, version, mainnet=None)` will instantiate a `P2wpkhAddress`
from a `P2wpkhScript`, raising `WrongScriptType` exception in case another type of script
is provided.
* `P2wshAddress.from_script(script, version, mainnet=None)` will instantiate a `P2wshAddress`
representing the script address if a `P2wshscript` is provided, while returning the address
of the script embedded in P2WSH format if other script types are provided.

The only scripts that directly support an address (i.e. `P2pkhScript`, `P2wpkhScript`,
`P2shscript`, `P2wshScript`) also provide a helper method `address()` to return the script
address, for all other script types will return `None` if the `address()` method is called
and will need to be explicitly converted to P2SH or P2WSH format to obtain an address. Some
examples follow:

```python
>>> str(P2pkhAddress.from_script(P2pkhScript(pubk)))
'mkGY1QBotzNCrpJaEsje3BpYJsktksi3gJ'
>>> str(P2pkhScript(pubk).address())
'mkGY1QBotzNCrpJaEsje3BpYJsktksi3gJ'
>>> str(P2pkhAddress.from_script(P2shScript(P2pkhScript(pubk))))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File ".../btcpy/btcpy/structs/address.py", line 120, in from_script
raise WrongScriptType('Trying to produce P2pkhAddress from {} script'.format(script.__class__.__name__))
btcpy.structs.address.WrongScriptType: Trying to produce P2pkhAddress from P2shScript script
>>> str(P2shAddress.from_script(P2shScript(P2pkhScript(pubk))))
'2NAJWD6EnXMVt16HUp5vmfwPjz4FemvPhYt'
>>> str(P2shScript(P2pkhScript(pubk)).address())
'2NAJWD6EnXMVt16HUp5vmfwPjz4FemvPhYt'
>>> str(P2wpkhAddress.from_script(P2wpkhV0Script(pubk)))
'tb1qxs0gs9dzukv863jud3wpldtrjh9edeqqqzahcz'
>>> str(P2wpkhV0Script(pubk).address())
'tb1qxs0gs9dzukv863jud3wpldtrjh9edeqqqzahcz'
>>> str(P2wpkhAddress.from_script(P2shScript(P2wpkhV0Script(pubk))))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File ".../btcpy/btcpy/structs/address.py", line 158, in from_script
raise WrongScriptType('Trying to produce P2pkhAddress from {} script'.format(script.__class__.__name__))
btcpy.structs.address.WrongScriptType: Trying to produce P2pkhAddress from P2shScript script
```

## Transactions

### Creating transactions
Expand Down Expand Up @@ -754,14 +823,20 @@ python3 -m unittest tests/integration.py
Contributors are invited to run these tests before submitting PRs. Also, contributions to improve and
expand these tests are highly welcome.

# Roadmp to v1
This library's stable version 1 will be released once the following changes are made:
* More efficient script matching (i.e. scripts should be able to specify fast matching conditions
instead of trying to parse the raw bytes to decide whether the template is matched)
* Caching on SegWit digest computation to avoid quadratic hashing
* Generation of private keys through secure entropy sources
* An extensive documentation of all modules, classes and their parameters is produced

# TODO
Since this library is still a work in progress, the following roadmap lists the improvements to be done:
Since this library is still a work in progress, the following roadmap lists the improvements to be
done eventually:
* Expanding the test suites
* Improving and expanding this documentation
* Adding docstrings where missing (many places)
* Handling `OP_CODESEPARATOR`s in the signing process
* Adding caching to segwit digest computation to avoid quadratic hashing
* Add further transaction creation helpers
* Add RPC calls to Bitcoin Core nodes
* Add networking with Bitcoin Core nodes
* Add methods to generate private keys from entropy
26 changes: 26 additions & 0 deletions btcpy/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class Constants(object):

_lookup = {'base58.prefixes': {'1': ('p2pkh', 'mainnet'),
'm': ('p2pkh', 'testnet'),
'n': ('p2pkh', 'testnet'),
'3': ('p2sh', 'mainnet'),
'2': ('p2sh', 'testnet')},
'base58.raw_prefixes': {('mainnet', 'p2pkh'): bytearray(b'\x00'),
('testnet', 'p2pkh'): bytearray(b'\x6f'),
('mainnet', 'p2sh'): bytearray(b'\x05'),
('testnet', 'p2sh'): bytearray(b'\xc4')},
'bech32.net_to_hrp': {'mainnet': 'bc',
'testnet': 'tb'},
'bech32.hrp_to_net': {'bc': 'mainnet',
'tb': 'testnet'},
'xkeys.prefixes': {'mainnet': 'x', 'testnet': 't'},
'xpub.version': {'mainnet': b'\x04\x88\xb2\x1e', 'testnet': b'\x04\x35\x87\xcf'},
'xprv.version': {'mainnet': b'\x04\x88\xad\xe4', 'testnet': b'\x04\x35\x83\x94'},
'wif.prefixes': {'mainnet': 0x80, 'testnet': 0xef}}

@staticmethod
def get(key):
try:
return Constants._lookup[key]
except KeyError:
raise ValueError('Unknown constant: {}'.format(key))
48 changes: 23 additions & 25 deletions btcpy/lib/codecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

from .bech32 import decode, encode
from ..setup import is_mainnet, net_name
from ..structs.address import Address, SegWitAddress
from ..constants import Constants
from ..structs.address import Address, P2pkhAddress, P2shAddress, P2wpkhAddress, P2wshAddress


class CouldNotDecode(ValueError):
Expand Down Expand Up @@ -45,32 +46,21 @@ def check_network(cls, network):

class Base58Codec(Codec):

raw_prefixes = {('mainnet', 'p2pkh'): bytearray(b'\x00'),
('testnet', 'p2pkh'): bytearray(b'\x6f'),
('mainnet', 'p2sh'): bytearray(b'\x05'),
('testnet', 'p2sh'): bytearray(b'\xc4')}

prefixes = {'1': ('p2pkh', 'mainnet'),
'm': ('p2pkh', 'testnet'),
'n': ('p2pkh', 'testnet'),
'3': ('p2sh', 'mainnet'),
'2': ('p2sh', 'testnet')}

hash_len = 20

@staticmethod
def encode(address):
try:
prefix = Base58Codec.raw_prefixes[(address.network, address.type)]
prefix = Constants.get('base58.raw_prefixes')[(address.network, address.get_type())]
except KeyError:
raise CouldNotEncode('Impossible to encode address type: {}, network: {}'.format(address.type,
raise CouldNotEncode('Impossible to encode address type: {}, network: {}'.format(address.get_type(),
address.network))
return b58encode_check(bytes(prefix + address.hash))

@staticmethod
def decode(string, check_network=True):
try:
addr_type, network = Base58Codec.prefixes[string[0]]
addr_type, network = Constants.get('base58.prefixes')[string[0]]
except KeyError:
raise CouldNotDecode('Impossible to decode address {}'.format(string))
hashed_data = bytearray(b58decode_check(string))[1:]
Expand All @@ -81,23 +71,24 @@ def decode(string, check_network=True):
if check_network:
Base58Codec.check_network(network)

return Address(addr_type, hashed_data, network == 'mainnet')

if addr_type == 'p2pkh':
cls = P2pkhAddress
elif addr_type == 'p2sh':
cls = P2shAddress
else:
raise ValueError('Unknown address type: {}'.format(addr_type))

class Bech32Codec(Codec):
return cls(hashed_data, network == 'mainnet')

net_to_hrp = {'mainnet': 'bc',
'testnet': 'tb'}

hrp_to_net = {'bc': 'mainnet',
'tb': 'testnet'}
class Bech32Codec(Codec):

lengths = {42: 'p2wpkh',
62: 'p2wsh'}

@staticmethod
def encode(address):
prefix = Bech32Codec.net_to_hrp[address.network]
prefix = Constants.get('bech32.net_to_hrp')[address.network]
return encode(prefix, address.version, address.hash)

@staticmethod
Expand All @@ -112,7 +103,7 @@ def decode(string, check_network=True):

string = string.lower()
try:
network = Bech32Codec.hrp_to_net[string[:2]]
network = Constants.get('bech32.hrp_to_net')[string[:2]]
addr_type = Bech32Codec.lengths[len(string)]
except KeyError:
raise CouldNotDecode('Impossible to decode address {}'.format(string))
Expand All @@ -124,4 +115,11 @@ def decode(string, check_network=True):
if check_network:
Bech32Codec.check_network(network)

return SegWitAddress(addr_type, bytearray(hashed_data), version, network == 'mainnet')
if addr_type == 'p2wpkh':
cls = P2wpkhAddress
elif addr_type == 'p2wsh':
cls = P2wshAddress
else:
raise ValueError('Unknown address type: {}'.format(addr_type))

return cls(bytearray(hashed_data), version, network == 'mainnet')
Loading

0 comments on commit 8585ab5

Please sign in to comment.