Skip to content

Commit

Permalink
Enable ed25519 support
Browse files Browse the repository at this point in the history
  • Loading branch information
Arjan Zijderveld committed Oct 16, 2020
1 parent 3658e5e commit f393a3d
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 40 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,14 +235,19 @@ The `period` specifies the number of blocks the extrinsic is valid counted from
### Keypair creation and signing

```python

mnemonic = Keypair.generate_mnemonic()
keypair = Keypair.create_from_mnemonic(mnemonic)
signature = keypair.sign("Test123")
if keypair.verify("Test123", signature):
print('Verified')
```

By default a keypair is using SR25519 cryptograhpy, alternatively ED25519 can be explictly specified:

```python
keypair = Keypair.create_from_mnemonic(mnemonic, crypto_type=Keypair.ED25519)
```

### Create keypair using Subkey wrapper (using local subkey binary)

```python
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ urllib3>=1.25.9
xxhash>=1.3.0
pytest>=4.4.0

scalecodec>=0.10.25
scalecodec>=0.10.26
py-sr25519-bindings>=0.1.2
py-ed25519-bindings>=0.1.1
py-bip39-bindings>=0.1.6
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,9 @@
'requests>=2.24.0',
'urllib3>=1.25.10',
'xxhash>=1.3.0',
'scalecodec>=0.10.25',
'scalecodec>=0.10.26',
'py-sr25519-bindings>=0.1.2',
'py-ed25519-bindings>=0.1.1',
'py-bip39-bindings>=0.1.6'
],

Expand Down
80 changes: 58 additions & 22 deletions substrateinterface/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,20 @@
from .utils.ss58 import ss58_decode, ss58_encode
from bip39 import bip39_to_mini_secret, bip39_generate
import sr25519
import ed25519


logger = logging.getLogger(__name__)


class Keypair:

def __init__(self, ss58_address=None, public_key=None, private_key=None, address_type=42):
ED25519 = 0
SR25519 = 1

def __init__(self, ss58_address=None, public_key=None, private_key=None, address_type=42, crypto_type=SR25519):

self.crypto_type = crypto_type

if ss58_address and not public_key:
public_key = ss58_decode(ss58_address)
Expand All @@ -65,7 +71,7 @@ def __init__(self, ss58_address=None, public_key=None, private_key=None, address
if private_key:
private_key = '0x{}'.format(private_key.replace('0x', ''))

if len(private_key) != 130:
if self.crypto_type == self.SR25519 and len(private_key) != 130:
raise ValueError('Secret key should be 64 bytes long')

self.private_key = private_key
Expand All @@ -78,28 +84,46 @@ def generate_mnemonic(cls, words=12):
return bip39_generate(words)

@classmethod
def create_from_mnemonic(cls, mnemonic, address_type=42):
def create_from_mnemonic(cls, mnemonic, address_type=42, crypto_type=SR25519):
seed_array = bip39_to_mini_secret(mnemonic, "")

keypair = cls.create_from_seed(
seed_hex=binascii.hexlify(bytearray(seed_array)).decode("ascii"),
address_type=address_type
address_type=address_type,
crypto_type=crypto_type
)
keypair.mnemonic = mnemonic

return keypair

@classmethod
def create_from_seed(cls, seed_hex, address_type=42):
keypair = sr25519.pair_from_seed(bytes.fromhex(seed_hex.replace('0x', '')))
public_key = keypair[0].hex()
private_key = keypair[1].hex()
ss58_address = ss58_encode(keypair[0], address_type)
return cls(ss58_address=ss58_address, public_key=public_key, private_key=private_key, address_type=address_type)
def create_from_seed(cls, seed_hex, address_type=42, crypto_type=SR25519):

if crypto_type == cls.SR25519:
public_key, private_key = sr25519.pair_from_seed(bytes.fromhex(seed_hex.replace('0x', '')))
elif crypto_type == cls.ED25519:
private_key, public_key = ed25519.ed_from_seed(bytes.fromhex(seed_hex.replace('0x', '')))
else:
raise ValueError('crypto_type "{}" not supported'.format(crypto_type))

public_key = public_key.hex()
private_key = private_key.hex()

ss58_address = ss58_encode(public_key, address_type)

return cls(
ss58_address=ss58_address, public_key=public_key, private_key=private_key,
address_type=address_type, crypto_type=crypto_type
)

@classmethod
def create_from_private_key(cls, private_key, public_key=None, ss58_address=None, address_type=42):
return cls(ss58_address=ss58_address, public_key=public_key, private_key=private_key, address_type=address_type)
def create_from_private_key(
cls, private_key, public_key=None, ss58_address=None, address_type=42, crypto_type=SR25519
):
return cls(
ss58_address=ss58_address, public_key=public_key, private_key=private_key,
address_type=address_type, crypto_type=crypto_type
)

def sign(self, data):
"""
Expand All @@ -122,12 +146,16 @@ def sign(self, data):
data = data.encode()

if not self.private_key:
raise ConfigurationError('No private key set to create sr25519 signatures')
raise ConfigurationError('No private key set to create signatures')

if self.crypto_type == self.SR25519:

signature = sr25519.sign((bytes.fromhex(self.public_key[2:]), bytes.fromhex(self.private_key[2:])), data)
elif self.crypto_type == self.ED25519:
signature = ed25519.ed_sign(bytes.fromhex(self.public_key[2:]), bytes.fromhex(self.private_key[2:]), data)
else:
raise ValueError("Crypto type not supported")

signature = sr25519.sign(
(bytes.fromhex(self.public_key[2:]), bytes.fromhex(self.private_key[2:])),
data
)
return "0x{}".format(signature.hex())

def verify(self, data, signature):
Expand All @@ -145,7 +173,15 @@ def verify(self, data, signature):
if type(signature) is not bytes:
raise TypeError("Signature should be of type bytes or a hex-string")

return sr25519.verify(signature, data, bytes.fromhex(self.public_key[2:]))
if self.crypto_type == self.SR25519:
return sr25519.verify(signature, data, bytes.fromhex(self.public_key[2:]))
elif self.crypto_type == self.ED25519:
return ed25519.ed_verify(signature, data, bytes.fromhex(self.public_key[2:]))
else:
raise ValueError("Crypto type not supported")

def __repr__(self):
return '<Keypair (ss58_address={})>'.format(self.ss58_address)


class SubstrateInterface:
Expand Down Expand Up @@ -1037,14 +1073,14 @@ def create_signed_extrinsic(self, call, keypair: Keypair, era=None, nonce=None,
signature_version = int(signature[0:2], 16)
signature = '0x{}'.format(signature[2:])
else:
signature_version = 1
signature_version = keypair.crypto_type

else:
# Create signature payload
signature_payload = self.generate_signature_payload(call=call, era=era, nonce=nonce, tip=tip)

# Set Signature version to sr25519
signature_version = 1
# Set Signature version to crypto type of keypair
signature_version = keypair.crypto_type

# Sign payload
signature = keypair.sign(signature_payload)
Expand Down Expand Up @@ -1317,7 +1353,7 @@ def get_type_definition(self, type_string, block_hash=None):
"""
type_registry = self.get_type_registry(block_hash=block_hash)
return type_registry.get(type_string)
return type_registry.get(type_string.lower())

def get_metadata_modules(self, block_hash=None):
"""
Expand Down
36 changes: 22 additions & 14 deletions test/test_create_extrinsics.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
# limitations under the License.

import unittest

from scalecodec.type_registry import load_type_registry_preset
from substrateinterface import SubstrateInterface, Keypair, SubstrateRequestException
from test import settings

Expand All @@ -36,12 +38,20 @@ def setUpClass(cls):
)

def test_compatibility_polkadot_runtime(self):
self.polkadot_substrate.get_runtime_block()
self.assertLessEqual(self.polkadot_substrate.runtime_version, 24)
type_reg = load_type_registry_preset("polkadot")

runtime_data = self.polkadot_substrate.rpc_request('state_getRuntimeVersion', [])
self.assertLessEqual(
runtime_data['result']['specVersion'], type_reg.get('runtime_id'), 'Current runtime is incompatible'
)

def test_compatibility_kusama_runtime(self):
self.kusama_substrate.get_runtime_block()
self.assertLessEqual(self.kusama_substrate.runtime_version, 2025)
type_reg = load_type_registry_preset("kusama")

runtime_data = self.polkadot_substrate.rpc_request('state_getRuntimeVersion', [])
self.assertLessEqual(
runtime_data['result']['specVersion'], type_reg.get('runtime_id'), 'Current runtime is incompatible'
)

def test_create_balance_transfer(self):
# Create new keypair
Expand Down Expand Up @@ -106,20 +116,18 @@ def test_create_mortal_extrinsic(self):
# Extrinsic should be successful if account had balance, eitherwise 'Bad proof' error should be raised
self.assertEqual(e.args[0]['data'], 'Inability to pay some fees (e.g. account balance too low)')

def test_generate_signature_payload(self):

call = self.polkadot_substrate.compose_call(
call_module='Balances',
call_function='transfer',
def test_create_unsigned_extrinsic(self):
call = self.kusama_substrate.compose_call(
call_module='Timestamp',
call_function='set',
call_params={
'dest': 'EaG2CRhJWPb7qmdcJvy3LiWdh26Jreu9Dx6R1rXxPmYXoDk',
'value': 2 * 10 ** 3
'now': 1602857508000,
}
)

signature_payload = self.polkadot_substrate.generate_signature_payload(call=call, nonce=2)

self.assertEqual(str(signature_payload), '0x0500586cb27c291c813ce74e86a60dad270609abf2fc8bee107e44a80ac00225c409411f000800180000000500000091b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c391b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3')
extrinsic = self.kusama_substrate.create_unsigned_extrinsic(call)
self.assertEqual(str(extrinsic.data), '0x280402000ba09cc0317501')


if __name__ == '__main__':
Expand Down
100 changes: 100 additions & 0 deletions test/test_helper_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Python Substrate Interface Library
#
# Copyright 2018-2020 Stichting Polkascan (Polkascan Foundation).
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import unittest
from unittest.mock import MagicMock

from scalecodec import ScaleBytes, Bytes
from scalecodec.metadata import MetadataDecoder

from substrateinterface import SubstrateInterface
from test.fixtures import metadata_v12_hex


class TestHelperFunctions(unittest.TestCase):

@classmethod
def setUpClass(cls):

cls.substrate = SubstrateInterface(url='dummy', address_type=42, type_registry_preset='kusama')
metadata_decoder = MetadataDecoder(ScaleBytes(metadata_v12_hex))
metadata_decoder.decode()
cls.substrate.get_block_metadata = MagicMock(return_value=metadata_decoder)

def mocked_request(method, params):
if method == 'chain_getRuntimeVersion':
return {
"jsonrpc": "2.0",
"result": {"specVersion": 2023},
"id": 1
}

cls.substrate.rpc_request = MagicMock(side_effect=mocked_request)

def test_decode_scale(self):
self.assertEqual(self.substrate.decode_scale('Compact<u32>', '0x08'), 2)

def test_encode_scale(self):
self.assertEqual(self.substrate.encode_scale('Compact<u32>', 3), '0x0c')

def test_get_type_definition(self):
self.assertDictEqual(self.substrate.get_type_definition('Bytes'), {
'decoder_class': 'Bytes',
'is_primitive_core': False,
'is_primitive_runtime': True,
'spec_version': 2023,
'type_string': 'Bytes'}
)

def test_get_metadata_modules(self):
for module in self.substrate.get_metadata_modules():
self.assertIn('module_id', module)
self.assertIn('name', module)
self.assertEqual(module['spec_version'], 2023)

def test_get_metadata_call_function(self):
call_function = self.substrate.get_metadata_call_function("Balances", "transfer")
self.assertEqual(call_function['module_name'], "Balances")
self.assertEqual(call_function['call_name'], "transfer")
self.assertEqual(call_function['spec_version'], 2023)

def test_get_metadata_event(self):
event = self.substrate.get_metadata_event("Balances", "Transfer")
self.assertEqual(event['module_name'], "Balances")
self.assertEqual(event['event_name'], "Transfer")
self.assertEqual(event['spec_version'], 2023)

def test_get_metadata_constant(self):
constant = self.substrate.get_metadata_constant("System", "BlockHashCount")
self.assertEqual(constant['module_name'], "System")
self.assertEqual(constant['constant_name'], "BlockHashCount")
self.assertEqual(constant['spec_version'], 2023)

def test_get_metadata_storage_function(self):
storage = self.substrate.get_metadata_storage_function("System", "Account")
self.assertEqual(storage['module_name'], "System")
self.assertEqual(storage['storage_name'], "Account")
self.assertEqual(storage['spec_version'], 2023)

def test_get_metadata_error(self):
error = self.substrate.get_metadata_error("System", "InvalidSpecName")
self.assertEqual(error['module_name'], "System")
self.assertEqual(error['error_name'], "InvalidSpecName")
self.assertEqual(error['spec_version'], 2023)


if __name__ == '__main__':
unittest.main()
Loading

0 comments on commit f393a3d

Please sign in to comment.