Skip to content
Open
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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,9 +375,12 @@ config file:
```

* The canonical list is returned by `FlowFees.getFeeReceiverAddresses()` on
chain. On startup, the server validates the configured addresses against
that list and exits with a fatal error if any on-chain receiver is
missing from the config.
chain. The server validates the configured addresses against that list in
the background: retrying until an access node responds, and then
re-checking periodically so receivers added on chain while the server is
running are still detected. If any on-chain receiver is missing from the
config, the server logs an error and reports the mismatch via the
`fee_receiver_validation_status` method of the `/call` endpoint.

* `data_dir: string`

Expand Down
117 changes: 109 additions & 8 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/hex"
"fmt"
"net/http"
"strings"
"sync"
"time"

Expand All @@ -27,6 +28,7 @@ const (
callAccountPublicKeys = "account_public_keys"
callBalanceValidationStatus = "balance_validation_status"
callEcho = "echo"
callFeeValidationStatus = "fee_receiver_validation_status"
callLatestBlock = "latest_block"
callListAccounts = "list_accounts"
callVerifyAddress = "verify_address"
Expand All @@ -48,6 +50,7 @@ var (
callAccountPublicKeys,
callBalanceValidationStatus,
callEcho,
callFeeValidationStatus,
callLatestBlock,
callListAccounts,
callVerifyAddress,
Expand Down Expand Up @@ -96,13 +99,18 @@ type Server struct {
scriptSetContract []byte
validation *validation
validationMu sync.RWMutex // protects validation
feeValidation *feeValidation
feeValidationMu sync.RWMutex // protects feeValidation
}

// Run initializes the server and starts serving Rosetta API calls.
func (s *Server) Run(ctx context.Context) {
s.compileScripts()
s.validation = &validation{
status: "not_started",
status: validationNotStarted,
}
s.feeValidation = &feeValidation{
status: validationNotStarted,
}
go s.validateBalances(ctx)
s.feeAddrs = s.Chain.Contracts.FeeAddresses()
Expand Down Expand Up @@ -212,37 +220,95 @@ func (s *Server) setIndexedStateErr(format string, a ...interface{}) {
s.mu.Unlock()
s.validationMu.Lock()
defer s.validationMu.Unlock()
if s.validation.status == "failure" {
if s.validation.status == validationFailure {
return
}
s.validation = &validation{
err: msg,
status: "failure",
status: validationFailure,
}
}

func (s *Server) getFeeValidationStatus() *feeValidation {
s.feeValidationMu.RLock()
defer s.feeValidationMu.RUnlock()
return s.feeValidation
}

func (s *Server) setFeeValidationRetrying(format string, a ...interface{}) {
msg := fmt.Sprintf(format, a...)
log.Errorf("%s", msg)
s.feeValidationMu.Lock()
defer s.feeValidationMu.Unlock()
// We only track transient errors while we're still waiting for the first
// definitive result. Once we have one, it stays in place until the next
// definitive result replaces it.
if s.feeValidation.status == validationSuccess || s.feeValidation.status == validationFailure {
return
}
s.feeValidation = &feeValidation{
err: msg,
status: validationInProgress,
}
}

func (s *Server) setFeeValidationFailure(onchain []string, missing []string) {
msg := fmt.Sprintf(
"On-chain fee receiver account(s) %s are missing from the configured fee addresses: "+
"fee deposits to them would be misclassified as transfers; add them to .contracts.fee_receivers",
strings.Join(missing, ", "),
)
log.Errorf("%s", msg)
s.feeValidationMu.Lock()
defer s.feeValidationMu.Unlock()
s.feeValidation = &feeValidation{
err: msg,
missing: missing,
onchain: onchain,
status: validationFailure,
}
}

func (s *Server) setFeeValidationSuccess(onchain []string) {
s.feeValidationMu.Lock()
prev := s.feeValidation.status
s.feeValidation = &feeValidation{
onchain: onchain,
status: validationSuccess,
}
s.feeValidationMu.Unlock()
// We only log on transitions so that the periodic re-checks don't flood
// the logs.
if prev != validationSuccess {
log.Infof(
"Validated the configured fee addresses against the on-chain fee receivers: %s",
strings.Join(onchain, ", "),
)
}
}

func (s *Server) setValidationProgress(accounts int, checked int) {
s.validationMu.Lock()
defer s.validationMu.Unlock()
if s.validation.status == "failure" || s.validation.status == "success" {
if s.validation.status == validationFailure || s.validation.status == validationSuccess {
return
}
s.validation = &validation{
accounts: accounts,
checked: checked,
status: "in_progress",
status: validationInProgress,
}
}

func (s *Server) setValidationSuccess(accounts int) {
s.validationMu.Lock()
defer s.validationMu.Unlock()
if s.validation.status == "failure" {
if s.validation.status == validationFailure {
return
}
s.validation = &validation{
accounts: accounts,
status: "success",
status: validationSuccess,
}
}

Expand Down Expand Up @@ -293,9 +359,44 @@ type txnIntent struct {
sender []byte
}

// validationStatus enumerates the states a background validation process can
// be in.
type validationStatus int

const (
validationNotStarted validationStatus = iota
validationInProgress
validationSuccess
validationFailure
)

// String returns the status in the form reported by the /call endpoint.
func (v validationStatus) String() string {
switch v {
case validationNotStarted:
return "not_started"
case validationInProgress:
return "in_progress"
case validationSuccess:
return "success"
case validationFailure:
return "failure"
default:
log.Fatalf("Unsupported validation status %d", int(v))
panic("unreachable code")
}
}

type validation struct {
accounts int
checked int
err string
status string
status validationStatus
}

type feeValidation struct {
err string
missing []string
onchain []string
status validationStatus
}
37 changes: 28 additions & 9 deletions api/call_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ func (s *Server) Call(ctx context.Context, r *types.CallRequest) (*types.CallRes
return s.balanceValidationStatus(ctx)
case callEcho:
return s.echo(r.Parameters)
case callFeeValidationStatus:
return s.feeReceiverValidationStatus()
case callLatestBlock:
return s.latestBlock(ctx, r.Parameters)
case callListAccounts:
Expand Down Expand Up @@ -176,40 +178,57 @@ func (s *Server) accountPublicKeys(ctx context.Context, params map[string]interf
func (s *Server) balanceValidationStatus(ctx context.Context) (*types.CallResponse, *types.Error) {
v := s.getValidationStatus()
switch v.status {
case "failure":
case validationFailure:
return &types.CallResponse{
Result: map[string]interface{}{
"error": v.err,
"status": v.status,
"status": v.status.String(),
},
}, nil
case "in_progress":
case validationInProgress:
return &types.CallResponse{
Result: map[string]interface{}{
"accounts": v.accounts,
"checked": v.checked,
"status": v.status,
"status": v.status.String(),
},
}, nil
case "not_started":
case validationNotStarted:
return &types.CallResponse{
Result: map[string]interface{}{
"status": v.status,
"status": v.status.String(),
},
}, nil
case "success":
case validationSuccess:
return &types.CallResponse{
Result: map[string]interface{}{
"accounts": v.accounts,
"status": v.status,
"status": v.status.String(),
},
}, nil
default:
log.Fatalf("Unsupported validation status %q", v.status)
log.Fatalf("Unsupported validation status %d", int(v.status))
panic("unreachable code")
}
}

func (s *Server) feeReceiverValidationStatus() (*types.CallResponse, *types.Error) {
v := s.getFeeValidationStatus()
result := map[string]interface{}{
"status": v.status.String(),
}
if v.err != "" {
result["error"] = v.err
}
if v.onchain != nil {
result["fee_receivers"] = v.onchain
}
if v.missing != nil {
result["missing"] = v.missing
}
return &types.CallResponse{Result: result}, nil
}

func (s *Server) echo(params map[string]interface{}) (*types.CallResponse, *types.Error) {
return &types.CallResponse{
Idempotent: true,
Expand Down
Loading
Loading