Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standard Functions for Preauthorized Actions #662

Closed
alex-miller-0 opened this issue Jul 3, 2017 · 48 comments
Closed

Standard Functions for Preauthorized Actions #662

alex-miller-0 opened this issue Jul 3, 2017 · 48 comments
Labels

Comments

@alex-miller-0
Copy link

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

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 Provable Stateful Functions: ERC20 Extension Provable Stateful Functions Jul 3, 2017
@alex-miller-0
Copy link
Author

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

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

@coder5876
Copy link

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
Copy link
Author

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
Copy link
Author

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

@alexvandesande
Copy link

@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
Copy link
Contributor

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
Copy link
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
Copy link
Author

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

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

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

alexvandesande commented Jul 4, 2017 via email

@emansipater
Copy link

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

@coder5876
Copy link

@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
Copy link

@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
Copy link

@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
Copy link
Author

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

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

@coder5876
Copy link

@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
Copy link
Contributor

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
Copy link
Author

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
Copy link
Author

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

@Arachnid
Copy link
Contributor

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
Copy link
Author

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

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

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

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
Copy link
Author

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 Provable Stateful Functions Standard Functions for Preauthorized Actions Jul 11, 2017
@alex-miller-0
Copy link
Author

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

@emansipater
Copy link

@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
Copy link
Contributor

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

@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
Copy link
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
Copy link

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
Copy link
Contributor

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

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
Copy link
Contributor

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

@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
Copy link
Contributor

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

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
Copy link
Author

@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
Copy link

@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
Copy link

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

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
Copy link
Author

@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!

@github-actions
Copy link

github-actions bot commented Jan 2, 2022

There has been no activity on this issue for two months. It will be closed in a week if no further activity occurs. If you would like to move this EIP forward, please respond to any outstanding feedback or add a comment indicating that you have addressed all required feedback and are ready for a review.

@github-actions github-actions bot added the stale label Jan 2, 2022
@github-actions
Copy link

This issue was closed due to inactivity. If you are still pursuing it, feel free to reopen it and respond to any feedback or request a review in a comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

12 participants