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

Implement go bindings to secp256k1 #1

Merged
merged 10 commits into from
Mar 24, 2020
Merged

Implement go bindings to secp256k1 #1

merged 10 commits into from
Mar 24, 2020

Conversation

elichai
Copy link
Member

@elichai elichai commented Feb 24, 2020

Hi,
These are go bindings for the https://github.com/kaspanet/secp256k1 repository.
the repo is included as a subtree with the script ./depend/update-subtree.sh to pull updates from the subtree.

There are currently some integration problems into kaspad, mostly because kaspad does utxo multiset diffs. which aren't supported by the code, but this should probably be changed in kaspad if I understand things correctly.

As usual review is done best commit by commit :)

Copy link
Contributor

@svarogg svarogg left a comment

Choose a reason for hiding this comment

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

Please do go mod init.

Than run golint and go vet and fix all warnings they output (note: In some rare cases go vet might have false positives. This is not something we have every encountered, but unlike with golint there's no guarantee against them. If you think it does output any false positives, let's discuss it)

multiset.go Outdated Show resolved Hide resolved
multiset.go Outdated
func (multiset *MultiSet) Reset() {
ret := C.secp256k1_multiset_init(context, &multiset.set)
if ret != 1 {
panic("failed intializing a multiset. should never happen")
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 reset, not initialize (I guess copy-paste error?)

multiset.go Outdated Show resolved Hide resolved
multiset_test.go Outdated
for _, test := range testVectors {
data, err := hex.DecodeString(test.dataElementHex)
if err != nil {
t.Fatal(err)
Copy link
Contributor

Choose a reason for hiding this comment

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

In all Fatals - please include a short message explaining where the failure happened.
We have stack traces, but it's still very convenient when there's a short textual explanation on what has happened.

Note: good chance that after you execute the comment a few lines above, this will already be obsolete.

Copy link
Member Author

@elichai elichai Feb 25, 2020

Choose a reason for hiding this comment

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

multiset_test.go Outdated
}
m1.Combine(m2)
if m1.Finalize() != zeroHash {
t.Fatalf("m1 was expected to return to have zero hash, but was %s instead", m1.Finalize())
Copy link
Contributor

Choose a reason for hiding this comment

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

Grammar: to return to have

Copy link
Member Author

Choose a reason for hiding this comment

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

oldschnorr.go Outdated
func (key *SchnorrPublicKey) Negate() {
ret := C.secp256k1_ec_pubkey_negate(C.secp256k1_context_no_precomp, &key.pubkey)
if ret != 1 {
panic("Failed Negating the public key. should never happen")
Copy link
Contributor

Choose a reason for hiding this comment

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

Error messages shouldn't start with capital letters

secp256k1.go Outdated
@@ -0,0 +1,92 @@
package secp256k1

// // **This is CGO's build system. yes, in comments.**
Copy link
Contributor

Choose a reason for hiding this comment

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

Complaining about the language's quirks in the code is very understandable, but undesirable

Copy link
Member Author

Choose a reason for hiding this comment

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

It's both complaining and being highly explicit that these are not comments. suggested language?

Copy link
Contributor

Choose a reason for hiding this comment

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

How about something like:
"CGO defs. CGO parses the following comments as build instructions."

}
}

type PrivateKey struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

type PrivateKey [32]byte is much more concise and nicer to use

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, it might be a good idea to move PrivateKey/PublicKey and all related methods to a separate file/s.

Copy link
Member Author

Choose a reason for hiding this comment

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

Didn't known that type PrivateKey [32]byte is a thing.

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like you missed fixing these comments.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it's better to not let users access the private key directly like that, the struct adds a visibility barrier (with no cost I assume).

And this actually increase the line count, because it requires me to assign temporaries before returning(because you can't take the address of an array without assigning it)

"testing"
)

// TODO: Add test vectors. (even though they're included in libsecp itself)
Copy link
Contributor

Choose a reason for hiding this comment

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

Here too: no TODOs in the code, create a ticket instead


const LoopsN = 150

var Secp256k1Order, _ = new(big.Int).SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16)
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't ignore errors.

You can do initialization logic in TestMain instead.

What I'd do is have the string representation as a const, than have TestMain parse it and assign global var Secp256k1Order

Copy link
Member Author

Choose a reason for hiding this comment

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

What do you think about that vs doing this?
var Secp256k1Order = new(big.Int).SetBytes([]byte{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 186, 174, 220, 230, 175, 72, 160, 59, 191, 210, 94, 140, 208, 54, 65, 65})

This is way longer and not readable, but it skips the need for TestMain

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess this can work. It is indeed ugly and not readible, but A) this is a test, so not as critical. and B) it's not as though the hexa was in any way more readable, just shorter.

Copy link
Contributor

@svarogg svarogg left a comment

Choose a reason for hiding this comment

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

Another note about govet/golint: we can help setup Jenkins to run this automatically on PRs, to make sure we are always clear.

depend/update-subtree.sh Show resolved Hide resolved
secp256k1.go Outdated
@@ -0,0 +1,92 @@
package secp256k1

// // **This is CGO's build system. yes, in comments.**
Copy link
Contributor

Choose a reason for hiding this comment

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

How about something like:
"CGO defs. CGO parses the following comments as build instructions."

oldschnorr.go Outdated
}

func isZeroed(slice []C.uchar) bool {
for _, v := range slice {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please avoid one-character names. "v" makes readers needlessly stop to think "wait, what's v? Is it something special?" for a moment before realizing that "oh yeah, it's just value"


// TODO: Add test vectors. (even though they're included in libsecp itself)

const LoopsN = 150
Copy link
Contributor

Choose a reason for hiding this comment

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

The name LoopsN is a bit unclear. Please either rename it or add a short explanation.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, does LoopsN need to be exported? If you aren't using it anywhere outside of this package, please unexport it.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's a test variable for how many times to run the loops in the tests. it's used both in secp256k1_test and in multiset_test, I can put different ones in each file though

Copy link
Contributor

Choose a reason for hiding this comment

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

Unlike Rust, files don't define scope. Meaning that since both files are inside the package "secp256k1" if you unexport LoopsN here it will still be available in multiset_test.go.

}
}

func TestPrivateKey_Add_fail(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please capitalize the f in fail.

t.Errorf("A zeroed key is invalid")
}
if err4 != nil || err5 != nil {
t.Errorf("It should be possible to add zero to a key")
Copy link
Contributor

Choose a reason for hiding this comment

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

Please check each error individually and add it to the error string here and everywhere. That way when the test fails we'll know exactly and immediately where it fails instead of having to connect a debugger.

copy(msg32[:], test.message)
valid := pubkey.SchnorrVerify(msg32, sig)
if valid != test.valid {
t.Errorf("Schnorr test vector %d didn't produce correct result", i)
Copy link
Contributor

Choose a reason for hiding this comment

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

Please specify in the error what was the result and what was the expected result.

msg := [32]byte{}
n, err := rand.Read(msg[:])
if err != nil || n != 32 {
panic("benchmark failed")
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add the error to the string.

multiset_test.go Outdated
copy(data[:], Secp256k1Order.Bytes())
_, err := ParseMultiSet(data)
if err == nil {
t.Errorf("Shouldn't be able to parse a multiset bigger with x bigger than the field size")
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add the error itself to the string.

multiset_test.go Outdated
list[i] = data
}
if set.Finalize() == set2.Finalize() {
t.Errorf("sets are the same when they should be different: set %x\n", set.Finalize())
Copy link
Contributor

Choose a reason for hiding this comment

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

Please print both sets.

Copy link
Member Author

Choose a reason for hiding this comment

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

The if only executes if they're the same though

Copy link
Contributor

Choose a reason for hiding this comment

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

Oops, yeah, my bad.

@elichai
Copy link
Member Author

elichai commented Feb 26, 2020

multiset_test.go Outdated
data := [100]byte{}
n, err := rand.Read(data[:])
if err != nil || n != len(data) {
t.Fatalf("Failed generating random data ''%v' ''%d' ", err, n)
Copy link
Contributor

@stasatdaglabs stasatdaglabs Feb 27, 2020

Choose a reason for hiding this comment

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

Please switch the order of err and n, and annotate n. Likewise in other tests.

multiset_test.go Outdated
list[i] = data
}
if set.Finalize() == set2.Finalize() {
t.Errorf("sets are the same when they should be different: set %x\n", set.Finalize())
Copy link
Contributor

Choose a reason for hiding this comment

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

Oops, yeah, my bad.

multiset_test.go Outdated
}
parsedSet, err := ParseMultiSet(serializedEmpty)
if err != nil {
t.Errorf("error: '%v' happened when parsing: ''%x'", err, serializedEmpty)
Copy link
Contributor

Choose a reason for hiding this comment

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

Please put the error at the end of the string. This is because the error string might be quite long.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good idea. Thanks.

@elichai elichai force-pushed the subtree branch 2 times, most recently from a20b377 to 41cded5 Compare February 27, 2020 12:59
multiset.go Outdated Show resolved Hide resolved
multiset.go Show resolved Hide resolved
multiset.go Outdated
// which is a rolling(homomorphic) hash that you can add and remove elements from
// and receiving the same resulting hash as-if you never hashed that element.
// Because of that the order of adding and removing elements doesn't matter.
// The type should only be created via the supplied methods.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd revise to something like "Use NewMultiset to initialize a MultiSet"

multiset.go Outdated Show resolved Hide resolved
multiset.go Outdated Show resolved Hide resolved
multiset_test.go Outdated
panic("bad benchmark")
}
}
if set == NewMultiset() { // To prevent optimizing out the loop
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this a real scenario that really happens?

If yes, please note that once you do that NewMultiset returns a pointer, you might had this functionality broken.

Copy link
Member Author

Choose a reason for hiding this comment

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

I honestly don't know :)
I know this is in fact a problem I've encountered many times in C/C++ and Rust, don't know how aggressive is Go's optimizer and if it gives special treatment for benchmarks

Example: bitcoin-core/secp256k1#667 bitcoin-core/secp256k1#678

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmmmm... I really doubt that's going to happen.
The benchmark loop is a standard golang construct. I have written a ton of benchmark this way and never encountered a similar problem.

I'd rather remove this piece of code, and re-introduce only if we find it's needed.

oldschnorr.go Outdated Show resolved Hide resolved
oldschnorr.go Outdated

// SchnorrVerify verifies a schnorr signature using the public key and the input hashed message.
// Notice: the [32] byte array *MUST* be a hash of a message you hashed yourself.
func (key *SchnorrPublicKey) SchnorrVerify(hash [32]byte, signature SchnorrSignature) bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add type Hash [32]byte and use pointer to it instead of direct [32]byte.
Also use it wherever ecmh returns a hash, and define similar type for ecmh's [64]byte serialization format.

Copy link
Member Author

Choose a reason for hiding this comment

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

What does that help/give?

Copy link
Contributor

Choose a reason for hiding this comment

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

Than you will be able to pass by reference easily.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

oldschnorr.go Outdated Show resolved Hide resolved
}
}

type PrivateKey struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like you missed fixing these comments.

multiset.go Outdated

// NewMultiset return an empty initialized set.
// when finalized it should be equal to a finalized set with all elements removed.
func NewMultiset() MultiSet {
Copy link
Contributor

Choose a reason for hiding this comment

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

It should always be a pointer except for simple types (ints, bools, etc) or types that are reference-types already (strings, maps, slices, interfaces).
In other words - if it's a struct, you should probably pass it around as a pointer.

Indeed, in some cases deep examination would prove that some structs are better of passed-by-value. However, these are the kinds of micro-optimizations that we would rather refrain from, for the sake of simplicity.

That said, in cases where there's a proven bottleneck that would benefit a lot from passing something by-value, no doubt we would consider doing it. Nevertheless - the default go-to should be pass-by-pointer.

multiset.go Outdated

// NewMultiset return an empty initialized set.
// when finalized it should be equal to a finalized set with all elements removed.
func NewMultiset() MultiSet {
Copy link
Contributor

Choose a reason for hiding this comment

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

Also note that all MultiSet functions have a pointer to MultiSet as the receiver.
If you wanted MultiSet to be a value-type for some reason, you would also want it's methods to have a value receiver)

multiset.go Outdated
}
}

// Add a data element to the multiset. This will hash the data onto the curve and add it.
Copy link
Contributor

Choose a reason for hiding this comment

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

Comments in go should be in the form "[Function name] [does blablabla]".
Yes, what you did satisfies the linter, because comment indeed starts with the name of the function, but it misses the point.

Please fix in all the codebase

Copy link
Member Author

Choose a reason for hiding this comment

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

Are the new ones ok? not the best in wording :)

Copy link
Contributor

Choose a reason for hiding this comment

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

The new ones are looking fine. Wording can be always improved, but it is absolutely fine.

multiset.go Outdated Show resolved Hide resolved
multiset_test.go Outdated Show resolved Hide resolved
multiset_test.go Outdated
panic("bad benchmark")
}
}
if set == NewMultiset() { // To prevent optimizing out the loop
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmmmm... I really doubt that's going to happen.
The benchmark loop is a standard golang construct. I have written a ton of benchmark this way and never encountered a similar problem.

I'd rather remove this piece of code, and re-introduce only if we find it's needed.

return key.privateKey
}

// Negate a private key in place.
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is Negate needed?

Asking more out of curiosity than anything else

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought it was needed for BIP32 but I think that's not true.
But I guess there are other more complex applications that require this bitcoin-core/secp256k1#408

secp256k1_test.go Outdated Show resolved Hide resolved
oldschnorr.go Outdated Show resolved Hide resolved

const LoopsN = 150

var Secp256k1Order, _ = new(big.Int).SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16)
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess this can work. It is indeed ugly and not readible, but A) this is a test, so not as critical. and B) it's not as though the hexa was in any way more readable, just shorter.

oldschnorr.go Outdated
// DeserializeSchnorrSignature deserializes a 64 byte serialized schnorr signature into a SchnorrSignature type.
func DeserializeSchnorrSignature(serializedSignature [64]byte) SchnorrSignature {
signature := SchnorrSignature{}
signature.signature = serializedSignature
Copy link
Contributor

Choose a reason for hiding this comment

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

You can write this whole function in one line return SchnorrSignature{signature:serializedSignature}

oldschnorr.go Outdated Show resolved Hide resolved
@elichai
Copy link
Member Author

elichai commented Mar 22, 2020

Should I just implement IsEqual(), Clone() and String() on all the structs?

@svarogg
Copy link
Contributor

svarogg commented Mar 23, 2020

Should I just implement

@svarogg svarogg closed this Mar 23, 2020
@svarogg svarogg reopened this Mar 23, 2020
@svarogg
Copy link
Contributor

svarogg commented Mar 23, 2020

Should I just implement IsEqual(), Clone() and String() on all the structs?

It is redundant in many cases, but for hardcore-library-level code that you are writing:
String() - in 99% cases yes
IsEqual() - in all cases that comparing makes sense
Clone() - only for objects where cloning is something that will happen

@svarogg svarogg merged commit 5bc7b08 into master Mar 24, 2020
@elichai elichai deleted the subtree branch March 24, 2020 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants