summaryrefslogtreecommitdiff
path: root/libgoal/participation.go
blob: 66ba9e4a589209e312776e670a88f205ea054cb5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
// Copyright (C) 2019-2021 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 libgoal

import (
	"fmt"
	"io/ioutil"
	"math"
	"os"
	"path/filepath"

	"github.com/algorand/go-algorand/config"
	"github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated"
	"github.com/algorand/go-algorand/data/account"
	"github.com/algorand/go-algorand/data/basics"
	"github.com/algorand/go-algorand/protocol"
	"github.com/algorand/go-algorand/util/db"
)

// chooseParticipation chooses which participation keys to use for going online
// based on the address, round number, and available participation databases
func (c *Client) chooseParticipation(address basics.Address, round basics.Round) (part account.Participation, err error) {
	genID, err := c.GenesisID()
	if err != nil {
		return
	}

	// Get a list of files in the participation keys directory
	keyDir := filepath.Join(c.DataDir(), genID)
	files, err := ioutil.ReadDir(keyDir)
	if err != nil {
		return
	}
	// This lambda will be used for finding the desired file.
	checkIfFileIsDesiredKey := func(file os.FileInfo, expiresAfter basics.Round) (part account.Participation, err error) {
		var handle db.Accessor
		var partCandidate account.PersistedParticipation

		// If it can't be a participation key database, skip it
		if !config.IsPartKeyFilename(file.Name()) {
			return
		}

		filename := file.Name()

		// Fetch a handle to this database
		handle, err = db.MakeErasableAccessor(filepath.Join(keyDir, filename))
		if err != nil {
			// Couldn't open it, skip it
			return
		}

		// Fetch an account.Participation from the database
		partCandidate, err = account.RestoreParticipation(handle)
		if err != nil {
			// Couldn't read it, skip it
			handle.Close()
			return
		}
		defer partCandidate.Close()

		// Return the Participation valid for this round that relates to the passed address
		// that expires farthest in the future.
		// Note that algod will sign votes with all possible Participations. so any should work
		// in the short-term.
		// In the future we should allow the user to specify exactly which partkeys to register.
		if partCandidate.FirstValid <= round && round <= partCandidate.LastValid && partCandidate.Parent == address && partCandidate.LastValid > expiresAfter {
			part = partCandidate.Participation
		}
		return
	}

	// Loop through each of the files; pick the one that expires farthest in the future.
	var expiry basics.Round
	for _, info := range files {
		// Use above lambda so the deferred handle closure happens each loop
		partCandidate, err := checkIfFileIsDesiredKey(info, expiry)
		if err == nil && (!partCandidate.Parent.IsZero()) {
			part = partCandidate
			expiry = part.LastValid
		}
	}
	if part.Parent.IsZero() {
		// Couldn't find one
		err = fmt.Errorf("Couldn't find a participation key database for address %v valid at round %v in directory %v", address.GetUserAddress(), round, keyDir)
		return
	}
	return
}

func participationKeysPath(dataDir string, address basics.Address, firstValid, lastValid basics.Round) (string, error) {
	// Build /<dataDir>/<genesisID>/<address>.<first_round>.<last_round>.partkey
	first := uint64(firstValid)
	last := uint64(lastValid)
	fileName := config.PartKeyFilename(address.String(), first, last)
	return filepath.Join(dataDir, fileName), nil
}

// GenParticipationKeys creates a .partkey database for a given address, fills
// it with keys, and installs it in the right place
func (c *Client) GenParticipationKeys(address string, firstValid, lastValid, keyDilution uint64) (part account.Participation, filePath string, err error) {
	return c.GenParticipationKeysTo(address, firstValid, lastValid, keyDilution, "")
}

// GenParticipationKeysTo creates a .partkey database for a given address, fills
// it with keys, and saves it in the specified output directory.
func (c *Client) GenParticipationKeysTo(address string, firstValid, lastValid, keyDilution uint64, outDir string) (part account.Participation, filePath string, err error) {
	// Parse the address
	parsedAddr, err := basics.UnmarshalChecksumAddress(address)
	if err != nil {
		return
	}

	firstRound, lastRound := basics.Round(firstValid), basics.Round(lastValid)

	// Get the current protocol for ephemeral key parameters
	stat, err := c.Status()
	if err != nil {
		return
	}

	proto, ok := c.consensus[protocol.ConsensusVersion(stat.LastVersion)]
	if !ok {
		err = fmt.Errorf("consensus protocol %s not supported", stat.LastVersion)
		return
	}

	// If output directory wasn't specified, store it in the current ledger directory.
	if outDir == "" {
		// Get the GenesisID for use in the participation key path
		var genID string
		genID, err = c.GenesisID()
		if err != nil {
			return
		}

		outDir = filepath.Join(c.DataDir(), genID)
	}
	// Connect to the database
	partKeyPath, err := participationKeysPath(outDir, parsedAddr, firstRound, lastRound)
	if err != nil {
		return
	}
	partdb, err := db.MakeErasableAccessor(partKeyPath)
	if err != nil {
		return
	}

	if keyDilution == 0 {
		keyDilution = proto.DefaultKeyDilution
	}

	// Fill the database with new participation keys
	newPart, err := account.FillDBWithParticipationKeys(partdb, parsedAddr, firstRound, lastRound, keyDilution)
	part = newPart.Participation
	partdb.Close()
	return part, partKeyPath, err
}

// InstallParticipationKeys creates a .partkey database for a given address,
// based on an existing database from inputfile.  On successful install, it
// deletes the input file.
func (c *Client) InstallParticipationKeys(inputfile string) (part account.Participation, filePath string, err error) {
	proto, ok := c.consensus[protocol.ConsensusCurrentVersion]
	if !ok {
		err = fmt.Errorf("Unknown consensus protocol %s", protocol.ConsensusCurrentVersion)
		return
	}

	// Get the GenesisID for use in the participation key path
	var genID string
	genID, err = c.GenesisID()
	if err != nil {
		return
	}

	outDir := filepath.Join(c.DataDir(), genID)

	inputdb, err := db.MakeErasableAccessor(inputfile)
	if err != nil {
		return
	}
	defer inputdb.Close()

	partkey, err := account.RestoreParticipation(inputdb)
	if err != nil {
		return
	}

	if partkey.Parent == (basics.Address{}) {
		err = fmt.Errorf("Cannot install partkey with missing (zero) parent address")
		return
	}

	newdbpath, err := participationKeysPath(outDir, partkey.Parent, partkey.FirstValid, partkey.LastValid)
	if err != nil {
		return
	}

	newdb, err := db.MakeErasableAccessor(newdbpath)
	if err != nil {
		return
	}

	newpartkey := partkey
	newpartkey.Store = newdb
	err = newpartkey.Persist()
	if err != nil {
		newpartkey.Close()
		return
	}

	// After successful install, remove the input copy of the
	// partkey so that old keys cannot be recovered after they
	// are used by algod.  We try to delete the data inside
	// sqlite first, so the key material is zeroed out from
	// disk blocks, but regardless of whether that works, we
	// delete the input file.  The consensus protocol version
	// is irrelevant for the maxuint64 round number we pass in.
	errCh := partkey.DeleteOldKeys(basics.Round(math.MaxUint64), proto)
	err = <-errCh
	if err != nil {
		newpartkey.Close()
		return
	}
	os.Remove(inputfile)
	part = newpartkey.Participation
	newpartkey.Close()
	return part, newdbpath, nil
}

// ListParticipationKeys returns the available participation keys,
// as a response object.
func (c *Client) ListParticipationKeys() (partKeyFiles generated.ParticipationKeysResponse, err error) {
	algod, err := c.ensureAlgodClient()
	if err == nil {
		partKeyFiles, err = algod.GetParticipationKeys()
	}
	return
}

// ListParticipationKeyFiles returns the available participation keys,
// as a map from database filename to Participation key object.
func (c *Client) ListParticipationKeyFiles() (partKeyFiles map[string]account.Participation, err error) {
	genID, err := c.GenesisID()
	if err != nil {
		return
	}

	// Get a list of files in the participation keys directory
	keyDir := filepath.Join(c.DataDir(), genID)
	files, err := ioutil.ReadDir(keyDir)
	if err != nil {
		return
	}

	partKeyFiles = make(map[string]account.Participation)
	for _, file := range files {
		// If it can't be a participation key database, skip it
		if !config.IsPartKeyFilename(file.Name()) {
			continue
		}

		filename := file.Name()

		// Fetch a handle to this database
		handle, err := db.MakeErasableAccessor(filepath.Join(keyDir, filename))
		if err != nil {
			// Couldn't open it, skip it
			continue
		}

		// Fetch an account.Participation from the database
		part, err := account.RestoreParticipation(handle)
		if err != nil {
			// Couldn't read it, skip it
			handle.Close()
			continue
		}

		partKeyFiles[filename] = part.Participation
		part.Close()
	}

	return
}