Как работать с закрытыми ключами с помощью библиотеки Web3.py

27 июля 2023

Локальные и размещенные узлы

Локальный узел

Локальный узел запускается и контролируется вами. Это так же безопасно, насколько безопасна среда, в которой вы его запускаете. Когда вы запускаете geth на своем компьютере, например, вы запускаете локальный узел.

Размещенный узел

Хостинговый узел контролируется кем-то другим. Когда вы подключаетесь к Infura, вы подключаетесь к размещенному узлу.

 

Локальные и размещенные ключи

Локальный закрытый ключ

Ключ — это 32 битное число, которые вы можете использовать для подписи транзакций и сообщений перед их отправкой на ваш узел. Вы должны использовать send_raw_transaction() при работе с локальными ключами вместо send_transaction().

 

Размещенный закрытый ключ

Это распространенный способ использования учетных записей с локальными узлами. Каждая учетная запись, возвращенная w3.eth.accounts имеет размещенный закрытый ключ, хранящийся в вашем узле. Это позволяет вам использовать send_transaction().

Предупреждение
Недопустимо, чтобы размещенный узел предлагал размещенные закрытые ключи. Это дает другим людям полный контроль над вашей учетной записью. «Не ваши ключи, не ваш Эфир», по мудрым словам Андреаса Антонопулоса.

 

Некоторые распространенные способы использования локальных закрытых ключей

Очень распространенной причиной работы с локальными закрытыми ключами является взаимодействие с размещенным узлом.

Вот некоторые общие вещи, которые вы, возможно, захотите сделать с локальным закрытым ключом :

  • Подписать транзакцию
  • Подписать контрактную сделку
  • Подписать сообщение
  • Проверить сообщение

Использование приватных ключей обычно w3.eth.accountтак или иначе связано. Читайте дальше или ознакомьтесь с полным списком того, что вы можете сделать в документации для eth_account.Account.

 

Создание закрытого ключа

Каждый адрес Ethereum имеет соответствующий закрытый ключ. Чтобы создать новую учетную запись Ethereum, вы можете просто сгенерировать случайное число, которое действует как закрытый ключ.

  • Закрытый ключ — это просто случайное неугадываемое или криптографически безопасное 256-битное целое число.
  • Действительный закрытый ключ > 0 и < максимальное значение закрытого ключа (число выше порядка эллиптической кривой FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141)
  • Закрытые ключи не имеют контрольных сумм.

Чтобы создать закрытый ключ с помощью web3.py и командной строки, вы можете сделать:

shell python -c "from web3 import Web3; w3 = Web3(); acc = w3.eth.account.create(); print(f'private key={w3.toHex(acc.privateKey)}, account={acc.address}')"

Эта команда выводит новую пару закрытого и открытого ключа:

private key=0x480c4aec9fa..., account=0x9202a9d5D2d129CB400a40e00aC822a53ED81167
  • Никогда не храните закрытый ключ вместе с источником. Используйте переменные среды для хранения ключа. Подробнее читайте ниже.
  • Вы также можете импортировать необработанный шестнадцатеричный закрытый ключ в MetaMask и любой другой кошелек — закрытый ключ может использоваться между вашим кодом Python и любым количеством кошельков.

Пополнение нового счета

Если вы создаете закрытый ключ, он поставляется со своим собственным адресом Ethereum. По умолчанию баланс этого адреса равен нулю. Прежде чем вы сможете отправлять какие-либо транзакции со своей учетной записи, вам необходимо пополнить счет.

  • Для локальной тестовой среды любая среда загружается с учетными записями, на которых есть ETH. Переместите ETH из учетных записей по умолчанию во вновь созданную учетную запись.
  • Для общедоступной сети вам необходимо купить ETH на бирже криптовалют.
  • Для тестовой сети вам нужно кран раздачи токенов тестовой сети ( https://faucet.paradigm.xyz/ )

Чтение закрытого ключа из переменной среды

В этом примере мы передаем закрытый ключ нашему приложению Python в переменной среды. Затем этот закрытый ключ добавляется в цепочку ключей для подписи транзакций с промежуточным программным обеспечением (middleware) Signing.

Если вы не знакомы, обратите внимание, что вы можете экспортировать свои приватные ключи из Metamask и других кошельков .

Предупреждение

  • Никогда не делитесь своими закрытыми ключами.
  • Никогда не помещайте свои закрытые ключи в исходный код.
  • Никогда не добавляйте закрытые ключи в репозиторий Git.

 

Пример account_test_script.py

 

import os
from eth_account import Account
from eth_account.signers.local import LocalAccount
from web3 import Web3, EthereumTesterProvider
from web3.middleware import construct_sign_and_send_raw_middleware

w3 = Web3(EthereumTesterProvider())
private_key = os.environ.get("PRIVATE_KEY")
assert private_key is not None, "You must set PRIVATE_KEY environment variable"
assert private_key.startswith("0x"), "Private key must start with 0x hex prefix"

account: LocalAccount = Account.from_key(private_key)
w3.middleware_onion.add(construct_sign_and_send_raw_middleware(account))

print(f"Your hot wallet address is {account.address}")

# Now you can use web3.eth.send_transaction(), Contract.functions.xxx.transact() functions
# with your local private key through middleware and you no longer get the error
# "ValueError: The method eth_sendTransaction does not exist/is not available

 

Пример того, как запустить это в оболочке UNIX:

 

# Generate a new 256-bit random integer using openssl UNIX command that acts as a private key.
# You can also do:
# python -c "from web3 import Web3; w3 = Web3(); acc = w3.eth.account.create(); print(f'private key={w3.to_hex(acc.key)}, account={acc.address}')"
# Store this in a safe place, like in your password manager.
export PRIVATE_KEY=0x`openssl rand -hex 32`

# Run our script
python account_test_script.py

 

Это вывелдет на экрвн:

Your hot wallet address is 0x27C8F899bb69E1501BBB96d09d7477a2a7518918

 

Извлечение закрытого ключа из ключевого файла geth

Примечание

Объем доступной оперативной памяти должен быть больше 1 ГБ.

 

with open('~/.ethereum/keystore/UTC--...--5ce9454909639D2D17A3F753ce7d93fa0b9aB12E') as keyfile:
    encrypted_key = keyfile.read()
    private_key = w3.eth.account.decrypt(encrypted_key, 'correcthorsebatterystaple')
    # tip: do not save the key or password anywhere, especially into a shared source file

 

Подписывание сообщения

Предупреждение

Не существует единого формата сообщения, который был бы широко принят сообществом. Обратите внимание на несколько вариантов, таких как EIP-683 , EIP-712 и EIP-719 . Считайте, что подход w3.eth.sign() устарел.

 

В этом примере мы будем использовать тот же механизм хеширования сообщений, который предоставляется w3.eth.sign().

 

>>> from web3 import Web3, EthereumTesterProvider
>>> from eth_account.messages import encode_defunct

>>> w3 = Web3(EthereumTesterProvider())
>>> msg = "I♥SF"
>>> private_key = b"\xb2\\}\xb3\x1f\xee\xd9\x12''\xbf\t9\xdcv\x9a\x96VK-\xe4\xc4rm\x03[6\xec\xf1\xe5\xb3d"
>>> message = encode_defunct(text=msg)
>>> signed_message = w3.eth.account.sign_message(message, private_key=private_key)
>>> signed_message
SignedMessage(messageHash=HexBytes('0x1476abb745d423bf09273f1afd887d951181d25adc66c4834a70491911b7f750'),
 r=104389933075820307925104709181714897380569894203213074526835978196648170704563,
 s=28205917190874851400050446352651915501321657673772411533993420917949420456142,
 v=28,
 signature=HexBytes('0xe6ca9bba58c88611fad66a6ce8f996908195593807c4b38bd528d2cff09d4eb33e5bfbbf4d3e39b1a2fd816a7680c19ebebaf3a141b239934ad43cb33fcec8ce1c'))

 

Проверить сообщение

Исходные данные: исходное сообщение и подпись:

>>> message = encode_defunct(text="I♥SF")
>>> w3.eth.account.recover_message(message, signature=signed_message.signature)
'0x5ce9454909639D2D17A3F753ce7d93fa0b9aB12E'

 

Подготовить сообщение для встраивания в Solidity

Допустим, вы хотите, чтобы контракт проверял подписанное сообщение, например, если вы создаете платежные каналы, и вы хотите проверить значение в Remix или web3.js.

Возможно, вы создали signed_message локально, как в Sign a Message . Если это так, это подготовит его для Solidity:

 

>>> from web3 import Web3

# ecrecover in Solidity expects v as a native uint8, but r and s as left-padded bytes32
# Remix / web3.js expect r and s to be encoded to hex
# This convenience method will do the pad & hex for us:
>>> def to_32byte_hex(val):
...  return Web3.to_hex(Web3.to_bytes(val).rjust(32, b'\0'))

>>> ec_recover_args = (msghash, v, r, s) = (
...  Web3.to_hex(signed_message.messageHash),

...  signed_message.v,
...  to_32byte_hex(signed_message.r),
...  to_32byte_hex(signed_message.s),
... )
>>> ec_recover_args
('0x1476abb745d423bf09273f1afd887d951181d25adc66c4834a70491911b7f750',
 28,
 '0xe6ca9bba58c88611fad66a6ce8f996908195593807c4b38bd528d2cff09d4eb3',
 '0x3e5bfbbf4d3e39b1a2fd816a7680c19ebebaf3a141b239934ad43cb33fcec8ce')

 

Вместо этого вы могли получить сообщение и подпись в шестнадцатеричном коде. Затем это подготовит его для Solidity:

 

>>> from web3 import Web3
>>> from eth_account.messages import encode_defunct, _hash_eip191_message

>>> hex_message = '0x49e299a55346'
>>> hex_signature = '0xe6ca9bba58c88611fad66a6ce8f996908195593807c4b38bd528d2cff09d4eb33e5bfbbf4d3e39b1a2fd816a7680c19ebebaf3a141b239934ad43cb33fcec8ce1c'

# ecrecover in Solidity expects an encoded version of the message


# - encode the message
>>> message = encode_defunct(hexstr=hex_message)

# - hash the message explicitly
>>> message_hash = _hash_eip191_message(message)

# Remix / web3.js expect the message hash to be encoded to a hex string
>>> hex_message_hash = Web3.to_hex(message_hash)

# ecrecover in Solidity expects the signature to be split into v as a uint8,
#   and r, s as a bytes32
# Remix / web3.js expect r and s to be encoded to hex
>>> sig = Web3.to_bytes(hexstr=hex_signature)
>>> v, hex_r, hex_s = Web3.to_int(sig[-1]), Web3.to_hex(sig[:32]), Web3.to_hex(sig[32:64])

# ecrecover in Solidity takes the arguments in order = (msghash, v, r, s)
>>> ec_recover_args = (hex_message_hash, v, hex_r, hex_s)
>>> ec_recover_args
('0x1476abb745d423bf09273f1afd887d951181d25adc66c4834a70491911b7f750',
 28,
 '0xe6ca9bba58c88611fad66a6ce8f996908195593807c4b38bd528d2cff09d4eb3',
 '0x3e5bfbbf4d3e39b1a2fd816a7680c19ebebaf3a141b239934ad43cb33fcec8ce')

 

Проверка сообщения с помощью ecrecover в Solidity

Создайте простой контракт ecrecover в Remix :

pragma solidity ^0.4.19;

contract Recover {
  function ecr (bytes32 msgh, uint8 v, bytes32 r, bytes32 s) public pure
  returns (address sender) {
    return ecrecover(msgh, v, r, s);
  }
}

 

Затем вызовите ecr с этими аргументами из сообщения «Подготовка для ecrecover» в Solidity в Remix,

"0x1476abb745d423bf09273f1afd887d951181d25adc66c4834a70491911b7f750",
28,
"0xe6ca9bba58c88611fad66a6ce8f996908195593807c4b38bd528d2cff09d4eb3",
"0x3e5bfbbf4d3e39b1a2fd816a7680c19ebebaf3a141b239934ad43cb33fcec8ce"

 

Сообщение проверено, потому что в ответ мы получаем правильного отправителя сообщения: 0x5ce9454909639d2d17a3f753ce7d93fa0b9ab12e.

 

Подписание транзакции

Создайте транзакцию, подпишите ее локально, а затем отправьте на свой узел для трансляции с расширением send_raw_transaction().

 

>>> transaction = {
...    'to': '0xF0109fC8DF283027b6285cc889F5aA624EaC1F55',
...    'value': 1000000000,
...    'gas': 2000000,
...    'maxFeePerGas': 2000000000,
...    'maxPriorityFeePerGas': 1000000000,
...    'nonce': 0,
...    'chainId': 1,
...    'type': '0x2',  # the type is optional and, if omitted, will be interpreted based on the provided transaction parameters
...    'accessList': (  # accessList is optional for dynamic fee transactions
...        {
...            'address': '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae',
...            'storageKeys': (
...                '0x0000000000000000000000000000000000000000000000000000000000000003',
...                '0x0000000000000000000000000000000000000000000000000000000000000007',
...            )
...        },
...        {
...            'address': '0xbb9bc244d798123fde783fcc1c72d3bb8c189413',
...            'storageKeys': ()
...        },
...    )
...}
>>> key = '0x4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318'
>>> signed = w3.eth.account.sign_transaction(transaction, key)
>>> signed.rawTransaction
HexBytes('0x02f8e20180843b9aca008477359400831e848094f0109fc8df283027b6285cc889f5aa624eac1f55843b9aca0080f872f85994de0b295669a9fd93d5f28d9ec85e40f4cb697baef842a00000000000000000000000000000000000000000000000000000000000000003a00000000000000000000000000000000000000000000000000000000000000007d694bb9bc244d798123fde783fcc1c72d3bb8c189413c001a0b9ec671ccee417ff79e06e9e52bfa82b37cf1145affde486006072ca7a11cf8da0484a9beea46ff6a90ac76e7bbf3718db16a8b4b09cef477fb86cf4e123d98fde')
>>> signed.hash
HexBytes('0xe85ce7efa52c16cb5c469c7bde54fbd4911639fdfde08003f65525a85076d915')
>>> signed.r
84095564551732371065849105252408326384410939276686534847013731510862163857293
>>> signed.s
32698347985257114675470251181312399332782188326270244072370350491677872459742
>>> signed.v
1

# When you run send_raw_transaction, you get back the hash of the transaction:
>>> w3.eth.send_raw_transaction(signed.rawTransaction) 
'0xe85ce7efa52c16cb5c469c7bde54fbd4911639fdfde08003f65525a85076d915'

 

Подписние транзакции контракта

Чтобы подписать транзакцию локально, которая вызовет смарт-контракт:

  1. Инициализируйте объект Contract
  2. Создайте транзакцию
  3. Подпишите транзакцию, используя w3.eth.account.sign_transaction()
  4. Отправьте транзакцию send_raw_transaction()

 

# When running locally, execute the statements found in the file linked below to load the EIP20_ABI variable.
# See: https://github.com/carver/ethtoken.py/blob/v0.0.1-alpha.4/ethtoken/abi.py

>>> from web3 import Web3, EthereumTesterProvider
>>> w3 = Web3(EthereumTesterProvider())

>>> unicorns = w3.eth.contract(address="0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359", abi=EIP20_ABI)

>>> nonce = w3.eth.get_transaction_count('0x5ce9454909639D2D17A3F753ce7d93fa0b9aB12E') 

# Build a transaction that invokes this contract's function, called transfer
>>> unicorn_txn = unicorns.functions.transfer(
...    '0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359',
...    1,
... ).build_transaction({
...    'chainId': 1,
...    'gas': 70000,
...    'maxFeePerGas': w3.to_wei('2', 'gwei'),
...    'maxPriorityFeePerGas': w3.to_wei('1', 'gwei'),
...    'nonce': nonce,
... })

>>> unicorn_txn
{'value': 0,
 'chainId': 1,
 'gas': 70000,
 'maxFeePerGas': 2000000000,
 'maxPriorityFeePerGas': 1000000000,
 'nonce': 0,
 'to': '0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359',
 'data': '0xa9059cbb000000000000000000000000fb6916095ca1df60bb79ce92ce3ea74c37c5d3590000000000000000000000000000000000000000000000000000000000000001'}

>>> private_key = b"\xb2\\}\xb3\x1f\xee\xd9\x12''\xbf\t9\xdcv\x9a\x96VK-\xe4\xc4rm\x03[6\xec\xf1\xe5\xb3d"
>>> signed_txn = w3.eth.account.sign_transaction(unicorn_txn, private_key=private_key)
>>> signed_txn.hash
HexBytes('0x748db062639a45e519dba934fce09c367c92043867409160c9989673439dc817')

>>> signed_txn.rawTransaction
HexBytes('0x02f8b00180843b9aca0084773594008301117094fb6916095ca1df60bb79ce92ce3ea74c37c5d35980b844a9059cbb000000000000000000000000fb6916095ca1df60bb79ce92ce3ea74c37c5d3590000000000000000000000000000000000000000000000000000000000000001c001a0cec4150e52898cf1295cc4020ac0316cbf186071e7cdc5ec44eeb7cdda05afa2a06b0b3a09c7fb0112123c0bef1fd6334853a9dcf3cb5bab3ccd1f5baae926d449')

>>> signed_txn.r
93522894155654168208483453926995743737629589441154283159505514235904280342434

>>> signed_txn.s
48417310681110102814014302147799665717176259465062324746227758019974374282313

>>> signed_txn.v
1

>>> w3.eth.send_raw_transaction(signed_txn.rawTransaction) 

# When you run send_raw_transaction, you get the same result as the hash of the transaction:
>>> w3.to_hex(w3.keccak(signed_txn.rawTransaction))
'0x748db062639a45e519dba934fce09c367c92043867409160c9989673439dc817'