-
Notifications
You must be signed in to change notification settings - Fork 8
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
QIP-0014 CREATE and CREATE2 specification #44
base: master
Are you sure you want to change the base?
Changes from all commits
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,170 @@ | ||
``` | ||
QIP: 14 | ||
Layer: Consensus (hard fork) | ||
Title: CREATE + CREATE2 opcode | ||
Author: alanorwick <[email protected]> | ||
Comments-Summary: No comments yet. | ||
Comments-URI: https://github.com/quainetwork/qips/wiki/Comments:QIP-0014 | ||
Status: Draft | ||
Type: Informational | ||
Created: 2024-07-31 | ||
License: BSD-2-Clause | ||
``` | ||
|
||
|
||
## Abstract | ||
|
||
This QIP proposes modifications to the CREATE opcode in Quai Network to ensure shard-specific address generation, while maintaining CREATE2 functionality as is. The proposal aims to adapt Ethereum's address creation mechanism to Quai's sharded architecture. | ||
|
||
## Motivation | ||
|
||
Quai Network's sharded architecture requires addresses to be shard-specific. The current Ethereum-style CREATE opcode does not guarantee this, potentially leading to cross-shard conflicts and inefficiencies. This proposal seeks to address this issue while maintaining as much compatibility with Ethereum as possible. | ||
|
||
## Specification | ||
|
||
### CREATE Opcode Modification | ||
|
||
1. The CREATE opcode will generate an address using the following method: | ||
|
||
a. Split the bytecode_data into two parts: | ||
- `hashCode`: All but the last 4 bytes of bytecode_data | ||
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 if a user provides a valid contract but no salt? 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. then it is not a valid contract, according to this spec 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. It would be a valid contract and then result in a grinded address from the EVM. 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 they did not provide salt? the last 4 bytes of the data must always be interpreted as salt (otherwise ambiguity can lead to insecure edge cases). In that case, anyone that tries to interact with a contract, will ignore the last 4 bytes when they interpret the contract code. Anyone that did not provide salt at last 4 bytes, will probably have a problem when the last 4 bytes are ignored (presumably initializer data) 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. Correct, the last 4 bytes of data would also be interpreted as a salt. A contract is allowed to have appended extra data. If the contract bytecode itself has last 4 bytes that result in a valid address then it would not have extra 4 bytes appended. 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. It's impossible to know without EVM execution whether the 4 bytes at the end are actual bytecode or appended extra data. |
||
- `potentialSalt`: The last 4 bytes of bytecode_data | ||
|
||
b. Calculate the initial address: | ||
|
||
```address = keccak256(([sender_address, sender_nonce, keccak256(hashCode), potentialSalt])[12:]``` | ||
|
||
c. If the resulting address is valid for the current shard: | ||
- The full bytecode_data (including the last 4 bytes) is used as the contract bytecode | ||
- The `potentialSalt` is considered part of the actual bytecode | ||
|
||
d. If the resulting address is not valid for the current shard: | ||
- Proceed to step 2 for grinding | ||
|
||
2. If the resulting address is not valid for the current shard: | ||
The gas cost per iteration of bytecode modification and checking is the `KeccackGasCost` of the size of `concat(sender_address, sender_nonce, hashCodeHash, salt)`. | ||
|
||
a. Initialize a "grind nonce" to 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. Why init to zero? Should start with the nonce the user already provided. 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. The salt is assumed to be part of the bytecode unless the user provided one is not valid. This QIP defines the first check of the salt potentially be part of the bytecode itself. The salt is then appended if the prior four bytes (bytecode or extra data) does not result in a valid address. 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 a good idea. When I interpret contract data later, how am I supposed to know if the last 4 bytes are actual contract data, or if they are just salt for the address? This could potentially lead to issues, if a contract was interpreted one way at deployment, but then interpreted differently when it is executed. The salt needs to be explicitly specified for every contract. 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 you provide a scheme that perfectly separates the bytecode and salt? What if the bytecode itself results in a valid address? |
||
|
||
b. Append 4 bytes representing the grind nonce to the original bytecode_data: | ||
``` | ||
extended_bytecode = bytecode_data + [4]byte(grind_nonce) | ||
``` | ||
c. Split the extended_bytecode: | ||
- `hashCode`: All but the last 4 bytes of extended_bytecode | ||
- `salt`: The last 4 bytes of extended_bytecode | ||
|
||
d. Generate a new address using: | ||
`address = keccak256([sender_address, sender_nonce, keccak256(hashCode), salt])[12:]` | ||
|
||
e. If the address is still not valid, increment the grind nonce and repeat steps b, c, and d until a valid shard-specific address is found, until 1,000 tries has been exceeded, or the gas limit has been exceed. | ||
d. Exceeding 1,000 tries without finding a valid shard address will result in a error, thus failing the contract deployment. | ||
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. Why arbitrarily cap at 1000 iterations? This is the reason the gas limit exists. If a user is willing to pay the gas for 1001 iterations, then they should get all 1001 iterations. 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 agree with this approach. 1,000 tries was from the prior implementation. |
||
|
||
### Gas Calculation Example | ||
|
||
To illustrate the gas cost of the grinding process, consider the following example: | ||
|
||
Input data: `0003590fc75d4136fd78ccf325764e51df61b282000000000000000086404fb8bc0b1b2625ac08825a59fe8d28ac927c571eccb1bc2c1a57291b163e00000025` | ||
|
||
Number of grinding attempts: 36 | ||
|
||
Gas calculation formula: | ||
```go | ||
func calculateKeccakGas(data []byte) int64 { | ||
keccakBaseGas := int64(params.Sha3Gas) // 30 | ||
wordCount := (len(data) + 31) / 32 | ||
return keccakBaseGas + int64(wordCount) * int64(params.Sha3WordGas) // Sha3WordGas = 6 | ||
} | ||
``` | ||
|
||
|
||
In this example: | ||
- Data length: 66 bytes | ||
- Word count: (66 + 31) / 32 = 3 words | ||
- Gas per Keccak operation: 30 + (3 * 6) = 48 gas | ||
|
||
Total gas cost for grinding: | ||
48 gas * 36 attempts = 1,728 gas | ||
|
||
## Gas Efficiency Comparison | ||
|
||
To illustrate the gas efficiency of our proposed CREATE opcode modification, let's compare two approaches: | ||
|
||
1. **Proposed Grinding Process**: Hash all but the last 4 bytes of the bytecode once, then iterate with a 4-byte salt. | ||
2. **Full Bytecode Hashing**: Hash the entire bytecode on each iteration. | ||
|
||
### Example Scenario | ||
|
||
- Contract Bytecode Length: 489 bytes | ||
- Number of iterations needed: 36 | ||
|
||
### Gas Calculation Results | ||
|
||
1. **Proposed Grinding Process**: | ||
- Initial bytecode hash: 126 gas | ||
- Each iteration: 48 gas | ||
- Total gas: 126 + (48 * 36) = 1,854 gas | ||
|
||
2. **Full Bytecode Hashing**: | ||
- Each iteration: 126 gas | ||
- Total gas: 126 * 36 = 4,536 gas | ||
|
||
### Gas Savings | ||
|
||
By using the proposed grinding process instead of full bytecode hashing, we save: | ||
|
||
4,536 - 1,854 = 2,682 gas | ||
|
||
This represents a 59.13% reduction in gas costs for this scenario. | ||
|
||
The gas savings become even more significant for larger contracts or when more iterations are required to find a valid shard-specific address. This efficiency is achieved by hashing the majority of the bytecode only once and then iterating on a small 4-byte salt, rather than repeatedly hashing the entire bytecode. | ||
|
||
### CREATE2 Opcode | ||
|
||
No changes are proposed for the CREATE2 opcode. It will continue to function as in Ethereum: | ||
``` | ||
address = keccak256(0xff ++ sender_address ++ salt ++ keccak256(init_code))[12:] | ||
``` | ||
|
||
## Rationale | ||
|
||
### Ethereum's CREATE and CREATE2 | ||
|
||
Ethereum implemented CREATE and CREATE2 with distinct goals: | ||
|
||
1. CREATE: Provides a simple, deterministic way to generate contract addresses based on the sender's address and nonce. This method ensures uniqueness but doesn't allow predicting future addresses easily. | ||
|
||
2. CREATE2: Introduced to allow for deterministic address generation regardless of sender state, enabling counterfactual instantiation and other advanced patterns. | ||
|
||
### Quai's Modifications | ||
|
||
1. CREATE Modification: | ||
- Maintains compatibility with Ethereum's CREATE for cases where the initial address is valid for the shard. | ||
- Introduces grinding only when necessary, minimizing divergence from Ethereum. | ||
- Ensures all addresses are shard-specific, maintaining Quai's sharded architecture integrity. | ||
|
||
2. No Change to CREATE2: | ||
- CREATE2's deterministic nature already allows for shard-specific address generation by careful selection of the salt. | ||
- Maintaining CREATE2 as-is preserves compatibility with existing Ethereum patterns and contracts that rely on its behavior. | ||
|
||
## Backwards Compatibility | ||
|
||
This change will affect the addresses generated by CREATE in cases where grinding is necessary. Existing contracts and those deployed with CREATE2 will not be affected. However, any systems or contracts that rely on predicting future CREATE-generated addresses may need to be updated to account for the potential grinding step. | ||
|
||
## Security Considerations | ||
|
||
- The grinding process in CREATE may slightly increase the computational cost of contract deployment in some cases. | ||
- Care must be taken to ensure that the grinding process does not introduce new attack vectors, such as DoS opportunities. | ||
- The implications of this change on gas costs and block limits should be carefully considered. | ||
- Address calculation and bytecode integrity: | ||
- The new address calculation method, which uses the last 4 bytes of the init code as a salt, introduces potential ambiguities and security risks: | ||
1. Bytecode HashCode Truncation: If a valid address is found on the first attempt, it could result in the last 4 bytes of the actual bytecode being interpreted as the salt, effectively truncating the deployed code in the hashCode. | ||
2. Salt Overloading: The system overloads the meaning of the last 4 bytes, using them as both potential bytecode and a salt for address grinding. | ||
3. Nil Salt Possibility: In cases where a valid address is found immediately, the "salt" could effectively be nil (all zeros), which might have unintended consequences for contracts expecting a non-zero salt. | ||
4. Bytecode Length Variability: The final deployed bytecode length could vary based on how many grinding attempts were needed, potentially leading to unexpected behavior in contracts that rely on specific code lengths. | ||
5. Incorrect Salt Provided: A user could provide a false salt value or random data beyond the bytecode. This will lead to an additional 4 bytes appended at the end of the EVM computation. | ||
- Careful consideration must be given to how this address calculation method interacts with existing and future smart contract patterns, especially those that may rely on precise bytecode lengths or content. | ||
|
||
## Copyright | ||
This QIP licensed under the BSD 2-clause license. | ||
|
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.
I would prefer not to change the behavior of existing opcodes, and instead propose a new opcode (OP_CREATE3) which does what you want. Changing the behavior of CREATE or CREATE2 would break EVM compatibility
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.
This is a great point. I would query @jdowning100 or @mechanikalk on the downstream impact on contract deployment. Many contracts that use create (AMMS/NFTs/proxy contracts/etc.) are using solidity and expecting
CREATE
to result in a valid address no matter what. So we would need to define whether it is more important for the result ofCREATE
to match expectation or the behavior ofCREATE
to match expectation.We would likely need to add this new
CREATE3
opcode in solidity as well if we choose the latter.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.
The middle ground which I proposed to @mechanikalk is hashing all of the bytecode during the grinding since then the
CREATE
computationkeccak256(([sender_address, sender_nonce, bytecode])[12:]
fits the prior spec. If we include the grinding, this comes at the cost of the keccak on the entire length of the bytecode which is more expensive.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.
Yeah, I understand this would require updating any contract which uses CREATE to use CREATE3 instead, but that is definitely the preferred way to do this. We can even deprecate CREATE and CREATE2 if we think that those methods of contract deployment are ill-advised.
Deprecating old opcodes and supplanting them with new opcodes is the way to extend an instruction set, like the EVM. Changing the behavior of existing opcodes is dangerous.
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.
I invite @mechanikalk @jdowning100 to weigh in
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.
I agree on @mechanikalk and @jdowning100 weighing in. We implemented the current CREATE logic during Iron Age in order to get AMMs / contracts deploying contracts to work in a minimal fashion. Relying on Chesterton's Fence principle we should further assess what caused us to implement it in CREATE versus making CREATE3.