Skip to content

Commit

Permalink
Merge pull request #114 from pipermerriam/piper/implement-delegated-s…
Browse files Browse the repository at this point in the history
…igning-manager

Delegated and PrivateKey signing managers
  • Loading branch information
pipermerriam committed Oct 10, 2016
2 parents cd0cf58 + 6b4df1b commit e89f185
Show file tree
Hide file tree
Showing 32 changed files with 1,021 additions and 110 deletions.
61 changes: 54 additions & 7 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,64 @@ before_install:
- geth makedag 0 ~/.ethash
env:
matrix:
- TOX_ENV=py27
- TOX_ENV=py34
- TOX_ENV=py35
# admin
- TOX_ENV=py27-admin
- TOX_ENV=py34-admin
- TOX_ENV=py35-admin
# eth
- TOX_ENV=py27-eth
- TOX_ENV=py34-eth
- TOX_ENV=py35-eth
# mining
- TOX_ENV=py27-mining
- TOX_ENV=py34-mining
- TOX_ENV=py35-mining
# providers
- TOX_ENV=py27-providers
- TOX_ENV=py34-providers
- TOX_ENV=py35-providers
# version
- TOX_ENV=py27-version
- TOX_ENV=py34-version
- TOX_ENV=py35-version
# contracts
- TOX_ENV=py27-contracts
- TOX_ENV=py34-contracts
- TOX_ENV=py35-contracts
# filtering
- TOX_ENV=py27-filtering
- TOX_ENV=py34-filtering
- TOX_ENV=py35-filtering
# net
- TOX_ENV=py27-net
- TOX_ENV=py34-net
- TOX_ENV=py35-net
# txpool
- TOX_ENV=py27-txpool
- TOX_ENV=py34-txpool
- TOX_ENV=py35-txpool
# db
- TOX_ENV=py27-db
- TOX_ENV=py34-db
- TOX_ENV=py35-db
# managers
- TOX_ENV=py27-managers
- TOX_ENV=py34-managers
- TOX_ENV=py35-managers
# personal
- TOX_ENV=py27-personal
- TOX_ENV=py34-personal
- TOX_ENV=py35-personal
# utilities
- TOX_ENV=py27-utilities
- TOX_ENV=py34-utilities
- TOX_ENV=py35-utilities
- TOX_ENV=flake8
cache:
pip: true
directories:
- ~/.ethash
- .tox/py27/.hypothesis
- .tox/py34/.hypothesis
- .tox/py35/.hypothesis
- $HOME/.ethash
- $TRAVIS_BUILD_DIR/.tox/
install:
- "travis_retry pip install setuptools --upgrade"
- "travis_retry pip install tox"
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Contents
overview
filters
contracts
managers
web3.main
web3.eth
web3.db
Expand Down
99 changes: 99 additions & 0 deletions docs/managers.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
Managers
========

.. py:module:: web3.providers.manager
.. py:currentmodule:: web3.providers.manager
Managers control the flow of RPC requests that get passed to and from whatever
provider is in use.


RequestManager
--------------

.. py:class:: RequestManager(provider)
This is the default manager that web3 will use.



Delegated Signing Manager
-------------------------


.. py:class:: DelegatedSigningManager(wrapped_manager, signing_manager)
This manager incercepts any attempt to send a transaction and instead
routes it through the ``eth_sendRawTransaction`` method, using the
``signing_manager`` to sign the transactions, and the ``wrapped_manager``
for the actual sending of the transaction.

Any calls to the following RPC methods will incercepted:

* ``eth_sendTransaction``
* ``personal_sendTransaction``
* ``personal_signAndSendTransaction``

The ``signing_manager`` is only used for the signing of transactions via
the ``eth_sign`` RPC method. Any account which you wish to send from needs
to be unlocked on whatever node this manager is connected to.

The ``wrapped_manager`` is used for all other RPC methods.

This manager is useful for using a public Ethreum node such as Infura for
your RPC interactions while keeping your private keys held on a local node
that does not need to be connected or synced with the network.


.. code-block:: python
# setup RPC provider connected to infura.
>>> web3 = Web3(Web3.RPCProvider(host='mainnet.infura.io', path='your-infura-access-key'))
# create second manager connected to local node (which must be unlocked)
>>> signature_manager = web3.RequestManager(IPCProvider())
# Setup the signing manager.
>>> delegated_manager = Web3.DelegatedSigningManager(web3._requestManager, signature_manager)
>>> web3.setManager(delegated_manager)
>>> web3.eth.sendTransaction({
... 'from': '0x...'
... ...
... })
In this example the transaction will be signed using the locally unlocked IPC
node and then the public Infura RPC node is used relay the pre-signed
transaction to the network using the ``eth_sendRawTransaction`` method.


Private Key Signing Manager
---------------------------

.. py:class:: PrivateKeySigningManager(wrapped_manager, keys={})
This manager is similar to the ``DelegatedSigningManager`` except that
rather than delegating to a node to do the signing, it holds the private
keys and does the signing itself.

The optional ``keys`` constructor should be a mapping between ethereum
address and private key encoded as bytes.

.. py:method:: PrivateKeySigningManager.register_private_key(key)
This method registers a private key with the manager which will allow
sending from the derived address.


.. code-block:: python
>>> web3 = Web3(Web3.RPCProvider(host='mainnet.infura.io', path='your-infura-access-key'))
>>> pk_manager = Web3.PrivateKeySigningManager(web3._requestManager)
>>> pk_manager.register_private_key(b'the-private-key-as-bytes')
>>> web3.setManager(pk_manager)
>>> web3.eth.sendTransaction({
... 'from': '0x...' # the public address for the registered private key.
... ...
... })
In this example, the transaction will be signed using the private key it was
given, after which it will be sent using the ``eth_sendRawTransaction`` through
the connected Infura RPC node.
109 changes: 109 additions & 0 deletions tests/managers/test_delegated_signing_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import pytest

from web3.providers.manager import (
DelegatedSigningManager,
)
from web3.utils.currency import denoms

from flaky import flaky


@pytest.fixture()
def web3_signer(web3_rpc_empty, wait_for_block):
wait_for_block(web3_rpc_empty)
web3_rpc_empty.miner.stop()
return web3_rpc_empty


@pytest.fixture()
def web3_sender(web3_signer, web3_ipc_empty, wait_for_transaction, wait_for_block):
wait_for_block(web3_ipc_empty)

signer_cb = web3_signer.eth.coinbase
sender_cb = web3_ipc_empty.eth.coinbase

assert signer_cb != sender_cb

# fund the account
fund_txn_hash = web3_ipc_empty.eth.sendTransaction({
'from': sender_cb,
'to': signer_cb,
'value': 10 * denoms.ether,
})
wait_for_transaction(web3_ipc_empty, fund_txn_hash)

with pytest.raises(ValueError):
web3_ipc_empty.eth.sendTransaction({
'from': signer_cb,
'to': sender_cb,
'value': 0,
})

web3_ipc_empty._requestManager = DelegatedSigningManager(
wrapped_manager=web3_ipc_empty._requestManager,
signature_manager=web3_signer._requestManager,
)

return web3_ipc_empty


@flaky(max_runs=3)
def test_delegated_signing_manager(web3_sender,
web3_signer,
wait_for_transaction):
sender_cb = web3_sender.eth.coinbase
signer_cb = web3_signer.eth.coinbase

assert sender_cb in web3_sender.eth.accounts
assert signer_cb not in web3_sender.eth.accounts

txn_hash = web3_sender.eth.sendTransaction({
'from': signer_cb,
'to': sender_cb,
'value': 12345,
})
txn_receipt = wait_for_transaction(web3_sender, txn_hash)
txn = web3_sender.eth.getTransaction(txn_hash)

assert txn['from'] == signer_cb
assert txn['to'] == sender_cb
assert txn['value'] == 12345


@flaky(max_runs=3)
def test_delegated_signing_manager_tracks_nonces_correctly(web3_sender,
web3_signer,
wait_for_transaction):
sender_cb = web3_sender.eth.coinbase
signer_cb = web3_signer.eth.coinbase

assert sender_cb in web3_sender.eth.accounts
assert signer_cb not in web3_sender.eth.accounts

num_to_send = web3_sender.eth.getBlock('latest')['gasLimit'] // 21000 + 10

all_txn_hashes = []
for i in range(num_to_send):
all_txn_hashes.append(web3_sender.eth.sendTransaction({
'from': signer_cb,
'to': sender_cb,
'value': i,
}))

all_txn_receipts = [
wait_for_transaction(web3_sender, txn_hash)
for txn_hash in all_txn_hashes
]
all_txns = [
web3_sender.eth.getTransaction(txn_hash)
for txn_hash in all_txn_hashes
]

assert all([
idx == txn['nonce']
for idx, txn in enumerate(all_txns)
])
block_numbers = {
txn_receipt['blockNumber'] for txn_receipt in all_txn_receipts
}
assert len(block_numbers) > 1
63 changes: 63 additions & 0 deletions tests/managers/test_private_key_signing_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import pytest

from web3.providers.manager import (
PrivateKeySigningManager,
)
from web3.utils.address import to_address
from web3.utils.currency import denoms


@pytest.fixture()
def account_private_key():
from eth_tester_client.utils import mk_random_privkey
return mk_random_privkey()


@pytest.fixture()
def account_public_key(account_private_key):
from ethereum.utils import privtoaddr
from eth_tester_client.utils import encode_address
return to_address(encode_address(privtoaddr(account_private_key)))


@pytest.fixture()
def web3_pk_signer(web3_ipc_persistent,
account_public_key,
account_private_key,
wait_for_block,
wait_for_transaction):
pk_signing_manager = PrivateKeySigningManager(web3_ipc_persistent._requestManager)
pk_signing_manager.register_private_key(account_private_key)
assert account_public_key in pk_signing_manager.keys

wait_for_block(web3_ipc_persistent)

fund_txn_hash = web3_ipc_persistent.eth.sendTransaction({
'from': web3_ipc_persistent.eth.coinbase,
'to': account_public_key,
'value': 10 * denoms.ether,
})
wait_for_transaction(web3_ipc_persistent, fund_txn_hash)

with pytest.raises(ValueError):
web3_ipc_persistent.eth.sendTransaction({
'from': account_public_key,
'to': web3_ipc_persistent.eth.coinbase,
'value': 1,
})

web3_ipc_persistent._requestManager = pk_signing_manager
return web3_ipc_persistent


def test_private_key_signing_manager(web3_pk_signer, account_public_key, wait_for_transaction):
assert account_public_key not in web3_pk_signer.eth.accounts
txn_hash = web3_pk_signer.eth.sendTransaction({
'from': account_public_key,
'to': web3_pk_signer.eth.coinbase,
'value': 12345,
})
txn_receipt = wait_for_transaction(web3_pk_signer, txn_hash)
txn = web3_pk_signer.eth.getTransaction(txn_hash)

assert txn['from'] == account_public_key
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 4 additions & 0 deletions tests/utilities/test_address.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ def test_is_strict_address(value, expected):
@pytest.mark.parametrize(
"value,expected",
(
(
b'\xd3\xcd\xa9\x13\xde\xb6\xf6yg\xb9\x9dg\xac\xdf\xa1q,)6\x01',
'0xd3cda913deb6f67967b99d67acdfa1712c293601',
),
(
b'0xc6d9d2cd449a754c494264e1809c50e34d64562b',
'0xc6d9d2cd449a754c494264e1809c50e34d64562b',
Expand Down

0 comments on commit e89f185

Please sign in to comment.