Skip to content
Merged
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
55 changes: 41 additions & 14 deletions crypto/batchverifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,35 @@ func ed25519_randombytes_unsafe(p unsafe.Pointer, len C.size_t) {
const minBatchVerifierAlloc = 16
const useSingleVerifierDefault = true

// MakeBatchVerifier creates a BatchVerifier instance with the provided options.
// ed25519BatchVerifierFactory is the global singleton used for batch signature verification.
// By default it uses the libsodium implementation. This can be changed during initialization
// (e.g., by the config package when algod loads) to use the ed25519consensus implementation.
var ed25519BatchVerifierFactory func(hint int) BatchVerifier = makeLibsodiumBatchVerifier
Comment thread
jannotti marked this conversation as resolved.

// SetEd25519BatchVerifier allows the config package to switch the implementation
// at startup based on configuration. Pass true to use ed25519consensus, false for libsodium.
func SetEd25519BatchVerifier(useEd25519Consensus bool) {
if useEd25519Consensus {
ed25519BatchVerifierFactory = makeEd25519ConsensusBatchVerifier
} else {
ed25519BatchVerifierFactory = makeLibsodiumBatchVerifier
}
}
Comment thread
jannotti marked this conversation as resolved.

// MakeBatchVerifier creates a BatchVerifier instance.
func MakeBatchVerifier() BatchVerifier {
return MakeBatchVerifierWithHint(minBatchVerifierAlloc)
return ed25519BatchVerifierFactory(minBatchVerifierAlloc)
}

// MakeBatchVerifierWithHint creates a cgoBatchVerifier instance. This function pre-allocates
// amount of free space to enqueue signatures without expanding
// MakeBatchVerifierWithHint creates a BatchVerifier instance. This function pre-allocates
// space to enqueue signatures without expanding.
func MakeBatchVerifierWithHint(hint int) BatchVerifier {
return ed25519BatchVerifierFactory(hint)
}

func makeLibsodiumBatchVerifier(hint int) BatchVerifier {
// preallocate enough storage for the expected usage. We will reallocate as needed.
if hint < minBatchVerifierAlloc {
if hint <= 0 {
hint = minBatchVerifierAlloc
}
return &cgoBatchVerifier{
Expand Down Expand Up @@ -152,7 +171,7 @@ func (b *cgoBatchVerifier) VerifyWithFeedback() (failed []bool, err error) {
}
allValid, failed := cgoBatchVerificationImpl(messages, msgLengths, b.publicKeys, b.signatures)
if allValid {
return failed, nil
return nil, nil
}
return failed, ErrBatchHasFailedSigs
}
Expand All @@ -170,7 +189,7 @@ func (b *cgoBatchVerifier) singleVerify() (failed []bool, err error) {
if containsFailed {
return failed, ErrBatchHasFailedSigs
}
return failed, nil
return nil, nil
}

// cgoBatchVerificationImpl invokes the ed25519 batch verification algorithm.
Expand All @@ -185,18 +204,26 @@ func cgoBatchVerificationImpl(messages []byte, msgLengths []uint64, publicKeys [
signatures2D := make([]*C.uchar, numberOfSignatures)

// call the batch verifier
// Use unsafe.SliceData to safely get pointers to underlying arrays
allValid := C.ed25519_batch_wrapper(
&messages2D[0], &publicKeys2D[0], &signatures2D[0],
(*C.uchar)(&messages[0]),
(*C.ulonglong)(&msgLengths[0]),
(*C.uchar)(&publicKeys[0][0]),
(*C.uchar)(&signatures[0][0]),
(**C.uchar)(unsafe.SliceData(messages2D)),
(**C.uchar)(unsafe.SliceData(publicKeys2D)),
(**C.uchar)(unsafe.SliceData(signatures2D)),
(*C.uchar)(unsafe.SliceData(messages)),
(*C.ulonglong)(unsafe.SliceData(msgLengths)),
(*C.uchar)(unsafe.SliceData(publicKeys[0][:])),
(*C.uchar)(unsafe.SliceData(signatures[0][:])),
C.size_t(numberOfSignatures),
(*C.int)(&valid[0]))
(*C.int)(unsafe.SliceData(valid)))
Comment thread
cce marked this conversation as resolved.

if allValid == 0 { // all signatures valid
return true, nil
}

// not all signatures valid, identify the failed signatures
failed = make([]bool, numberOfSignatures)
for i := 0; i < numberOfSignatures; i++ {
Comment thread
jannotti marked this conversation as resolved.
failed[i] = (valid[i] == 0)
}
return allValid == 0, failed
return false, failed
}
123 changes: 123 additions & 0 deletions crypto/batchverifier_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright (C) 2019-2025 Algorand, Inc.
// This file is part of go-algorand
//
// go-algorand is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// go-algorand is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with go-algorand. If not, see <https://www.gnu.org/licenses/>.

package crypto

import (
cryptorand "crypto/rand"
"io"
"testing"

"github.com/algorand/go-algorand/test/partitiontest"
"github.com/stretchr/testify/require"
)

func randSignedMsg(t testing.TB, r io.Reader) (SignatureVerifier, Hashable, Signature) {
mlen := 100
msg := TestingHashable{data: make([]byte, mlen)}
n, err := r.Read(msg.data)
require.NoError(t, err)
require.Equal(t, n, mlen)
var s Seed
n, err = r.Read(s[:])
require.NoError(t, err)
require.Equal(t, 32, n)
secrets := GenerateSignatureSecrets(s)
return secrets.SignatureVerifier, msg, secrets.Sign(msg)
}

// BenchmarkBatchVerifierImpls benchmarks different batch verification implementations
// with realistic batch sizes (100 batches of 64 signatures each)
func BenchmarkBatchVerifierImpls(b *testing.B) {
partitiontest.PartitionTest(b)

numBatches := 100
batchSize := 64
msgs := make([][]Hashable, numBatches)
pks := make([][]SignatureVerifier, numBatches)
sigs := make([][]Signature, numBatches)
r := cryptorand.Reader
for i := 0; i < numBatches; i++ {
for j := 0; j < batchSize; j++ {
pk, msg, sig := randSignedMsg(b, r)
msgs[i] = append(msgs[i], msg)
pks[i] = append(pks[i], pk)
sigs[i] = append(sigs[i], sig)
}
}

b.Log("running with", b.N, "iterations using", len(msgs), "batches of", batchSize, "signatures")
runImpl := func(b *testing.B, bv BatchVerifier,
msgs [][]Hashable, pks [][]SignatureVerifier, sigs [][]Signature) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
batchIdx := i % numBatches
for j := range msgs[batchIdx] {
bv.EnqueueSignature(pks[batchIdx][j], msgs[batchIdx][j], sigs[batchIdx][j])
}
require.NoError(b, bv.Verify())
}
}

b.Run("libsodium_single", func(b *testing.B) {
bv := makeLibsodiumBatchVerifier(batchSize)
bv.(*cgoBatchVerifier).useSingle = true
runImpl(b, bv, msgs, pks, sigs)
})
b.Run("libsodium_batch", func(b *testing.B) {
bv := makeLibsodiumBatchVerifier(batchSize)
bv.(*cgoBatchVerifier).useSingle = false
runImpl(b, bv, msgs, pks, sigs)
})
b.Run("ed25519consensus", func(b *testing.B) {
bv := makeEd25519ConsensusBatchVerifier(batchSize)
runImpl(b, bv, msgs, pks, sigs)
})
}

func BenchmarkCanonicalityCheck(b *testing.B) {
partitiontest.PartitionTest(b)

const maxN = 10000
pubkeys := make([]SignatureVerifier, maxN)
sigs := make([]Signature, maxN)
for i := 0; i < maxN; i++ {
var s Seed
RandBytes(s[:])
sigSecrets := GenerateSignatureSecrets(s)
pubkeys[i] = sigSecrets.SignatureVerifier
msg := randString()
sigs[i] = sigSecrets.Sign(msg)
}

b.Run("pubkey_check", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = isCanonicalPoint(pubkeys[i%maxN])
}
})

b.Run("signature_R_check", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = isCanonicalPoint([32]byte(sigs[i%maxN][:32]))
}
})

b.Run("both_checks", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = !isCanonicalPoint(pubkeys[i%maxN]) || !isCanonicalPoint([32]byte(sigs[i%maxN][:32]))
}
})
}
Loading
Loading