Loading...
Loading...
ecrecover
и странные массивы байтов, которые называли "подписями".// Подпись как один массив байтов длиной 65 байтbytes memory signature = "0x7f8e9d2a3b4c1f6e8a9c5d7b2e4a6c8f1d3b5e7a9c2d4f6e8a1c3b5d7e9f2a4c6e8a1c3b5d7e9f2a4c6e8a1c";
[32 байта r] + [32 байта s] + [1 байт v] = 65 байт всего
Позиции:0-31: r компонент32-63: s компонент64: v компонент
// Те же данные, но разделённые на компонентыuint8 v = 27; // 1 байтbytes32 r = 0x7f8e9d2a3b4c1f6e8a9c5d7b2e4a6c8f1d3b5e7a9c2d4f6e8a1c3b5d7e9f2a4c6e8; // 32 байтаbytes32 s = 0x1c3b5d7e9f2a4c6e8a1c3b5d7e9f2a4c6e8a1c3b5d7e9f2a4c6e8a1c3b5d7e9f2; // 32 байта
contract SignatureConverter { // Из полной подписи в компоненты function splitSignature(bytes memory signature) public pure returns (uint8 v, bytes32 r, bytes32 s) { require(signature.length == 65, "Неверная длина подписи");
assembly { // Первые 32 байта (после длины) r := mload(add(signature, 32)) // Следующие 32 байта s := mload(add(signature, 64)) // Последний байт v := byte(0, mload(add(signature, 96))) } }
// Из компонентов в полную подпись function combineSignature(uint8 v, bytes32 r, bytes32 s) public pure returns (bytes memory signature) { signature = new bytes(65); assembly { // Записываем r в первые 32 байта mstore(add(signature, 32), r) // Записываем s в следующие 32 байта mstore(add(signature, 64), s) // Записываем v в последний байт mstore8(add(signature, 96), v) } }}
// JavaScript - кошелёк возвращает полную подписьconst signature = await signer.signMessage(message);// "0x7f8e9d...c6e8a1c"
// Solidity - ecrecover работает только с компонентамиaddress signer = ecrecover(messageHash, v, r, s);
ecrecover
и спросил:
— «Эта функция выглядит как заклинание! Что она вообще делает?»ecrecover
делает это математически — она восстанавливает адрес подписавшего из самой подписи!// ecrecover принимает 4 параметра:address signer = ecrecover( messageHash, // 1. Хеш сообщения (что подписывали) v, // 2-4. Компоненты подписи (как подписывали) r, s);
📝 Сообщение: "Перевести 100 токенов Бобу" ↓🔒 Хеш сообщения: 0xabc123... ↓✍️ Подпись (r, s, v): 0x7f8e9d... + 0x2a3b4c... + 27 ↓🔍 ecrecover(hash, v, r, s) ↓👤 Адрес подписавшего: 0x742d35Cc6643C0532925a3b8F39dF7Ac...
Результат: "Это сообщение подписал именно этот адрес!"
contract UnderstandEcrecover { // Простая функция для понимания ecrecover function whoSignedThis( string memory message, bytes memory signature ) public pure returns (address signer) { // Шаг 1: Создаем хеш сообщения bytes32 messageHash = keccak256(abi.encodePacked(message));
// Шаг 2: Добавляем Ethereum префикс (как делают кошельки) bytes32 ethHash = keccak256(abi.encodePacked( "\x19Ethereum Signed Message:\n32", messageHash ));
// Шаг 3: Разбираем подпись на компоненты (bytes32 r, bytes32 s, uint8 v) = splitSignature(signature);
// Шаг 4: МАГИЯ! ecrecover говорит нам КТО это подписал signer = ecrecover(ethHash, v, r, s);
// Теперь мы знаем адрес подписавшего! }
function splitSignature(bytes memory sig) public pure returns (bytes32 r, bytes32 s, uint8 v) { require(sig.length == 65, "Неверная длина подписи"); assembly { r := mload(add(sig, 32)) s := mload(add(sig, 64)) v := byte(0, mload(add(sig, 96))) } }}
address recovered = ecrecover(hash, v, r, s);require(recovered != address(0), "Невалидная подпись");require(recovered == expectedSigner, "Неверный подписант");
"\x19Ethereum Signed Message:\n32"
и спросил:
— «Что это за магическая строка? Зачем она нужна?»function getEthSignedMessageHash(bytes32 messageHash) public pure returns (bytes32) { return keccak256(abi.encodePacked( "\x19Ethereum Signed Message:\n32", messageHash ));}
"\x19Ethereum Signed Message:\n32" + messageHash
\x19
— специальный байт, который никогда не встречается в начале валидных RLP-кодированных транзакций"Ethereum Signed Message:"
— текстовая метка для ясности\n32
— указывает, что далее идет 32-байтный хеш сообщенияmessageHash
— ваш хеш сообщения// ❌ ОПАСНО: подписываем напрямую хешfunction dangerousPrepareDataForSigning(bytes32 messageHash) public pure returns (bytes32) { return messageHash; // Может быть использован как транзакция!}
// ✅ БЕЗОПАСНО: добавляем префиксfunction safePrepareDataForSigning(bytes32 messageHash) public pure returns (bytes32) { return keccak256(abi.encodePacked( "\x19Ethereum Signed Message:\n32", messageHash ));}
eth_sign
или personal_sign
. Поэтому в смарт-контракте мы тоже должны его добавлять для проверки подписи.contract AlexSignedTransfers { mapping(address => uint256) public balances; mapping(address => uint256) public nonces; // Защита от повторного использования
event Transfer(address indexed from, address indexed to, uint256 amount); event SignedTransfer(address indexed from, address indexed to, uint256 amount, uint256 nonce);
constructor() { balances[msg.sender] = 1000000; // Начальный баланс }
// Обычный перевод function transfer(address to, uint256 amount) public { require(balances[msg.sender] >= amount, "Недостаточно средств");
balances[msg.sender] -= amount; balances[to] += amount;
emit Transfer(msg.sender, to, amount); }
// Перевод по подписи (мета-транзакция) function transferWithSignature( address from, address to, uint256 amount, uint256 nonce, bytes memory signature ) public { require(nonce == nonces[from], "Неверный nonce"); require(balances[from] >= amount, "Недостаточно средств");
// Создаем хеш сообщения bytes32 messageHash = keccak256(abi.encodePacked( from, to, amount, nonce, address(this) ));
// Создаем Ethereum Signed Message Hash bytes32 ethSignedMessageHash = keccak256(abi.encodePacked( "\x19Ethereum Signed Message:\n32", messageHash ));
// Проверяем подпись address signer = ecrecover(ethSignedMessageHash, getV(signature), getR(signature), getS(signature)); require(signer == from, "Неверная подпись");
// Выполняем перевод balances[from] -= amount; balances[to] += amount; nonces[from]++; // Увеличиваем nonce
emit SignedTransfer(from, to, amount, nonce); }
// Получение компонентов подписи function getR(bytes memory signature) public pure returns (bytes32) { bytes32 r; assembly { r := mload(add(signature, 32)) } return r; }
function getS(bytes memory signature) public pure returns (bytes32) { bytes32 s; assembly { s := mload(add(signature, 64)) } return s; }
function getV(bytes memory signature) public pure returns (uint8) { uint8 v; assembly { v := byte(0, mload(add(signature, 96))) } return v; }
// Предварительный просмотр хеша для подписи function getTransferHash( address from, address to, uint256 amount, uint256 nonce ) public view returns (bytes32) { return keccak256(abi.encodePacked(from, to, amount, nonce, address(this))); }}
contract SignatureAuth { mapping(address => bool) public authorizedUsers; mapping(bytes32 => bool) public usedNonces;
event UserAuthorized(address indexed user, uint256 timestamp);
function authorize( uint256 timestamp, bytes32 nonce, bytes memory signature ) public { require(!usedNonces[nonce], "Nonce уже использован"); require(block.timestamp <= timestamp + 300, "Подпись устарела"); // 5 минут
bytes32 messageHash = keccak256(abi.encodePacked( "Authorize access", timestamp, nonce, address(this) ));
bytes32 ethSignedMessageHash = keccak256(abi.encodePacked( "\x19Ethereum Signed Message:\n32", messageHash ));
address signer = recoverSigner(ethSignedMessageHash, signature); require(signer != address(0), "Невалидная подпись");
usedNonces[nonce] = true; authorizedUsers[signer] = true;
emit UserAuthorized(signer, timestamp); }
function recoverSigner(bytes32 hash, bytes memory signature) internal pure returns (address) { (bytes32 r, bytes32 s, uint8 v) = splitSignature(signature); return ecrecover(hash, v, r, s); }
function splitSignature(bytes memory sig) internal pure returns (bytes32 r, bytes32 s, uint8 v) { require(sig.length == 65, "Неверная длина подписи"); assembly { r := mload(add(sig, 32)) s := mload(add(sig, 64)) v := byte(0, mload(add(sig, 96))) } }}
contract MultiSigWallet { address[] public owners; uint256 public required;
struct Transaction { address to; uint256 value; bytes data; bool executed; uint256 confirmations; }
mapping(uint256 => Transaction) public transactions; mapping(uint256 => mapping(address => bool)) public confirmations; uint256 public transactionCount;
event TransactionSubmitted(uint256 indexed txId); event TransactionConfirmed(uint256 indexed txId, address indexed owner); event TransactionExecuted(uint256 indexed txId);
constructor(address[] memory _owners, uint256 _required) { require(_owners.length >= _required, "Недостаточно владельцев"); owners = _owners; required = _required; }
function submitTransaction( address to, uint256 value, bytes memory data ) public returns (uint256) { require(isOwner(msg.sender), "Только владелец");
uint256 txId = transactionCount++; transactions[txId] = Transaction({ to: to, value: value, data: data, executed: false, confirmations: 0 });
emit TransactionSubmitted(txId); return txId; }
function confirmTransaction(uint256 txId) public { require(isOwner(msg.sender), "Только владелец"); require(!confirmations[txId][msg.sender], "Уже подтверждено");
confirmations[txId][msg.sender] = true; transactions[txId].confirmations++;
emit TransactionConfirmed(txId, msg.sender);
if (transactions[txId].confirmations >= required) { executeTransaction(txId); } }
function executeTransaction(uint256 txId) internal { Transaction storage txn = transactions[txId]; require(!txn.executed, "Уже выполнено");
txn.executed = true; (bool success, ) = txn.to.call{value: txn.value}(txn.data); require(success, "Выполнение не удалось");
emit TransactionExecuted(txId); }
function isOwner(address account) public view returns (bool) { for (uint i = 0; i < owners.length; i++) { if (owners[i] == account) { return true; } } return false; }}
Термин | Определение | Пример использования |
---|---|---|
Цифровая подпись | Криптографический механизм для доказательства авторства и целостности | Подписание транзакций приватным ключом |
Подпись (bytes) | Полный формат подписи как массив из 65 байт | 0x7f8e9d... (используют кошельки) |
Подпись (v,r,s) | Разделённый формат: v (1 байт) + r (32 байта) + s (32 байта) | ecrecover(hash, v, r, s) (смарт-контракты) |
ecrecover | Функция восстановления адреса подписавшего по подписи | ecrecover(hash, v, r, s) → адрес |
Nonce | Уникальное число для предотвращения повторных атак | Защита от replay attacks в подписях |
Ethereum Signed Message | Стандарт EIP-191 с префиксом \x19Ethereum Signed Message:\n32 | Безопасное подписание сообщений |
Мета-транзакция | Транзакция, выполняемая одним адресом от имени другого по подписи | Безгазовые переводы через подписи |
Аутентификация | Проверка подлинности отправителя сообщения | Подтверждение владельца через подпись |
Non-repudiation | Невозможность отказаться от авторства подписанного сообщения | "Не могу сказать, что не я подписывал" |
Replay Attack | Атака повторного использования старой подписи | Защита через nonce и временные метки |