-
Notifications
You must be signed in to change notification settings - Fork 470
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add transaction signing with in accounts
sub-namespace.
#279
Conversation
Hey there @nlordell, I couldn't find a way to contact you so I'm writing this here. My team is trying to achieve this exact functionality this week so that we can use our own private key instead of relying on unlocked accounts on ethereum nodes. We want to ask when can we expect this to be merged into master and published in a new release? We also have a question that may be better asked of @tomusdrw: what is the best way, given a contract's abi, to sign calls to the contract's methods? ideally, we would like to have a |
Hi guys! I didn't have time to look closely at the PR but will definitely do that in the upcoming 36h. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! A bunch of code style grumbles though.
src/api/accounts.rs
Outdated
SignTransactionFuture::new(self, tx, key) | ||
} | ||
|
||
/// Hash a message. The data will be UTF-8 HEX decoded and enveloped as |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you change the doc comments to follow a pattern:
/// <description summary>
///
/// <more elaborate documentation>
So here it would be:
/// Hash a message according to EIP-191.
///
/// The data will be UTF-8 HEX encoded and enveloped as follows...
src/api/accounts.rs
Outdated
to: self.tx.to.unwrap_or_default(), | ||
value: self.tx.value.unwrap_or_default(), | ||
gas_price, | ||
gas: self.tx.gas.unwrap_or(TRANSACTION_BASE_GAS_FEE), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
21k will really never be enough to cover the transaction. I think we should rather default to something higher like 100k
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I chose 21K as this is the base transaction amount and exactly what it uses to send eth from one address to another (with no contracts involved). I think if we want to be extra cleaver, we could estimate the gas price when it is not provided.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd leave the estimation to the user - we can suggest them to run estimateGas
before making a call. BTW is it easy to go from the payload required for estimateGas
and the payload that goes to this function? We should consider adding a From
implementations to make such workflow easier.
Indeed 21k
is ok for simple transfers, but if only you'll try to send some data, we will need more gas. I'm still in favour of increasing the "default sensible" gas here - the user will get the rest refunded anyway and I think it can prevent a class of simple errors. Alternatively we can make gas
a required field and force the user to always set it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, web3js requires gas parameter for web3.eth.accounts.signTransaction
. I like having a default gas as I find it makes things more ergonomic (so we can implement Default
for TransactionParameters
). I think I'll hike it up to 100k
then.
I'll add the Into
and From
impls between TransactionParameters
and CallRequest
which is used for estimate gas.
#[test] | ||
fn raw_tx_params_sign() { | ||
// retrieved test vector from: | ||
// https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#eth-accounts-signtransaction |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️ Thank you for all the tests.
@@ -1,5 +1,6 @@ | |||
//! `Web3` implementation | |||
|
|||
mod accounts; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please re-export accounts::Accounts
otherwise the docs are not visible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops!
src/types/signed.rs
Outdated
pub gas: Option<U256>, | ||
/// Gas price (None for sensible default) | ||
pub gas_price: Option<U256>, | ||
/// Transfered value (None for no transfer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why None
and not H256::zero()
or Default::default()
? I think it makes it really confusing if there is any difference between None
and Some(0)
(afaik there isn't).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For gas price specifically there is a difference. Some(0)
means that you want to pay 0
for gas (transaction will never be mined though). None
on the other hand will tell the signing function to get the current recommended gas price and use that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I only meant value
and data
field. The rest is fine.
src/types/signed.rs
Outdated
pub gas_price: Option<U256>, | ||
/// Transfered value (None for no transfer) | ||
pub value: Option<U256>, | ||
/// Data (None for empty data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here. I think it would be best to drop the Option
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. We can drop Option
for to
, value
, and data
. I had used Option
originally to signal that it providing this transaction parameter was optional and when it was not provided some default would be used. For the aforementioned fields, the default also happens to be Default::default()
so we can get rid of Option
for them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd keep it for to
as well. There is a difference between contract creation and sending to 0x0
address.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see it in ethereum_transaction
, it gets encoded differently if it's there or if its missing.
src/api/accounts.rs
Outdated
} | ||
|
||
/// Recovers the Ethereum address which was used to sign the given data. | ||
pub fn recover<S, Sig>(&self, message: S, signature: Sig) -> Result<Address, Error> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be useful to have another recover
for raw hashes? Maybe make the message
an enum
and allow passing etiher Hash
or AsRef<str>
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops! That was a bit of an oversight.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tomusdrw Actually, thinking about it now it would be nice to be able to do something like:
let signed_data = accounts.sign("hello world", &key);
let address = accounts.recover(signed_data)?;
assert_eq!(address, key.public().address());
let signed_tx = accounts.sign(tx, &key).await?;
let address = accounts.recover(signed_tx)?;
assert_eq!(address, key.public().address());
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but how do you recover something you didn't sign yourself? Also since we don't have specialisation either the method names need to be different or you need to accept impl Into<SomeEnum>
as a parameter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went ahead and implemented this. If you don't like it, let me know and I can change it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, for some reason I didn't see you message when I commented. Yes you can recover things that you did not sign yourself. A Recovery
struct with the recovery data can be constructed fairly easily, we do this in one of the unit tests.
Let me know if you like how I implemented it, if not I can change it to something else.
Happy to accept a PR introducing this feature @reuvenpo. |
I'll build it on this PR, so i'll wait until this one has finished cooking :) |
@tomusdrw I think I addressed all of your comments. Note that I changed how recovery works, let me know what you think of the new implementation. Take a look at the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good!
/// The data is a UTF-8 encoded string and will enveloped as follows: | ||
/// `"\x19Ethereum Signed Message:\n" + message.length + message` and hashed | ||
/// using keccak256. | ||
pub fn hash_message<S>(&self, message: S) -> H256 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can now use impl Trait
for this:
pub fn hash_message<S>(&self, message: S) -> H256 | |
pub fn hash_message(&self, message: impl AsRef<str>) -> H256 { |
Most likely it applies to every generic method introduced in this PR. But it's not critical, just a hint :)
#[derive(Clone, Debug, PartialEq)] | ||
pub enum RecoveryMessage { | ||
/// Message string | ||
String(String), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't that be Data(Bytes)
? I think you can sign arbitraty (non-utf8) bytes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that we have restricted hashing to AsRef<str>
anyway, so it's good as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait a second, that's a good point - shouldn't this crate allow hashing arbitrary data?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, feel free to do a follow up PR on this, or maybe @nlordell is interested :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like @reuvenpo beat me to it!
…)" This reverts commit 5e8e0a4.
Here is my code:
And I got an error at the last line as attached image. I tried to switch |
Also adds a few related methods like
hash_message
,sign
andrecover
. This closes #116.