Skip to content

Commit 1dd40a8

Browse files
authored
batch verification: add batch verification implementation (#3031)
## Summary In this change, we plan to integrate the ed25519 batch verification algorithm. This will speed up the process of verifying the digital signature of several transactions by roughly 2.4X. This PR implements the algorithm according to the following paper: https://eprint.iacr.org/2020/1244.pdf This PR fixes a **bug** in the paper (Listing 1.2) that didn't reject the points (-0,-1) and (-0,1) Another blog article with similar background: https://hdevalence.ca/blog/2020-10-04-its-25519am **Changes Overview** Changes made on the single ed255519 verification : 1. Signatures that contain small order group R value will now be accepted. 2. use the cofactor version of the equation to validate the signature. (multiple the result by the cofactor and compare to natural element) 3. add ge25519_is_canonical_vartime function (check if a given y point has a canonical or non-canonical encoding ): - optimize this function using the the quick check. - reject two non-canonical points by comparison (points (-0,-1) and (-0,1), this resolves a bug in the paper). Add the batch verification implementation from https://github.com/floodyberry/ed25519-donna to our fork of libsodium. Changes made on the batch verification (to match the single verification): 1. reject small order A (PK) 2. reject non-canonical A (PK) 3. reject non-canonical R 4. reject non-canonical S 5. use the cofactor equation ## Test Plan The following tests were added to the libsodium's test suite. 1. Test against the White Paper's test vector 2. Test against all non-canonical options for R 3. Test against all non-canonical options for S
1 parent cd19002 commit 1dd40a8

39 files changed

+4916
-77
lines changed

agreement/vote.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ func (uv unauthenticatedVote) verify(l LedgerReader) (vote, error) {
129129

130130
ephID := basics.OneTimeIDForRound(rv.Round, m.Record.KeyDilution(proto))
131131
voteID := m.Record.VoteID
132-
if !voteID.Verify(ephID, rv, uv.Sig) {
132+
if !voteID.Verify(ephID, rv, uv.Sig, proto.EnableBatchVerification) {
133133
return vote{}, fmt.Errorf("unauthenticatedVote.verify: could not verify FS signature on vote by %v given %v: %+v", rv.Sender, voteID, uv)
134134
}
135135

config/consensus.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,9 @@ type ConsensusParams struct {
395395
// to be taken offline, that would be proposed to be taken offline.
396396
MaxProposedExpiredOnlineAccounts int
397397

398+
//EnableBatchVerification enable the use of the batch verification algorithm.
399+
EnableBatchVerification bool
400+
398401
// When rewards rate changes, use the new value immediately.
399402
RewardsCalculationFix bool
400403
}
@@ -1057,6 +1060,8 @@ func initConsensusProtocols() {
10571060

10581061
vFuture.MaxProposedExpiredOnlineAccounts = 32
10591062

1063+
vFuture.EnableBatchVerification = true
1064+
10601065
vFuture.RewardsCalculationFix = true
10611066

10621067
Consensus[protocol.ConsensusFuture] = vFuture

crypto/batchverifier.go

Lines changed: 108 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,40 +16,78 @@
1616

1717
package crypto
1818

19-
import "errors"
19+
// #cgo CFLAGS: -Wall -std=c99
20+
// #cgo darwin,amd64 CFLAGS: -I${SRCDIR}/libs/darwin/amd64/include
21+
// #cgo darwin,amd64 LDFLAGS: ${SRCDIR}/libs/darwin/amd64/lib/libsodium.a
22+
// #cgo darwin,arm64 CFLAGS: -I${SRCDIR}/libs/darwin/arm64/include
23+
// #cgo darwin,arm64 LDFLAGS: ${SRCDIR}/libs/darwin/arm64/lib/libsodium.a
24+
// #cgo linux,amd64 CFLAGS: -I${SRCDIR}/libs/linux/amd64/include
25+
// #cgo linux,amd64 LDFLAGS: ${SRCDIR}/libs/linux/amd64/lib/libsodium.a
26+
// #cgo linux,arm64 CFLAGS: -I${SRCDIR}/libs/linux/arm64/include
27+
// #cgo linux,arm64 LDFLAGS: ${SRCDIR}/libs/linux/arm64/lib/libsodium.a
28+
// #cgo linux,arm CFLAGS: -I${SRCDIR}/libs/linux/arm/include
29+
// #cgo linux,arm LDFLAGS: ${SRCDIR}/libs/linux/arm/lib/libsodium.a
30+
// #cgo windows,amd64 CFLAGS: -I${SRCDIR}/libs/windows/amd64/include
31+
// #cgo windows,amd64 LDFLAGS: ${SRCDIR}/libs/windows/amd64/lib/libsodium.a
32+
// #include <stdint.h>
33+
// #include "sodium.h"
34+
// enum {
35+
// sizeofPtr = sizeof(void*),
36+
// sizeofULongLong = sizeof(unsigned long long),
37+
// };
38+
import "C"
39+
import (
40+
"errors"
41+
"unsafe"
42+
)
2043

2144
// BatchVerifier enqueues signatures to be validated in batch.
2245
type BatchVerifier struct {
23-
messages []Hashable // contains a slice of messages to be hashed. Each message is varible length
24-
publicKeys []SignatureVerifier // contains a slice of public keys. Each individual public key is 32 bytes.
25-
signatures []Signature // contains a slice of signatures keys. Each individual signature is 64 bytes.
46+
messages []Hashable // contains a slice of messages to be hashed. Each message is varible length
47+
publicKeys []SignatureVerifier // contains a slice of public keys. Each individual public key is 32 bytes.
48+
signatures []Signature // contains a slice of signatures keys. Each individual signature is 64 bytes.
49+
useBatchVerification bool
2650
}
2751

2852
const minBatchVerifierAlloc = 16
2953

3054
// Batch verifications errors
3155
var (
3256
ErrBatchVerificationFailed = errors.New("At least one signature didn't pass verification")
33-
ErrZeroTranscationsInBatch = errors.New("Could not validate empty signature set")
57+
ErrZeroTransactionInBatch = errors.New("Could not validate empty signature set")
3458
)
3559

60+
//export ed25519_randombytes_unsafe
61+
func ed25519_randombytes_unsafe(p unsafe.Pointer, len C.size_t) {
62+
randBuf := (*[1 << 30]byte)(p)[:len:len]
63+
RandBytes(randBuf)
64+
}
65+
66+
// MakeBatchVerifierWithAlgorithmDefaultSize create a BatchVerifier instance. This function pre-allocates
67+
// amount of free space to enqueue signatures without expanding. this function always use the batch
68+
// verification algorithm
69+
func MakeBatchVerifierWithAlgorithmDefaultSize() *BatchVerifier {
70+
return MakeBatchVerifier(minBatchVerifierAlloc, true)
71+
}
72+
3673
// MakeBatchVerifierDefaultSize create a BatchVerifier instance. This function pre-allocates
37-
// amount of free space to enqueue signatures without exapneding
38-
func MakeBatchVerifierDefaultSize() *BatchVerifier {
39-
return MakeBatchVerifier(minBatchVerifierAlloc)
74+
// amount of free space to enqueue signatures without expanding
75+
func MakeBatchVerifierDefaultSize(enableBatchVerification bool) *BatchVerifier {
76+
return MakeBatchVerifier(minBatchVerifierAlloc, enableBatchVerification)
4077
}
4178

4279
// MakeBatchVerifier create a BatchVerifier instance. This function pre-allocates
4380
// a given space so it will not expaned the storage
44-
func MakeBatchVerifier(hint int) *BatchVerifier {
81+
func MakeBatchVerifier(hint int, enableBatchVerification bool) *BatchVerifier {
4582
// preallocate enough storage for the expected usage. We will reallocate as needed.
4683
if hint < minBatchVerifierAlloc {
4784
hint = minBatchVerifierAlloc
4885
}
4986
return &BatchVerifier{
50-
messages: make([]Hashable, 0, hint),
51-
publicKeys: make([]SignatureVerifier, 0, hint),
52-
signatures: make([]Signature, 0, hint),
87+
messages: make([]Hashable, 0, hint),
88+
publicKeys: make([]SignatureVerifier, 0, hint),
89+
signatures: make([]Signature, 0, hint),
90+
useBatchVerification: enableBatchVerification,
5391
}
5492
}
5593

@@ -85,14 +123,69 @@ func (b *BatchVerifier) GetNumberOfEnqueuedSignatures() int {
85123
// if the batch is zero an appropriate error is return.
86124
func (b *BatchVerifier) Verify() error {
87125
if b.GetNumberOfEnqueuedSignatures() == 0 {
88-
return ErrZeroTranscationsInBatch
126+
return ErrZeroTransactionInBatch
89127
}
90128

129+
if b.useBatchVerification {
130+
var messages = make([][]byte, b.GetNumberOfEnqueuedSignatures())
131+
for i, m := range b.messages {
132+
messages[i] = hashRep(m)
133+
}
134+
if batchVerificationImpl(messages, b.publicKeys, b.signatures) {
135+
return nil
136+
}
137+
return ErrBatchVerificationFailed
138+
}
139+
return b.verifyOneByOne()
140+
}
141+
142+
func (b *BatchVerifier) verifyOneByOne() error {
91143
for i := range b.messages {
92-
verifier := SignatureVerifier(b.publicKeys[i])
93-
if !verifier.Verify(b.messages[i], b.signatures[i]) {
144+
verifier := b.publicKeys[i]
145+
if !verifier.Verify(b.messages[i], b.signatures[i], false) {
94146
return ErrBatchVerificationFailed
95147
}
96148
}
97149
return nil
98150
}
151+
152+
// batchVerificationImpl invokes the ed25519 batch verification algorithm.
153+
// it returns true if all the signatures were authentically signed by the owners
154+
func batchVerificationImpl(messages [][]byte, publicKeys []SignatureVerifier, signatures []Signature) bool {
155+
156+
numberOfSignatures := len(messages)
157+
158+
messagesAllocation := C.malloc(C.size_t(C.sizeofPtr * numberOfSignatures))
159+
messagesLenAllocation := C.malloc(C.size_t(C.sizeofULongLong * numberOfSignatures))
160+
publicKeysAllocation := C.malloc(C.size_t(C.sizeofPtr * numberOfSignatures))
161+
signaturesAllocation := C.malloc(C.size_t(C.sizeofPtr * numberOfSignatures))
162+
valid := C.malloc(C.size_t(C.sizeof_int * numberOfSignatures))
163+
164+
defer func() {
165+
// release staging memory
166+
C.free(messagesAllocation)
167+
C.free(messagesLenAllocation)
168+
C.free(publicKeysAllocation)
169+
C.free(signaturesAllocation)
170+
C.free(valid)
171+
}()
172+
173+
// load all the data pointers into the array pointers.
174+
for i := 0; i < numberOfSignatures; i++ {
175+
*(*uintptr)(unsafe.Pointer(uintptr(messagesAllocation) + uintptr(i*C.sizeofPtr))) = uintptr(unsafe.Pointer(&messages[i][0]))
176+
*(*C.ulonglong)(unsafe.Pointer(uintptr(messagesLenAllocation) + uintptr(i*C.sizeofULongLong))) = C.ulonglong(len(messages[i]))
177+
*(*uintptr)(unsafe.Pointer(uintptr(publicKeysAllocation) + uintptr(i*C.sizeofPtr))) = uintptr(unsafe.Pointer(&publicKeys[i][0]))
178+
*(*uintptr)(unsafe.Pointer(uintptr(signaturesAllocation) + uintptr(i*C.sizeofPtr))) = uintptr(unsafe.Pointer(&signatures[i][0]))
179+
}
180+
181+
// call the batch verifier
182+
allValid := C.crypto_sign_ed25519_open_batch(
183+
(**C.uchar)(unsafe.Pointer(messagesAllocation)),
184+
(*C.ulonglong)(unsafe.Pointer(messagesLenAllocation)),
185+
(**C.uchar)(unsafe.Pointer(publicKeysAllocation)),
186+
(**C.uchar)(unsafe.Pointer(signaturesAllocation)),
187+
C.size_t(len(messages)),
188+
(*C.int)(unsafe.Pointer(valid)))
189+
190+
return allValid == 0
191+
}

crypto/batchverifier_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import (
2727
func TestBatchVerifierSingle(t *testing.T) {
2828
partitiontest.PartitionTest(t)
2929
// test expected success
30-
bv := MakeBatchVerifier(1)
30+
bv := MakeBatchVerifierWithAlgorithmDefaultSize()
3131
msg := randString()
3232
var s Seed
3333
RandBytes(s[:])
@@ -36,8 +36,8 @@ func TestBatchVerifierSingle(t *testing.T) {
3636
bv.EnqueueSignature(sigSecrets.SignatureVerifier, msg, sig)
3737
require.NoError(t, bv.Verify())
3838

39-
// test expected failuire
40-
bv = MakeBatchVerifier(1)
39+
// test expected failure
40+
bv = MakeBatchVerifierWithAlgorithmDefaultSize()
4141
msg = randString()
4242
RandBytes(s[:])
4343
sigSecrets = GenerateSignatureSecrets(s)
@@ -52,7 +52,7 @@ func TestBatchVerifierBulk(t *testing.T) {
5252
partitiontest.PartitionTest(t)
5353
for i := 1; i < 64*2+3; i++ {
5454
n := i
55-
bv := MakeBatchVerifier(n)
55+
bv := MakeBatchVerifier(n, true)
5656
var s Seed
5757

5858
for i := 0; i < n; i++ {
@@ -71,7 +71,7 @@ func TestBatchVerifierBulk(t *testing.T) {
7171
func TestBatchVerifierBulkWithExpand(t *testing.T) {
7272
partitiontest.PartitionTest(t)
7373
n := 64
74-
bv := MakeBatchVerifier(1)
74+
bv := MakeBatchVerifierWithAlgorithmDefaultSize()
7575
var s Seed
7676
RandBytes(s[:])
7777

@@ -87,7 +87,7 @@ func TestBatchVerifierBulkWithExpand(t *testing.T) {
8787
func TestBatchVerifierWithInvalidSiganture(t *testing.T) {
8888
partitiontest.PartitionTest(t)
8989
n := 64
90-
bv := MakeBatchVerifier(1)
90+
bv := MakeBatchVerifierWithAlgorithmDefaultSize()
9191
var s Seed
9292
RandBytes(s[:])
9393

@@ -109,7 +109,7 @@ func TestBatchVerifierWithInvalidSiganture(t *testing.T) {
109109

110110
func BenchmarkBatchVerifier(b *testing.B) {
111111
c := makeCurve25519Secret()
112-
bv := MakeBatchVerifier(1)
112+
bv := MakeBatchVerifier(1, true)
113113
for i := 0; i < b.N; i++ {
114114
str := randString()
115115
bv.EnqueueSignature(c.SignatureVerifier, str, c.Sign(str))
@@ -121,6 +121,6 @@ func BenchmarkBatchVerifier(b *testing.B) {
121121

122122
func TestEmpty(t *testing.T) {
123123
partitiontest.PartitionTest(t)
124-
bv := MakeBatchVerifierDefaultSize()
124+
bv := MakeBatchVerifierWithAlgorithmDefaultSize()
125125
require.Error(t, bv.Verify())
126126
}

crypto/compactcert/builder.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ func (b *Builder) Add(pos uint64, sig crypto.OneTimeSignature, verifySig bool) e
9999

100100
// Check signature
101101
ephID := basics.OneTimeIDForRound(b.SigRound, p.KeyDilution)
102-
if verifySig && !p.PK.Verify(ephID, b.Msg, sig) {
102+
if verifySig && !p.PK.Verify(ephID, b.Msg, sig, b.EnableBatchVerification) {
103103
return fmt.Errorf("signature does not verify under ID %v", ephID)
104104
}
105105

crypto/compactcert/structs.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ type Params struct {
2828
ProvenWeight uint64 // Weight threshold proven by the certificate
2929
SigRound basics.Round // Ephemeral signature round to expect
3030
SecKQ uint64 // Security parameter (k+q) from analysis document
31+
32+
EnableBatchVerification bool // whether ED25519 batch verification is enabled
3133
}
3234

3335
// CompactOneTimeSignature is crypto.OneTimeSignature with omitempty

crypto/compactcert/verifier.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func (v *Verifier) Verify(c *Cert) error {
5656
parts[pos] = crypto.HashObj(r.Part)
5757

5858
ephID := basics.OneTimeIDForRound(v.SigRound, r.Part.KeyDilution)
59-
if !r.Part.PK.Verify(ephID, v.Msg, r.SigSlot.Sig.OneTimeSignature) {
59+
if !r.Part.PK.Verify(ephID, v.Msg, r.SigSlot.Sig.OneTimeSignature, v.EnableBatchVerification) {
6060
return fmt.Errorf("signature in reveal pos %d does not verify", pos)
6161
}
6262
}

crypto/crypto_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,22 @@ func randString() (b TestingHashable) {
4343
func signVerify(t *testing.T, c *SignatureSecrets, c2 *SignatureSecrets) {
4444
s := randString()
4545
sig := c.Sign(s)
46-
if !c.Verify(s, sig) {
46+
if !c.Verify(s, sig, true) {
4747
t.Errorf("correct signature failed to verify (plain)")
4848
}
4949

5050
s2 := randString()
5151
sig2 := c.Sign(s2)
52-
if c.Verify(s, sig2) {
52+
if c.Verify(s, sig2, true) {
5353
t.Errorf("wrong message incorrectly verified (plain)")
5454
}
5555

5656
sig3 := c2.Sign(s)
57-
if c.Verify(s, sig3) {
57+
if c.Verify(s, sig3, true) {
5858
t.Errorf("wrong key incorrectly verified (plain)")
5959
}
6060

61-
if c.Verify(s2, sig3) {
61+
if c.Verify(s2, sig3, true) {
6262
t.Errorf("wrong message+key incorrectly verified (plain)")
6363
}
6464
}

crypto/curve25519.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,19 @@ func ed25519Sign(secret ed25519PrivateKey, data []byte) (sig ed25519Signature) {
112112
return
113113
}
114114

115-
func ed25519Verify(public ed25519PublicKey, data []byte, sig ed25519Signature) bool {
115+
func ed25519Verify(public ed25519PublicKey, data []byte, sig ed25519Signature, useBatchVerificationCompatibleVersion bool) bool {
116116
// &data[0] will make Go panic if msg is zero length
117117
d := (*C.uchar)(C.NULL)
118118
if len(data) != 0 {
119119
d = (*C.uchar)(&data[0])
120120
}
121121
// https://download.libsodium.org/doc/public-key_cryptography/public-key_signatures#detached-mode
122-
result := C.crypto_sign_ed25519_verify_detached((*C.uchar)(&sig[0]), d, C.ulonglong(len(data)), (*C.uchar)(&public[0]))
122+
var result C.int
123+
if useBatchVerificationCompatibleVersion {
124+
result = C.crypto_sign_ed25519_bv_compatible_verify_detached((*C.uchar)(&sig[0]), d, C.ulonglong(len(data)), (*C.uchar)(&public[0]))
125+
} else {
126+
result = C.crypto_sign_ed25519_verify_detached((*C.uchar)(&sig[0]), d, C.ulonglong(len(data)), (*C.uchar)(&public[0]))
127+
}
123128
return result == 0
124129
}
125130

@@ -211,15 +216,15 @@ func (s *SignatureSecrets) SignBytes(message []byte) Signature {
211216
//
212217
// It returns true if this is the case; otherwise, it returns false.
213218
//
214-
func (v SignatureVerifier) Verify(message Hashable, sig Signature) bool {
219+
func (v SignatureVerifier) Verify(message Hashable, sig Signature, useBatchVerificationCompatibleVersion bool) bool {
215220
cryptoSigSecretsVerifyTotal.Inc(map[string]string{})
216-
return ed25519Verify(ed25519PublicKey(v), hashRep(message), ed25519Signature(sig))
221+
return ed25519Verify(ed25519PublicKey(v), hashRep(message), ed25519Signature(sig), useBatchVerificationCompatibleVersion)
217222
}
218223

219224
// VerifyBytes verifies a signature, where the message is not hashed first.
220225
// Caller is responsible for domain separation.
221226
// If the message is a Hashable, Verify() can be used instead.
222-
func (v SignatureVerifier) VerifyBytes(message []byte, sig Signature) bool {
227+
func (v SignatureVerifier) VerifyBytes(message []byte, sig Signature, useBatchVerificationCompatibleVersion bool) bool {
223228
cryptoSigSecretsVerifyBytesTotal.Inc(map[string]string{})
224-
return ed25519Verify(ed25519PublicKey(v), message, ed25519Signature(sig))
229+
return ed25519Verify(ed25519PublicKey(v), message, ed25519Signature(sig), useBatchVerificationCompatibleVersion)
225230
}

crypto/curve25519_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func TestSignVerifyEmptyMessage(t *testing.T) {
3333
partitiontest.PartitionTest(t)
3434
pk, sk := ed25519GenerateKey()
3535
sig := ed25519Sign(sk, []byte{})
36-
if !ed25519Verify(pk, []byte{}, sig) {
36+
if !ed25519Verify(pk, []byte{}, sig, true) {
3737
t.Errorf("sig of an empty message failed to verify")
3838
}
3939
}
@@ -43,7 +43,7 @@ func TestVerifyZeros(t *testing.T) {
4343
var pk SignatureVerifier
4444
var sig Signature
4545
for x := byte(0); x < 255; x++ {
46-
if pk.VerifyBytes([]byte{x}, sig) {
46+
if pk.VerifyBytes([]byte{x}, sig, true) {
4747
t.Errorf("Zero sig with zero pk successfully verified message %x", x)
4848
}
4949
}
@@ -84,7 +84,7 @@ func BenchmarkSignVerify(b *testing.B) {
8484

8585
for i := 0; i < b.N; i++ {
8686
sig := c.Sign(s)
87-
_ = c.Verify(s, sig)
87+
_ = c.Verify(s, sig, true)
8888
}
8989
}
9090

@@ -108,6 +108,6 @@ func BenchmarkVerify(b *testing.B) {
108108
b.ResetTimer()
109109

110110
for i := 0; i < b.N; i++ {
111-
_ = c.Verify(strs[i], sigs[i])
111+
_ = c.Verify(strs[i], sigs[i], true)
112112
}
113113
}

0 commit comments

Comments
 (0)