Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented New REST interfaces #3099

Merged
merged 4 commits into from
Oct 21, 2021
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
1 change: 1 addition & 0 deletions daemon/algod/api/server/v2/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ var (
errFailedToAbortCatchup = "failed to abort catchup : %v"
errFailedToStartCatchup = "failed to start catchup : %v"
errOperationNotAvailableDuringCatchup = "operation not available during catchup"
errRESTPayloadZeroLength = "payload was of zero length"
)
99 changes: 95 additions & 4 deletions daemon/algod/api/server/v2/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated"
"github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/private"
"github.com/algorand/go-algorand/data"
"github.com/algorand/go-algorand/data/account"
"github.com/algorand/go-algorand/data/basics"
"github.com/algorand/go-algorand/data/bookkeeping"
"github.com/algorand/go-algorand/data/transactions"
Expand Down Expand Up @@ -67,33 +68,123 @@ type NodeInterface interface {
StartCatchup(catchpoint string) error
AbortCatchup(catchpoint string) error
Config() config.Local
InstallParticipationKey(partKeyBinary *[]byte) (account.ParticipationID, error)
ListParticipationKeys() ([]account.ParticipationRecord, error)
GetParticipationKey(account.ParticipationID) (account.ParticipationRecord, error)
RemoveParticipationKey(account.ParticipationID) error
}

// GetParticipationKeys Return a list of participation keys
// (GET /v2/participation)
func (v2 *Handlers) GetParticipationKeys(ctx echo.Context) error {
return ctx.String(http.StatusNotImplemented, "Endpoint not implemented.")
partKeys, err := v2.Node.ListParticipationKeys()

if err != nil {
return badRequest(ctx, err, err.Error(), v2.Log)
}

response := []generated.ParticipationKey{}

for _, participationRecord := range partKeys {
info := generated.ParticipationKey{
"ID": participationRecord.ParticipationID.String(),
"Address": participationRecord.Account.String(),
"FirstValid": participationRecord.FirstValid,
"LastValid": participationRecord.LastValid,
// TODO Coming soon (tm)
"VoteID": crypto.OneTimeSignatureVerifier{},
// TODO Coming soon (tm)
"SelectionID": crypto.VRFVerifier{},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"VoteKeyDilution": participationRecord.KeyDilution,
}

response = append(response, info)
}

return ctx.JSON(http.StatusOK, response)

}

// AddParticipationKey Add a participation key to the node
// (POST /v2/participation)
func (v2 *Handlers) AddParticipationKey(ctx echo.Context) error {

return ctx.String(http.StatusNotImplemented, "Endpoint not implemented.")
buf := new(bytes.Buffer)
_, err := buf.ReadFrom(ctx.Request().Body)
if err != nil {
return badRequest(ctx, err, err.Error(), v2.Log)
}
partKeyBinary := buf.Bytes()

if len(partKeyBinary) == 0 {
err := fmt.Errorf(errRESTPayloadZeroLength)
return badRequest(ctx, err, err.Error(), v2.Log)
}

partID, err := v2.Node.InstallParticipationKey(&partKeyBinary)

if err != nil {
return badRequest(ctx, err, err.Error(), v2.Log)
}

response := generated.PostParticipationResponse{PartId: partID.String()}
return ctx.JSON(http.StatusOK, response)

}

// DeleteParticipationKeyByID Delete a given participation key by id
// (DELETE /v2/participation/{participation-id})
func (v2 *Handlers) DeleteParticipationKeyByID(ctx echo.Context, participationID string) error {

return ctx.String(http.StatusNotImplemented, "Endpoint not implemented.")
decodedParticipationID, err := account.ParticipationIDFromString(participationID)

if err != nil {
return badRequest(ctx, err, err.Error(), v2.Log)
}

err = v2.Node.RemoveParticipationKey(decodedParticipationID)

if err != nil {

if errors.Is(err, account.ErrParticipationIDNotFound) {
return ctx.JSON(http.StatusOK, generated.ErrorResponse{Message: "participation id not found"})
}

return badRequest(ctx, err, err.Error(), v2.Log)
}

return ctx.NoContent(http.StatusOK)
}

// GetParticipationKeyByID Get participation key info by id
// (GET /v2/participation/{participation-id})
func (v2 *Handlers) GetParticipationKeyByID(ctx echo.Context, participationID string) error {

return ctx.String(http.StatusNotImplemented, "Endpoint not implemented.")
decodedParticipationID, err := account.ParticipationIDFromString(participationID)

if err != nil {
return badRequest(ctx, err, err.Error(), v2.Log)
}

participationRecord, err := v2.Node.GetParticipationKey(decodedParticipationID)

if err != nil {
return badRequest(ctx, err, err.Error(), v2.Log)
}

response := generated.ParticipationKey{
"ID": participationRecord.ParticipationID.String(),
"Address": participationRecord.Account.String(),
"FirstValid": participationRecord.FirstValid,
"LastValid": participationRecord.LastValid,
// TODO add this
"VoteID": crypto.OneTimeSignatureVerifier{},
// TODO add this
"SelectionID": crypto.VRFVerifier{},
"VoteKeyDilution": participationRecord.KeyDilution,
}

return ctx.JSON(http.StatusOK, response)
}

// RegisterParticipationKeys registers participation keys.
Expand Down
16 changes: 16 additions & 0 deletions daemon/algod/api/server/v2/test/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,22 @@ type mockNode struct {
err error
}

func (m mockNode) InstallParticipationKey(partKeyBinary *[]byte) (account.ParticipationID, error) {
panic("implement me")
}

func (m mockNode) ListParticipationKeys() ([]account.ParticipationRecord, error) {
panic("implement me")
}

func (m mockNode) GetParticipationKey(id account.ParticipationID) (account.ParticipationRecord, error) {
panic("implement me")
}

func (m mockNode) RemoveParticipationKey(id account.ParticipationID) error {
panic("implement me")
}

func makeMockNode(ledger *data.Ledger, genesisID string, nodeError error) mockNode {
return mockNode{
ledger: ledger,
Expand Down
19 changes: 19 additions & 0 deletions data/account/participationRegistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package account
import (
"context"
"database/sql"
"encoding/base32"
"errors"
"fmt"
"strings"
Expand All @@ -41,6 +42,24 @@ func (pid ParticipationID) IsZero() bool {
return (crypto.Digest(pid)).IsZero()
}

func (pid ParticipationID) String() string {
return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(pid[:])
}

// ParticipationIDFromString takes a string and returns a ParticipationID object
func ParticipationIDFromString(str string) (d ParticipationID, err error) {
decoded, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(str)
if err != nil {
return d, err
}
if len(decoded) != len(d) {
msg := fmt.Sprintf(`Attempted to decode a string which was not a participation id: "%v"`, str)
return d, errors.New(msg)
}
copy(d[:], decoded[:])
return d, err
}

// ParticipationRecord contains all metadata relating to a set of participation keys.
type ParticipationRecord struct {
ParticipationID ParticipationID
Expand Down
139 changes: 139 additions & 0 deletions node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -56,6 +57,7 @@ import (
"github.com/algorand/go-algorand/util/metrics"
"github.com/algorand/go-algorand/util/timers"
"github.com/algorand/go-deadlock"
uuid "github.com/satori/go.uuid"
)

// StatusReport represents the current basic status of the node
Expand Down Expand Up @@ -798,6 +800,143 @@ func (node *AlgorandFullNode) checkForParticipationKeys() {
}
}

// ListParticipationKeys returns all participation keys currently installed on the node
func (node *AlgorandFullNode) ListParticipationKeys() (partKeys []account.ParticipationRecord, err error) {
return node.accountManager.Registry().GetAll(), nil
}

// GetParticipationKey retries the information of a participation id from the node
func (node *AlgorandFullNode) GetParticipationKey(partKey account.ParticipationID) (account.ParticipationRecord, error) {
rval := node.accountManager.Registry().Get(partKey)

if rval.IsZero() {
return account.ParticipationRecord{}, account.ErrParticipationIDNotFound
}

return node.accountManager.Registry().Get(partKey), nil
}

// RemoveParticipationKey given a participation id, remove the records from the node
func (node *AlgorandFullNode) RemoveParticipationKey(partKey account.ParticipationID) error {

// Need to remove the file and then remove the entry in the registry
// Let's first get the recorded information from the registry so we can lookup the file

partRecord := node.accountManager.Registry().Get(partKey)

if partRecord.IsZero() {
return account.ErrParticipationIDNotFound
}

genID := node.GenesisID()

outDir := filepath.Join(node.rootDir, genID)

filename := config.PartKeyFilename(partRecord.ParticipationID.String(), uint64(partRecord.FirstValid), uint64(partRecord.LastValid))
fullyQualifiedFilename := filepath.Join(outDir, filepath.Base(filename))

err := node.accountManager.Registry().Delete(partKey)
if err != nil {
return err
}

err = node.accountManager.Registry().Flush()
if err != nil {
return err
}

// Only after deleting and flushing do we want to remove the file
_ = os.Remove(fullyQualifiedFilename)

return nil
}

func createTemporaryParticipationKey(outDir string, partKeyBinary *[]byte) (string, error) {
var sb strings.Builder

// Create a temporary filename with a UUID so that we can call this function twice
// in a row without worrying about collisions
sb.WriteString("tempPartKeyBinary.")
sb.WriteString(uuid.NewV4().String())
sb.WriteString(".bin")

tempFile := filepath.Join(outDir, filepath.Base(sb.String()))

file, err := os.Create(tempFile)

if err != nil {
return "", err
}

_, err = file.Write(*partKeyBinary)

file.Close()

if err != nil {
os.Remove(tempFile)
return "", err
}

return tempFile, nil
}

// InstallParticipationKey Given a participation key binary stream install the participation key
func (node *AlgorandFullNode) InstallParticipationKey(partKeyBinary *[]byte) (account.ParticipationID, error) {
genID := node.GenesisID()

outDir := filepath.Join(node.rootDir, genID)

fullyQualifiedTempFile, err := createTemporaryParticipationKey(outDir, partKeyBinary)
// We need to make sure no tempfile is created/remains if there is an error
// However, we will eventually rename this file but if we fail in-between
// this point and the rename we want to ensure that we remove the temporary file
// After we rename, this will fail anyway since the file will not exist

// Explicitly ignore the error with a closure
defer func(name string) {
_ = os.Remove(name)
}(fullyQualifiedTempFile)

if err != nil {
return account.ParticipationID{}, err
}

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

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

if partkey.Parent == (basics.Address{}) {
return account.ParticipationID{}, fmt.Errorf("cannot install partkey with missing (zero) parent address")
}

// Tell the AccountManager about the Participation (dupes don't matter) so we ignore the return value
_ = node.accountManager.AddParticipation(partkey)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On closer inspection, AddParticipation returns a bool not an error.:

// The return value indicates if the key has been added (true) or    
// if this is a duplicate key (false).     

The code would currently overwrite the duplicate key, I think that's probably fine. In the future when we stop writing the file to begin with, it would just be a no-op.


err = node.accountManager.Registry().Flush()
if err != nil {
return account.ParticipationID{}, err
}

newFilename := config.PartKeyFilename(partkey.ID().String(), uint64(partkey.FirstValid), uint64(partkey.LastValid))
newFullyQualifiedFilename := filepath.Join(outDir, filepath.Base(newFilename))

err = os.Rename(fullyQualifiedTempFile, newFullyQualifiedFilename)

if err != nil {
return account.ParticipationID{}, nil
}

return partkey.ID(), nil
}

func (node *AlgorandFullNode) loadParticipationKeys() error {
// Generate a list of all potential participation key files
genesisDir := filepath.Join(node.rootDir, node.genesisID)
Expand Down
5 changes: 5 additions & 0 deletions test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,9 @@ To run a specific test:
~$ ./e2e_client_runner.py /full/path/to/e2e_subs/test_script.sh
```

Make sure to install the Algorand Python SDK before running:
```
pip install py-algorand-sdk
```

Tests in the `e2e_subs/serial` directory are executed serially instead of in parallel. This should only be used when absolutely necessary.
Loading