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

Abstraction of transaction origin and signature.md #208

Merged
merged 10 commits into from
Mar 27, 2018
94 changes: 94 additions & 0 deletions EIPS/abstraction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
### Preamble

EIP: <to be assigned>
Title: Abstraction of transaction origin and signature
Author: Vitalik Buterin
Type: Standard Track
Category: Core
Status: Draft
Created: 2017-02-10

### Summary

Implements a set of changes that serve the combined purpose of "abstracting out" signature verification and nonce checking, allowing users to create "account contracts" that perform any desired signature/nonce checks instead of using the mechanism that is currently hard-coded into transaction processing.

### Parameters

* METROPOLIS_FORK_BLKNUM: TBD
Copy link
Member

Choose a reason for hiding this comment

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

Now this should say CONSTANTINOPLE_FORK_BLKNUM.

* CHAIN_ID: same as used for EIP 155 (ie. 1 for mainnet, 3 for testnet)
* NULL_SENDER: 2**160 - 1

### Specification
Copy link

Choose a reason for hiding this comment

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

could we clarify that MaxAddress can be cleared? (sender be touched?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What's MaxAddress?

Copy link
Contributor

Choose a reason for hiding this comment

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

NULL_SENDER


If `block.number >= METROPOLIS_FORK_BLKNUM`, then:
1. If the signature of a transaction is `(CHAIN_ID, 0, 0)` (ie. `r = s = 0`, `v = CHAIN_ID`), then treat it as valid and set the sender address to `NULL_SENDER`
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not formatted correctly.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Was v = CHAIN_ID really the intention here?
Currently according to #155 v = CHAIN_ID * 2 + 35 or v = CHAIN_ID * 2 + 36
(implemented only as v = CHAIN_ID * 2 + 35 in cpp-ethereum)

So do we really need additional v rule for zero signature instead of following EIP155 here, too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are two ways in which v is set in EIP 155. The first is the v value which appears in the actual transaction. This is set according to that formula. The second is the value which is put into the v slot when computing the hash. This is set to equal the CHAIN_ID, just as here.

Copy link
Member

@gumb0 gumb0 Apr 28, 2017

Choose a reason for hiding this comment

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

So v here in this EIP refers not to the actual value in Tx RLP, but to the one extracted from it using EIP155 formula, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Aah! I see what you mean, sorry I take back my previous comment. I would still prefer v in the transaction being equal to the chain ID, as that's the easiest thing to do. There is no need to set the v value to 35 + chain_id * 2 here.

2. Transactions of this form MUST have gasprice = 0, nonce = 0, value = 0, and do NOT increment the nonce of account NULL_SENDER.
3. Create a new opcode at `0xfb`, `CREATE2`, with 4 stack arguments (value, salt, mem_start, mem_size) which sets the creation address to `sha3(sender + salt + sha3(init code)) % 2**160`, where `salt` is always represented as a 32-byte value.
Copy link

@jimpo jimpo Sep 20, 2017

Choose a reason for hiding this comment

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

Is + uint256 addition or concatenation?

4. Add to _all_ contract creation operations, including transactions and opcodes, the rule that if a contract at that address already exists and has non-empty code OR non-empty nonce, the operation fails and returns 0 as if the init code had run out of gas. If an account has empty code and nonce but nonempty balance, the creation operation may still succeed.

### Rationale

The goal of these changes is to set the stage for abstraction of account security. Instead of having an in-protocol mechanism where ECDSA and the default nonce scheme are enshrined as the only "standard" way to secure an account, we take initial steps toward a model where in the long term all accounts are contracts, contracts can pay for gas, and users are free to define their own security model.

Under EIP 86, we can expect users to store their ether in contracts, whose code might look like the following (example in Serpent):

```python
# Get signature from tx data
sig_v = ~calldataload(0)
Copy link
Contributor

Choose a reason for hiding this comment

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

When I first read this with the ~ I found it confusing as it is used to invert all the bits in Python, which is equivalent to ~x = -x - 1.

sig_r = ~calldataload(32)
sig_s = ~calldataload(64)
# Get tx arguments
tx_nonce = ~calldataload(96)
tx_to = ~calldataload(128)
tx_value = ~calldataload(160)
tx_gasprice = ~calldataload(192)
Copy link
Contributor

@jamesray1 jamesray1 Oct 6, 2017

Choose a reason for hiding this comment

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

There's no gas limit included like in the yellow paper. What's the rationale for that? Rather the gas limit is fixed below as 50000, which doesn't seem right given we vary the gas limit.

Another thing that confuses me is that the yellow paper instructions don't look like they take variable arguments, e.g. all instructions take an operand starting from the first item then if there are more items consecutively going up the items in the stack, memory, or other data. But this code uses e.g. sload(-1), and mstore(signing data). So it might be helpful to revise the yellow paper to point out that the opcodes can take variable arguments (provided they are within the data that is called), not just, for example, having to work first with the first item in storage or memory.

screenshot from 2017-10-06 15-29-39

From the above screenshot it looks like calldataload just loads the first input data item to the first word in memory. But given that they are known as opcodes, thus implying operators, it makes sense that they take at least one or more operands.

Copy link
Contributor

Choose a reason for hiding this comment

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

In the YP, on the image above, the CALLDATALOAD 1 1 : that means it pops one off the stack and pushes one off the stack. So it takes one operand/parameter and returns one.

The CALLDATALOAD just places the data on the stack, does nothing with memory. The CALLDATACOPY is the one to use for getting it into memory.

Copy link
Contributor

@jamesray1 jamesray1 Oct 6, 2017

Choose a reason for hiding this comment

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

OK, my mistake, obviously I didn't read it carefully, didn't check and think carefully about what I was writing was valid, and maybe was getting it mixed up with other opcodes that do involve memory. Thanks for clarifying.

tx_data = string(~calldatasize() - 224)
~calldataload(tx_data, 224, ~calldatasize())
# Get signing hash
signing_data = string(~calldatasize() - 64)
Copy link
Contributor

@jamesray1 jamesray1 Oct 6, 2017

Choose a reason for hiding this comment

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

Just trying to understand this line with why it uses - 64. Signing data is passed into SHA3 below, which takes an arbitrary byte array loaded from a tuple of the first item in memory to the sum of the first and second items, minus 1, as follows:

screenshot from 2017-10-06 16-23-41

~calldatasize() returns in this case a byte array of minimum length 224 bytes, plus the arbitrary length input data field.

So for signing the data I presume that there are two words that aren't needed from the input data fields, although it's not clear to me immediately from reading what they are. The purpose is to abstract out the security model, so v, r, and s then don't seem to be needed; i.e. leave the security model to be defined externally.

However, ~calldatasize just returns the size of the input data, rather than copying it. So this line doesn't seem to make sense when the variable it evaluates to, signing_data, is passed into SHA3. You could have two input data that are the same size with different data that when passed into SHA3 will return the same hash.

So wouldn't it make more sense to copy the input data, except for v, r and s (presuming that these are the three fields, rather than two as specified, that don't need to be copied) into signing_data?

More info: https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm#Signature_generation_algorithm

~mstore(signing_data, tx.startgas)
~calldataload(signing_data + 32, 96, ~calldatasize() - 96)
signing_hash = sha3(signing_data:str)
# Perform usual checks
prev_nonce = ~sload(-1)
Copy link
Contributor

@jamesray1 jamesray1 Oct 6, 2017

Choose a reason for hiding this comment

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

~sload(-1) reads the last word in storage, which is presumably where the nonce is stored. I'm not sure where this is defined in the current version of the yellow paper. The prev_nonce would be the nonce of the sender account. In the yellow paper the account nonce is the first item in the state.

Word # in stack/ stack item no. initial stack bytes
1 v 0
2 r 32
3 s 64
4 nonce 96
5 to 128
6 value 160
7 gasprice 192
8 data 224

Addresses are 160 bits, but above it sets aside 32 bytes, but I guess it does that for efficiency with extracting it, rather than performing bitwise operation on a word, where other data is stored in the word.

Copy link
Contributor

Choose a reason for hiding this comment

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

In the context of this EIP, the nonce is not the same nonce as the YP talks about. The EVM has a 'native' nonce per account, whereas in the context of this EIP, the nonce is counter within the state.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah OK, it's like the sequence number that Vitalik referred to in EIP 101. #28

assert tx_nonce == prev_nonce + 1
assert self.balance >= tx_value + tx_gasprice * tx.startgas
assert ~ecrecover(signing_hash, sig_v, sig_r, sig_s) == <pubkey hash here>
Copy link
Contributor

@jamesray1 jamesray1 Oct 6, 2017

Choose a reason for hiding this comment

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

Should we also abstract the pubkey verification? It could even be abstracted internally, by having a number of precompile contracts where you can call one with an opcode sigVerify. The opcode could even have a default option (where you can call it without any argument). However, I guess that there is not as much benefit to abstract the signature verification algorithm compared to abstracting the signature generation algorithm.

# Update nonce
~sstore(-1, prev_nonce + 1)
# Pay for gas
~send(MINER_CONTRACT, tx_gasprice * tx.startgas)
# Make the main call
~call(msg.gas - 50000, tx_to, tx_value, tx_data, len(tx_data), 0, 0)
# Get remaining gas payments back
~call(20000, MINER_CONTRACT, 0, [msg.gas], 32, 0, 0)
```

This can be thought of as a "forwarding contract". It accepts data from the "entry point" address 2**160 - 1 (an account that anyone can send transactions from), expecting that data to be in the format `[sig, nonce, to, value, gasprice, data]`. The forwarding contract verifies the signature, and if the signature is correct it sets up a payment to the miner and then sends a call to the desired address with the provided value and data.

The benefits that this provides lie in the most interesting cases:

- **Multisig wallets**: currently, sending from a multisig wallet requires each operation to be ratified by the participants, and each ratification is a transaction. This could be simplified by having one ratification transaction include signatures from the other participants, but even still it introduces complexity because the participants' accounts all need to be stocked up with ETH. With this EIP, it will be possible to just have the contract store the ETH, send a transaction containing all signatures to the contract directly, and the contract can pay the fees.
- **Ring signature mixers**: the way that ring signature mixers work is that N individuals send 1 coin into a contract, and then use a linkable ring signature to withdraw 1 coin later on. The linkable ring signature ensures that the withdrawal transaction cannot be linked to the deposit, but if someone attempts to withdraw twice then those two signatures can be linked and the second one prevented. However, currently there is a privacy risk: to withdraw, you need to have coins to pay for gas, and if these coins are not properly mixed then you risk compromising your privacy. With this EIP, you can pay for gas straight our of your withdrawn coins.

Choose a reason for hiding this comment

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

With this EIP, you can pay for gas straight ourt of your withdrawn coins.

- **Custom cryptography**: users can upgrade to ed25519 signatures, Lamport hash ladder signatures or whatever other scheme they want on their own terms; they do not need to stick with ECDSA.
- **Non-cryptographic modifications**: users can require transactions to have expiry times (this being standard would allow old empty/dust accounts to be flushed from the state securely), use k-parallelizable nonces (a scheme that allows transactions to be confirmed slightly out-of-order, reducing inter-transaction dependence), or make other modifications.

(2) and (3) introduce a feature similar to bitcoin's P2SH, allowing users to send funds to addresses that provably map to only one particular piece of code. Something like this is crucial in the long term because, in a world where all accounts are contracts, we need to preserve the ability to send to an account before that account exists on-chain, as that's a basic functionality that exists in all blockchain protocols today.

### Miner and transaction replaying strategy

Note that miners would need to have a strategy for accepting these transactions. This strategy would need to be very discriminating, because otherwise they run the risk of accepting transactions that do not pay them any fees, and possibly even transactions that have no effect (eg. because the transaction was already included and so the nonce is no longer current).

One simple strategy is to have a set of regexps that the to address of an account would be checked against, each regexp corresponding to a "standard account type" which is known to be "safe" (in the sense that if an account has that code, and a particular check involving the account balances, account storage and transaction data passes, then if the transaction is included in a block the miner will get paid), and mine and relay transactions that pass these checks.

One example would be to check as follows:

1. Check that the to address has code which is the compiled version of the Serpent code above, with `<pubkey hash here>` replaced with any public key hash.
2. Check that the signature in the transaction data verifies with that key hash.
3. Check that the gasprice in the transaction data is sufficiently high
4. Check that the nonce in the state matches the nonce in the transaction data
5. Check that there is enough ether in the account to pay for the fee
Copy link

Choose a reason for hiding this comment

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

I think that this is not enough to ensure the miner gets paid. As a transaction can throw in an external contract which will undo the previous payment.

Even if the miner executed the entire transaction and confirmed they got paid that does not mean that for another block.number or block.coinbase the same transaction will not throw and they will not get paid.

Copy link
Member

@VoR0220 VoR0220 Jul 1, 2017

Choose a reason for hiding this comment

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

if it throws it's still consuming gas, and that would be the case with revert as well. Either way, the miner will be paid.

edit: Nvm, I see this is talking about the fees directly not the gas.


If all five checks pass, relay and/or mine the transaction.

A looser but still effective strategy would be to accept any code that fits the same general format as the above, consuming only a limited amount of gas to perform nonce and signature checks and having a guarantee that transaction fees will be paid to the miner. Another strategy is to, alongside other approaches, try to process any transaction that asks for less than 250,000 gas, and include it only if the miner's balance is appropriately higher after executing the transaction than before it.