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

POC: use wrapper function for batch verification #5695

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions crypto/batchverifier.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#include "sodium.h"
int ed25519_batch_wrapper(const unsigned char *messages1D,
const unsigned long long *mlen,
const unsigned char *publicKeys1D,
const unsigned char *signatures1D,
size_t num,
int *valid) {
int ret;
const unsigned char **messages2D, **publicKeys2D, **signatures2D;
messages2D = malloc(num * sizeof(unsigned char *));
Copy link
Contributor

Choose a reason for hiding this comment

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

These "same" operations are done in go. What is special about doing this in a C wrapper function?

publicKeys2D = malloc(num * sizeof(unsigned char *));
signatures2D = malloc(num * sizeof(unsigned char *));

// fill 2-D arrays for messages, pks, sigs from provided 1-D arrays
unsigned long long mpos = 0;
for (size_t i = 0; i < num; i++) {
messages2D[i] = &messages1D[mpos];
mpos += mlen[i];
publicKeys2D[i] = &publicKeys1D[i*crypto_sign_ed25519_PUBLICKEYBYTES];
signatures2D[i] = &signatures1D[i*crypto_sign_ed25519_BYTES];
}
ret = crypto_sign_ed25519_open_batch(messages2D, mlen, publicKeys2D, signatures2D, num, valid);

free(messages2D);
free(publicKeys2D);
free(signatures2D);
return ret;
}
104 changes: 42 additions & 62 deletions crypto/batchverifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,28 @@ package crypto
// #cgo windows,amd64 CFLAGS: -I${SRCDIR}/libs/windows/amd64/include
// #cgo windows,amd64 LDFLAGS: ${SRCDIR}/libs/windows/amd64/lib/libsodium.a
// #include <stdint.h>
// #include "sodium.h"
// enum {
// sizeofPtr = sizeof(void*),
// sizeofULongLong = sizeof(unsigned long long),
// };
// int ed25519_batch_wrapper(const unsigned char *messages1D,
// const unsigned long long *mlen,
// const unsigned char *publicKeys1D,
// const unsigned char *signatures1D,
// size_t num,
// int *valid_p);
import "C"
import (
"errors"
"runtime"
"unsafe"
)

// BatchVerifier enqueues signatures to be validated in batch.
type BatchVerifier struct {
messages []Hashable // contains a slice of messages to be hashed. Each message is varible length
publicKeys []SignatureVerifier // contains a slice of public keys. Each individual public key is 32 bytes.
signatures []Signature // contains a slice of signatures keys. Each individual signature is 64 bytes.
messageHashReps []byte // contains a slice of concatenated bytes of the HashRep of the messages to be hashed. Each message is varible length
messageLens []C.ulonglong // the lengths of each message in messageHashReps
publicKeys []byte // contains a slice of concatenated public keys. Each individual public key is 32 bytes.
signatures []byte // contains a slice of concatenated signatures. Each individual signature is 64 bytes.
Comment on lines +53 to +54
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 this change from proper indexed type to byte?
What is wrong with the existing type?

}

const minBatchVerifierAlloc = 16
Expand Down Expand Up @@ -75,38 +80,44 @@ func MakeBatchVerifierWithHint(hint int) *BatchVerifier {
hint = minBatchVerifierAlloc
}
return &BatchVerifier{
messages: make([]Hashable, 0, hint),
publicKeys: make([]SignatureVerifier, 0, hint),
signatures: make([]Signature, 0, hint),
messageHashReps: make([]byte, 0), // XXX can we get a better hint?
Copy link
Contributor

Choose a reason for hiding this comment

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

The hint is 16 for streamed transactions, and cannot be improved without guessing.
In the case of block validation, it is precise, since we know the number of transactions.

As for the message size, for blocks it can be calculated, but not for the case of streamed transactions.

messageLens: make([]C.ulonglong, hint),
publicKeys: make([]byte, 0, hint*ed25519PublicKeySize),
signatures: make([]byte, 0, hint*ed25519SignatureSize),
}
}

// EnqueueSignature enqueues a signature to be enqueued
func (b *BatchVerifier) EnqueueSignature(sigVerifier SignatureVerifier, message Hashable, sig Signature) {
// do we need to reallocate ?
if len(b.messages) == cap(b.messages) {
if len(b.messageLens) == cap(b.messageLens) {
b.expand()
}
b.messages = append(b.messages, message)
b.publicKeys = append(b.publicKeys, sigVerifier)
b.signatures = append(b.signatures, sig)
msgHashRep := HashRep(message)
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 the opposite of what we need to do.
Instead of calling this here, as we are accumulating elements into the batch, we should call HashRep later on, so that we don't perform unnecessary data copies.

b.messageHashReps = append(b.messageHashReps, msgHashRep...)
Copy link
Contributor

@algonautshant algonautshant Aug 22, 2023

Choose a reason for hiding this comment

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

This is the first guaranteed extra copy we are doing here. In the current code, this copy does not exist. the HashRep call returned buffer is not copied, but passed by reference to C.
Here, it is getting copied to b.messageHashReps.

b.messageLens = append(b.messageLens, C.ulonglong(len(msgHashRep)))
b.publicKeys = append(b.publicKeys, sigVerifier[:]...)
b.signatures = append(b.signatures, sig[:]...)
}

func (b *BatchVerifier) expand() {
messages := make([]Hashable, len(b.messages), len(b.messages)*2)
publicKeys := make([]SignatureVerifier, len(b.publicKeys), len(b.publicKeys)*2)
signatures := make([]Signature, len(b.signatures), len(b.signatures)*2)
copy(messages, b.messages)
messageHashReps := make([]byte, len(b.messageHashReps), len(b.messageHashReps)*2)
Copy link
Contributor

Choose a reason for hiding this comment

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

Potentially, many more copies are made here, which were not done in the past. This is a regression, and will also impact the block validation.

messageLens := make([]C.ulonglong, len(b.messageLens), len(b.messageLens)*2)
publicKeys := make([]byte, len(b.publicKeys), len(b.publicKeys)*2*ed25519PublicKeySize)
signatures := make([]byte, len(b.signatures), len(b.signatures)*2*ed25519SignatureSize)
copy(messageLens, b.messageLens)
copy(messageHashReps, b.messageHashReps)
copy(publicKeys, b.publicKeys)
copy(signatures, b.signatures)
b.messages = messages
b.messageHashReps = messageHashReps
b.messageLens = messageLens
b.publicKeys = publicKeys
b.signatures = signatures
}

// GetNumberOfEnqueuedSignatures returns the number of signatures currently enqueued into the BatchVerifier
func (b *BatchVerifier) GetNumberOfEnqueuedSignatures() int {
return len(b.messages)
return len(b.messageLens)
}

// Verify verifies that all the signatures are valid. in that case nil is returned
Expand All @@ -123,11 +134,7 @@ func (b *BatchVerifier) VerifyWithFeedback() (failed []bool, err error) {
if b.GetNumberOfEnqueuedSignatures() == 0 {
return nil, nil
}
var messages = make([][]byte, b.GetNumberOfEnqueuedSignatures())
for i := range b.messages {
messages[i] = HashRep(b.messages[i])
}
allValid, failed := batchVerificationImpl(messages, b.publicKeys, b.signatures)
allValid, failed := batchVerificationImpl(b.messageHashReps, b.messageLens, b.publicKeys, b.signatures)
if allValid {
return failed, nil
}
Expand All @@ -137,50 +144,23 @@ func (b *BatchVerifier) VerifyWithFeedback() (failed []bool, err error) {
// batchVerificationImpl invokes the ed25519 batch verification algorithm.
// it returns true if all the signatures were authentically signed by the owners
// otherwise, returns false, and sets the indexes of the failed sigs in failed
func batchVerificationImpl(messages [][]byte, publicKeys []SignatureVerifier, signatures []Signature) (allSigsValid bool, failed []bool) {

numberOfSignatures := len(messages)

messagesAllocation := C.malloc(C.size_t(C.sizeofPtr * numberOfSignatures))
messagesLenAllocation := C.malloc(C.size_t(C.sizeofULongLong * numberOfSignatures))
publicKeysAllocation := C.malloc(C.size_t(C.sizeofPtr * numberOfSignatures))
signaturesAllocation := C.malloc(C.size_t(C.sizeofPtr * numberOfSignatures))
valid := C.malloc(C.size_t(C.sizeof_int * numberOfSignatures))

defer func() {
// release staging memory
C.free(messagesAllocation)
C.free(messagesLenAllocation)
C.free(publicKeysAllocation)
C.free(signaturesAllocation)
C.free(valid)
}()

// load all the data pointers into the array pointers.
for i := 0; i < numberOfSignatures; i++ {
*(*uintptr)(unsafe.Pointer(uintptr(messagesAllocation) + uintptr(i*C.sizeofPtr))) = uintptr(unsafe.Pointer(&messages[i][0]))
*(*C.ulonglong)(unsafe.Pointer(uintptr(messagesLenAllocation) + uintptr(i*C.sizeofULongLong))) = C.ulonglong(len(messages[i]))
*(*uintptr)(unsafe.Pointer(uintptr(publicKeysAllocation) + uintptr(i*C.sizeofPtr))) = uintptr(unsafe.Pointer(&publicKeys[i][0]))
*(*uintptr)(unsafe.Pointer(uintptr(signaturesAllocation) + uintptr(i*C.sizeofPtr))) = uintptr(unsafe.Pointer(&signatures[i][0]))
}
func batchVerificationImpl(messageHashReps []byte, messageLens []C.ulonglong, publicKeys []byte, signatures []byte) (allSigsValid bool, failed []bool) {
numberOfSignatures := len(messageLens)

valid := make([]C.int, numberOfSignatures)

// call the batch verifier
allValid := C.crypto_sign_ed25519_open_batch(
(**C.uchar)(unsafe.Pointer(messagesAllocation)),
(*C.ulonglong)(unsafe.Pointer(messagesLenAllocation)),
(**C.uchar)(unsafe.Pointer(publicKeysAllocation)),
(**C.uchar)(unsafe.Pointer(signaturesAllocation)),
C.size_t(len(messages)),
(*C.int)(unsafe.Pointer(valid)))

runtime.KeepAlive(messages)
runtime.KeepAlive(publicKeys)
runtime.KeepAlive(signatures)
allValid := C.ed25519_batch_wrapper(
(*C.uchar)(&messageHashReps[0]),
(*C.ulonglong)(&messageLens[0]),
(*C.uchar)(&publicKeys[0]),
(*C.uchar)(&signatures[0]),
C.size_t(numberOfSignatures),
(*C.int)(&valid[0]))

failed = make([]bool, numberOfSignatures)
for i := 0; i < numberOfSignatures; i++ {
cint := *(*C.int)(unsafe.Pointer(uintptr(valid) + uintptr(i*C.sizeof_int)))
failed[i] = (cint == 0)
failed[i] = (valid[i] == 0)
}
return allValid == 0, failed
}
10 changes: 9 additions & 1 deletion crypto/batchverifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,14 @@ func BenchmarkBatchVerifierBig(b *testing.B) {
}
}

func (b *BatchVerifier) getSignature(i int) Signature {
if i > len(b.messageLens) {
panic("getSignature for i greater than length of messages")
}
sigbuf := b.signatures[i*ed25519SignatureSize : (i+1)*ed25519SignatureSize]
return *(*[ed25519SignatureSize]byte)(sigbuf)
}

// BenchmarkBatchVerifierBigWithInvalid builds over BenchmarkBatchVerifierBig by introducing
// invalid sigs to even numbered batch sizes. This shows the impact of invalid sigs on the
// performance. Basically, all the gains from batching disappear.
Expand All @@ -171,7 +179,7 @@ func BenchmarkBatchVerifierBigWithInvalid(b *testing.B) {
failed, err := bv.VerifyWithFeedback()
if err != nil {
for i, f := range failed {
if bv.signatures[i] == badSig {
if bv.getSignature(i) == badSig {
require.True(b, f)
} else {
require.False(b, f)
Expand Down
15 changes: 11 additions & 4 deletions crypto/curve25519.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,17 @@ func init() {
type Seed ed25519Seed

/* Classical signatures */
type ed25519Signature [64]byte
type ed25519PublicKey [32]byte
type ed25519PrivateKey [64]byte
type ed25519Seed [32]byte
const (
ed25519SignatureSize = 64
ed25519PublicKeySize = 32
ed25519PrivateKeySize = 64
ed25519SeedSize = 32
)

type ed25519Signature [ed25519SignatureSize]byte
type ed25519PublicKey [ed25519PublicKeySize]byte
type ed25519PrivateKey [ed25519PrivateKeySize]byte
type ed25519Seed [ed25519SeedSize]byte

// MasterDerivationKey is used to derive ed25519 keys for use in wallets
type MasterDerivationKey [masterDerivationKeyLenBytes]byte
Expand Down
29 changes: 24 additions & 5 deletions crypto/onetimesig.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package crypto

import "C"
import (
"encoding/binary"
"fmt"
Expand Down Expand Up @@ -319,11 +320,29 @@ func (v OneTimeSignatureVerifier) Verify(id OneTimeSignatureIdentifier, message
Batch: id.Batch,
}

allValid, _ := batchVerificationImpl(
[][]byte{HashRep(batchID), HashRep(offsetID), HashRep(message)},
[]PublicKey{PublicKey(v), PublicKey(batchID.SubKeyPK), PublicKey(offsetID.SubKeyPK)},
[]Signature{Signature(sig.PK2Sig), Signature(sig.PK1Sig), Signature(sig.Sig)},
)
// Get the hash representations of the batch ID, offset ID, and message.
batchIDHashRep := HashRep(batchID)
offsetIDHashRep := HashRep(offsetID)
messageHashRep := HashRep(message)
// Append the hash representations and lengths to slices for batch verification.
msgHashReps := make([]byte, 0)
msgLens := make([]C.ulonglong, 0, 3)
msgHashReps = append(msgHashReps, batchIDHashRep...)
msgLens = append(msgLens, C.ulonglong(len(batchIDHashRep)))
msgHashReps = append(msgHashReps, offsetIDHashRep...)
msgLens = append(msgLens, C.ulonglong(len(offsetIDHashRep)))
msgHashReps = append(msgHashReps, messageHashRep...)
msgLens = append(msgLens, C.ulonglong(len(messageHashRep)))
// Append the public keys and signatures to slices for batch verification.
pks := make([]byte, 0, ed25519PublicKeySize*3)
pks = append(pks, v[:]...)
pks = append(pks, batchID.SubKeyPK[:]...)
pks = append(pks, offsetID.SubKeyPK[:]...)
sigs := make([]byte, 0, ed25519SignatureSize*3)
sigs = append(sigs, sig.PK2Sig[:]...)
sigs = append(sigs, sig.PK1Sig[:]...)
sigs = append(sigs, sig.Sig[:]...)
allValid, _ := batchVerificationImpl(msgHashReps, msgLens, pks, sigs)
return allValid
}

Expand Down