Skip to content
This repository was archived by the owner on Aug 12, 2024. It is now read-only.

Commit 30d3ded

Browse files
authored
Feature/rlp for epoch v0.10 (#16)
* Compatibility with epoch 0.10.0 - Replace MsgPack with RLP
1 parent 98a79d8 commit 30d3ded

16 files changed

+196
-53
lines changed

.flake8

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[flake8]
2+
max-line-length = 160

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@ venv
33
**/*.pyc
44
.pytest_cache
55
/epoch/
6+
.vscode
7+
aepp_sdk.egg-info
8+
dist
9+
build

AUTHORS

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Tom Wallroth <[email protected]>
2+
Andrea Giacobino <[email protected]>

LICENSE LICENSE.txt

File renamed without changes.

Makefile

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# build output folder
2+
DIST_FOLDER = dist
3+
BUILD_FOLDER = build
4+
5+
.PHONY: list
6+
list:
7+
@echo clean build lint test publish-test publish
8+
9+
default: build
10+
11+
build: build-dist
12+
13+
build-dist:
14+
@echo build
15+
python setup.py sdist
16+
python setup.py bdist_wheel
17+
@echo done
18+
19+
test: test-all
20+
21+
test-all:
22+
@echo run pytest
23+
pytest
24+
@echo done
25+
26+
lint: lint-all
27+
28+
lint-all:
29+
@echo lint aeternity
30+
flake8 aeternity
31+
@echo done
32+
33+
clean:
34+
@echo remove '$(DIST_FOLDER)','$(BUILD_FOLDER)' folders
35+
@rm -rf $(DIST_FOLDER) $(BUILD_FOLDER)
36+
@echo done
37+
38+
publish:
39+
@echo publish on pypi.org
40+
twine upload dist/*
41+
@echo done
42+
43+
publish-test:
44+
@echo publish on test.pypi.org
45+
twine upload --repository-url https://test.pypi.org/legacy/ dist/*
46+
@echo done

aeternity/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
from .config import Config
55
from .contract import Contract
66

7-
__version__ = '0.1.0'
7+
__version__ = '0.1.2'

aeternity/aens.py

+1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ def update_status(self):
104104
if self.status == NameStatus.UNKNOWN:
105105
self.status = NameStatus.AVAILABLE
106106

107+
107108
def is_available(self):
108109
self.update_status()
109110
return self.status == NameStatus.AVAILABLE

aeternity/config.py

+18-4
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,30 @@ class ConfigException(Exception):
1515
ae_default_websocket_host = 'localhost'
1616
ae_default_websocket_port = 3114
1717

18+
1819
class Config:
1920
default_configs = None
2021

21-
def __init__(self, external_host=None, internal_host=None, websocket_host=None,
22-
docker_semantics=False):
22+
def __init__(self,
23+
external_host=None,
24+
internal_host=None,
25+
websocket_host=None,
26+
docker_semantics=False,
27+
secure_connection=False):
2328
try:
2429
if external_host is None:
2530
host = os.environ.get('AE_EXTERNAL_HOST', ae_default_external_host)
2631
port = os.environ.get('AE_EXTERNAL_PORT', ae_default_external_port)
2732
external_host = f'{host}:{port}'
33+
if "{}".format(port) == '443':
34+
secure_connection = True
2835
self.external_host_port = external_host
2936
if internal_host is None:
3037
host = os.environ.get('AE_INTERNAL_HOST', ae_default_internal_host)
3138
port = os.environ.get('AE_INTERNAL_PORT', ae_default_internal_port)
3239
internal_host = f'{host}:{port}'
40+
if "{}".format(port) == '443':
41+
secure_connection = True
3342
self.internal_host_port = internal_host
3443
if websocket_host is None:
3544
host = os.environ.get('AE_WEBSOCKET_HOST', ae_default_websocket_host)
@@ -48,9 +57,14 @@ def __init__(self, external_host=None, internal_host=None, websocket_host=None,
4857

4958
internal_host_suffix = 'internal/' if docker_semantics else ''
5059

60+
# set the schema for http connection
61+
url_schema = 'http'
62+
if secure_connection:
63+
url_schema = 'https'
64+
5165
self.websocket_url = f'ws://{self.websocket_host_port}/websocket'
52-
self.http_api_url = f'http://{self.external_host_port}/v2'
53-
self.internal_api_url = f'http://{self.internal_host_port}/{internal_host_suffix}v2'
66+
self.http_api_url = f'{url_schema}://{self.external_host_port}/v2'
67+
self.internal_api_url = f'{url_schema}://{self.internal_host_port}/{internal_host_suffix}v2'
5468

5569
self.name_url = f'{self.http_api_url}/name'
5670
self.pubkey = None

aeternity/epoch.py

+14-9
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class EpochRequestError(Exception):
6060
# Transaction types:
6161
#
6262
CoinbaseTx = namedtuple('CoinbaseTx', [
63-
'block_height', 'account', 'data_schema', 'type', 'vsn'
63+
'block_height', 'account', 'data_schema', 'type', 'vsn', 'reward'
6464
])
6565
AENSClaimTx = namedtuple('AENSClaimTx', [
6666
'account', 'fee', 'name', 'name_salt', 'nonce', 'type', 'vsn'
@@ -112,8 +112,10 @@ class EpochRequestError(Exception):
112112

113113

114114
transaction_type_mapping = {
115+
'coinbase_tx': CoinbaseTx,
115116
'aec_coinbase_tx': CoinbaseTx,
116117
'aec_spend_tx': SpendTx,
118+
'spend_tx': SpendTx,
117119
'aens_claim_tx': AENSClaimTx,
118120
'aens_preclaim_tx': AENSPreclaimTx,
119121
'aens_transfer_tx': AENSTransferTx,
@@ -132,7 +134,6 @@ def transaction_from_dict(data):
132134
try:
133135
tx = transaction_type_mapping[tx_type](**data['tx'])
134136
except KeyError:
135-
print(data['tx'])
136137
raise ValueError(f'Cannot deserialize transaction of type {tx_type}')
137138

138139
data = data.copy() # don't mutate the input
@@ -269,7 +270,7 @@ def send(self, message):
269270
self._connection.send(message)
270271

271272
def spend(self, keypair, recipient_pubkey, amount):
272-
transaction = self.create_spend_transaction(recipient_pubkey, amount)
273+
transaction = self.create_spend_transaction(recipient_pubkey, amount, sender=keypair)
273274
signed_transaction, signature = keypair.sign_transaction(transaction)
274275
resp = self.send_signed_transaction(signed_transaction)
275276
return resp, transaction.tx_hash
@@ -332,7 +333,8 @@ def _get_burn_key(cls):
332333
keypair.signing_key
333334

334335
def get_pubkey(self):
335-
return self.internal_http_get('account/pub-key')['pub_key']
336+
pub_key = self.internal_http_get('account/pub-key')
337+
return pub_key['pub_key']
336338

337339
def get_height(self):
338340
return int(self.external_http_get('top')['height'])
@@ -495,15 +497,18 @@ def get_transactions_in_block_range(self, from_height, to_height, tx_types=None,
495497
data = self.internal_http_get('/block/txs/list/height', params=params)
496498
return [transaction_from_dict(tx) for tx in data['transactions']]
497499

498-
def create_spend_transaction(self, recipient, amount, fee=1):
499-
from aeternity import AEName
500-
assert AEName.validate_pointer(recipient)
501-
sender = self.get_pubkey()
500+
def create_spend_transaction(self, recipient, amount, fee=1, sender=None):
501+
if sender is None:
502+
from aeternity import AEName
503+
assert AEName.validate_pointer(recipient)
504+
origin = self.get_pubkey()
505+
else:
506+
origin = sender.get_address()
502507
response = self.external_http_post(
503508
'tx/spend',
504509
json=dict(
505510
amount=amount,
506-
sender=sender,
511+
sender=origin,
507512
fee=fee,
508513
recipient_pubkey=recipient,
509514
)

aeternity/signing.py

+64-25
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,45 @@
11
import os
2-
from collections import namedtuple
32

43
from hashlib import sha256
54

65
import base58
7-
import msgpack
6+
import rlp
87
from Crypto.Cipher import AES
98
from Crypto.Hash import SHA256
10-
from ecdsa import SECP256k1, SigningKey, VerifyingKey
9+
from ecdsa import SECP256k1, SigningKey
1110
import ecdsa
1211

12+
# RLP version number
13+
# https://github.com/aeternity/protocol/blob/epoch-v0.10.1/serializations.md#binary-serialization
14+
VSN = 1
15+
16+
# The list of tags can be found here:
17+
# https://github.com/aeternity/protocol/blob/epoch-v0.10.1/serializations.md#table-of-object-tags
18+
TAG_SIGNED_TX = 11
19+
TAG_SPEND_TX = 12
20+
1321

1422
class SignableTransaction:
23+
1524
def __init__(self, tx_json_data):
16-
self.tx_json_data = tx_json_data
1725
self.tx_hash = tx_json_data['tx_hash']
18-
self.tx_msg_packed = base58.b58decode_check(tx_json_data['tx'][3:])
19-
self.tx_unpacked = msgpack.unpackb(self.tx_msg_packed)
26+
self.tx = tx_json_data['tx']
27+
self.tx_unsigned = base58.b58decode_check(tx_json_data['tx'][3:])
28+
self.tx_decoded = rlp.decode(self.tx_unsigned)
29+
30+
# decode a transaction
31+
def btoi(byts):
32+
return int.from_bytes(byts, byteorder='big')
33+
34+
self.tag = btoi(self.tx_decoded[0])
35+
self.vsn = btoi(self.tx_decoded[1])
36+
self.fields = {}
37+
if self.tag == TAG_SPEND_TX:
38+
self.fields['sender'] = base58.b58encode_check(self.tx_decoded[2])
39+
self.fields['recipient'] = base58.b58encode_check(self.tx_decoded[3])
40+
self.fields['amount'] = btoi(self.tx_decoded[4])
41+
self.fields['fee'] = btoi(self.tx_decoded[5])
42+
self.fields['nonce'] = btoi(self.tx_decoded[6])
2043

2144

2245
class KeyPair:
@@ -25,29 +48,36 @@ def __init__(self, signing_key, verifying_key):
2548
self.verifying_key = verifying_key
2649

2750
def get_address(self):
51+
"""get the keypair public_key base58 encoded and prefixed (ak$...)"""
2852
pub_key = self.verifying_key.to_string()
2953
return 'ak$' + base58.b58encode_check(b'\x04' + pub_key)
3054

31-
def encode_transaction_message(self, unpacked_tx, signatures):
32-
if not isinstance(signatures, list):
33-
signatures = [signatures]
34-
message = [
35-
b"sig_tx", # SIG_TX_TYPE
36-
1, # VSN
37-
unpacked_tx,
38-
signatures
39-
]
40-
str = base58.b58encode_check(msgpack.packb(message, use_bin_type=True))
41-
return "tx$" + str
42-
43-
def sign_transaction_message(self, msgpacked_tx):
44-
return self.signing_key.sign(msgpacked_tx, sigencode=ecdsa.util.sigencode_der)
55+
def get_private_key(self):
56+
"""get the private key hex encoded"""
57+
return self.signing_key.to_string().hex()
58+
59+
def sign_transaction_message(self, message):
60+
"""sign a message with using the private key"""
61+
signature = self.signing_key.sign(message, sigencode=ecdsa.util.sigencode_der)
62+
return signature
63+
64+
def sign_verify(self, message, signature):
65+
"""verifify message signature, raise an error if the message cannot be verified"""
66+
assert self.verifying_key.verify(signature, message, sigdecode=ecdsa.util.sigdecode_der)
67+
68+
def encode_transaction_message(self, unsigned_tx, signatures):
69+
"""prepare a signed transaction message"""
70+
tag = bytes([TAG_SIGNED_TX])
71+
vsn = bytes([VSN])
72+
payload = rlp.encode([tag, vsn, signatures, unsigned_tx])
73+
return f"tx${base58.b58encode_check(payload)}"
4574

4675
def sign_transaction(self, transaction):
47-
signature = self.sign_transaction_message(msgpacked_tx=transaction.tx_msg_packed)
48-
encoded_msg = self.encode_transaction_message(transaction.tx_unpacked, [signature])
76+
signature = self.sign_transaction_message(message=transaction.tx_unsigned)
77+
tx_encoded = self.encode_transaction_message(transaction.tx_unsigned, [signature])
78+
self.sign_verify(transaction.tx_unsigned, signature)
4979
b58_signature = 'sg$' + base58.b58encode_check(signature)
50-
return encoded_msg, b58_signature
80+
return tx_encoded, b58_signature
5181

5282
def save_to_folder(self, folder, password):
5383
enc_key = self._encrypt_key(self.signing_key.to_string(), password)
@@ -70,8 +100,17 @@ def generate(cls):
70100

71101
@classmethod
72102
def from_public_private_key_strings(cls, public, private):
73-
signing_key = SigningKey.from_string(private, curve=SECP256k1, hashfunc=sha256)
74-
return KeyPair(signing_key, signing_key.get_verifying_key())
103+
"""create a keypair from a aet address and private key string
104+
105+
:param public: the aet address, used to verify the private key
106+
:param private: the hex encoded private key
107+
:return: a keypair object or raise error if the public key doesnt match
108+
"""
109+
pk = bytes.fromhex(private)
110+
signing_key = SigningKey.from_string(pk, curve=SECP256k1, hashfunc=sha256)
111+
kp = KeyPair(signing_key, signing_key.get_verifying_key())
112+
assert kp.get_address() == public
113+
return kp
75114

76115
@classmethod
77116
def sha256(cls, data):

aeternity/tests/__init__.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
import logging
2+
from aeternity import Config
23

3-
logging.getLogger("requests").setLevel(logging.WARNING)
4-
logging.getLogger("urllib3").setLevel(logging.WARNING)
4+
logging.getLogger("requests").setLevel(logging.DEBUG)
5+
logging.getLogger("urllib3").setLevel(logging.DEBUG)
6+
7+
KEY_PATH = '/Users/andrea/Documents/workspaces/blockchain/aeternity/epoch/deployment/ansible/files/tester'
8+
KEY_PASSWORD = 'secret'
9+
10+
11+
Config.set_defaults(Config(
12+
external_host='sdk-testnet.aepps.com:443',
13+
internal_host='sdk-testnet.aepps.com:443/internal',
14+
secure_connection=True
15+
))

0 commit comments

Comments
 (0)