Skip to content

Commit 7a49dea

Browse files
ccealgorandskiy
authored andcommitted
crypto: add go-based BatchVerifier implementation (algorand#6440)
This adds a pure-Go ed25519 BatchVerifier implementation based on the ed25519consensus library, with additional checks to preserve our ed25519 validation criteria, last updated in algorand#3031. Like our libsodium batch verification implementation, the IsCanonicalY check here is also based on the "Taming the Many EdDSAs" paper in https://eprint.iacr.org/2020/1244 New tests added to compare ed25519 criteria results match our existing libsodium- and ed25519-donna-based batch verification implementation (from algorand#3031 and defined in algorandfoundation/specs#60). New test helpers run test vectors with different batch sizes, taken from - 12 edge cases from "Taming the many EdDSAs" Appendix C, Table 6c, also used in our libsodium fork's tests in batch verification: add ed25519 batch verification implementation algorand#3031 - 1025 successful cases from our libsodium fork's tests, also added in batch verification: add ed25519 batch verification implementation algorand#3031. - 196 edge cases used to draw the 14x14 grid visualizations from the blog post "It's 255:19AM. Do you know what your validation criteria are?" and used in ed25519consensus - 768 edge cases from the Go crypto package's crypto/ed25519/ed25519vectors_test.go
1 parent f62b788 commit 7a49dea

File tree

12 files changed

+1656
-60
lines changed

12 files changed

+1656
-60
lines changed

crypto/batchverifier.go

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,35 @@ func ed25519_randombytes_unsafe(p unsafe.Pointer, len C.size_t) {
7373
const minBatchVerifierAlloc = 16
7474
const useSingleVerifierDefault = true
7575

76-
// MakeBatchVerifier creates a BatchVerifier instance with the provided options.
76+
// ed25519BatchVerifierFactory is the global singleton used for batch signature verification.
77+
// By default it uses the libsodium implementation. This can be changed during initialization
78+
// (e.g., by the config package when algod loads) to use the ed25519consensus implementation.
79+
var ed25519BatchVerifierFactory func(hint int) BatchVerifier = makeLibsodiumBatchVerifier
80+
81+
// SetEd25519BatchVerifier allows the config package to switch the implementation
82+
// at startup based on configuration. Pass true to use ed25519consensus, false for libsodium.
83+
func SetEd25519BatchVerifier(useEd25519Consensus bool) {
84+
if useEd25519Consensus {
85+
ed25519BatchVerifierFactory = makeEd25519ConsensusBatchVerifier
86+
} else {
87+
ed25519BatchVerifierFactory = makeLibsodiumBatchVerifier
88+
}
89+
}
90+
91+
// MakeBatchVerifier creates a BatchVerifier instance.
7792
func MakeBatchVerifier() BatchVerifier {
78-
return MakeBatchVerifierWithHint(minBatchVerifierAlloc)
93+
return ed25519BatchVerifierFactory(minBatchVerifierAlloc)
7994
}
8095

81-
// MakeBatchVerifierWithHint creates a cgoBatchVerifier instance. This function pre-allocates
82-
// amount of free space to enqueue signatures without expanding
96+
// MakeBatchVerifierWithHint creates a BatchVerifier instance. This function pre-allocates
97+
// space to enqueue signatures without expanding.
8398
func MakeBatchVerifierWithHint(hint int) BatchVerifier {
99+
return ed25519BatchVerifierFactory(hint)
100+
}
101+
102+
func makeLibsodiumBatchVerifier(hint int) BatchVerifier {
84103
// preallocate enough storage for the expected usage. We will reallocate as needed.
85-
if hint < minBatchVerifierAlloc {
104+
if hint <= 0 {
86105
hint = minBatchVerifierAlloc
87106
}
88107
return &cgoBatchVerifier{
@@ -152,7 +171,7 @@ func (b *cgoBatchVerifier) VerifyWithFeedback() (failed []bool, err error) {
152171
}
153172
allValid, failed := cgoBatchVerificationImpl(messages, msgLengths, b.publicKeys, b.signatures)
154173
if allValid {
155-
return failed, nil
174+
return nil, nil
156175
}
157176
return failed, ErrBatchHasFailedSigs
158177
}
@@ -170,7 +189,7 @@ func (b *cgoBatchVerifier) singleVerify() (failed []bool, err error) {
170189
if containsFailed {
171190
return failed, ErrBatchHasFailedSigs
172191
}
173-
return failed, nil
192+
return nil, nil
174193
}
175194

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

187206
// call the batch verifier
207+
// Use unsafe.SliceData to safely get pointers to underlying arrays
188208
allValid := C.ed25519_batch_wrapper(
189-
&messages2D[0], &publicKeys2D[0], &signatures2D[0],
190-
(*C.uchar)(&messages[0]),
191-
(*C.ulonglong)(&msgLengths[0]),
192-
(*C.uchar)(&publicKeys[0][0]),
193-
(*C.uchar)(&signatures[0][0]),
209+
(**C.uchar)(unsafe.SliceData(messages2D)),
210+
(**C.uchar)(unsafe.SliceData(publicKeys2D)),
211+
(**C.uchar)(unsafe.SliceData(signatures2D)),
212+
(*C.uchar)(unsafe.SliceData(messages)),
213+
(*C.ulonglong)(unsafe.SliceData(msgLengths)),
214+
(*C.uchar)(unsafe.SliceData(publicKeys[0][:])),
215+
(*C.uchar)(unsafe.SliceData(signatures[0][:])),
194216
C.size_t(numberOfSignatures),
195-
(*C.int)(&valid[0]))
217+
(*C.int)(unsafe.SliceData(valid)))
218+
219+
if allValid == 0 { // all signatures valid
220+
return true, nil
221+
}
196222

223+
// not all signatures valid, identify the failed signatures
197224
failed = make([]bool, numberOfSignatures)
198225
for i := 0; i < numberOfSignatures; i++ {
199226
failed[i] = (valid[i] == 0)
200227
}
201-
return allValid == 0, failed
228+
return false, failed
202229
}

crypto/batchverifier_bench_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright (C) 2019-2025 Algorand, Inc.
2+
// This file is part of go-algorand
3+
//
4+
// go-algorand is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as
6+
// published by the Free Software Foundation, either version 3 of the
7+
// License, or (at your option) any later version.
8+
//
9+
// go-algorand is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with go-algorand. If not, see <https://www.gnu.org/licenses/>.
16+
17+
package crypto
18+
19+
import (
20+
cryptorand "crypto/rand"
21+
"io"
22+
"testing"
23+
24+
"github.com/algorand/go-algorand/test/partitiontest"
25+
"github.com/stretchr/testify/require"
26+
)
27+
28+
func randSignedMsg(t testing.TB, r io.Reader) (SignatureVerifier, Hashable, Signature) {
29+
mlen := 100
30+
msg := TestingHashable{data: make([]byte, mlen)}
31+
n, err := r.Read(msg.data)
32+
require.NoError(t, err)
33+
require.Equal(t, n, mlen)
34+
var s Seed
35+
n, err = r.Read(s[:])
36+
require.NoError(t, err)
37+
require.Equal(t, 32, n)
38+
secrets := GenerateSignatureSecrets(s)
39+
return secrets.SignatureVerifier, msg, secrets.Sign(msg)
40+
}
41+
42+
// BenchmarkBatchVerifierImpls benchmarks different batch verification implementations
43+
// with realistic batch sizes (100 batches of 64 signatures each)
44+
func BenchmarkBatchVerifierImpls(b *testing.B) {
45+
partitiontest.PartitionTest(b)
46+
47+
numBatches := 100
48+
batchSize := 64
49+
msgs := make([][]Hashable, numBatches)
50+
pks := make([][]SignatureVerifier, numBatches)
51+
sigs := make([][]Signature, numBatches)
52+
r := cryptorand.Reader
53+
for i := 0; i < numBatches; i++ {
54+
for j := 0; j < batchSize; j++ {
55+
pk, msg, sig := randSignedMsg(b, r)
56+
msgs[i] = append(msgs[i], msg)
57+
pks[i] = append(pks[i], pk)
58+
sigs[i] = append(sigs[i], sig)
59+
}
60+
}
61+
62+
b.Log("running with", b.N, "iterations using", len(msgs), "batches of", batchSize, "signatures")
63+
runImpl := func(b *testing.B, bv BatchVerifier,
64+
msgs [][]Hashable, pks [][]SignatureVerifier, sigs [][]Signature) {
65+
b.ResetTimer()
66+
for i := 0; i < b.N; i++ {
67+
batchIdx := i % numBatches
68+
for j := range msgs[batchIdx] {
69+
bv.EnqueueSignature(pks[batchIdx][j], msgs[batchIdx][j], sigs[batchIdx][j])
70+
}
71+
require.NoError(b, bv.Verify())
72+
}
73+
}
74+
75+
b.Run("libsodium_single", func(b *testing.B) {
76+
bv := makeLibsodiumBatchVerifier(batchSize)
77+
bv.(*cgoBatchVerifier).useSingle = true
78+
runImpl(b, bv, msgs, pks, sigs)
79+
})
80+
b.Run("libsodium_batch", func(b *testing.B) {
81+
bv := makeLibsodiumBatchVerifier(batchSize)
82+
bv.(*cgoBatchVerifier).useSingle = false
83+
runImpl(b, bv, msgs, pks, sigs)
84+
})
85+
b.Run("ed25519consensus", func(b *testing.B) {
86+
bv := makeEd25519ConsensusBatchVerifier(batchSize)
87+
runImpl(b, bv, msgs, pks, sigs)
88+
})
89+
}
90+
91+
func BenchmarkCanonicalityCheck(b *testing.B) {
92+
partitiontest.PartitionTest(b)
93+
94+
const maxN = 10000
95+
pubkeys := make([]SignatureVerifier, maxN)
96+
sigs := make([]Signature, maxN)
97+
for i := 0; i < maxN; i++ {
98+
var s Seed
99+
RandBytes(s[:])
100+
sigSecrets := GenerateSignatureSecrets(s)
101+
pubkeys[i] = sigSecrets.SignatureVerifier
102+
msg := randString()
103+
sigs[i] = sigSecrets.Sign(msg)
104+
}
105+
106+
b.Run("pubkey_check", func(b *testing.B) {
107+
for i := 0; i < b.N; i++ {
108+
_ = isCanonicalPoint(pubkeys[i%maxN])
109+
}
110+
})
111+
112+
b.Run("signature_R_check", func(b *testing.B) {
113+
for i := 0; i < b.N; i++ {
114+
_ = isCanonicalPoint([32]byte(sigs[i%maxN][:32]))
115+
}
116+
})
117+
118+
b.Run("both_checks", func(b *testing.B) {
119+
for i := 0; i < b.N; i++ {
120+
_ = !isCanonicalPoint(pubkeys[i%maxN]) || !isCanonicalPoint([32]byte(sigs[i%maxN][:32]))
121+
}
122+
})
123+
}

0 commit comments

Comments
 (0)