-
Notifications
You must be signed in to change notification settings - Fork 5.3k
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
Changes from 8 commits
b0e2bb2
4120d21
174d6a5
976a003
f5a508f
3100749
94be170
bd136e6
6ea5732
fd171bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | |||||||||||||||||||||||||||||
* CHAIN_ID: same as used for EIP 155 (ie. 1 for mainnet, 3 for testnet) | |||||||||||||||||||||||||||||
* NULL_SENDER: 2**160 - 1 | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
### Specification | |||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could we clarify that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's There was a problem hiding this comment. Choose a reason for hiding this commentThe 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` | |||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not formatted correctly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How? I'm not seeing anything wrongly formatted https://github.com/ethereum/EIPs/blob/4120d2180a1cf62151066d9fb16742867315d649/EIPS/abstraction.md#specification There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was So do we really need additional There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are two ways in which There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | |||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is |
|||||||||||||||||||||||||||||
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) | |||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | |||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the YP, on the image above, the The There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | |||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: ~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? |
|||||||||||||||||||||||||||||
~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) | |||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the context of this EIP, the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | |||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | |||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
|||||||||||||||||||||||||||||
- **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 | |||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
There was a problem hiding this comment.
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
.