Skip to content

Commit d3dc437

Browse files
authored
account manager: avoid taking locks for long period of time (#3717)
## Summary The `DeleteOldKeys` method was taking the lock for the duration of the keys deletion. This is not required, as the mutex really just need to be held to synchronize the list of participation keys. The underlying `OneTimeSignatureSecrets` already have a synchronization lock, which is taken as need. ## Test Plan Unit test added. The output of the test help to detect the timing issues addressed in this PR. Before this PR, calling Key() 10 times took 4.1 seconds. With this PR, it takes 255us.
1 parent 5a72986 commit d3dc437

File tree

5 files changed

+237
-38
lines changed

5 files changed

+237
-38
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright (C) 2019-2022 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 mocks
18+
19+
import (
20+
"time"
21+
22+
"github.com/algorand/go-algorand/data/account"
23+
"github.com/algorand/go-algorand/data/basics"
24+
)
25+
26+
// MockParticipationRegistry is a dummy ParticipationRegistry that doesn't do anything
27+
type MockParticipationRegistry struct {
28+
}
29+
30+
// Insert adds a record to storage and computes the ParticipationID
31+
func (m *MockParticipationRegistry) Insert(record account.Participation) (account.ParticipationID, error) {
32+
return account.ParticipationID{}, nil
33+
}
34+
35+
// AppendKeys appends state proof keys to an existing Participation record. Keys can only be appended
36+
// once, an error will occur when the data is flushed when inserting a duplicate key.
37+
func (m *MockParticipationRegistry) AppendKeys(id account.ParticipationID, keys account.StateProofKeys) error {
38+
return nil
39+
}
40+
41+
// Delete removes a record from storage.
42+
func (m *MockParticipationRegistry) Delete(id account.ParticipationID) error {
43+
return nil
44+
}
45+
46+
// DeleteExpired removes all records from storage which are expired on the given round.
47+
func (m *MockParticipationRegistry) DeleteExpired(round basics.Round) error {
48+
return nil
49+
}
50+
51+
// Get a participation record.
52+
func (m *MockParticipationRegistry) Get(id account.ParticipationID) account.ParticipationRecord {
53+
return account.ParticipationRecord{}
54+
}
55+
56+
// GetAll of the participation records.
57+
func (m *MockParticipationRegistry) GetAll() []account.ParticipationRecord {
58+
return []account.ParticipationRecord{}
59+
}
60+
61+
// GetForRound fetches a record with voting secrets for a particular round.
62+
func (m *MockParticipationRegistry) GetForRound(id account.ParticipationID, round basics.Round) (account.ParticipationRecordForRound, error) {
63+
return account.ParticipationRecordForRound{}, nil
64+
}
65+
66+
// GetStateProofForRound fetches a record with stateproof secrets for a particular round.
67+
func (m *MockParticipationRegistry) GetStateProofForRound(id account.ParticipationID, round basics.Round) (account.StateProofRecordForRound, error) {
68+
return account.StateProofRecordForRound{}, nil
69+
}
70+
71+
// Register updates the EffectiveFirst and EffectiveLast fields. If there are multiple records for the account
72+
// then it is possible for multiple records to be updated.
73+
func (m *MockParticipationRegistry) Register(id account.ParticipationID, on basics.Round) error {
74+
return nil
75+
}
76+
77+
// Record sets the Last* field for the active ParticipationID for the given account.
78+
func (m *MockParticipationRegistry) Record(account basics.Address, round basics.Round, participationType account.ParticipationAction) error {
79+
return nil
80+
}
81+
82+
// Flush ensures that all changes have been written to the underlying data store.
83+
func (m *MockParticipationRegistry) Flush(timeout time.Duration) error {
84+
return nil
85+
}
86+
87+
// Close any resources used to implement the interface.
88+
func (m *MockParticipationRegistry) Close() {
89+
90+
}

crypto/falconWrapper.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ func (d *FalconVerifier) GetSignatureFixedLengthHashableRepresentation(signature
109109
return ctSignature[:], err
110110
}
111111

112-
// NewFalconSigner creates a falconSinger that is used to sign and verify falcon signatures
112+
// NewFalconSigner creates a falconSigner that is used to sign and verify falcon signatures
113113
func NewFalconSigner() (*FalconSigner, error) {
114114
var seed FalconSeed
115115
RandBytes(seed[:])

data/accountManager.go

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ import (
3535
type AccountManager struct {
3636
mu deadlock.Mutex
3737

38+
// syncronized by mu
3839
partKeys map[account.ParticipationKeyIdentity]account.PersistedParticipation
3940

4041
// Map to keep track of accounts for which we've sent
4142
// AccountRegistered telemetry events
43+
// syncronized by mu
4244
registeredAccounts map[string]bool
4345

4446
registry account.ParticipationRegistry
@@ -58,9 +60,6 @@ func MakeAccountManager(log logging.Logger, registry account.ParticipationRegist
5860

5961
// Keys returns a list of Participation accounts, and their keys/secrets for requested round.
6062
func (manager *AccountManager) Keys(rnd basics.Round) (out []account.ParticipationRecordForRound) {
61-
manager.mu.Lock()
62-
defer manager.mu.Unlock()
63-
6463
for _, part := range manager.registry.GetAll() {
6564
if part.OverlapsInterval(rnd, rnd) {
6665
partRndSecrets, err := manager.registry.GetForRound(part.ParticipationID, rnd)
@@ -76,9 +75,6 @@ func (manager *AccountManager) Keys(rnd basics.Round) (out []account.Participati
7675

7776
// StateProofKeys returns a list of Participation accounts, and their stateproof secrets
7877
func (manager *AccountManager) StateProofKeys(rnd basics.Round) (out []account.StateProofRecordForRound) {
79-
manager.mu.Lock()
80-
defer manager.mu.Unlock()
81-
8278
for _, part := range manager.registry.GetAll() {
8379
if part.OverlapsInterval(rnd, rnd) {
8480
partRndSecrets, err := manager.registry.GetStateProofForRound(part.ParticipationID, rnd)
@@ -168,37 +164,40 @@ func (manager *AccountManager) DeleteOldKeys(latestHdr bookkeeping.BlockHeader,
168164

169165
manager.mu.Lock()
170166
pendingItems := make(map[string]<-chan error, len(manager.partKeys))
171-
func() {
172-
defer manager.mu.Unlock()
173-
for _, part := range manager.partKeys {
174-
// We need a key for round r+1 for agreement.
175-
nextRound := latestHdr.Round + 1
176-
177-
if latestHdr.CompactCert[protocol.CompactCertBasic].CompactCertNextRound > 0 {
178-
// We need a key for the next compact cert round.
179-
// This would be CompactCertNextRound+1 (+1 because compact
180-
// cert code uses the next round's ephemeral key), except
181-
// if we already used that key to produce a signature (as
182-
// reported in ccSigs).
183-
nextCC := latestHdr.CompactCert[protocol.CompactCertBasic].CompactCertNextRound + 1
184-
if ccSigs[part.Parent] >= nextCC {
185-
nextCC = ccSigs[part.Parent] + basics.Round(latestProto.CompactCertRounds) + 1
186-
}
187-
188-
if nextCC < nextRound {
189-
nextRound = nextCC
190-
}
191-
}
192167

193-
// we pre-create the reported error string here, so that we won't need to have the participation key object if error is detected.
194-
first, last := part.ValidInterval()
195-
errString := fmt.Sprintf("AccountManager.DeleteOldKeys(): key for %s (%d-%d), nextRound %d",
196-
part.Address().String(), first, last, nextRound)
197-
errCh := part.DeleteOldKeys(nextRound, agreementProto)
168+
partKeys := make([]account.PersistedParticipation, 0, len(manager.partKeys))
169+
for _, part := range manager.partKeys {
170+
partKeys = append(partKeys, part)
171+
}
172+
manager.mu.Unlock()
173+
for _, part := range partKeys {
174+
// We need a key for round r+1 for agreement.
175+
nextRound := latestHdr.Round + 1
176+
177+
if latestHdr.CompactCert[protocol.CompactCertBasic].CompactCertNextRound > 0 {
178+
// We need a key for the next compact cert round.
179+
// This would be CompactCertNextRound+1 (+1 because compact
180+
// cert code uses the next round's ephemeral key), except
181+
// if we already used that key to produce a signature (as
182+
// reported in ccSigs).
183+
nextCC := latestHdr.CompactCert[protocol.CompactCertBasic].CompactCertNextRound + 1
184+
if ccSigs[part.Parent] >= nextCC {
185+
nextCC = ccSigs[part.Parent] + basics.Round(latestProto.CompactCertRounds) + 1
186+
}
198187

199-
pendingItems[errString] = errCh
188+
if nextCC < nextRound {
189+
nextRound = nextCC
190+
}
200191
}
201-
}()
192+
193+
// we pre-create the reported error string here, so that we won't need to have the participation key object if error is detected.
194+
first, last := part.ValidInterval()
195+
errString := fmt.Sprintf("AccountManager.DeleteOldKeys(): key for %s (%d-%d), nextRound %d",
196+
part.Address().String(), first, last, nextRound)
197+
errCh := part.DeleteOldKeys(nextRound, agreementProto)
198+
199+
pendingItems[errString] = errCh
200+
}
202201

203202
// wait for all disk flushes, and report errors as they appear.
204203
for errString, errCh := range pendingItems {

data/accountManager_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (C) 2019-2022 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 data
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"strconv"
23+
"testing"
24+
"time"
25+
26+
"github.com/stretchr/testify/require"
27+
28+
"github.com/algorand/go-algorand/components/mocks"
29+
"github.com/algorand/go-algorand/config"
30+
"github.com/algorand/go-algorand/data/account"
31+
"github.com/algorand/go-algorand/data/basics"
32+
"github.com/algorand/go-algorand/data/bookkeeping"
33+
"github.com/algorand/go-algorand/logging"
34+
"github.com/algorand/go-algorand/protocol"
35+
"github.com/algorand/go-algorand/test/partitiontest"
36+
"github.com/algorand/go-algorand/util/db"
37+
)
38+
39+
func TestAccountManagerKeys(t *testing.T) {
40+
partitiontest.PartitionTest(t)
41+
log := logging.TestingLog(t)
42+
log.SetLevel(logging.Error)
43+
registry := &mocks.MockParticipationRegistry{}
44+
45+
acctManager := MakeAccountManager(log, registry)
46+
47+
databaseFiles := make([]string, 0)
48+
defer func() {
49+
for _, fileName := range databaseFiles {
50+
os.Remove(fileName)
51+
os.Remove(fileName + "-shm")
52+
os.Remove(fileName + "-wal")
53+
}
54+
}()
55+
56+
// create participation keys
57+
const numPartKeys = 10
58+
for partKeyIdx := 0; partKeyIdx < numPartKeys; partKeyIdx++ {
59+
rootFilename := t.Name() + "_root_" + strconv.Itoa(partKeyIdx) + ".sqlite"
60+
partFilename := t.Name() + "_part_" + strconv.Itoa(partKeyIdx) + ".sqlite"
61+
os.Remove(rootFilename)
62+
os.Remove(partFilename)
63+
rootAccessor, err := db.MakeAccessor(rootFilename, false, true)
64+
require.NoError(t, err)
65+
66+
root, err := account.GenerateRoot(rootAccessor)
67+
require.NoError(t, err)
68+
69+
accessor, err := db.MakeErasableAccessor(partFilename)
70+
require.NoError(t, err)
71+
accessor.SetLogger(log)
72+
73+
part, err := account.FillDBWithParticipationKeys(accessor, root.Address(), 0, 100, 10000)
74+
require.NoError(t, err)
75+
76+
rootAccessor.Close()
77+
databaseFiles = append(databaseFiles, rootFilename)
78+
databaseFiles = append(databaseFiles, partFilename)
79+
80+
acctManager.AddParticipation(part)
81+
}
82+
83+
keyDeletionDone := make(chan struct{}, 1)
84+
nextRoundCh := make(chan basics.Round, 2)
85+
// kick off key deletion thread.
86+
go func() {
87+
defer close(keyDeletionDone)
88+
ccSigs := make(map[basics.Address]basics.Round)
89+
agreementProto := config.Consensus[protocol.ConsensusCurrentVersion]
90+
header := bookkeeping.BlockHeader{}
91+
for rnd := range nextRoundCh {
92+
header.Round = rnd
93+
acctManager.DeleteOldKeys(header, ccSigs, agreementProto)
94+
}
95+
}()
96+
97+
testStartTime := time.Now()
98+
keysTotalDuration := time.Duration(0)
99+
for i := 1; i < 10; i++ {
100+
nextRoundCh <- basics.Round(i)
101+
startTime := time.Now()
102+
acctManager.Keys(basics.Round(i))
103+
keysTotalDuration += time.Since(startTime)
104+
}
105+
close(nextRoundCh)
106+
<-keyDeletionDone
107+
testDuration := time.Since(testStartTime)
108+
require.Lessf(t, keysTotalDuration, testDuration/100, fmt.Sprintf("the time to aquire the keys via Keys() was %v whereas blocking on keys deletion took %v", keysTotalDuration, testDuration))
109+
t.Logf("Calling AccountManager.Keys() while AccountManager.DeleteOldKeys() was busy, 10 times in a row, resulted in accumulated delay of %v\n", keysTotalDuration)
110+
}

node/node.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,11 +1010,11 @@ func insertStateProofToRegistry(part account.PersistedParticipation, node *Algor
10101010
return nil
10111011
}
10121012
keys := part.StateProofSecrets.GetAllKeys()
1013-
keysSinger := make(account.StateProofKeys, len(keys))
1013+
keysSigner := make(account.StateProofKeys, len(keys))
10141014
for i := uint64(0); i < uint64(len(keys)); i++ {
1015-
keysSinger[i] = keys[i]
1015+
keysSigner[i] = keys[i]
10161016
}
1017-
return node.accountManager.Registry().AppendKeys(partID, keysSinger)
1017+
return node.accountManager.Registry().AppendKeys(partID, keysSigner)
10181018

10191019
}
10201020

0 commit comments

Comments
 (0)