-
Notifications
You must be signed in to change notification settings - Fork 1
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
Blake2b F precompile #4
Conversation
So far the precompiled Istanbul contracts list contains exactly the same contracts as the one for Byzantium. We'll work on adding a support for Blake2 F precompile in Istanbul in the next commits. Next to PrecompiledContractsIstanbul list, I introduced all the required control structures for the fork.
The contract calls keep-network/blake2 implementation of the F compression function and lives under address 0x09. The called F function is optimized for 64-bit WORDs. Test cases were generated from test vectors from keep-network/blake2 library.
core/vm/contracts.go
Outdated
"errors" | ||
"math/big" | ||
|
||
"github.com/keep-network/blake2" | ||
|
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.
nit: no need blank line here ;)
core/vm/contracts.go
Outdated
h[4] = binary.BigEndian.Uint64(input[32:40]) | ||
h[5] = binary.BigEndian.Uint64(input[40:48]) | ||
h[6] = binary.BigEndian.Uint64(input[48:56]) | ||
h[7] = binary.BigEndian.Uint64(input[56:64]) |
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 EIP needs to specify how to handle cases where the input data is too short. Other precompiles typially return an error in that case. It's very important that such decisions are not left "as implementation details", since it's consensus-critical behaviour.
In this case, the client woud panic
if given an input shorter than 64
bytes, afaict
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'll add it to the EIP spec, waiting for the draft and competing PRs to be resolved
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.
We now expect the input to be exactly 213 bytes long.
The PR was opened as an initial implementation before we figure out some details. I should probably open it in a Draft mode.
core/vm/contracts.go
Outdated
f = true | ||
} | ||
|
||
rounds := binary.BigEndian.Uint32(input[316:320]) |
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.
actually, panic
on shorter than 320
bytes in length
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.
Yep, it was wrong. We now expect the input to be exactly 213 bytes long.
core/vm/contracts.go
Outdated
type blake2F struct{} | ||
|
||
func (c *blake2F) RequiredGas(input []byte) uint64 { | ||
return 1 //TODO: implement |
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.
Even if you don't quite know what constants to use, perhaps you have an idea about the general pricing scheme? Like, is it rounds * K *input_size
? Would be good to have something, even if we determine the constants later. It would make it easier for others people to help tune the constants.
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 benchmarks so far lead me to believe that a round is ~ 1 gas (!), and that this precompile will be dominated by calling cost. We can provide a more robust set of benchmarks that including input size as a floating param.
More discussion here
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 believe the methodology of pricing relative to ecrecover
is sound. Once we have a wider range of inputs on benchmarks this will be straightforward.
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 opened this PR in an early-mode before we had a chance to discuss benchmarks and gas price. Based on what we know so far, and using the same methodology as for the EIP-1108 it seems that it should be 1 gas/round.
I was hoping to remove this TODO and put a final value here after we agree on the gas in the EIP. I am not sure how the process should look here.
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 set the gas price to 1/round based on our benchmarks.
core/vm/contracts.go
Outdated
t[1] = binary.BigEndian.Uint64(input[248:256]) | ||
|
||
var f bool | ||
if input[287] == 1 { |
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.
Interesting -- this right here checks if the f
is strictly b 0000 0001
. So if it's b 1111 1111
, it would be set to false
. I think it would be more sensible to look purely at the lsb (and, naturally, to have that specified in the EIP).
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 updated the check to:
if input[208] == 0x00000001 {
f = true
}
I am not sure if it is what you meant by looking at the least significant bit.
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 meant something like
if (input[208] & 0x1) != 0 {
f = true
}
There are basically three variants:
- Check if
input[208] != 0
. Sob 1111 1110
counts astrue
- Check if
input[208] == 1
(what you do now). Sob 1111 1111
counts asfalse
- Check if only least significant bit.
My order of preference would be 3, 1, 2. Another option would be to do exactly what solidity does, whatever that is, to resolve an input parameter into a bool
. @axic what would solidity do ?
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 just experimented a bit with solidity. Afaict, for boolean
, the rule seems to be nonzero = true
(variant 1). So if we want to follow the same logic, the check would be
f := (input[208] != 0)
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.
Good point, updated as requested.
core/vm/contracts.go
Outdated
|
||
rounds := binary.BigEndian.Uint32(input[316:320]) | ||
|
||
blake2.F(&h, m, t, f, int(rounds)) |
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.
Is it really safe to cast a uint32
to int
-- what about 32-bit systems? Will it become negative on 0xffff ffff
?
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.
We no longer cast to int
- the number of rounds is uint32
everywhere now.
Why is all the padding needed? The caller sends along |
We decided to switch the behaviour of F precompile to accept abi.encodePacked parameters instead of left-padded ones (for values smaller than 32 bytes).
No changes in what modules are imported. I've just removed the empty line and auto-formatted imports.
We no longer have padding. We now accept exactly 213 bytes of abi-encoded parameters. |
core/vm/contracts.go
Outdated
type blake2F struct{} | ||
|
||
func (c *blake2F) RequiredGas(input []byte) uint64 { | ||
if len(input) != 213 { |
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.
If input larger than 213
is used, this will throw. If that's the intended behaviour, then the EIP must also be updated to explicitly state that only exactly 213
bytes is accepted.
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.
Yes, my intention was to update the EIP to state it explicitly that exactly 213
bytes is accepted. I am not sure if there is any use case for passing more bytes and ignoring some of them. I personally think it's less error-prone to expect the exact length and fail when the length does not match.
core/vm/contracts.go
Outdated
} | ||
|
||
var ( | ||
blake2FInputLength = 213 |
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 should be a const
, no?
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, it could be:
const blake2FInputLength = 213
var errBlake2FIncorrectInputLength = errors.New(
"input length for Blake2 F precompile should be exactly 213 bytes",
)
but thought it's better to initialize them in one block, similarly how it's done above the bn256Pairing
struct definition. I wasn't sure about the conventions used in the project.
Please let me know what's the preference here and I'll alter 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.
I think const
is preferrable, rather than calling it a var
just for the sake of indentation...
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.
Good point, updated.
|
||
func (c *blake2F) Run(input []byte) ([]byte, error) { | ||
if len(input) != 213 { | ||
return nil, errBlake2FIncorrectInputLength |
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.
If it's only used in this place, might as well define the error inline
return nil, errBlake2FIncorrectInputLength | |
return nil, errors.New("invalid input length for Blake2 F precompile") |
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.
It's also used in tests, see TestPrecompileBlake2FMalformedInput
.
Co-Authored-By: Martin Holst Swende <martin@swende.se>
core/vm/contracts.go
Outdated
t[1] = binary.BigEndian.Uint64(input[200:208]) | ||
|
||
var f bool | ||
if input[208] == 0x00000001 { |
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.
if input[208] == 0x00000001 { | |
if input[208] != 0x00000000 { |
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 will be checked in a different way, just != 0
We now point to keep-network/blake2b/compression to include only what is really needed - F compression function
compression.F accepts `m` parameter as an array of uint64 instead of byte slice now. I have also improved test vectors to cover various cases of rounds parameter.
Gas price depends solely on the number of rounds. Having this parameter as the first one lets to evaluate gas price easier.
I made a mistake when refactoring `Run(input []byte)` to do the same and not updated `RequiredGas(input []byte)`. Fixing my mistake now.
All comments addressed. I still need to do some more extensive tests but I think it's ready for another round of review. |
Instead of pointing to a commit with no official tag, we now point to an exact release number.
Tested with https://github.com/pdyraga/f-precompile-call, works as expected:
|
for i := 0; i < 8; i++ { | ||
offset := i * 8 | ||
binary.BigEndian.PutUint64(output[offset:offset+8], h[i]) | ||
} |
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 code may be using the wrong byte order, as discussed starting at ethereum/EIPs#2129 (comment) .
Is this PR now 'final' ? If so, could you open a PR against go-ethereum proper? |
@holiman I need to update EIP with the requested test vectors and double-check possible byte ordering problems. Please expect a PR against |
How is it going? Istanbul is not far off now, and we're trying to get things in as soon as possible, to allow more time for testing and test creation. |
Working on it today, sorry for the delay. |
"revision": "02884c605a94951d05165fd56c69d6239f63b3d1", | ||
"revisionTime": "2019-07-02T09:51:07Z", | ||
"version": "v1.0", | ||
"versionExact": "v1.0" |
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.
Since this is actually code taken verbatim from Go (as far as I understand it), please move it to crypto/blake2b
inside the repo. I don't like the idea that we have a middleman repo in between for code that's copy pasted from elsewhere anyway.
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 not entirely copy-paste. Go code does not expose F
compression function, we had to modify it to allow for an arbitrary number of rounds, have the interface compatible with what we expect from the precompile and removed some pieces that are not really a part of compression but rather hashing. Please compare:
https://github.com/keep-network/blake2b/blob/master/compression/f.go#L39
https://github.com/golang/crypto/blob/master/blake2b/blake2b_generic.go#L30
@@ -965,5 +973,5 @@ | |||
"revisionTime": "2017-08-11T01:42:03Z" | |||
} | |||
], | |||
"rootPath": "github.com/ethereum/go-ethereum" | |||
"rootPath": "github.com/keep-network/go-ethereum" |
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.
Take care to fix this if you open a PR upstream.
BLAKE2b RFC Appendix A specifies test vector that we have implemented here. See https://tools.ietf.org/html/rfc7693#appendix-A For now, we leave blake2FTests are slice because we'll probably add more test vectors there.
The final block indicator flag for F precompile must be either 0 or 1.
func (c *blake2F) Run(input []byte) ([]byte, error) { | ||
if len(input) != blake2FInputLength { | ||
return nil, errBlake2FIncorrectInputLength | ||
} | ||
if input[212] != blake2FNonFinalBlockBytes && input[212] != blake2FFinalBlockBytes { |
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 you changed it to be super strict. Is that in line with the EIP? It wasn't when I checked yesterday, IIRC
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.
Yes, the EIP still says
The boolean f parameter is considered as true if at least 1 bit out of the 8 bits is set.
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 am going to update EIP in an ~hour. I should probably push the EIP change first, sorry. Changed based on this discussion.
@pdyraga please file this PR against go-ethereum, and let's do the remaining the work there. We're getting to a point where we are cutting very close to deadlines. So just open a PR, that's enough if you don't have more time right now. If you do have some more time, it'd be great if you also rebase it against current Otherwise, one if us will have to take this branch, rebase it, and file it ourselves (if it eventually get's squashed, it will be better if you are the PR submitter, so you get commit-credi). |
I'll do it today @holiman I am currently working on the last test vectors for the EIP. Should have both ready today. |
Great! |
@holiman Regarding squashing commits - is the preferred approach to have just one commit in the PR or did you mean that I should just clean up the history a little bit? |
Thanks for all the comments. Closing here, and opening on |
Here we present an implementation of Blake2b F compression function precompile based on the ethereum/EIPs#2129
Description
The precompile lives at the address
0x09
and should be called with a standardstaticcall
instruction. The expected input data structure is as follows:where:
rounds
- number of roundsh
- state vectorm
- message block vectort_0, t_1
- offset counterf
- final block indicator flagThere is a sample Truffle project prepared which can be used to play with this precompile:
https://github.com/pdyraga/f-precompile-call :
Istanbul hardfork
Since we hope to have this implementation included in the Istanbul hardfork, all control structures related to precompiles and hardfork have been added in this PR. Before you'll be able to use this precompile you need to enable Istanbul HF. One possibility is to use
genesis.json
and set"istanbulBlock": 0
in"config"
section.Sample call