-
Notifications
You must be signed in to change notification settings - Fork 2.3k
feat(forge): add createWallet() cheat code for new Wallet struct
#5332
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
Conversation
| }, | ||
| )??, | ||
| HEVMCalls::GetNonce(inner) => { | ||
| HEVMCalls::GetNonce1(inner) => { |
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.
Because of the placement of getNonce(Wallet) in abi/abi/HEVM.sol, getNonce(address) was renamed to HEVMCalls::GetNonce1
| HEVMCalls::GetNonce0(inner) => { | ||
| correct_sender_nonce( | ||
| b160_to_h160(data.env.tx.caller), | ||
| &mut data.journaled_state, | ||
| &mut data.db, | ||
| state, | ||
| )?; | ||
|
|
||
| // TODO: this is probably not a good long-term solution since it might mess up the gas | ||
| // calculations | ||
| data.journaled_state.load_account(h160_to_b160(inner.0.addr), data.db)?; | ||
|
|
||
| // we can safely unwrap because `load_account` insert inner.0 to DB. | ||
| let account = data.journaled_state.state().get(&h160_to_b160(inner.0.addr)).unwrap(); | ||
| abi::encode(&[Token::Uint(account.info.nonce.into())]).into() |
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.
Should all be the same as HEVMCalls::GetNonce1 except .get(&h160_to_b160(inner.0.addr))
| let pub_key = key.verifying_key().as_affine().to_encoded_point(false); | ||
| let pub_key_x = pub_key.x().ok_or("No x coordinate was found")?; | ||
| let pub_key_y = pub_key.y().ok_or("No y coordinate was found")?; | ||
|
|
||
| let pub_key_x = U256::from(pub_key_x.as_slice()); | ||
| let pub_key_y = U256::from(pub_key_y.as_slice()); |
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.
This is the best way I could find to extract the x and y coordinates. I'm not extremely familiar with the details of ECDSA algorithms, so I'm not 100% sure if this is accurate either.
| if let Some(label) = label { | ||
| state.labels.insert(addr, label); | ||
| } | ||
|
|
||
| Ok((addr, pub_key_x, pub_key_y, private_key).encode().into()) |
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.
Ideally, private_key or addr should be reconstructed with pub_key_x and pub_key_y to ensure that the coordinates are correct, but I couldn't find a great way do that yet. I may make another commit adding that and reference that commit here, but I'm currently looking for suggestions 👍
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 think it's ok right now to construct the addr from the private key—is the usual way you see it implemented in most libraries, even if it should be possible to construct it from the pubkeys—constructing from the privkey is just one more step.
| HEVMCalls::Sign0(inner) => sign(inner.0, inner.1.into(), data.env.cfg.chain_id.into()), | ||
| HEVMCalls::CreateWallet0(inner) => { | ||
| create_wallet(U256::from(keccak256(&inner.0)), Some(inner.0.clone()), state) | ||
| } | ||
| HEVMCalls::CreateWallet1(inner) => create_wallet(inner.0, None, state), | ||
| HEVMCalls::CreateWallet2(inner) => create_wallet(inner.0, Some(inner.1.clone()), state), | ||
| HEVMCalls::Sign1(inner) => { | ||
| sign(inner.0.private_key, inner.1.into(), data.env.cfg.chain_id.into()) | ||
| } |
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.
Just wanted to compile a quick list to explain the functionality of each changed line here:
Sign0: the same as the oldSign. Just needed to be renamed for newfunction sign(Wallet)CreateWallet0:function createWallet(string)to derive private key and label wallet withstringCreateWallet1:function createWallet(uint256)create new wallet with private keyuint256(no label)CreateWallet2:function createWallet(uint256,string)create new wallet with private keyuint256and label withstringSign1:function sign(Wallet,bytes32)but uses wallet's private key
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 add these as comments, they're great!
| function testCreateWalletStringPrivAndLabel() public { | ||
| bytes memory privKey = "this is a priv key"; | ||
| Cheats.Wallet memory wallet = cheats.createWallet(string(privKey)); | ||
|
|
||
| address expectedAddr = cheats.addr(wallet.privateKey); | ||
| assertEq(expectedAddr, wallet.addr); | ||
|
|
||
| string memory label = cheats.getLabel(wallet.addr); | ||
| assertEq(label, string(privKey), "labelled address != wallet.addr"); | ||
| } |
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.
Ideally, there'd be another section of this test that checks the validity of wallet.publicKeyX and wallet.publicKeyY, but I wasn't sure what the best way of doing that was
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.
One way would be to verify that the address derived from the coordinates equals the address derived via the addr(unit privKey)(address) cheatcode.
Deriving the Ethereum address is specified in the Yellow Paper "Appendix F: Signing Transactions" §134. A possible Solidity implementation is:
function addressOf(uint xCoordinate, uint yCoordinate) internal pure returns (address) {
return address(uint160(keccak256(abi.encode(xCoordinate, yCoordinate))));
}|
@pmerkleplant what was your original use case for pubkeys? Mainly asking because there's three ways we can represent them, and I'm wondering if we should choose one and you can convert between them as needed (maybe tricky in solidity), or just give all 3 in the // Approach 1
uint256 publicKeyX;
uint256 publicKeyY;
// Approach 2
bytes publicKey; // 0x04{x}{y}
// Approach 3;
bytes compressedPublicKey; // 0x02{x} or 0x03{x} |
Our use case is to aggregate public keys, i.e. perform elliptic curve point addition. We also have to derive the Ethereum address of the resulting public key. We hope that the cheatcode makes our fuzzing test suite faster, in which we receive sets of random private keys and compute and aggregate their public keys. Therefore, the most suitable approach for us would be the first/currently implemented one, i.e. receiving the x and y Affine coordinates as I would personally argue against approach 3 as the y coordinate would need to be computed again, making even deriving the Ethereum address computation heavy and error-prone. @mds1 Out of curiosity, could you elaborate on where approach 2 is coming from? Is this a format standardized somewhere? @trevorgjohnson Many thanks for the work so far! |
Got it, thanks. I have a use case as part of ERC-5564/ERC-6538 where we may store compressed public keys in a registry, but starting with the x/y coordinates and compressing/uncompressing in solidity shouldn't be too bad, so I'd be ok with moving forward with the current approach for the
This is a convention from Standards for Efficient Cryptography Group where:
It's used by popular libs such as ethers.js and noble-secp256k1. You can read more about it and find references to more data in mastering ethereum here |
…Nonce' to 'GetNonce1'
0abac5b to
2512ac8
Compare
|
I rebased the current version of `master |
2512ac8 to
ee61825
Compare
pmerkleplant
left a comment
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.
Approving the Solidity part for my use case. Note that I'm not skilled to review the Rust changes.
testdata/cheats/Wallet.t.sol
Outdated
| assertEq(label, string(privKey), "labelled address != wallet.addr"); | ||
| } | ||
|
|
||
| function testCreateWalletPrivKeyNoLabel(uint248 pk) public { |
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.
Instead of receiving the private key as type uint248, one could receive a private key seed of type uint256 and create the private key via:
uint internal constant Q = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
// Let pk ∊ [1, Q).
uint pk = bound(pkSeed, 1, Q-1);This would ensure the whole eligible set of private keys is testable. (Note that type(uint248).max < Q, meaning the highest possible private keys are not testable)
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.
+1, this would be a stronger test (applies to the other tests also)
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.
Agreed 👍 testing using a higher range of testable private keys was added here 3f7b4e8
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.
thanks! just a nit, but you can remove the vm.assume(pkSeed != 0); checks now, since they are redundant given the bound usage
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.
Ha yea that's a good catch! There was an issue with bound not being defined, so I fixed that and this nit in this commit here: 8c36615
|
Please add :) |
|
ah whoops, we let this go stale but i think there's still interest here. @trevorgjohnson sorry! mind fixing the conflict to give this a review? |
No worries! The conflicts should be resolved in d7bcc7d |
Evalir
left a comment
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.
lgtm, fairly straightforward even if it's sensitive code.
my only ask is that let's comment out everything you laid out in the pr on the actual code, as they're great!
| // TODO: this is probably not a good long-term solution since it might mess up the gas | ||
| // calculations | ||
| data.journaled_state.load_account(h160_to_b160(inner.0.addr), data.db)?; |
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.
@mattsse wdyt here? seems ok, but might mess with gas yes
| Ok((sig.v, r_bytes, s_bytes).encode().into()) | ||
| } | ||
|
|
||
| fn create_wallet(private_key: U256, label: Option<String>, state: &mut Cheatcodes) -> Result { |
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.
let's add a bit of docs on what it does, even if it's a bit obvious :D
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 to me! Were you thinking about just adding some comments above this? Or just adding some create_wallet documentation in the foundry book?
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.
Actually, I decided to add them here since I saw that multiple other functions also included some documentation. Let me know what you think 👍
| if let Some(label) = label { | ||
| state.labels.insert(addr, label); | ||
| } | ||
|
|
||
| Ok((addr, pub_key_x, pub_key_y, private_key).encode().into()) |
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 think it's ok right now to construct the addr from the private key—is the usual way you see it implemented in most libraries, even if it should be possible to construct it from the pubkeys—constructing from the privkey is just one more step.
| HEVMCalls::Sign0(inner) => sign(inner.0, inner.1.into(), data.env.cfg.chain_id.into()), | ||
| HEVMCalls::CreateWallet0(inner) => { | ||
| create_wallet(U256::from(keccak256(&inner.0)), Some(inner.0.clone()), state) | ||
| } | ||
| HEVMCalls::CreateWallet1(inner) => create_wallet(inner.0, None, state), | ||
| HEVMCalls::CreateWallet2(inner) => create_wallet(inner.0, Some(inner.1.clone()), state), | ||
| HEVMCalls::Sign1(inner) => { | ||
| sign(inner.0.private_key, inner.1.into(), data.env.cfg.chain_id.into()) | ||
| } |
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 add these as comments, they're great!
8c36615 to
f4e105d
Compare
| // [function sign(uint256,bytes32)] Used to sign bytes32 digests using the given private key | ||
| HEVMCalls::Sign0(inner) => sign(inner.0, inner.1.into(), data.env.cfg.chain_id.into()), | ||
| // [function createWallet(string)] Used to derive private key and label the wallet with the | ||
| // same string | ||
| HEVMCalls::CreateWallet0(inner) => { | ||
| create_wallet(U256::from(keccak256(&inner.0)), Some(inner.0.clone()), state) | ||
| } | ||
| // [function createWallet(uint256)] creates a new wallet with the given private key | ||
| HEVMCalls::CreateWallet1(inner) => create_wallet(inner.0, None, state), |
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.
@Evalir, I looked around for an existing doc schema for each of these match call arms, but I couldn't find any so I made my own. Let me know what you think 👍
|
Amazing! just need forge-std PR + book update |
Motivation
Issue #4790
Solution
Add the struct
Walletto contain itsprivateKey,addr,publicKeyX, andpublicKeyY.createWalletwas added to createWallet's andsign, andgetNoncewere also added to as helpers forWallet