Skip to content

Conversation

@trevorgjohnson
Copy link
Contributor

Motivation

Issue #4790

Solution

Add the struct Wallet to contain its privateKey, addr, publicKeyX, and publicKeyY.
createWallet was added to create Wallet's and sign, and getNonce were also added to as helpers for Wallet

},
)??,
HEVMCalls::GetNonce(inner) => {
HEVMCalls::GetNonce1(inner) => {
Copy link
Contributor Author

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

Comment on lines 522 to 536
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()
Copy link
Contributor Author

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))

Comment on lines 131 to 136
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());
Copy link
Contributor Author

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.

Comment on lines 138 to 142
if let Some(label) = label {
state.labels.insert(addr, label);
}

Ok((addr, pub_key_x, pub_key_y, private_key).encode().into())
Copy link
Contributor Author

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 👍

Copy link
Member

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.

Comment on lines 247 to 255
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())
}
Copy link
Contributor Author

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 old Sign. Just needed to be renamed for new function sign(Wallet)
  • CreateWallet0: function createWallet(string) to derive private key and label wallet with string
  • CreateWallet1: function createWallet(uint256) create new wallet with private key uint256 (no label)
  • CreateWallet2: function createWallet(uint256,string) create new wallet with private key uint256 and label with string
  • Sign1: function sign(Wallet,bytes32) but uses wallet's private key

Copy link
Member

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!

Comment on lines 12 to 30
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");
}
Copy link
Contributor Author

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

Copy link

@pmerkleplant pmerkleplant Jul 8, 2023

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))));
}

@Evalir Evalir requested review from Evalir and mattsse July 7, 2023 18:41
@mds1
Copy link
Collaborator

mds1 commented Jul 7, 2023

@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 Wallet struct (but that feels cluttered)

// Approach 1
uint256 publicKeyX;
uint256 publicKeyY;

// Approach 2
bytes publicKey; // 0x04{x}{y}

// Approach 3;
bytes compressedPublicKey; // 0x02{x} or 0x03{x}

@pmerkleplant
Copy link

@pmerkleplant what was your original use case for pubkeys?

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 uints.

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!

@mds1
Copy link
Collaborator

mds1 commented Jul 8, 2023

Therefore, the most suitable approach for us would be the first/currently implemented one, i.e. receiving the x and y Affine coordinates as uints.
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.

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 Wallet struct

Out of curiosity, could you elaborate on where approach 2 is coming from? Is this a format standardized somewhere?

This is a convention from Standards for Efficient Cryptography Group where:

  • uncompressed pubkeys are 65 bytes (0x04 prefix + 32 byte x-coordinate + 32 byte y-coordinate
  • compressed pubkeys are 33 btyes (0x02 OR 0x03 prefix + 32 byte x-coordinate, where 0x02 means the y value is even, and 0x03 means it's odd

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

@trevorgjohnson
Copy link
Contributor Author

I rebased the current version of `master

Copy link

@pmerkleplant pmerkleplant left a 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.

assertEq(label, string(privKey), "labelled address != wallet.addr");
}

function testCreateWalletPrivKeyNoLabel(uint248 pk) public {

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)

Copy link
Collaborator

@mds1 mds1 Aug 9, 2023

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)

Copy link
Contributor Author

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

Copy link
Collaborator

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

Copy link
Contributor Author

@trevorgjohnson trevorgjohnson Aug 10, 2023

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

@trevorgjohnson
Copy link
Contributor Author

Hello @Evalir @mattsse, just wanted to check and see if you guys have had a chance to check out the newest changes to this PR yet. Thanks!

@Austinhs
Copy link

Austinhs commented Aug 9, 2023

Please add :)

@Evalir
Copy link
Member

Evalir commented Aug 9, 2023

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?

@trevorgjohnson
Copy link
Contributor Author

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

Copy link
Member

@Evalir Evalir left a 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!

Comment on lines 534 to 536
// 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)?;
Copy link
Member

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 {
Copy link
Member

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

Copy link
Contributor Author

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?

Copy link
Contributor Author

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 👍

Comment on lines 138 to 142
if let Some(label) = label {
state.labels.insert(addr, label);
}

Ok((addr, pub_key_x, pub_key_y, private_key).encode().into())
Copy link
Member

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.

Comment on lines 247 to 255
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())
}
Copy link
Member

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!

Comment on lines 251 to 259
// [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),
Copy link
Contributor Author

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 👍

@Evalir
Copy link
Member

Evalir commented Aug 15, 2023

Amazing! just need forge-std PR + book update

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants