Closed PhABC closed 2 years ago
Realizing that this is more than just validating signatures; we're actually asking the question "given this action, does the caller have the ability to it, given this proof"?
so perhaps the better metaphor is
isValidAction(bytes _action, bytes _proof) {}
where the default behavior might be to treat _action
as a Bouncer data hash and _proof
as a signature. But like we noticed before, they could be anything.
Although this implies that the proof handling and the access control are part of the same function/contract where they could potentially be different, although I'm not sure how that separation would generalize at all (i.e. in the case of ECDSA signatures, the validity is just the recovery, which produces an address
and the access control is a "does this address have this permission" check against the _action
data).
Thoughts, @PhABC?
That's a good point! A signature is usually used to guarantee that a given address allows something, but other schemes not relying on signature methods could also achieve the same goal.
Pinging @alexvandesande & @abandeali1 for comments.
I personally prefer using a 32 byte hash over an _action
byte array. Any action should be able to be represented as a hash, and this really simplifies the implementation details. You're probably going to end up hashing _action
and verifying a signature at some point anyways. Any extra logic could live in the caller contract.
I could see it making sense to use the word proof
instead of signature
, though.
@abandeali1
The reason why we opted for a dynamic byte array is because the called
contract might request specific data that the caller
contract is not aware of.
For instance, imagine you use a smart account that has some key management properties, where different keys have different roles (RBAC style). Now imagine that the private key on your phone can sell some game assets, but can't any other types of tokens. Your ledger private key can sell any type of asset. If the caller contract (e.g. 0x contract) only passes the hash of the message, the smart account has no way of knowing what the asset being traded is. The smart account would see two hashes that were indeed signed by two private key you own, but it would have no way of verifying if the action signed is valid based on it's own internal conditions. In this case, your smart account would either need to allow all your private keys to exchange assets via 0x (let them sign hashes) or prevent them all.
In general, what i'm trying to say is that I think there could always be additional rules not encompassed by the caller contract that the called contract requires.
Likewise, my worry is that a hash loses information that may be necessary for validation.
That said, I'm also worried about the complexity of passing arbitrary data via a bytes call; we've just accidentally re-created the need for abi encoding/decoding and whoops, we should probably just use solidity for that, so now we're back where we started, creating context-specific methods. It may make more sense to simply restrict this to hash + proof validation, and nothing more? that covers bouncer and identity contracts, the two main cases I'd like to use this technique for.
You can always encode any extra data about the hash
in the signature
field. For example:
function isValidSignature(bytes32 hash, bytes signature) external {
uint8 signatureType = uint8(signature[0]);
bytes32 msgHash = keccak256(hash, signatureType);
if (signatureType == 0) {
// Do something specific to signatureType
return _isValidSignature(msgHash, signature);
} else if ...
}
You can imagine adding any amount of extra data to signature
in a similar way. This keeps the simplest implementation simple, but it's still pretty flexible if necessary.
@abandeali1 I'm not convinced that overloading the signature ("proof") argument is a ton better than bytes action, bytes proof
.
@PhABC any feedback on restricting this to hash+signature based validation?
@shrugs my point is that it can be done with just a hash+signature, but I wouldn't expect that to be frequently used.
Thanks for writing this up @PhABC, it's quite good.
The discussion around @abandeali1's suggestion is very interesting, but we need to show some examples of usage in order to best decide on fixed or arbitrary size _data
(i.e. the message that is signed).
I think a good example to analyze is that of an m-of-n multisig smart account. A valid signature for this account could be the concatenation of m (signature, signer) pairs, where the m signers are in the set of n signers, and each signature is valid for the message and respective signer. I think this is enough motivation for the signature to be of arbitrary size.
Now suppose that a family of messages can only be considered signed when the full set of n signers approve. It's common to increase the required number of signatures for large value transfers, so I think it would make sense for the signature scheme to have something analogous to that. The validity of the signature then depends on some parameter in the message being signed. Per @abandeali1's suggestion, the parameter should be included as part of the signature, but then the contract would have to additionally verify that the parameter in the signature is indeed the one contained in the message. If the message is hashed, then the entire message has to also be included in the signature in order to verify the hash. That would be a lot of redundant bytes being passed around. IMO this justifies having the message be of arbitrary size, and not necessarily a hash.
Do you all agree the scenario described makes sense?
A mostly unrelated question about the EIP. Would it be valid for an implementation of isValidSignature
to change its behavior depending on the caller? I have the feeling that this should be forbidden by the spec ("MUST NOT"), but unfortunately I don't think there's a way to enforce it in the API.
I forgot to comment on the suggestion to use the names "action" and "proof". Although I kind of like "proof", I would prefer to keep the current names as they're consistent with those of cryptographic signing, and agnostic to any particular use case.
@frangio overloading the signature
field should actually result in fewer bytes being passed around and should be cheaper, assuming that isValidSignature
is being called by another contract (i.e the data is being copied to memory and is not included in the transaction calldata).
With bytes data, bytes signature
, the ABI encoded bytes will look like: [dataOffset][signatureOffset][dataLength][data][signatureLength][signature], where everything is 32 bytes except for data
and signature
. This gives us a total of 128 + data.length + signature.length bytes.
With bytes32 hash, bytes signature
, the ABI encoded bytes will look like: [hash][signatureOffset][signatureLength][signature]. This gives us a total of 96 + data.length + signature.length bytes (assuming that the signature
field includes everything that would otherwise be in the data
field).
So in the worst case scenario of using bytes32 hash, bytes signature
, we save 32 bytes but add some extra hashing and complexity.
Now let's look that the case where we we use bytes data, bytes signature
but data
only contains a 32 bytes hash. The ABI encoded bytes are [dataOffset][signatureOffset][dataLength][hash][signatureLength][signature]. This means we add 64 bytes vs using bytes32 hash, bytes signature
. There's an extra hidden cost here though - if calling isValidSignature
from another contract, we now have to convert our 32 byte hash into a byte array before the call and then parse the hash out of byte array after the call. This isn't particularly easy without using some inline assembly (currently), and I'm guessing that the function will be used this way the majority of the time.
To be honest, the cost differences here are probably minimal. I think the main downside for the bytes data, bytes signature
approach is converting a bytes32 to and from a byte array. With the bytes32 hash, bytes signature
approach, the downside is confusion over potentially overloading signature
. Which is better likely comes down to how often we think each case is likely to be used.
The complexity of converting a bytes32
to bytes
is not something I'd like people to have to deal with, great point, @abandeali1. And indeed, verifying the signature of a hash seems to be the primary use-case, @frangio.
I'm happy with, in order of preference:
1) scoping this issue to just be hash + signature validation and encouraging people to overload signature for extra data
2) scoping this issue to just be hash + signature validation and not encouraging people to overload signature, and instead encouraging a different signature (ideally without overloading the function name) for application-specific action validation
3) using bytes, bytes
as the arguments and providing a minimal assembly bytes32->bytes conversion library within OpenZeppelin to make it as easy as possible.
For naming, I'd prefer isValidAction
, but am definitely fine with isValidSignature
.
how about some fun voting, multiple choice ok:
isValidSignature
isValidAction
(hmm, github should have twitter-style polls that only followers of an issue at time-of-poll can vote on)
It seems like we'll need to include more information than just action/signature. I'm having trouble visualizing how to abstract signature validation like this so that contracts can recursively "sign". I think we need to add the "delegate" concept here so we can do "recursive" validity. i.e. I'm trying to sign something with my contract, but I want to proxy that validity to another contract.
I ended up writing something like this; does it make sense?
// added this function to ECRecovery, so it's used as _hash.isSignedBy(signer, sig)
function isSignedBy(bytes32 _hash, address _signer, bytes _sig)
internal
view
returns (bool)
{
// if the signer address supports SignatureDelegation, delegate to it
if (_signer.supportsInterface(InterfaceId_SignatureDelegate)) {
return ISignatureDelegate(_signer).isValidAction(_signer, _hash, _sig);
}
// otherwise make sure the hash was personally signed by the EOA account
return _signer == recover(toEthSignedMessageHash(_hash), _sig);
}
/**
* @dev An action is valid iff the _sig of the _action is from an key with the ACTION role
*/
function isValidAction(address _signer, bytes32 _action, bytes _sig)
view
public
returns (bool)
{
// permission
bool hasPermission = _signer == address(this) || hasRole(_signer, ROLE_ACTION);
// validity
bool isValid = _action.isSignedBy(_signer, _sig);
return hasPermission && isValid;
}
I haven't written any tests for this, but this allows, for example, a multisig to own an identity that owns another identity that can sign 0x orders and operate with off-chain tooling that expects signatures.
we'll need to include more information
So, the information that you added was the account for which you need to verify validity, right?
That's an interesting thing to point out. With ecrecover
the signature and the signed message get you back the account. But there's no way to do that when the signer is a contract, so you need to know what account you will validate the signature against. IMO this doesn't affect this EIP at all, but it would be valuable to add a note about it in the document, because it will affect the design of APIs which do signature validation.
Regarding (action, proof)
vs (data, signature)
@shrugs I do like both term pairs ; "proof" is probably more future proof, while "signature" will be more easily understood for a while given the current practices in the space. Action might not be general enough however, where it's not clear how the term "action" fits with the hash of an 0x order for example. Regardless, we can always change the terms in the future since argument naming changes should be backward compatible.
Regarding bytes
vs bytes32
Both allow for passing extra data to the receiving contract, so it's really a question of efficiency. It does seem like both have tradeoffs ; bytes32
will be cheaper when people sign hashes, while bytes
will be cheaper when receiver contracts need additional data. I am fairly convinced that in the next few years identity contracts will allow for fine grain action based permissions for different private keys (e.g. address A
can sell any assets, while address B
can only sell cryptokitties). However, identity contracts are still quite primitive and I agree that most data being signed will be hashes for a while.
In terms of converting bytes32
to bytes
, you can use abi.encodePacked()
, such as ;
contract C {
function toBytes() public pure returns (bytes) {
bytes32 a;
return abi.encodePacked(a);
}
}
which costs about 210 gas. Not sure about bytes => bytes32
however.
Edit : Christian from the Solidity team told me the abi.decode()
will be able to do bytes => bytes32
if the bytes
array is only composed of a bytes32
, which is true in the case of passing a hash. Hence, it seems to me like we should assume converting bytes <=> bytes32
should be relatively straightforward in the near future.
Regarding passing signer as argument
That's a good point @shrugs. As a side note, I think ISignatureDelegate(_signer).isValidAction(_signer, _hash, _sig);
will not allow an EOA account to have use a signature validator contract, since _signer.supportsInterface(InterfaceId_SignatureDelegate)
would always return false when signer is an EOA. Imo, if your EOA holds your fund and you want to use a specific signature scheme, something like what 0x v2 is doing might be more appropriate ;
isValid = IValidator(validatorAddress).isValidSignature(
hash,
signerAddress,
signature
);
Code from here.
The above also works for contracts being signers. Also, why couldn't signer
be also part of the signature()
argument instead of an additional argument? @abandeali1 you guys probably thought about having signerAddress
as part of signature
vs as a separate argument.
Although I'm realizing there still needs to be a way to recover the contract signer for a signature. Maybe it could be the highly compacted chain of contract addresses like [address] [address] [v] [r] [s]
? Then you can remove the prefix and send it along to the next one while checking permissions.
@phabcd if the account ~different~ doesn't support the interface, it's expected to be an EOA account that signed on behalf of itself so imo that still makes sense. Is that right, you think?
@phabcd if the account different support the interface, it's expected to be an EOA account that signed on behalf of itself so imo that still makes sense. Is that right, you think?
With respect to what? I'm not sure I get what this is specifically referring to.
sorry, was on mobile;
basically, the logic to evaluate isValidSignature
is
1) does the _signerAddress have the permission in my security model?
2) is that signature valid? in which case we either
1) if supports interface, delegate to the _signerAddress.isValidSignature(hash, signature)
2) otherwise, expect that this is an EOA account so check that _signerAddress = hash.toEthSignedMessage.recover(signature)
Ah, I understand. So the idea behind having an EOA account using a signature validator contract is for the scenario where the user wants to hold its funds in EOA but does not want to use ECDSA signature scheme, hence need to use a validator contract. It's a pretty niche case, but you could imagine things like BLS signatures or other aggregation signature methods to more efficiently verify many signatures in batch or even quantum secure signature schemes.
I think we're misunderstood, again; I hadn't considered that case, actually.
Removing any preconditions, here is some proposed code.
// interface
function isValidSignature(bytes _action, bytes _sig)
function isSignedBy(bytes _action, address _signer, bytes _sig)
internal
view
returns (bool)
{
// if the signer address supports signature validation, ask for its permissions/validity
// which means _sig can be anything
if (_signer.supportsInterface(InterfaceId_SignatureDelegate)) {
return ISignatureValidator(_signer).isValidSignature(_action, _sig);
}
// otherwise make sure the hash was personally signed by the EOA account
// which means _sig should be highly compacted vrs
bytes32 signedHash = toEthSignedMessageHash(BytesConverter.toBytes32(_action));
return _signer == recover(signedHash, _sig);
}
// "isValidSignature" implementation for identity contracts
/**
* @dev An action is valid iff the _sig of the _action is from an key with the ACTION purpose
* @param _action
* @param _sig [[address] [address] [...]] <address> <v> <r> <s>
*/
function isValidSignature(bytes _action, bytes _sig)
view
public
returns (bool)
{
// the fact that this method has been called means the caller knows/expects that
// this contract has permission to do a thing.
(nextSigner, sig) = splitNextSignerAndSig(_sig);
// permission
bytes32 keyId = KeyUtils.idForAddress(nextSigner);
bool hasPermission = keyHasPurpose(keyId, PURPOSE_ACTION);
// validity
bool isValid = _action.isSignedBy(nextSigner, _sig);
return hasPermission && isValid;
}
// now the 0x contracts can do something like
// https://github.com/0xProject/0x-monorepo/blob/development/packages/contracts/src/2.0.0/protocol/Exchange/MixinSignatureValidator.sol#L188
BytesConverter.toBytes(hash).isSignedBy(signerAddress, signature);
// ...
and everything should cascade downwards.
So identity contracts can own identity contracts (i.e., multisigs can own multisigs) and we can encoded arbitrary validation logic as well (your identity contract would support BLS verification).
The recursive nature of validation is what I think we were missing. without it, we can only do single-level signature validation.
So identity contracts can own identity contracts (i.e., multisigs can own multisigs) and we can encoded arbitrary validation logic as well (your identity contract would support BLS verification).
Is there anything in the current EIP text that you think doesn't enable this?
I was going to suggest that isSignedBy
should simply check if the signer is a contract, and not necessarily use supportsInterface
, but I suppose this is a problem because of selector collisions, right? I mean, that a contract may reply to isValidSignature
even if it doesn't really implement this method. This could be a real problem.
presumably we're acting on the contract's behalf, so if it lied and said something was authorized when it wasn't, that'd be its own issue... but I'd have to take some time to think of attack scenarios.
@frangio if a contract doesn't support a method, it might just return garbage data; that's why 165 does two calls to make doubly sure that it actually supports the supportInterface(bytes4)
interface.
But the current interface definition in the spec is fine, since it claims bytes
on both arguments, yeah. Once we get abi.decode
, though, we'll be able to do this parsing of the array much easier. It was just merged in solidity's repo, but idk if it's worth waiting for it to land in order to change this spec to use it?
Great conversation! Aragon is currently implementing an Actor app (WIP implementation, spec and discussion) that allows DAOs to interact with other protocols, and implements signature validation as well.
After reading the discussion, for signature validation we have opted for implementing both isValidSignature(bytes32 hash, bytes sig)
and isValidSignature(bytes data, bytes sig)
(just performs isValidSignature(keccak256(data), sig)
), but when the Actor app has a designated signer that is also a contract, it uses the isValidSignature(bytes32 hash, bytes sig)
.
Even though isValidSignature(bytes data, bytes sig)
seems more flexible, given that there are so many ways that protocols can decide how to hash the data that will be signed (i.e. ERC712 requires multiple hashes and it is not as easy as hash = keccak256(data)
), it seems more complicated to rely on Identity contracts to implement every possible transformation from data to the hash. Also given that msg.sender
cannot be relied on for hashing the data in some way or another (as isValidSignature
may have been forwarded by another contract).
As proposed by @abandeali1, in case an implementation of isValidSignature
needs to behave differently depending on what data is being signed, a proof of what type of data (or a proof to a particular value within the hash) is being signed can be provided, appended to the signature bytes array.
That being said, I think getting this initial ERC draft finalized soon is really important, to avoid more protocols and contracts being deployed that either only use ECDSA signatures or a one-off isValidSignature
type check isn't standard. A standard for having contracts 'sign data' is already really useful and it will be very beneficial for the ecosystem now. Another standard for how to differentiate the data being signed and be able to have more granular rules can be worked on later, and being able to pass arbitrary data in the signature field is future-proof enough IMO.
PS: I really like the idea of using proof
instead of signature
, specially considering any data different from a signature can be passed.
This is definitely something we will need for smart contract based accounts.
If I understood correctly though, such function is intended to be implemented by an identity contract and as such any application relying on it to validate signed messages becomes vulnerable to such contract revoking signatures at will.
Imagine a application's smart contract who accept signed messages from anyone and the users rely on these signature being valid for the duration of the process. Let say a user could get punished (withdraw from a deposit) from the content of the signed message. It could be some sort of state channel mechanism for example.
Now when it is time for a user to publish the signed message, if the smart contract ask the identity smart contract for the validity of these messages through the function mentioned in this proposal, the malicious user could decide to revoke its signature through its identity contract and avoid to be punished. As such application smart contracts would need to trust these identity smart contract in order to provide a fair process. Unless we come up with only one (or a limited set) of these identity contract and everybody use it (unlikely), this is not an option.
I was imagining another possibility for validating messages. We could have a signer registry who allow any addresses (including identity smart contract) to declare the signers allowed to sign on behalf of them. Only one of such registry would be need to be trusted and only one would need to exist (think like #820)
While the identity contracts should be still able to revoke approved signers using such registry, they should not be able to do instantly since it could break the trust of time based processes.
The idea would be that when an address ask the registry to revoke one of its signer, the registry timestamp the request so other smart contract can continue to apply time based logic on the signature and refuse to accept old enough signature but still accept recently revoked one.
function approve(address signer)
function revoke(address signer)
function hasBeenApproved(address signer) returns (boolean)
function revokeTimestamp(address signer) returns (uint256 timestamp) // 0 mean it has not been revoked
to check signer validity you need to first call hasBeenApproved
which will always return true once a signer has been approved once. Hence you also need to check when was it revoked if ever by calling revokeTimestamp
it it returns 0 you are good to go, if it return a specific timestamp it will depend on your application logic.
Note by the way that the registry solution as presented would only allow EOA signers but there might be way for the identity smart contract to register a validator smart contract (that could use the function isValidSignature(bytes32 hash, bytes sig)
mentioned here) as one acting on its behalf. Though it will face the same problem as stated above : such smart contract could revoke access at will, depending of its code.
To solve this issue, we could propose an approved set of validator smart contract whose role is clearly defined (supporting specific signature protocols) and does nothing else than approving signatures. As new standard comes into being we could update the set.
@wighawag brings up a super interesting point. The semantics of cryptographic signatures cannot be replicated in identity contracts as generally as we attempted in this ERC. Having a central registry would solve a part of the problem, in that a signature can be guaranteed to remain valid once it's been observed valid on-chain. However, a cryptographic signature observed valid off-chain will also be valid on-chain, which isn't necessarily true for identity contracts. This could be problematic for some protocols.
I think the ERC is useful, but these subtleties will be confusing to users and render it tricky to use correctly. What does everyone else think?
@izqui
As proposed by @abandeali1, in case an implementation of isValidSignature needs to behave differently depending on what data is being signed, a proof of what type of data (or a proof to a particular value within the hash) is being signed can be provided, appended to the signature bytes array.
I think this is probably the easiest solution in order to have one function only instead of two. isValidSignature(hash, proof)
where proof
seem to encompass both the signature and additional information that would be required for the function to return true. Implementing both could help gathering data however on which method is the most appropriate.
Perhaps we could say isValidSignature(bytes32,bytes)
is mandatory and isValidSignature(bytes,bytes)
is optional?
@wighawag
Let say a user could get punished (withdraw from a deposit) from the content of the signed message. It could be some sort of state channel mechanism for example.
Yes, that is a risk indeed, where now the users need to make sure that the signature scheme of the party they are interacting with is sound, which is not trivial. Using a common registry with curated signature functions would be one way, but it might be hard/complicated to satisfy all possible "proof" requirements that projects might have.
I would argue that perhaps state-channels and applications where signatures are use for accountability (slashing, fault attrition, etc.) should force their users to agree on a given signature scheme when not using EOA accounts. Pointing to a signature library could be one of the parameter of the state-channel.
Nevertheless, it seems clear that having wallet.isValidSignature()
on the contract is not sufficient and that we also need something like validatorContract.isValidSignature()
. This is something that 0x also supports.
Any insights @abandeali1 ?
The above also works for contracts being signers. Also, why couldn't signer be also part of the signature() argument instead of an additional argument? @abandeali1 you guys probably thought about having signerAddress as part of signature vs as a separate argument.
There is a fine balance IMO of what params should be included in the signature
byte array and what should be passed in separately. The more data included in the signature, the less obvious it becomes for contracts to encode that data. You also end up with redundant data in the signature
, which is going to cost more gas (in the case of 0x orders, now the signerAddress
would be included in both the order and in the signature).
I think this is probably better off as 2 different standards for Wallets and Validators. Validators require some user approval to use, where that approval can be baked into Wallet contracts. In the short term, a standard for Wallets/smart accounts is probably more useful.
Perhaps we could say isValidSignature(bytes32,bytes) is mandatory and isValidSignature(bytes,bytes) is optional?
I actually like having both. In the next version of 0x there will probably be a signature type that passes in the entire order as well as a signature so that the validating contract can make arbitrary assertions about an order.
The concern @wighawag brings up is certainly interesting, but in my opinion this shouldn't be addressed in this EIP, it should just be made clear that this can happen.
I would add a couple of security features to the EIP, to avoid it being maliciously used with an attack similar to Gas token minting attack:
isValidSignature
implementations should assume they are always called with a staticcall
, and therefore MUST NOT modify state or call functions that modify state. Calling isValidSignature
SHOULD be done with a staticcall
.isValidSignature
function should be called with a certain amount of maximum gas (min(gasleft, SIGNATURE_CHECK_GAS)
). I'm not sure what this amount should be as verifying some types of signatures may require a large amount of gas, but just forwarding all the remaining gas to the signature check contract seems like a target for attacks (as non-sofisticated users don't check the gas fee).@abandeali1
I actually like having both. In the next version of 0x there will probably be a signature type that passes in the entire order as well as a signature so that the validating contract can make arbitrary assertions about an order.
And why not pass the order in the signature
argument in this case? For simplicity? Are you suggesting the signer contract would implement both functions (overload) and the calling contract would call either method?
@izqui
Calling isValidSignature SHOULD be done with a
STATICCALL
I'm in favor of this. However, it does prevent some interesting things (e.g. a certain user can only execute x trades a day), which would require an additional call. Perhaps this latter limitation should not be the focus of this ERC.
The isValidSignature function should be called with a certain amount of maximum gas
(min(gasleft, SIGNATURE_CHECK_GAS))
.
This could be quite hard to agree on a given value. I can imagine people implementing complex signature/proof validation (e.g., multi-sig, BLS, SNARK, etc.) and it feel odd to me to limit this. This is something dapps should protect against imo. E.g. in the case of 0x, if filling an order O
costs 2m gas to settle, then this can be added to the "fee" amount displayed.
Hi,
isValidSignature() should not return a bool but a magic number, since every function returns 1 (or true) on success. We must be sure that the act of checking the signature was effectively done, and not having to deal with a misconfigured contract with a default function that will return true because it did not revert, and misinterpreting the return value.
Making mandatory to register the interface isValidSignature() in ERC820 may be an alternative to be sure that the interface is present. This is my preferred choice.
@PhABC
@izqui
Calling isValidSignature SHOULD be done with a
STATICCALL
I'm in favor of this. However, it does prevent some interesting things (e.g. a certain user can only execute x trades a day), which would require an additional call. Perhaps this latter limitation should not be the focus of this ERC.
I think the contract may want to update the state. I think about replay attack that must be prevented, so a nonce may be used. In a 2-step verification scheme, the first step must write the state.
every function returns 1 (or true) on success.
This is not true, functions have no return data by default. A magic number would protect against function selector collisions though. Thoughts?
I think nonces and daily limits should be dealt with at a different layer. This ERC is only meant to provide a mechanism to check a signature against a signer. The contract using this interface can then judge whether to allow use of the signature in their protocol, possibly depending on the signed data contents.
The only argument I can think of against making isValidSignature
staticcall-able is that it prevents optimizing through caching of signatures. But it might be not that big of a deal.
This ERC should include a list of differences with cryptographic signatures to keep in mind. We've already mentioned that "validity" of a signature can change over time. Another one I just thought of is that usually in cryptography a signer can't claim to have signed an arbitrary signature, whereas this protocol makes it easy to do so (just by returning true
).
If we make it staticcall
-able I think we can safely do without a gas limit (it wouldn't be possible to mint gas token or anything like it), which is a huge argument in favor of it.
I think state modification should never be done in this function, specially since the signature is a public view
and Solidity 0.5 will do a static call by default.
every function returns 1 (or true) on success.
This is not true, functions have no return data by default. A magic number would protect against function selector collisions though. Thoughts?
Sorry, I made a confusion with status reply and return value. I correct: if the function called returns a non null value, this will be interpreted as true. When the default function is used as proxy, return values are often copied to be returned. We don't know what function will be called, we can only make assumptions. A magic value would remove any doubt. What would be the purpose of this ERC if some doubt are left about the intention of the verifier ?
All the cryptographic layer uses the principle "Anything but the right answer must fail", we should copy this. Currently, as we expect a non null value, it is "Anything but the wrong answer must success".
Hey all, updated the draft a bit to include some of the things discussed that reached some consensus (see PR here).
pragma solidity ^0.5.0;
contract ERC1271 {
// bytes4(keccak256("isValidSignature(bytes,bytes)")
bytes4 constant internal MAGICVALUE = 0x20c13b0b;
/**
* @dev Should return whether the signature provided is valid for the provided data
* @param _data Arbitrary length data signed on the behalf of address(this)
* @param _signature Signature byte array associated with _data
*
* MUST return the bytes4 magic value 0x20c13b0b when function passes.
* MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5)
* MUST allow external calls
*/
function isValidSignature(
bytes32 memory _data,
bytes memory _signature)
public
view
returns (bytes4 magicValue);
}
Things included :
STATICCALL
(as per @izqui 's suggestion)Things not included :
isValidSignature(bytes32, bytes)
and isValidSignature(bytes,bytes)
(per @izqui
and @abandeali1 's suggestion)One thing I've been pondering regarding @wighawag 's comment on how signature validation can change is if there was a way to have a function where validity can't change. Would having a isValidSignature()
function that is pure
work? If the function has no access to any storage, would this solve the problem? This is quite limited since it would not permit a lot of things, but wondering if there is something one could do that would render the signature "set in stone" and that would be easily verifiable.
I did not include both version of isValidSignature()
for now, wanted to get people's opinion first.
@PhABC Unless the full contract is pure, it is possible for the smart contract to change the logic of any function and thus bypass the purity of the function. Also purity is actually only enforced by solidity.
It would be nice though to have a create
evm operation variant that create only pure smart contract by blocking such smart contract to read or write state.
In that case using a registry smart contract, we could make identity contract delegate signature verification to such pure contract, since their logic would not change. Such registry would still need to implement revocation as I mentioned above.
You can always check the purity of the contract you are going to call by doing EXTCODECOPY
and analyzing the bytecode looking for blacklisted opcodes (calls, sload or whatever you want to protect against). But again, I think this should be out of the scope of this ERC.
@izqui Blacklisting is a not a solution as this would not be future proof since new opcode could allow to change state. Whitelisting, which would be a larger check, on the other hand would not allow new opcodes, which might restrict your contract to certain set of signature verifiers, so you would not be able to verify signature from such valid contracts even if they follow the rules of the standard. Plus checking bytecode like that for this purpose is not a nice pattern.
I don't think it is out of scope of this ERC, especially if we could come up with a solution but I also agree that this ERC is valid without it. It just limit its applicability.
I am fine either way, just commenting on potential issues and solutions.
Opcode checking is not super cheap but it is definitely doable, checking a whitelist is as expensive as a blacklist if you just use bitmaps.
It is doable, not arguing against that. And you are right that the gas difference is zero between whitelisting and blacklisting when using bitmaps (though there might be a more optimized checker for blacklist (due to the small amount of opcode to check))
But this is irrelevant anyway since it does not answer the most important point: whitelisting opcodes will make future smart contract (using new opcodes) that are semantically pure to fail to be accepted by contract that do such checking.
As such using such low level checks will run against the idea of the standard when new opcode are introduced.
Opcode checking is not super cheap but it is definitely doable [...]
It is doable, not arguing against that. [...]
I am. :)
Opcode checking is harder than it looks: https://gist.github.com/Arachnid/e8f0638dc9f5687ff8170a95c47eac1e#gistcomment-2662974
EVM has non-analyzable control-flow which makes accurate static analysis impossible. (WebAssembly is designed not to have this issue. There are proposals to break EVM to add this quality.)
Hey, we at cryptokitties.co would also like to see this problem solved.
We wrote up an alternate solution (https://github.com/ethereum/EIPs/issues/1654) that piggybacks on ERC-725 before we realized that this proposal was already in the works.
We would like to outline why we think there is still some value to the alternate solution:
We would be happy to hear the thoughts of folks on the thread.
Following a discussion with @PhABC at https://github.com/ethereum/EIPs/issues/1654, we'd like to pivot 1654 to be based on this EIP-1271 proposal instead of EIP-725.
Even though initially we thought 725 might be simpler as a base for off-chain authentication, we do come to appreciate the flexibility of 1271. We see the flexibility to support multi-sig wallets as an important feature.
Hey all, I would like to push this simple EIP forward and it seems like the only thing still in the air is supporting one or both isValidSignature(bytes32, bytes)
and isValidSignature(bytes, bytes)
. I personally think this EIP should support both functions, where a wallet that enforces data verification could revert (or return false) if the (bytes32, bytes)
is called.
Wallets that do not have conditions will probably want to implement the (bytes32,bytes)
function, but more complex wallets might want to verify the information themselves via the (bytes, bytes)
function.
What I would suggest is that wallet contract can implement either or both and the responsibility would be on the calling contract to know which method to call (similar to how @izqui is handling it). For instance, a dapp could require user to pass signature type as follow:
enum SignatureType {
Invalid,
WalletBytes,
WalletBytes32,
}
Depending on the user, they would specify for the dapp which method their wallet implements.
To be more explicit, wallets could also implement both functions. In the case of a wallet contract wants to impose some validity conditions that depends on what is being signed, it could implement the functions as follow:
isValidSignature(bytes32 hash, bytes signature) returns (bool) {
revert('INVALID_DATA');
}
isValidSignature(bytes data, bytes signature) returns (bool) {
...
}
In the case where a wallet does not have conditional logic and only cares about the hashes, it could implement the functions as follow:
isValidSignature(bytes32 hash, bytes signature) returns (bool) {
...
}
isValidSignature(bytes data, bytes signature) returns (bool) {
return isValidSignature(keccak256(data), signature);
}
In all cases, the dapp contract benefits from knowing which method to call on the wallet contract.
Other question:
Should isValidSignature
be able to revert or should it always return magical value / 0xdeadbeef
?
I agree with supporting both and leaving it to the dapp/user to decide which to use.
Should isValidSignature be able to revert or should it always return magical value / 0xdeadbeef?
Always returning something seems easier to develop with.
Another question: should these functions be allowed to update state? (i.e should they always be called via a staticcall
?)
Simple Description
Many blockchain based applications allow users to sign off-chain messages instead of directly requesting users to do an on-chain transaction. This is the case for decentralized exchanges with off-chain orderbooks like 0x and etherdelta. These applications usually assume that the message will be signed by the same address that owns the assets. However, one can hold assets directly in their regular account (controlled by a private key) or in a smart contract that acts as a wallet (e.g. a multisig contract). The current design of many smart contracts prevent contract based accounts from interacting with them, since contracts do not possess private keys and therefore can not directly sign messages. The proposal here outlines a standard way for contracts to verify if a provided signature is valid when the account is a contract.
See EIP draft.