Skip to content
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

feat(EIP712): parsing of TypedData payload; encoding + hashing; #833

Conversation

JeneaVranceanu
Copy link
Collaborator

@JeneaVranceanu JeneaVranceanu commented Oct 18, 2023

Summary of Changes

  • Implementation of EIP712 TypedData payload parsing;
  • Payload encoding & hashing.

Upd: this PR is focused on giving the ability to receive a JSON payload via eth_signTypedData RPC call, parse this payload, do some validations (though, validations can be improved for sure), encode message (a message is one of the fields of received JSON), hash message and according to given types (types also part of the received JSON and can be arbitrary), and prepare a special EIP712 hash that's used for signing of the received payload.

In order to parse and sign this payload you as a user must do the following:

let eip712TypedData = try EIP712Parser.parse(receivedPayloadFromRPC)
let signature = try Web3Signer.signEIP712(
    eip712TypedData,
    keystore: keystore,
    account: account,
    password: password)

Test Data or Screenshots

By submitting this pull request, you are confirming the following:
  • I have reviewed the Contribution Guidelines.
  • I have performed a self-review of my own code.
  • I have updated my repository to match the develop branch.
  • I have included test data or screenshots that prove my fix is effective or that my feature works.
  • I have checked that all tests work and swiftlint is not throwing any errors/warnings.

@JeneaVranceanu JeneaVranceanu marked this pull request as ready for review October 18, 2023 21:09
@yaroslavyaroslav
Copy link
Collaborator

@JeneaVranceanu I wonder is it ready be reviewed now, or there would be some fixes added?

@JeneaVranceanu
Copy link
Collaborator Author

@JeneaVranceanu I wonder is it ready be reviewed now, or there would be some fixes added?

I think it makes sense to look into the old EIP712 implementation that we already had (and maybe even remove it 👀).

@JeneaVranceanu
Copy link
Collaborator Author

I'll ping you by the end of this day with final thoughts/changes.

@JeneaVranceanu
Copy link
Collaborator Author

JeneaVranceanu commented Oct 19, 2023

@yaroslavyaroslav No more changes for this PR.
I've updated the documentation for EIP712Parser and the next steps will be:

  • Check that public class EIP712 covers all types possible;
  • Update EIP712Domain with all 5 properties according to the standard and add the ability to add more properties through a dictionary (maybe dictionary, not yet decided);
  • Implement a parsing of custom statically typed objects into EIP712TypedData. As EIP712TypedData has all the encoding and hashing in place it will make sense to transform data from custom objects into EIP712TypedData and then use encode, hash or whatever is needed.

But all of that is for a different PR. This PR will have to wait for a while to get all these updates 🙂
I suggest we review and merge this PR.

@awaiskhan1304
Copy link

Tested it with https://react-app.walletconnect.com/ and it is working just perfect.
Thanks @JeneaVranceanu.

@yaroslavyaroslav
Copy link
Collaborator

No more changes for this PR.

Started to reviewing it this Friday, considering to continue doing this today's evening. Quite tough workload I'm having now, sorry for a delay.

…alizedError type;

- updated ParsingError and AbstractKeystoreError with description;
- minor refactoring;
- added 2 more EIP712 tests;
@yaroslavyaroslav
Copy link
Collaborator

@JeneaVranceanu we have problems)

@JeneaVranceanu
Copy link
Collaborator Author

@JeneaVranceanu we have problems)

Yes, a failing test!
Added this morning. Have to implement support for custom type arrays.
And recursion support is missing.

Comment on lines 11 to 19
case invalidJsonFile
case elementTypeInvalid
case elementTypeInvalid(_ desc: String? = nil)
case elementNameInvalid
case functionInputInvalid
case functionOutputInvalid
case eventInputInvalid
case parameterTypeInvalid
case parameterTypeNotFound
case abiInvalid
Copy link
Collaborator Author

@JeneaVranceanu JeneaVranceanu Nov 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've decided not to add (_ desc: String? = nil) to all error types as it would trigger more changes across the library.
It's better to do that updated in a separate PR.

Comment on lines +19 to 25
// FIXME: this type is wrong - The minimum number of optional fields is 5, and those are
// string name the user readable name of signing domain, i.e. the name of the DApp or the protocol.
// string version the current major version of the signing domain. Signatures from different versions are not compatible.
// uint256 chainId the EIP-155 chain id. The user-agent should refuse signing if it does not match the currently active chain.
// address verifyingContract the address of the contract that will verify the signature. The user-agent may do contract specific phishing prevention.
// bytes32 salt an disambiguating salt for the protocol. This can be used as a domain separator of last resort.
public struct EIP712Domain: EIP712Hashable {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is something to deal with later.

}
guard let newNode = rootNode.derive(path: pathAppendix, derivePrivateKey: true) else {
throw AbstractKeystoreError.keyDerivationError
throw AbstractKeystoreError.keyDerivationError("BIP32Keystore. Failed to derive a new node. Check given parent node and path.")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We really need to revisit all our errors and make sure:

  1. We have description;
  2. The description makes sense to the developers who could get this error.

…nu/web3swift into feat/eip712-dynamic-parsing

* 'feat/eip712-dynamic-parsing' of github.com:JeneaVranceanu/web3swift:
  chore: docs update
  chore: updated error message
guard
size >= 128 && size <= 256 && size.isMultiple(of: 32),
let entropy = Data.randomBytes(length: size/8)
isCorrectSize,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although it got better, it still doesn't fit with our convention of guard else formatting.

if self.addresses?.count == 1 && account == self.addresses?.last {
guard let privateKey = try? self.getKeyData(password) else {
if account == addresses?.last {
guard let privateKey = try? getKeyData(password) else {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can make this bit a bit more straightforward? Like changing the if let above to an another guard statement.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upd: oh, I see that the execution flow continues after that line:32 throw statement. Maybe we should add at least TODO: refactor me at the very beginning of the whole file, as I believe this function should be split in to, but not this time?

}

internal struct EIP712TypeArray: Codable {
public let types: [String : [EIP712TypeProperty]]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this could be a concrete type rather the dictionary?

}

public struct EIP712TypedData {
public let types: [String: [EIP712TypeProperty]]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question about the concrete type here.

guard let primaryType = json["primaryType"] as? String else {
throw Web3Error.inputError(desc: "EIP712Parser. Top-level string field 'primaryType' missing.")
}
guard let domain = json["domain"] as? [String : AnyObject] else {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should add here some at least short reasoning about why it's left here as a dictionary, rather being casted in a concrete type?

guard let data = data(using: .utf8) else { return nil }
return try data.asJsonDictionary()
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're still haven't added SwiftLint ruleset here, and I wonder would we ever do this, but I believe that such a set that we're having in a stash contains the rule about keeping one empty line at the very end of each file.

guard let signature = try Web3Signer.signPersonalMessage(hash,
keystore: keystore,
account: account,
password: password ?? "",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this default password value necessary?

guard let signature = try Web3Signer.signPersonalMessage(hash,
keystore: keystore,
account: account,
password: password ?? "")
password: password ?? "",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this default password value necessary?

case .invalidJsonFile:
errorMessage = ["invalidJsonFile"]
case .elementTypeInvalid(let desc):
errorMessage = ["elementTypeInvalid", desc]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this line is a cause that we're messing here with an array of Strings rather a plain String?

var errorMessage: [String?]
switch self {
case .noEntropyError(let additionalDescription):
errorMessage = ["Entropy error (e.g. failed to generate a random array of bytes).", additionalDescription]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this line is a cause that we're messing here with an array of Strings rather a plain String? Here it looks quite more necessary than in the previous alike code snippet, yet I wonder if it's a best way in terms of code intention clarity to implement such behaviour?

@yaroslavyaroslav yaroslavyaroslav merged commit 8ae45e1 into web3swift-team:develop Feb 7, 2024
2 checks passed
@JeneaVranceanu JeneaVranceanu deleted the feat/eip712-dynamic-parsing branch February 7, 2024 20:56
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.

3 participants