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

Standard Functions for Preauthorized Actions #662

Open
alex-miller-0 opened this Issue Jul 3, 2017 · 46 comments

Comments

Projects
None yet
@alex-miller-0

alex-miller-0 commented Jul 3, 2017

Title

EIP: 662
Title: Standard Functions for Preauthorized Actions
Author: Alex Miller
Created: 2017-07-01
Version: 5

Specification

EIP661 is abstracted to include any stateful function.

A function is defined name provable_X, where X can be any state-updating function.

Note: The underscore is used to retain casing of the original function name.

This function may be executed by any actor with the correct parameters on the owner's behalf. It returns true if the state was successfully updated and false otherwise. The function may also be called locally to ensure a valid signature was passed or called.

Using provable functions

A hashed message is formed according to the following:

proof = sha3(sha3(...params), word, contract_address)

Where sha3 is the keccak-256 sha3 hash, ...params are the tightly packed parameters of the original function, word is the ABI definition of the provable function, and contract_address is the address of the contract where both the original and provable functions reside.

This message is signed by a user's private key and that signature can be passed by any Ethereum actor to the provable function as the first parameter.

Example

An example for ERC20's transfer function is as follows:

function provable_transfer(bytes32[3] sig, address to, uint value) returns (bool) {
  // First 4 bytes of keccak-256 hash of "transfer(bytes32[3],address,uint256)"
  bytes4 word = 0x5a43675c;
  bytes32 msg = sha3(sha3(address(to), uint(value)), bytes4(word), address(this), uint(nonce));
  address signer = ecrecover(msg, uint8(data[2]), data[0], data[1]); 
  uint nonce = nonces[signer];

  if (played[signer][msg] == true) { return false; }

  // Execute the original transfer function
  balances[signer] = safeSub(balances[signer], value);
  balances[to] = safeAdd(balances[to], value);
  Transfer(signer, to, value );

  // Update state variables
  played[signer][msg] = true;
  nonces[signer] += 1;
  
  return true;
}

The first parameter is an array where the following is true:

sig[1]    r of signature
sig[2]    s of signature
sig[3]    v of signature

Replay protection is added by checking the proof against an archived mapping and using nonces, which increment automatically for each signer:

mapping(address => mapping(bytes32 => bool)) played;
mapping(address => uint) nonces;

Rationale

These provable functions may be useful for applications that wish to call functions on the user's behalf without the user having to make a transaction. This would pass the gas cost on to the application.

The proposed methodology might be especially useful for 3rd-party token transfers, as this requires two transactions (the user must first approve some contract to move tokens and then that contract must be called to move the tokens).

However, this can be further extended to many non-token use cases. The proposed provable_X may be included with any function that the application wishes to be outsourced.

@emansipater

This comment has been minimized.

emansipater commented Jul 3, 2017

There are a lot more contracts than just ERC20 that will want to make use of this approach (multisig is an obvious example). Perhaps this should be made more general?

@alex-miller-0 alex-miller-0 changed the title from Provable Stateful Functions: ERC20 Extension to Provable Stateful Functions Jul 3, 2017

@alex-miller-0

This comment has been minimized.

alex-miller-0 commented Jul 3, 2017

Updated the EIP. I agree, it could be generalized beyond ERC20. Any stateful function could be outsourced with this standard.

Also updated naming convention. I imagine a 2nd layer application could look up whether a stateful function is extended by searching for isProvable_X, retaining the case of the original function.

@alexvandesande

This comment has been minimized.

Contributor

alexvandesande commented Jul 3, 2017

I'm not sure about the term "provable". I think "offline signed" or "off chain signed" transactions would be a better description?

In the particular token transfer case (which I think it's a great way to start the debate), I'd argue for a refactor, because now we have three functions that transfer tokens: transfer, transferFrom and this one. Then maybe it would be better that there should be an internal function to do the transfers and all the other functions would just be checking permissions.

Also I'd suggest adding a fee function to the proof: this would allow the original signer to post it to a public pool of transactions and then multiple services could compete to be the firsts to pick up and send the transaction and be paid in the tokens themselves.

Finally, what prevents proofs to be submitted multiple times? Maybe the contract should keep a record of submitted transactions to make sure they are not submitted over and over.

// Execute the original transfer function
function internal transfer(_from, _to, _value) {
  balances[_from] = safeSub(balances[_from], _value);
  balances[_to] = safeAdd(balances[_to], _value);
  Transfer(owner, to, value );
}

// Checks if the owner has a balance and then transfers
function transfer(_to, _value) {
    require(balanceOf[_from] > _value)
    transfer(msg.sender, _to, _value)
}

// Checks if the owner has authorization and then transfers
function transferFrom(_from, _to, _value) {
    require(allowance[msg,sender][_from] > _value);
    require(balanceOf[_from] > _value);
    transfer(msg.sender, _to, _value)
}
// Checks if the signed proof then transfers
function signedTransfer(address sender, bytes32[3] data, uint8 v, address to, uint value, uint fee) constant returns (bool) {
   // do all the proof checking here and remember this proof as submitted
   proofSubmitted[proof] = true;
    // If correct transfer value
    transfer(sender, _to, _value);
   // Pay fee in this currency to sender of this transaction
    transfer(sender, msg.owner, fee);
}

@christianlundkvist

This comment has been minimized.

christianlundkvist commented Jul 3, 2017

Cool stuff @alex-miller-0!

It's very close to what we've been working on at uPort - basically a service that can pay gas on behalf of the user and execute any Ethereum transaction through a user-controlled Proxy contract.

Main idea is to separate the paying of gas from access control:

uport-project/uport-identity#38

@alex-miller-0

This comment has been minimized.

alex-miller-0 commented Jul 3, 2017

an internal function to do the transfers and all the other functions would just be checking permissions.

Agreed. I like your implementation.

Also I'd suggest adding a fee function to the proof

This limits the spec to token transfers though, since you wouldn't be able to pay ether for outsourced transaction calls.

what prevents proofs to be submitted multiple times

Yeah, forgot to include replay protection. What about something like the following:

function provable_transfer(address owner, bytes32[3] data, uint8 v, uint expiration, address to, uint value) returns (bool) {
  // First 4 bytes of keccak-256 hash of "transfer"
  bytes4 word = 0xb483afd3;

  address signer = ecrecover(data[0], v, data[1], data[2]);
  if (signer != owner) { return false; }

  // Hash of the word, address of this contract, and all params of original function
  bytes32 proof = sha3(word, address(this), expiration, to, value);
  if (proof != data[0]) { return false; }
  else if (expiration < now) { return false; }
  else if (played[proof] == true) { return false; }

  // Execute the original transfer function
  balances[owner] = safeSub(balances[owner], value);
  balances[to] = safeAdd(balances[to], value);
  Transfer(owner, to, value );
  played[proof] = true;

  return true;
}

It would limit the spec to only functions with <4 parameters - not sure how useful that is.

@alex-miller-0

This comment has been minimized.

alex-miller-0 commented Jul 3, 2017

Aragon has a function that does what this spec is proposing. Note the arbitrarily sized bytes array.

@alexvandesande

This comment has been minimized.

Contributor

alexvandesande commented Jul 3, 2017

@alex-miller-0

Aragon does it in an interesting way. The problem with making arbitrary data calls is that the msg.sender will always be the transaction sender, not the message signer.

This limits the spec to token transfers though, since you wouldn't be able to pay ether for outsourced transaction calls.

That's true. But in any way you build it you'd need to have custom functions for it. We could have a generic token function that would send X tokens to msg.sender but then it would only make sense in this context. Other option is for the contract itself to calculate the fee on other factors (adjust fees to target a delay no longer than X minutes)

I think this shows that many projects are implementing basically the same ideas in different ways, and while it's encouraging to see different approaches, it means that some standardization would be very useful here. So the suggested pattern would be:

  1. Contract separates core functions (token transfers) from access functions (who and how can someone initiate a token transfer)

  2. Contract implements basic access controls (send token from msg.sender) and pre-auth access control (send token on behalf of signer)

  3. User signs message

  4. User shares message on whisper (or some other service)

  5. Transaction relayers compete to post messages to chain

  6. Optionally, contract rewards them for it somehow, either with ether directly or if a token contract, with tokens

@AlexeyAkhunov

This comment has been minimized.

Contributor

AlexeyAkhunov commented Jul 3, 2017

You need to also include some kind of nonce-mechanism to prevent using the same signed transfer multiple times. I am not sure what the mechanism should be, but I guess this is where it becomes tricky

@izqui

This comment has been minimized.

Contributor

izqui commented Jul 4, 2017

AFAIK the address owner parameter is redundant as you can always recover it from the signature, in the same way ETH transaction payloads don't have the from address.

Also something that could be interesting to think about is adding a fee parameter, that would go to the msg.sender of the transaction. This way we incentivize this party to send the token transfer because he is getting back some tokens in return for the ETH fee he had to pay.

@alex-miller-0

This comment has been minimized.

alex-miller-0 commented Jul 4, 2017

AFAIK the address owner parameter is redundant as you can always recover it from the signature, in the same way ETH transaction payloads don't have the from address.

Good point. Removed owner and also added replay protection with an archival mapping and expiration timestamp.

I'm still not sold on the fee parameter. That feels like a separate EIP to me. All I wanted to do here was write a standard for outsourcing transactions. I think the token transfer fee can be its own EIP frankly. What are everyone's thoughts?

@GNSPS

This comment has been minimized.

GNSPS commented Jul 4, 2017

I think the token transfer fee can be its own EIP frankly.

Yes, think so too. Mainly because as you put it before:

However, this can be further extended to many non-token use cases. The proposed provable_X and isProvable_X may be included with any function that the application wishes to be outsourced.

Which is way more generalist than what having a fee parameter implies.

I feel fee should either be dropped or the EIP renamed.

@emansipater

This comment has been minimized.

emansipater commented Jul 4, 2017

@alex-miller-0 @alexvandesande last summer when I was putting together something like this for multisig we did replay protection just like Aragon does it, except instead of combining the data itself with a nonce we just used a hash. So the part actually signed is:

hash(authed_hash, receivingContractAddress, nonce)

and that's what gets marked as spent on a per-address basis. Then the authed_hash is simply marked as having been authorised by the ecrecovered address, and after that point anyone is allowed to call a generic doAction(function,args[],authorisingAccount) command which just hashes the first two fields and checks if the hash has been authorised by the third, performing it with that authority if so. Performed actions are of course marked as no longer authorised. This has the advantage that you can have a second function expandActions(hashes[],authorisingAccount) which just hashes a whole list of hashes and checks if the root hash is authorised, so you can merkle together an entire series of actions and then just sign it once to authorise all of them.

To me that approach feels very simple and general. We could formalise it in an EIP and then just have an index of specific functions for that EIP that ERC20 tokens must support. If you want things like paying a fee to the publisher of the transaction (or any other complicated feature) it is simple: add an action that does that to your merkle tree.

@christianlundkvist I'm not surprised that uport is also doing something like this. Haven't had a chance to read through your approach but would the above make sense in your context as well?

@alexvandesande

This comment has been minimized.

Contributor

alexvandesande commented Jul 4, 2017

@emansipater

This comment has been minimized.

emansipater commented Jul 4, 2017

Oops, left out a parameter. It's actually supposed to be:

hash(authed_hash, receivingContractAdress, nonce)

because you want the actions to only be valid for one particular destination contract (in this example, one particular token contract).

@christianlundkvist

This comment has been minimized.

christianlundkvist commented Jul 4, 2017

@emansipater I really like the idea of having several transactions hashed/merkled together and doing one sig over all of them, very elegant! 😃

The method that uPort uses involves Proxy contracts, as in #121.

Here is a brief high-level overview of how uPort does this:

The user has a Proxy contract through which all transactions are routed. Another contract (Controller, or IdentityManager) is authorized to tell the Proxy to forward an arbitrary transaction (defined by a tuple (destination, value, data)).

The IdentityManager has a list of addresses that are authorized to forward transactions. There is a function something like forwardTx(sigV, sigR, sigS, destination, value, data) where the signature is over a data set like

(this, nonce, destination, value, data)

(see also here for similar thoughts: #191)

where nonce is a nonce for replay protection. If the signature is by one of the authorized addresses, then the "metatransaction" (destination, value, data) is forwarded and sent out from the Proxy. The receiving smart contract with address destination will see the Proxy address as msg.sender, but anyone is able to send the actual ETH transaction and pay for the gas.

There are some subtleties here also in that if the function throws, then the nonce is not updated. This might lead to narrow cases of replay attacks where an attacker can replay a transaction that previously failed.

@emansipater In general it seems there are two ways of handling this kind of delegation: either use a proxy contract and have the target smart contract do access control based on msg.sender, or have standardized functions in the target smart contract that knows how to interpret these kinds of "detached signatures" (as @alex-miller-0 is doing here).

uPort went with the former (proxy contracts) because it requires less buy-in from smart contract developers - they can just keep using msg.sender as they are used to, and any complex logic can be taken care of by the Proxy/IdentityManager combo. We also have other logic here also like key recovery by using a recovery network etc.

Having logic on the smart contract side works as well and I have done a little bit of thinking around that too, but it might take a while to reach a consensus on best practices around this.

@alexvandesande

This comment has been minimized.

Contributor

alexvandesande commented Jul 4, 2017

@emansipater I really like the approach you mention because it can really help scalability and makes transactions cheaper. I would support any approach that makes it more generic like that

@emansipater

This comment has been minimized.

emansipater commented Jul 4, 2017

@alexvandesande yes, this was developed in the context of state channels where each bit of gas has a substantial effect on the final throughput multiplier so that was our aim.

@christianlundkvist I see the optimal compromise as having this standard and then making the forwarding contracts compliant with it. That way either approach is possible, depending on whether the destination contract supports it directly or not.

@alex-miller-0

This comment has been minimized.

alex-miller-0 commented Jul 4, 2017

@emansipater Are you suggesting something like this?

mapping(bytes32 => bool) played;
mapping(address => nonce) nonce;

function provable_transfer(bytes32[3] data, uint8 v, address to, uint value) returns (bool) {
  
  // First 4 bytes of keccak-256 hash of "transfer"
  bytes4 word = 0xb483afd3;

  address signer = ecrecover(data[0], v, data[1], data[2]);
  nonce[signer] += 1;

  bytes32 proof = sha3(sha3(to, value), word, address(this), nonce[signer]);
  if (proof != data[0]) { return false; }
  else if (played[proof] == true) { return false; }
  played[proof] = true;

  // Execute the original transfer function
  balances[signer] = safeSub(balances[signer], value);
  balances[to] = safeAdd(balances[to], value);
  Transfer(signer, to, value );

  return true;
}
@emansipater

This comment has been minimized.

emansipater commented Jul 4, 2017

@alex-miller-0 No, more like

mapping(bytes32 => bool) played;
mapping(bytes32 => mapping(address => bool)) authorised;

function submitPreauthorisation(bytes32 authed_hash, bytes32 nonce, uint8 v, bytes32 r, bytes32 s) returns (bool) {
  
  address receivingAddress = address(this);
  bytes32 signedRoot = keccak256(authed_hash, receivingAddress, nonce);
  
  if (played[signedRoot]) {return false;}
  
  address signer = ecrecover(signedRoot, v, r, s);
  if (authorised[authed_hash][signer]) {return false;}
  
  authorised[authed_hash][signer] = true;
  played[signedRoot] = true;
  
  return true;
}

function doAsAuthorised(bytes function, bytes args[], address authorisedBy) returns (bool) {
  
  bytes32 actionSig = keccak256(function,args[]);
  
  if(authorised[actionSig][authorisedBy] ) {
    doAction(function, args[], authorisedBy);
    timesAuthorised[actionSig][authorisedBy] = false;
    return true;
  }
  
  return false;
  
}

function expandAuthorisation(bytes32[] hashList, address authorisedBy) returns bool {
  
  if(authorised[keccak256(hashList)][authorisedBy]) {
  
    for(uint i = 0; i < hashList.length, i++) {
      require(authorised[hashList[i]][authorisedBy] != true);
      authorised[hashList[i]][authorisedBy] = true;
    }
  
  authorised[keccak256(hashList)][authorisedBy] = false;
  return true;
  
  }
  
  return false;
  
}

if you'll forgive the hastily scribbled frankencode

@christianlundkvist

This comment has been minimized.

christianlundkvist commented Jul 4, 2017

@emansipater

I see the optimal compromise as having this standard and then making the forwarding contracts compliant with it. That way either approach is possible, depending on whether the destination contract supports it directly or not.

Yep this sounds like the best way :)

@Arachnid

This comment has been minimized.

Collaborator

Arachnid commented Jul 10, 2017

A new stateful function named provable_X, where X can be any state-updating function
A constant function named isProvable_X, where X can be any state-updating function

What's the purpose of isProvable_X? Why not just run the function as a local call to validate it?

Note that the first 4 bytes of a word are used to distinguish this stateful function. These are the first 4 bytes of the original function's name, this is importantly not the ABI definition.

Why not? That would make a lot more sense.

Also note the order of parameters. The first parameter is an array of hashes where the following is true:

h[0] Hash of tightly packed params

It's not necessary to include this, since you can calculate it from the inputs.

h[1] r of signature
h[2] s of signature
The second parameter is the v value of the signature.

You could easily make v a third element of the array without increase in ABI encoding length - or make them all individual parameters. The choice to put two in an array and the third separately seems odd.

Replay protection is added by checking the proof against an archived mapping:

mapping(bytes32 => bool) played;

This requires storage proportional to the number of past executions, which isn't ideal. What about a simple nonce scheme, instead?

@alex-miller-0

This comment has been minimized.

alex-miller-0 commented Jul 10, 2017

@Arachnid Great comments - thanks.

Why not? That would make a lot more sense.

Why not just run the function as a local call to validate it?

Agreed on both. I will change these on the next update.

You could easily make v a third element of the array

Can you show an example? I don't understand how this would work if arrays have to be typed. Unless you just mean a bytes[] array?

EDIT: I actually think you meant something like this:

bytes[3] sig
uint8 v = uint8(sig[2])

Is that correct?

This requires storage proportional to the number of past executions, which isn't ideal. What about a simple nonce scheme, instead?

I agree with the statement, but can't visualize what you are suggesting - will you provide an example?

@alex-miller-0

This comment has been minimized.

alex-miller-0 commented Jul 10, 2017

Updated the proposal with some of @Arachnid 's suggestions.

@Arachnid

This comment has been minimized.

Collaborator

Arachnid commented Jul 10, 2017

Is that correct?

Yes, that's what I had in mind - though I think that three individual args would work just as well.

I agree with the statement, but can't visualize what you are suggesting - will you provide an example?

It looks like you already updated the contract to use nonces.

As I mentioned earlier, though - you can remove the first element of the data, and simply calculate it. If it's incorrect it will return an invalid sender address, and so will be rejected.

@alex-miller-0

This comment has been minimized.

alex-miller-0 commented Jul 10, 2017

you can remove the first element of the data, and simply calculate it. If it's incorrect it will return an invalid sender address, and so will be rejected.

I tried to work this into the example function, but I can't figure out what you mean. Can you provide an example?

@emansipater

This comment has been minimized.

emansipater commented Jul 11, 2017

This requires storage proportional to the number of past executions, which isn't ideal. What about a simple nonce scheme, instead?

There's actually a really really good reason not to use a nonce scheme, which is that it allows for revoking of previously signed but not yet published messages. In state channels this is a security exploit, not a feature. I'll try to think if there's a simple approach that still preserves nonrevocability but doesn't waste so much space on replay prevention--it seems solvable.

A different, though somewhat related issue is the need for a) atomicity control and b) sequence/dependency control. In my opinion both of these are well worth including in this EIP. The first can be solved by just allowing arrays of actions in the doPreauthedAction() and then reverting if any one action fails (whole array is hashed to check whether authorised). Slightly complicates the parameter array portion but not too bad I think. It might not be obvious at first glance, but this also solves the second requirement, provided that we provide universal support for an action type which can make assertions about the state of PreauthedActions. That seems sensical anyways, so these two don't bloat the proposal much at all.

@emansipater

This comment has been minimized.

emansipater commented Jul 11, 2017

A new stateful function named provable_X, where X can be any state-updating function
A constant function named isProvable_X, where X can be any state-updating function

What's the purpose of isProvable_X? Why not just run the function as a local call to validate it?

Note that the first 4 bytes of a word are used to distinguish this stateful function. These are the first 4 bytes of the original function's name, this is importantly not the ABI definition.

Why not? That would make a lot more sense.

Highly agree with both (though I would clarify that this should be for whitelisted local calls only, obviously not arbitrary local calls). Also @alex-miller-0 can we update the EIP name to "Standard Functions for Preauthorized Actions" or something similar? The current name really is not appropriate--that term provable made sense for your other EIP but not for this one. I might also add that once this proposal is complete that other EIP proposal can be radically simplified by removing the signature checking etc. that this one will already do and just specifying a standard "destroyTokens" function which preauthorisations under this EIP can call.

@emansipater

This comment has been minimized.

emansipater commented Jul 11, 2017

I tried to work this into the example function, but I can't figure out what you mean. Can you provide an example?

He means that since you are passing the parameters anyways, you don't need to also pass the hash of them. You can just hash them to recover the hash. If the parameters are altered in any way and you get a different hash than the one which was actually signed, ecrecover will just return a different random sender. Since that sender will not have authorised anything, the operation will be rejected. So this is perfectly safe to do--it doesn't introduce any security issues.

@alex-miller-0

This comment has been minimized.

alex-miller-0 commented Jul 11, 2017

can we update the EIP name to "Standard Functions for Preauthorized Actions" or something similar

Yes. I will rename.

He means that...

Thanks - that helps. I will update my tests and update the EIP in a bit.

@alex-miller-0 alex-miller-0 changed the title from Provable Stateful Functions to Standard Functions for Preauthorized Actions Jul 11, 2017

@alex-miller-0

This comment has been minimized.

alex-miller-0 commented Jul 12, 2017

Proposal has been updated to include only v, r, s as input params.

@emansipater

This comment has been minimized.

emansipater commented Jul 12, 2017

@alex-miller-0 great! Now all we need to do is generalise this so that instead of a bunch of separate provable_x, provable_y, provable_z functions it just has the three more general functions submitPreauthorization, doAsAuthorized, and expandAuthorization, with specific function signatures either listed in a table or broken out in separate EIPs like the "burn" function of 661.

Regarding the incrementing vs non-replayable nonce approaches, Vitalik and I played around with this today but couldn't really come up with anything better than just having a bytes32 nonce and treating nonces below 10^10 as incrementing nonces while still storing a non-replay boolean for other possible values. People can then choose what they need according to their application. In theory most state channel actions won't go to chain, so the one-word-per-authorisation-event penalty probably won't be too bad, and applications that don't need nonrevocability can just use a regular nonce to save on storage costs.

@Arachnid

This comment has been minimized.

Collaborator

Arachnid commented Jul 16, 2017

A thought: Why not make this caller-side? If the caller has a wallet contract that supports third parties submitting signed messages on the owner's behalf, then that wallet can be used to call any contract using this functionality, without the need for explicit support by the called contracts.

Depending on the use-case, you can write a specific holding contract for just this purpose, such as the way my token drop idea works.

@emansipater

This comment has been minimized.

emansipater commented Jul 16, 2017

@Arachnid That's a perfectly viable approach for many use cases (see the uport discussion above), but since both types of use case need this spec I think this EIP should just be kept as generic as possible, and not specify whether it is caller-side or contract-side. Either one will have function signatures being activated on behalf of some external EOA.

For what it's worth, there are certain situations where the caller side approach fails or is at least unnecessarily complicated. One of the big ones is in the area of newly generated cold storage accounts, who then have to predict a contract address that hasn't been created yet (with the EOA itself being treated as the authentication reference they can securely calculate the EOA address before it exists).

@izqui

This comment has been minimized.

Contributor

izqui commented Jul 17, 2017

I like the incrementing-nonces below 10^10 idea.

@emansipater i really like how you can expand preauths in your implementation, however i think there is value in being able to approve and execute in just one call.

You could do it from another contract too but then you'd be adding the overhead of 2 extra 'CALL's .

@emansipater

This comment has been minimized.

emansipater commented Jul 17, 2017

There are a lot of situations (especially in state channels) where one wants to approve-but-not-execute. It wouldn't be too terrible I suppose to add a submitAndDo function as well, but if we want a submitExpandAndDo it starts to get really messy. Two calls is only 1400 gas and keeping them separate allows you to authorise piecemeal and then execute all at once--I think ecrecover is a bigger worry in terms of gas, and if we change doAsAuthorized and submitAuthorization to both accept different authorisations for different actions then there is probably a net total savings in terms of gas anyways (I assume that it will be common to aggregate multiple authorisations and submit them all together, which seems like the most efficient approach for, e.g. those publishing erc20-only transactions and taking fees in the token).

@Arachnid

This comment has been minimized.

Collaborator

Arachnid commented Jul 17, 2017

That's a perfectly viable approach for many use cases (see the uport discussion above), but since both types of use case need this spec I think this EIP should just be kept as generic as possible, and not specify whether it is caller-side or contract-side.

I'm not sure this can be kept entirely generic; there needs to be some kind of replay protection, which is probably going to be contract-type specific if it's implemented at the contract end instead of the account end.

For what it's worth, there are certain situations where the caller side approach fails or is at least unnecessarily complicated.

Can you provide an example?

One of the big ones is in the area of newly generated cold storage accounts, who then have to predict a contract address that hasn't been created yet (with the EOA itself being treated as the authentication reference they can securely calculate the EOA address before it exists).

I'm not sure I follow. Contract addresses can be calculated easily from creator and nonce, but I'm not sure how that's relevant; you can create a cold storage account that's able to sign transactions, and then deploy the wallet from an entirely different account.

There are a lot of situations (especially in state channels) where one wants to approve-but-not-execute.

Can you give an example? If the caller is offchain, can't they just do a local call to the function that would execute the transaction, and see if it throws?

@emansipater

This comment has been minimized.

emansipater commented Jul 17, 2017

That's a perfectly viable approach for many use cases (see the uport discussion above), but since both types of use case need this spec I think this EIP should just be kept as generic as possible, and not specify whether it is caller-side or contract-side.

I'm not sure this can be kept entirely generic; there needs to be some kind of replay protection, which is probably going to be contract-type specific if it's implemented at the contract end instead of the account end.

My existing suggestion works fine for that: the authorisation includes 1) the hash of the action (or action tree), 2) the address of the contract intended to process the authorisation and 3) a nonce. The combined hash of these three is what receives the replay protection. If it's contract-side the address in 2) is something like a token contract. If it's caller side then the address is now the address of the caller's wallet contract, so you end up signing a different hash. Different types of contracts implement different actions (the caller side one probably has a generic call capability like uport, while the token contracts have only token-specific actions enabled like transfer and burn). This means that the same spec with the same replay protection works simultaneously for both use cases even if used interchangeably (it also doesn't add any space since contracts know their own addresses). Or do you mean something else?

For what it's worth, there are certain situations where the caller side approach fails or is at least unnecessarily complicated.

Can you provide an example?

The immediately following sentence is the example.

One of the big ones is in the area of newly generated cold storage accounts, who then have to predict a contract address that hasn't been created yet (with the EOA itself being treated as the authentication reference they can securely calculate the EOA address before it exists).

I'm not sure I follow. Contract addresses can be calculated easily from creator and nonce, but I'm not sure how that's relevant; you can create a cold storage account that's able to sign transactions, and then deploy the wallet from an entirely different account.

Contract addresses can be calculated easily, but not securely. If the cold storage generated EOA acts as the recipient for an ERC-20 transfer, it is the only key that must be trusted to secure those funds. If there is also an additional account which must be used to deploy the wallet then that account has now become totally trusted by the cold storage key (because they have the ability to deploy a totally different contract at that same "recipient" address which does not enforce the requirement of getting a signature from cold storage before acting). Think of a hardware wallet on a cold storage machine trying to provide guarantees to a user--they can't because without actually accessing the chain they don't know whether or not there is actually a correctly functioning proxy at the destination to which authorisations are being provided.

Things get even more complicated if you try to set up a cold storage multisig account (where you might not even know in advance which of the multisig keys will have to actually create the wallet contract). There are various workarounds of course, but they are all stateful, which means you have to coordinate the cold action with an on-chain action, which is just silly. The root problem is that you can't securely allow a 3rd party service to deploy a contract for you, including a wallet contract.

There are a lot of situations (especially in state channels) where one wants to approve-but-not-execute.

Can you give an example? If the caller is offchain, can't they just do a local call to the function that would execute the transaction, and see if it throws?

Really simple situation: activating only one tiny part of a large and complex state channel state. Suppose you have a large and complex channel open with another party that has lots of different subchannels in it. Suppose you lose your connection to them, and as a result can't get them to countersign a withdrawal you need to make. However, you do have a signed root from them agreeing to the huge list of subchannel updates that compose the latest state prior to the lost connection. If you can't reestablish a connection, you have to publish at least one signature of theirs anyways, so the most efficient thing is to just publish the root. However, you only need to expand and execute the subchannel holding the funds you actually need. If the others aren't time sensitive, it costs you nothing to keep trying to reestablish a connection but not execute all the other approved actions (because that would drag the whole rest of the subchannels out onto the chain and cost a ton in fees). After a short delay, they finally show up again (local internet outage as it turns out) and immediately start countersigning all of your requests. You'll be very glad at this point that you didn't pay all the fees to dump your entire subchannel list to chain (not to mention the major privacy advantage of having waited)!

Obviously, in the above situation doing a local call doesn't actually start the timeout for the subchannel, so that wouldn't help. There are lots of non-state channel situations too, like granting a tree of "preauthorised debits" that may or may not be called upon by the authorised party, etc.

@MicahZoltu

This comment has been minimized.

Contributor

MicahZoltu commented Jul 18, 2017

Why does this need to be an EIP? Is this just a best-practice recommendation? If so, EIPs are not the right place for it. Is there great benefit derived from standardizing this? On the surface, it seems the answer is no since the provable_* function names are not well known, so one can't code against them without foreknowledge of the contract they are developing against.

@emansipater

This comment has been minimized.

emansipater commented Jul 18, 2017

@MicahZoltu the existing spec is still evolving in the discussion here, but the generic approach I've suggested would eliminate the provable_* scheme in favour of a standardised scheme based on function signatures (which would definitely warrant an EIP so that everyone can use a compatible standard and make life much easier for implementers).

@SergioDemianLerner

This comment has been minimized.

SergioDemianLerner commented Jul 21, 2017

The user-mode provable methods being discussed here have drawbacks we've analyzed in depth in RSK. It's highly preferable to hard-fork the system to allow that any method call accepts off-line signatures.
One of the drawbacks of user-mode provable methods is the impossibility of signature segregation, which will provide space savings in the future.

One example that solves the problem in a more generic way but still lacks signature segregation is creating an EXECUTE opcode. (we have an RSKIP for this). This opcode receives the address of a transaction tx in memory, a size, a gaslimit, and it executes the transaction. The tx gas limit and fee rate are ignored. The EXECUTE gas limit is used instead.
The semantics are however a bit different from a normal external transaction, if the transaction fails because it runs out of gas, the nonce of the source account of tx is NOT incremented (this is to prevent replay attacks with zero gas to block an original transaction).
This opcode provides a generic way to create provable contract methods. Any method becomes ready to accept an offline signature.
I has some benefits: it allows to create censorship resistant account messages, because the processing can be hidden in other contract processing.
But as I said before, it has some drawbacks: it doesn't allow signature segregation and doesn't behave well with the LTCP compression protocol: it doesn't allow the signature to be removed.
A better (or complementary) approach is just to create a new type of transaction that encapsulates another transaction, while the platform understands and parses both the outer and inner signatures. The platform checks the outer signature, recovers the outer account, debits the gas from this account, and the checks inner signature, and inner nonce. The final outcome is that the inner transaction executes almost as normal. This way signatures can be segregated and compressed.

All the methods described here (including this EIP) have one common drawback: having the off-line signed transaction T for an account A gives no guarantee to the holder of T that he will be able to execute T in the future, as the owner of A can always double-spend the nonce of T with any other transaction and render T invalid.
To overcome this limitation, a contract should not track nonces, but transaction hashes and expirations, and store the hash for some time to prevent double-spends.

@emansipater

This comment has been minimized.

emansipater commented Jul 24, 2017

A hard-fork to allow any function call to be made with an offline signature would be quite nice, especially if utilising both the merklable hash/expand format and the combined incrementing/onetime nonce scheme described in my comments above (to allow both revocable and non-revocable transactions). @SergioDemianLerner doesn't seem like you read through all the comments, but the idea is to support a 32 bit nonce, with values below 10^10 treated as normal nonces that must be one higher than the previous nonce, while values above that are simply ignored and the hash of the entire transaction (nonce included) is marked as "played" to guarantee non-revocability. The transaction authoriser can then choose between revocable and non-revocable transaction types. Expirations are nice touch to permit space reclamation--will have to think about how to efficiently implement that into the suggested scheme.

btw @alex-miller-0 what do you think about the increasingly detailed suggestions I'm making? Do they still fit with the original intent of your EIP? Personally I think that a general solution is the best way to solve your problem, but I would understand if you wanted me to make a separate EIP instead.

@alex-miller-0

This comment has been minimized.

alex-miller-0 commented Jul 24, 2017

@emansipater I appreciate your comments - I'm reading all of them. I think your approach is likely more valuable long-term, but the complexity seems higher than what I intended for this EIP, which was to create a very simple function derivative to outsource logic. I want to implement my proposed scheme for a single function in a contract I'm deploying soon and complexity in Ethereum scares me.

Anyway I get your rationale, but I'd prefer you make a separate EIP. I'll probably archive this one soon.

@emansipater

This comment has been minimized.

emansipater commented Jul 24, 2017

@alex-miller-0 I actually see implementing each function separately as the overall more complex scheme, but I suppose a separate EIP is probably appropriate.

@zmitton

This comment has been minimized.

zmitton commented Sep 9, 2017

Sorry for the late arrival, but I'm interested in the use-case of decentrilized exchange contracts. Right now, a contract can own tokens/control them, which can enable the "ask" side of an orderbook if the other token of the pairing is ETH. (i.e user sends eth to contract, "spits out tokens" by calling the token contract and transferring some of its tokens to the user). However, no "bids" are possible, and neither are any orderbooks where the user payment is not in ETH. Because a user sending tokens to the exchange contract does not touch the exchange contract at all. It only involves an update to a mapping elsewhere. The idea to have the user first call the exchange contract which should initiate the token swap is impossible without this EIP. The msg.sender in the token contract will be the exchange contract (unfortunately a delegateCall does not solve this either). Using approve function is possible but requires multiple separate transactions during which, situations can change.

It's hard to process everything in this thread, but isn't multiple sigs hashed together overkill here? @alexvandesande How about "embeddedSigner". Its how I'm thinking of it because all sigs are made offline, but the difference here is another signature besides the overall Eth transaction signature.

@alexvandesande

This comment has been minimized.

Contributor

alexvandesande commented Jan 5, 2018

Has there been a consensus in implementation? I'd like to move forward with a token standard that has a function for sending transactions without the end user needing ether to pay gas

@alex-miller-0

This comment has been minimized.

alex-miller-0 commented Jan 6, 2018

@alexvandesande I ended up not implementing this, but I still think the original design is a reasonable specification for simplicity. If you want to link an implementation to this issue or open up a ERC let us know!

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