diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9b90ee91..382d9a2f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -53,6 +53,9 @@ jobs: -p serai-processor-bin \ -p serai-bitcoin-processor \ -p serai-processor-ethereum-contracts \ + -p serai-processor-ethereum-primitives \ + -p serai-processor-ethereum-deployer \ + -p ethereum-serai \ -p serai-ethereum-processor \ -p serai-monero-processor \ -p tendermint-machine \ diff --git a/Cargo.lock b/Cargo.lock index d6224093..0253cf32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8721,6 +8721,32 @@ dependencies = [ "syn-solidity", ] +[[package]] +name = "serai-processor-ethereum-deployer" +version = "0.1.0" +dependencies = [ + "alloy-consensus", + "alloy-core", + "alloy-provider", + "alloy-rpc-types-eth", + "alloy-simple-request-transport", + "alloy-sol-macro", + "alloy-sol-types", + "alloy-transport", + "build-solidity-contracts", + "serai-processor-ethereum-primitives", +] + +[[package]] +name = "serai-processor-ethereum-primitives" +version = "0.1.0" +dependencies = [ + "alloy-consensus", + "alloy-core", + "group", + "k256", +] + [[package]] name = "serai-processor-frost-attempt-manager" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index b30112b2..c0010659 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,8 @@ members = [ "processor/bin", "processor/bitcoin", "processor/ethereum/contracts", + "processor/ethereum/primitives", + "processor/ethereum/deployer", "processor/ethereum/ethereum-serai", "processor/ethereum", "processor/monero", diff --git a/deny.toml b/deny.toml index ec948fef..8b630fb9 100644 --- a/deny.toml +++ b/deny.toml @@ -59,8 +59,10 @@ exceptions = [ { allow = ["AGPL-3.0"], name = "serai-processor-signers" }, { allow = ["AGPL-3.0"], name = "serai-bitcoin-processor" }, - { allow = ["AGPL-3.0"], name = "ethereum-serai" }, { allow = ["AGPL-3.0"], name = "serai-processor-ethereum-contracts" }, + { allow = ["AGPL-3.0"], name = "serai-processor-ethereum-primitives" }, + { allow = ["AGPL-3.0"], name = "serai-processor-ethereum-deployer" }, + { allow = ["AGPL-3.0"], name = "ethereum-serai" }, { allow = ["AGPL-3.0"], name = "serai-ethereum-processor" }, { allow = ["AGPL-3.0"], name = "serai-monero-processor" }, diff --git a/processor/ethereum/contracts/contracts/Deployer.sol b/processor/ethereum/contracts/contracts/Deployer.sol deleted file mode 100644 index 1c05e38a..00000000 --- a/processor/ethereum/contracts/contracts/Deployer.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity ^0.8.26; - -/* -The expected deployment process of the Router is as follows: - -1) A transaction deploying Deployer is made. Then, a deterministic signature is - created such that an account with an unknown private key is the creator of - the contract. Anyone can fund this address, and once anyone does, the - transaction deploying Deployer can be published by anyone. No other - transaction may be made from that account. - -2) Anyone deploys the Router through the Deployer. This uses a sequential nonce - such that meet-in-the-middle attacks, with complexity 2**80, aren't feasible. - While such attacks would still be feasible if the Deployer's address was - controllable, the usage of a deterministic signature with a NUMS method - prevents that. - -This doesn't have any denial-of-service risks and will resolve once anyone steps -forward as deployer. This does fail to guarantee an identical address across -every chain, though it enables letting anyone efficiently ask the Deployer for -the address (with the Deployer having an identical address on every chain). - -Unfortunately, guaranteeing identical addresses aren't feasible. We'd need the -Deployer contract to use a consistent salt for the Router, yet the Router must -be deployed with a specific public key for Serai. Since Ethereum isn't able to -determine a valid public key (one the result of a Serai DKG) from a dishonest -public key, we have to allow multiple deployments with Serai being the one to -determine which to use. - -The alternative would be to have a council publish the Serai key on-Ethereum, -with Serai verifying the published result. This would introduce a DoS risk in -the council not publishing the correct key/not publishing any key. -*/ - -contract Deployer { - event Deployment(bytes32 indexed init_code_hash, address created); - - error DeploymentFailed(); - - function deploy(bytes memory init_code) external { - address created; - assembly { - created := create(0, add(init_code, 0x20), mload(init_code)) - } - if (created == address(0)) { - revert DeploymentFailed(); - } - // These may be emitted out of order upon re-entrancy - emit Deployment(keccak256(init_code), created); - } -} diff --git a/processor/ethereum/contracts/src/lib.rs b/processor/ethereum/contracts/src/lib.rs index 45176067..d0a5c076 100644 --- a/processor/ethereum/contracts/src/lib.rs +++ b/processor/ethereum/contracts/src/lib.rs @@ -9,11 +9,6 @@ mod abigen; pub mod erc20 { pub use super::abigen::erc20::IERC20::*; } -pub mod deployer { - pub const BYTECODE: &str = - include_str!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-contracts/Deployer.bin")); - pub use super::abigen::deployer::Deployer::*; -} pub mod router { pub const BYTECODE: &str = include_str!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-contracts/Router.bin")); diff --git a/processor/ethereum/deployer/Cargo.toml b/processor/ethereum/deployer/Cargo.toml new file mode 100644 index 00000000..9b0ed146 --- /dev/null +++ b/processor/ethereum/deployer/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "serai-processor-ethereum-deployer" +version = "0.1.0" +description = "The deployer for Serai's Ethereum contracts" +license = "AGPL-3.0-only" +repository = "https://github.com/serai-dex/serai/tree/develop/processor/ethereum/deployer" +authors = ["Luke Parker "] +edition = "2021" +publish = false +rust-version = "1.79" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true + +[dependencies] +alloy-core = { version = "0.8", default-features = false } +alloy-consensus = { version = "0.3", default-features = false } + +alloy-sol-types = { version = "0.8", default-features = false } +alloy-sol-macro = { version = "0.8", default-features = false } + +alloy-rpc-types-eth = { version = "0.3", default-features = false } +alloy-transport = { version = "0.3", default-features = false } +alloy-simple-request-transport = { path = "../../../networks/ethereum/alloy-simple-request-transport", default-features = false } +alloy-provider = { version = "0.3", default-features = false } + +ethereum-primitives = { package = "serai-processor-ethereum-primitives", path = "../primitives", default-features = false } + +[build-dependencies] +build-solidity-contracts = { path = "../../../networks/ethereum/build-contracts", default-features = false } diff --git a/processor/ethereum/deployer/LICENSE b/processor/ethereum/deployer/LICENSE new file mode 100644 index 00000000..41d5a261 --- /dev/null +++ b/processor/ethereum/deployer/LICENSE @@ -0,0 +1,15 @@ +AGPL-3.0-only license + +Copyright (c) 2022-2024 Luke Parker + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License Version 3 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/processor/ethereum/deployer/README.md b/processor/ethereum/deployer/README.md new file mode 100644 index 00000000..6b439650 --- /dev/null +++ b/processor/ethereum/deployer/README.md @@ -0,0 +1,23 @@ +# Ethereum Smart Contracts Deployer + +The deployer for Serai's Ethereum contracts. + +## Goals + +It should be possible to efficiently locate the Serai Router on an blockchain with the EVM, without +relying on any centralized (or even federated) entities. While deploying and locating an instance of +the Router would be trivial, by using a fixed signature for the deployment transaction, the Router +must be constructed with the correct key for the Serai network (or set to have the correct key +post-construction). Since this cannot be guaranteed to occur, the process must be retryable and the +first successful invocation must be efficiently findable. + +## Methodology + +We define a contract, the Deployer, to deploy the router. This contract could use `CREATE2` with the +key representing Serai as the salt, yet this would be open to collision attacks with just 2**80 +complexity. Instead, we use `CREATE` which would require 2**80 on-chain transactions (infeasible) to +use as the basis of a collision. + +In order to efficiently find the contract for a key, the Deployer contract saves the addresses of +deployed contracts (indexed by the initialization code hash). This allows using a single call to a +contract with a known address to find the proper Router. diff --git a/processor/ethereum/deployer/build.rs b/processor/ethereum/deployer/build.rs new file mode 100644 index 00000000..1906f1df --- /dev/null +++ b/processor/ethereum/deployer/build.rs @@ -0,0 +1,5 @@ +fn main() { + let artifacts_path = + std::env::var("OUT_DIR").unwrap().to_string() + "/serai-processor-ethereum-deployer"; + build_solidity_contracts::build(&[], "contracts", &artifacts_path).unwrap(); +} diff --git a/processor/ethereum/deployer/contracts/Deployer.sol b/processor/ethereum/deployer/contracts/Deployer.sol new file mode 100644 index 00000000..24ea1cb4 --- /dev/null +++ b/processor/ethereum/deployer/contracts/Deployer.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.26; + +/* + The expected deployment process of the Router is as follows: + + 1) A transaction deploying Deployer is made. Then, a deterministic signature is + created such that an account with an unknown private key is the creator of + the contract. Anyone can fund this address, and once anyone does, the + transaction deploying Deployer can be published by anyone. No other + transaction may be made from that account. + + 2) Anyone deploys the Router through the Deployer. This uses a sequential nonce + such that meet-in-the-middle attacks, with complexity 2**80, aren't feasible. + While such attacks would still be feasible if the Deployer's address was + controllable, the usage of a deterministic signature with a NUMS method + prevents that. + + This doesn't have any denial-of-service risks and will resolve once anyone steps + forward as deployer. This does fail to guarantee an identical address across + every chain, though it enables letting anyone efficiently ask the Deployer for + the address (with the Deployer having an identical address on every chain). + + Unfortunately, guaranteeing identical addresses aren't feasible. We'd need the + Deployer contract to use a consistent salt for the Router, yet the Router must + be deployed with a specific public key for Serai. Since Ethereum isn't able to + determine a valid public key (one the result of a Serai DKG) from a dishonest + public key, we have to allow multiple deployments with Serai being the one to + determine which to use. + + The alternative would be to have a council publish the Serai key on-Ethereum, + with Serai verifying the published result. This would introduce a DoS risk in + the council not publishing the correct key/not publishing any key. +*/ + +contract Deployer { + struct Deployment { + uint64 block_number; + address created_contract; + } + mapping(bytes32 => Deployment) public deployments; + + error Reentrancy(); + error PriorDeployed(); + error DeploymentFailed(); + + function deploy(bytes memory init_code) external { + // Prevent re-entrancy + // If we did allow it, one could deploy the same contract multiple times (with one overwriting + // the other's set value in storage) + bool called; + // This contract doesn't have any other use of transient storage, nor is to be inherited, making + // this usage of the zero address safe + assembly { called := tload(0) } + if (called) { + revert Reentrancy(); + } + assembly { tstore(0, 1) } + + // Check this wasn't prior deployed + bytes32 init_code_hash = keccak256(init_code); + Deployment memory deployment = deployments[init_code_hash]; + if (deployment.created_contract == address(0)) { + revert PriorDeployed(); + } + + // Deploy the contract + address created_contract; + assembly { + created_contract := create(0, add(init_code, 0x20), mload(init_code)) + } + if (created_contract == address(0)) { + revert DeploymentFailed(); + } + + // Set the dpeloyment to storage + deployment.block_number = uint64(block.number); + deployment.created_contract = created_contract; + deployments[init_code_hash] = deployment; + } +} diff --git a/processor/ethereum/deployer/src/lib.rs b/processor/ethereum/deployer/src/lib.rs new file mode 100644 index 00000000..bf2d1a9c --- /dev/null +++ b/processor/ethereum/deployer/src/lib.rs @@ -0,0 +1,104 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] + +use std::sync::Arc; + +use alloy_core::primitives::{hex::FromHex, Address, U256, Bytes, TxKind}; +use alloy_consensus::{Signed, TxLegacy}; + +use alloy_sol_types::SolCall; + +use alloy_rpc_types_eth::{TransactionInput, TransactionRequest}; +use alloy_transport::{TransportErrorKind, RpcError}; +use alloy_simple_request_transport::SimpleRequest; +use alloy_provider::{Provider, RootProvider}; + +#[rustfmt::skip] +#[expect(warnings)] +#[expect(needless_pass_by_value)] +#[expect(clippy::all)] +#[expect(clippy::ignored_unit_patterns)] +#[expect(clippy::redundant_closure_for_method_calls)] +mod abi { + alloy_sol_macro::sol!("contracts/Deployer.sol"); +} + +/// The Deployer contract for the Serai Router contract. +/// +/// This Deployer has a deterministic address, letting it be immediately identified on any +/// compatible chain. It then supports retrieving the Router contract's address (which isn't +/// deterministic) using a single call. +#[derive(Clone, Debug)] +pub struct Deployer; +impl Deployer { + /// Obtain the transaction to deploy this contract, already signed. + /// + /// The account this transaction is sent from (which is populated in `from`) must be sufficiently + /// funded for this transaction to be submitted. This account has no known private key to anyone + /// so ETH sent can be neither misappropriated nor returned. + pub fn deployment_tx() -> Signed { + pub const BYTECODE: &str = + include_str!(concat!(env!("OUT_DIR"), "/serai-processor-ethereum-deployer/Deployer.bin")); + let bytecode = + Bytes::from_hex(BYTECODE).expect("compiled-in Deployer bytecode wasn't valid hex"); + + let tx = TxLegacy { + chain_id: None, + nonce: 0, + // 100 gwei + gas_price: 100_000_000_000u128, + // TODO: Use a more accurate gas limit + gas_limit: 1_000_000u128, + to: TxKind::Create, + value: U256::ZERO, + input: bytecode, + }; + + ethereum_primitives::deterministically_sign(&tx) + } + + /// Obtain the deterministic address for this contract. + pub(crate) fn address() -> Address { + let deployer_deployer = + Self::deployment_tx().recover_signer().expect("deployment_tx didn't have a valid signature"); + Address::create(&deployer_deployer, 0) + } + + /// Construct a new view of the Deployer. + pub async fn new( + provider: Arc>, + ) -> Result, RpcError> { + let address = Self::address(); + let code = provider.get_code_at(address).await?; + // Contract has yet to be deployed + if code.is_empty() { + return Ok(None); + } + Ok(Some(Self)) + } + + /// Find the deployment of a contract. + pub async fn find_deployment( + &self, + provider: Arc>, + init_code_hash: [u8; 32], + ) -> Result, RpcError> { + let call = TransactionRequest::default().to(Self::address()).input(TransactionInput::new( + abi::Deployer::deploymentsCall::new((init_code_hash.into(),)).abi_encode().into(), + )); + let bytes = provider.call(&call).await?; + let deployment = abi::Deployer::deploymentsCall::abi_decode_returns(&bytes, true) + .map_err(|e| { + TransportErrorKind::Custom( + format!("node returned a non-Deployment for function returning Deployment: {e:?}").into(), + ) + })? + ._0; + + if deployment.created_contract == [0; 20] { + return Ok(None); + } + Ok(Some(deployment)) + } +} diff --git a/processor/ethereum/ethereum-serai/Cargo.toml b/processor/ethereum/ethereum-serai/Cargo.toml index a2bec481..73c5b267 100644 --- a/processor/ethereum/ethereum-serai/Cargo.toml +++ b/processor/ethereum/ethereum-serai/Cargo.toml @@ -3,7 +3,7 @@ name = "ethereum-serai" version = "0.1.0" description = "An Ethereum library supporting Schnorr signing and on-chain verification" license = "AGPL-3.0-only" -repository = "https://github.com/serai-dex/serai/tree/develop/networks/ethereum" +repository = "https://github.com/serai-dex/serai/tree/develop/processor/ethereum/ethereum-serai" authors = ["Luke Parker ", "Elizabeth Binks "] edition = "2021" publish = false diff --git a/processor/ethereum/ethereum-serai/src/crypto.rs b/processor/ethereum/ethereum-serai/src/crypto.rs index d013eeff..fc51ae6b 100644 --- a/processor/ethereum/ethereum-serai/src/crypto.rs +++ b/processor/ethereum/ethereum-serai/src/crypto.rs @@ -15,11 +15,9 @@ use frost::{ pub use ethereum_schnorr_contract::*; -use alloy_core::primitives::{Parity, Signature as AlloySignature}; +use alloy_core::primitives::{Parity, Signature as AlloySignature, Address}; use alloy_consensus::{SignableTransaction, Signed, TxLegacy}; -use crate::abi::router::{Signature as AbiSignature}; - pub(crate) fn keccak256(data: &[u8]) -> [u8; 32] { alloy_core::primitives::keccak256(data).into() } @@ -28,11 +26,9 @@ pub(crate) fn hash_to_scalar(data: &[u8]) -> Scalar { >::reduce_bytes(&keccak256(data).into()) } -pub fn address(point: &ProjectivePoint) -> [u8; 20] { +pub(crate) fn address(point: &ProjectivePoint) -> [u8; 20] { let encoded_point = point.to_encoded_point(false); - // Last 20 bytes of the hash of the concatenated x and y coordinates - // We obtain the concatenated x and y coordinates via the uncompressed encoding of the point - keccak256(&encoded_point.as_ref()[1 .. 65])[12 ..].try_into().unwrap() + **Address::from_raw_public_key(&encoded_point.as_ref()[1 .. 65]) } /// Deterministically sign a transaction. @@ -64,18 +60,15 @@ pub fn deterministically_sign(tx: &TxLegacy) -> Signed { } } -/// The HRAm to use for the Schnorr contract. +/// The HRAm to use for the Schnorr Solidity library. +/// +/// This will panic if the public key being signed for is not representable within the Schnorr +/// Solidity library. #[derive(Clone, Default)] pub struct EthereumHram {} impl Hram for EthereumHram { #[allow(non_snake_case)] fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar { - let x_coord = A.to_affine().x(); - - let mut data = address(R).to_vec(); - data.extend(x_coord.as_slice()); - data.extend(m); - - >::reduce_bytes(&keccak256(&data).into()) + Signature::challenge(*R, &PublicKey::new(*A).unwrap(), m) } } diff --git a/processor/ethereum/ethereum-serai/src/lib.rs b/processor/ethereum/ethereum-serai/src/lib.rs index 76121401..1a013ddf 100644 --- a/processor/ethereum/ethereum-serai/src/lib.rs +++ b/processor/ethereum/ethereum-serai/src/lib.rs @@ -15,6 +15,7 @@ pub mod alloy { pub mod crypto; +/* pub(crate) mod abi { pub use contracts::erc20; pub use contracts::deployer; @@ -37,3 +38,4 @@ pub enum Error { #[error("couldn't make call/send TX")] ConnectionError, } +*/ diff --git a/processor/ethereum/ethereum-serai/src/machine.rs b/processor/ethereum/ethereum-serai/src/machine.rs index b9a0628e..404922f5 100644 --- a/processor/ethereum/ethereum-serai/src/machine.rs +++ b/processor/ethereum/ethereum-serai/src/machine.rs @@ -25,6 +25,19 @@ use crate::{ }, }; +/// The HRAm to use for the Schnorr Solidity library. +/// +/// This will panic if the public key being signed for is not representable within the Schnorr +/// Solidity library. +#[derive(Clone, Default)] +pub struct EthereumHram {} +impl Hram for EthereumHram { + #[allow(non_snake_case)] + fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar { + Signature::challenge(*R, &PublicKey::new(*A).unwrap(), m) + } +} + #[derive(Clone, PartialEq, Eq, Debug)] pub struct Call { pub to: [u8; 20], diff --git a/processor/ethereum/primitives/Cargo.toml b/processor/ethereum/primitives/Cargo.toml new file mode 100644 index 00000000..6c6ff886 --- /dev/null +++ b/processor/ethereum/primitives/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "serai-processor-ethereum-primitives" +version = "0.1.0" +description = "Primitives for Serai's Ethereum Processor" +license = "AGPL-3.0-only" +repository = "https://github.com/serai-dex/serai/tree/develop/processor/ethereum/primitives" +authors = ["Luke Parker "] +edition = "2021" +publish = false +rust-version = "1.79" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true + +[dependencies] +group = { version = "0.13", default-features = false } +k256 = { version = "^0.13.1", default-features = false, features = ["std", "arithmetic"] } + +alloy-core = { version = "0.8", default-features = false } +alloy-consensus = { version = "0.3", default-features = false, features = ["k256"] } diff --git a/processor/ethereum/primitives/LICENSE b/processor/ethereum/primitives/LICENSE new file mode 100644 index 00000000..41d5a261 --- /dev/null +++ b/processor/ethereum/primitives/LICENSE @@ -0,0 +1,15 @@ +AGPL-3.0-only license + +Copyright (c) 2022-2024 Luke Parker + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License Version 3 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/processor/ethereum/primitives/README.md b/processor/ethereum/primitives/README.md new file mode 100644 index 00000000..90da68c6 --- /dev/null +++ b/processor/ethereum/primitives/README.md @@ -0,0 +1,3 @@ +# Ethereum Processor Primitives + +This library contains miscellaneous primitives and helper functions. diff --git a/processor/ethereum/primitives/src/lib.rs b/processor/ethereum/primitives/src/lib.rs new file mode 100644 index 00000000..ccf41344 --- /dev/null +++ b/processor/ethereum/primitives/src/lib.rs @@ -0,0 +1,49 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] + +use group::ff::PrimeField; +use k256::{elliptic_curve::ops::Reduce, U256, Scalar}; + +use alloy_core::primitives::{Parity, Signature}; +use alloy_consensus::{SignableTransaction, Signed, TxLegacy}; + +/// The Keccak256 hash function. +pub fn keccak256(data: impl AsRef<[u8]>) -> [u8; 32] { + alloy_core::primitives::keccak256(data.as_ref()).into() +} + +/// Deterministically sign a transaction. +/// +/// This function panics if passed a transaction with a non-None chain ID. +pub fn deterministically_sign(tx: &TxLegacy) -> Signed { + pub fn hash_to_scalar(data: impl AsRef<[u8]>) -> Scalar { + >::reduce_bytes(&keccak256(data).into()) + } + + assert!( + tx.chain_id.is_none(), + "chain ID was Some when deterministically signing a TX (causing a non-deterministic signer)" + ); + + let sig_hash = tx.signature_hash().0; + let mut r = hash_to_scalar([sig_hash.as_slice(), b"r"].concat()); + let mut s = hash_to_scalar([sig_hash.as_slice(), b"s"].concat()); + loop { + // Create the signature + let r_bytes: [u8; 32] = r.to_repr().into(); + let s_bytes: [u8; 32] = s.to_repr().into(); + let v = Parity::NonEip155(false); + let signature = Signature::from_scalars_and_parity(r_bytes.into(), s_bytes.into(), v).unwrap(); + + // Check if this is a valid signature + let tx = tx.clone().into_signed(signature); + if tx.recover_signer().is_ok() { + return tx; + } + + // Re-hash until valid + r = hash_to_scalar(r_bytes); + s = hash_to_scalar(s_bytes); + } +}