Skip to content
This repository has been archived by the owner on Jul 9, 2021. It is now read-only.

Signature generalisation #376

Merged
merged 26 commits into from
Mar 1, 2018
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1f03bb7
Abstract signature to opaque bytearray
recmo Feb 10, 2018
2d8b77a
Add 'caller' signature type
recmo Feb 7, 2018
731d71e
Revert after cascade
recmo Feb 14, 2018
d99a8d2
Add external contract call signatures (relates to ZEIP 1 and 7)
recmo Feb 14, 2018
69484dd
Add EIP712 signatures (implements ZEIP-17)
recmo Feb 7, 2018
46b4ceb
Cache order maker signature (ZEIP-15)
recmo Feb 9, 2018
0fd2ded
Cancel signature abstraction
recmo Feb 7, 2018
c5d5c10
Implement EIP712 at verify-signature call site
recmo Feb 13, 2018
a348438
Make first value illegal
recmo Feb 21, 2018
2142890
Add SignatureType.Invalid and documentation
recmo Feb 22, 2018
5faeb90
Fix input length in fillOrderNoThrow
recmo Feb 22, 2018
3a3610b
Fix batchFillOrdersNoThrow name
recmo Feb 22, 2018
cdf7c4f
Fix hash argument name
recmo Feb 22, 2018
596bd4f
Revert cancel order signature abstraction
recmo Feb 22, 2018
6d4d9cb
Explicit returns
recmo Feb 22, 2018
3f39bc4
Make wrappers external again
recmo Feb 22, 2018
a9c810d
Add documentation
recmo Feb 23, 2018
3f130d7
Spelling fixes
recmo Feb 23, 2018
0c4b952
Remove unused return value
recmo Feb 23, 2018
b5a0c7d
Verify length on EIP712 signatures
recmo Feb 23, 2018
a8cec97
Fix documentation
recmo Feb 23, 2018
43e5bd2
Add Todos
recmo Feb 23, 2018
753f9b3
Add Trezor signatures
recmo Feb 24, 2018
0d9a464
Document noThrow wrapper and correct fixed array offset
recmo Feb 26, 2018
e5e6e8c
Add offset to signature
recmo Feb 26, 2018
095388f
Fix spelling of latter
recmo Feb 27, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@
*/

pragma solidity ^0.4.19;
pragma experimental ABIEncoderV2;

import "./MixinExchangeCore.sol";
import "./MixinSignatureValidatorEcrecover.sol";
import "./MixinSignatureValidator.sol";
import "./MixinSettlementProxy.sol";
import "./MixinWrapperFunctions.sol";

contract Exchange is
MixinExchangeCore,
MixinSignatureValidatorEcrecover,
MixinSignatureValidator,
MixinSettlementProxy,
MixinWrapperFunctions
{
Expand All @@ -37,7 +38,7 @@ contract Exchange is
)
public
MixinExchangeCore()
MixinSignatureValidatorEcrecover()
MixinSignatureValidator()
MixinSettlementProxy(_tokenTransferProxy, _zrxToken)
MixinWrapperFunctions()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,11 @@

pragma solidity ^0.4.19;

import "./mixins/MSignatureValidator.sol";

/// @dev Provides MSignatureValidator
contract MixinSignatureValidatorEcrecover is
MSignatureValidator
{
contract ISigner {

function isValidSignature(
address signer,
bytes32 hash,
uint8 v,
bytes32 r,
bytes32 s)
public
constant
returns (bool isValid)
{
isValid = signer == ecrecover(
keccak256("\x19Ethereum Signed Message:\n32", hash),
v,
r,
s
);
return isValid;
}
bytes signature)
public view
returns (bool isValid);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,22 @@
pragma solidity ^0.4.19;

contract LibOrder {


bytes32 constant orderSchemaHash = keccak256(
"address exchangeContractAddress",
"address makerAddress",
"address takerAddress",
"address makerTokenAddress",
"address takerTokenAddress",
"address feeRecipientAddress",
"uint256 makerTokenAmount",
"uint256 takerTokenAmount",
"uint256 makerFeeAmount",
"uint256 takerFeeAmount",
"uint256 expirationTimestamp",
"uint256 salt"
);

struct Order {
address maker;
address taker;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,15 @@ contract MixinExchangeCore is
/// @param orderAddresses Array of order's maker, taker, makerToken, takerToken, and feeRecipient.
/// @param orderValues Array of order's makerTokenAmount, takerTokenAmount, makerFee, takerFee, expirationTimestampInSec, and salt.
/// @param takerTokenFillAmount Desired amount of takerToken to fill.
/// @param v ECDSA signature parameter v.
/// @param r ECDSA signature parameters r.
/// @param s ECDSA signature parameters s.
/// @param signature Proof of signing order by maker.
/// @return Total amount of takerToken filled in trade.
function fillOrder(
address[5] orderAddresses,
uint256[6] orderValues,
uint256 takerTokenFillAmount,
uint8 v,
bytes32 r,
bytes32 s)
public
returns (uint256 takerTokenFilledAmount)
address[5] orderAddresses,
uint[6] orderValues,
uint takerTokenFillAmount,
bytes signature)
public
returns (uint256 takerTokenFilledAmount)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this the correct indentation?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, there was extra indentation before.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's the convention I'm used to and also matches with what I see in the tokenRegistery contract.

{
Order memory order = Order({
maker: orderAddresses[0],
Expand All @@ -100,29 +96,40 @@ contract MixinExchangeCore is
expirationTimestampInSec: orderValues[4],
orderHash: getOrderHash(orderAddresses, orderValues)
});

// Validate order and maker only if first time seen
// TODO: Read filled and cancelled only once
if (filled[order.orderHash] == 0 && cancelled[order.orderHash] == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have any data on how expensive is the signature validation? Two state reads also consume some gas.

Copy link
Member

Choose a reason for hiding this comment

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

An ecrecover is at least 3000 gas (and your gas profiler actually showed it being one of the most expensive parts of the function). An sload is only 200 I believe.

Copy link
Contributor Author

@recmo recmo Feb 23, 2018

Choose a reason for hiding this comment

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

Good question! ecrecover is 3000 gas + 36 for the hash + couple hundred overhead. State read is 200 gas. Let's ignore (partially) canceled orders for now and look at two scenarios:

  1. Unfilled order: Read both filled and cancelled, then verify signature ~ 4000 gas.
  2. Partially filled order: Read filled, short circuit, skip verify ~ 200 gas.

So depending on the ratio partial filled / new orders the expect gas cost is 4000 x + 200 (1-x) =
3800 x + 200.

Status quo is a fixed cost of about 3500. Setting these equal and solving for x gives 87%.

So if less than 87% of the fill orders are new orders, this method saves gas.

@tomhschmidt : Do we have empirical data on this?

But!

We need to optimize for the case where the order is valid. In this case, we would need to read both state variables anyway to process the order, so reading it earlier should not add gas cost. Making the optimization essentially free.

At the moment we don't optimize for this, and we read the same state again in getUnavailableTakerTokenAmount on line 124(244) and again on line 138. This is of course
redundant, as the state can not change in between.

I added a comment to remind us to optimize this.

require(order.makerTokenAmount > 0);
require(order.takerTokenAmount > 0);
require(isValidSignature(
keccak256(orderSchemaHash, order.orderHash),
order.maker,
signature
));
}

// Validate taker
if (order.taker != address(0)) {
Copy link
Member

@abandeali1 abandeali1 Feb 22, 2018

Choose a reason for hiding this comment

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

Is there an efficiency difference between this if block vs require(order.taker == address(0) || order.taker == msg.sender)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not that I know of. It is like this mostly because I reverted the taker abstraction. With taker abstraction we need a more complex expression than order.taker == msg.sender.

require(order.taker == msg.sender);
}
require(takerTokenFillAmount > 0);

require(order.taker == address(0) || order.taker == msg.sender);
require(order.makerTokenAmount > 0 && order.takerTokenAmount > 0 && takerTokenFillAmount > 0);
require(isValidSignature(
order.maker,
order.orderHash,
v,
r,
s
));

// Validate order expiration
if (block.timestamp >= order.expirationTimestampInSec) {
LogError(uint8(Errors.ORDER_EXPIRED), order.orderHash);
return 0;
}


// Validate order availability
uint256 remainingTakerTokenAmount = safeSub(order.takerTokenAmount, getUnavailableTakerTokenAmount(order.orderHash));
takerTokenFilledAmount = min256(takerTokenFillAmount, remainingTakerTokenAmount);
if (takerTokenFilledAmount == 0) {
LogError(uint8(Errors.ORDER_FULLY_FILLED_OR_CANCELLED), order.orderHash);
return 0;
}


// Validate fill order rounding
if (isRoundingError(takerTokenFilledAmount, order.takerTokenAmount, order.makerTokenAmount)) {
LogError(uint8(Errors.ROUNDING_ERROR_TOO_LARGE), order.orderHash);
return 0;
Expand Down Expand Up @@ -177,8 +184,10 @@ contract MixinExchangeCore is
orderHash: getOrderHash(orderAddresses, orderValues)
});

require(order.makerTokenAmount > 0);
Copy link
Member

Choose a reason for hiding this comment

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

Why this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I used to do assert(someThing && somethingElse && somethingOther) a lot in a past project. Then one day we get a stack trace from a customer, and it says basically: "assertion failed on line ...". We wasted a lot of time trying to figure out which of the clauses failed and learned a hard lesson on never using && in an assertion :)

If there is no performance difference, I prefer not using && in assertions. Imho it makes it more readable and debuggable.

require(order.takerTokenAmount > 0);
require(takerTokenCancelAmount > 0);
require(order.maker == msg.sender);
require(order.makerTokenAmount > 0 && order.takerTokenAmount > 0 && takerTokenCancelAmount > 0);

if (block.timestamp >= order.expirationTimestampInSec) {
LogError(uint8(Errors.ORDER_EXPIRED), order.orderHash);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*

Copyright 2017 ZeroEx Intl.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

*/

pragma solidity ^0.4.19;

import "./mixins/MSignatureValidator.sol";
import "./ISigner.sol";

/// @dev Provides MSignatureValidator
contract MixinSignatureValidator is
MSignatureValidator
{
enum SignatureType {
Illegal, // Default value
Invalid,
Caller,
Ecrecover,
EIP712,
Trezor,
Contract
}

function isValidSignature(
bytes32 hash,
address signer,
bytes signature)
public view
returns (bool isValid)
{
// TODO: Domain separation: make hash depend on role. (Taker sig should not be valid as maker sig, etc.)

require(signature.length >= 1);
SignatureType signatureType = SignatureType(uint8(signature[0]));

// Variables are not scoped in Solidity
uint8 v;
bytes32 r;
bytes32 s;
address recovered;

// Always illegal signature
// This is always an implicit option since a signer can create a
// signature array with invalid type or length. We may as well make
// it an explicit option. This aids testing and analysis. It is
// also the initialization value for the enum type.
if (signatureType == SignatureType.Illegal) {
Copy link
Contributor

Choose a reason for hiding this comment

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

All those ifs add to understanding but increase the gas consumption. Will we comment them out in a production version?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was not planning on that, and we need some way to distinguish the cases. What do you suggest instead?

A simple trick is to sort the branches by popularity.

revert();

// Always invalid signature
// Like Illegal, this is always implicitly available and therefore
// offered explicitly. It can be implicitly created by providing
// a correctly formatted but incorrect signature.
} else if (signatureType == SignatureType.Invalid) {
require(signature.length == 1);
isValid = false;
return isValid;

// Implicitly signed by caller
// The signer has initiated the call. In the case of non-contract
// accounts it means the transaction itself was signed.
// Example: let's say for a particular operation three signatures
// A, B and C are required. To submit the transaction, A and B can
// give a signature to C, who can then submit the transaction using
// `Caller` for his own signature. Or A and C can sign and B can
// submit using `Caller`. Having `Caller` allows this flexibility.
} else if (signatureType == SignatureType.Caller) {
Copy link
Member

Choose a reason for hiding this comment

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

Can you provide an example of how this is used? It seems to me that in the current contracts, msg.sender will always be order.maker.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The current PR excludes taker abstraction, so for a successful fillOrder, we always have msg.sender == order.taker. When taker abstract is added, this will no longer hold true, and this signature type can be used by either maker or taker if they call fillOrder.

In the current PR the cancelOrder function also takes a signature. If maker calls cancelOrder herself she can set the signature to SignatureType.Caller.

require(signature.length == 1);
Copy link
Member

Choose a reason for hiding this comment

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

Can you give an example of how this would be used? It seems like msg.sender will always be order.maker in this case, which seems a bit strange to me.

isValid = signer == msg.sender;
return isValid;

// Signed using web3.eth_sign
} else if (signatureType == SignatureType.Ecrecover) {
require(signature.length == 66);
v = uint8(signature[1]);
r = get32(signature, 2);
s = get32(signature, 34);
recovered = ecrecover(
Copy link
Member

Choose a reason for hiding this comment

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

address recovered

keccak256("\x19Ethereum Signed Message:\n32", hash),
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's add support for current Trezors which do not use ASCII characters for the message length encoding: ethereum/go-ethereum#14794.

Choose a reason for hiding this comment

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

I'm glad you're adding support for this, but can't give a thumbs up because it's obnoxious it's different in the first place.

Copy link
Contributor Author

@recmo recmo Feb 26, 2018

Choose a reason for hiding this comment

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

Actually, I need to side with the Trezor guys here. The way eth_sign does it is a security hole. It allows two different messages to have the same hash. I don't like their solution though, I would rather see some thing more standard like RLP encoding or ABI encoding.

Choose a reason for hiding this comment

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

Oh, agreed. varInt is much better than an ASCII representation. It's just so strange there doesn't appear to be an EIP around it and everyone (other than Trezor) just supports both ways like it's no big deal.

Copy link
Contributor

Choose a reason for hiding this comment

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

We are really trying to push the community towards ethereum/EIPs#712

v,
r,
s
);
isValid = signer == recovered;
return isValid;

// Signature using EIP712
} else if (signatureType == SignatureType.EIP712) {
require(signature.length == 66);
v = uint8(signature[1]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to assert the signature length?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes we do!

r = get32(signature, 2);
s = get32(signature, 34);
recovered = ecrecover(hash, v, r, s);
isValid = signer == recovered;
return isValid;

// Signature from Trezor hardware wallet
// It differs from web3.eth_sign in the encoding of message length
// (Bitcoin varint encoding vs ascii-decimal, the later is not
Copy link
Contributor

Choose a reason for hiding this comment

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

later -> latter

// self-terminating which leads to ambiguities).
// See also:
// https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer
// https://github.com/trezor/trezor-mcu/blob/master/firmware/ethereum.c#L602
// https://github.com/trezor/trezor-mcu/blob/master/firmware/crypto.c#L36
} else if (signatureType == SignatureType.Trezor) {
require(signature.length == 66);
v = uint8(signature[1]);
r = get32(signature, 2);
s = get32(signature, 34);
recovered = ecrecover(
keccak256("\x19Ethereum Signed Message:\n\x41", hash),
v,
r,
s
);
isValid = signer == recovered;
return isValid;

// Signature verified by signer contract
} else if (signatureType == SignatureType.Contract) {
isValid = ISigner(signer).isValidSignature(hash, signature);
Copy link
Contributor

Choose a reason for hiding this comment

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

isValidSignature seems too generic. Can we change to isValid0xOrderSignature or smth longer?

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 intended to be generic though. I could see people using the same function for other purposes as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As it is, it is pretty generic. Right now we only use it for maker signature, we will also use it for taker signature and maybe controller signature, etc. These would then sign different objects, not Orders's but FillOrder's and possibly other structures.

return isValid;
}

// Anything else is illegal (We do not return false because
// the signature may actually be valid, just not in a format
// that we currently support. In this case returning false
// may lead the caller to incorrectly believe that the
// signature was invalid.)
revert();
}

function get32(bytes b, uint256 index)
private pure
returns (bytes32 result)
{
require(b.length >= index + 32);

// Arrays are prefixed by a 256 bit length parameter
index += 32;

// Read the bytes32 from array memory
assembly {
result := mload(add(b, index))
}
return 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 an explicit return here.


}
Loading