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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,28 @@ config file:
use the zero address `"0000000000000000"` to disable support for proxy
accounts.

* The optional `fee_receivers` key lists accounts, in addition to the
FlowFees contract account, that receive transaction fee deposits. Networks
may distribute fees across several receiver accounts (testnet does so
since the concurrent fee collection upgrade), and without them configured,
fee deposits to those accounts would be misclassified as ordinary
transfers, e.g.

```json
{
"fee_receivers": [
"e1ac6b2740d204c2",
"05cbd2fa5128041d",
"139fb7c9c82c0e7c"
]
}
```

* 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.

* `data_dir: string`

* This defines the path to the data directory where the server stores data,
Expand Down
14 changes: 5 additions & 9 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ type Server struct {
Indexer *state.Indexer
Offline bool
Port uint16
feeAddr []byte
feeAddrs map[string]bool
genesis *model.BlockMeta
indexedStateErr *types.Error
mu sync.RWMutex // protects indexedStateErr
Expand All @@ -89,6 +89,7 @@ type Server struct {
scriptCreateProxyAccount []byte
scriptGetBalances []byte
scriptGetBalancesBasic []byte
scriptGetFeeReceivers []byte
scriptGetProxyNonce []byte
scriptGetProxyPublicKey []byte
scriptProxyTransfer []byte
Expand All @@ -104,14 +105,8 @@ func (s *Server) Run(ctx context.Context) {
status: "not_started",
}
go s.validateBalances(ctx)
feeAddr, err := hex.DecodeString(s.Chain.Contracts.FlowFees)
if err != nil {
log.Fatalf(
"Invalid FlowFees contract address %q: %s",
s.Chain.Contracts.FlowFees, err,
)
}
s.feeAddr = feeAddr
s.feeAddrs = s.Chain.Contracts.FeeAddresses()
go s.validateFeeReceivers(ctx)
s.genesis = s.Index.Genesis()
s.networks = []*types.NetworkIdentifier{{
Blockchain: "flow",
Expand Down Expand Up @@ -166,6 +161,7 @@ func (s *Server) compileScripts() {
s.scriptCreateProxyAccount = script.Compile("create_proxy_account", script.CreateProxyAccount, s.Chain)
s.scriptGetBalances = script.Compile("get_balances", script.GetBalances, s.Chain)
s.scriptGetBalancesBasic = script.Compile("get_balances_basic", script.GetBalancesBasic, s.Chain)
s.scriptGetFeeReceivers = script.Compile("get_fee_receivers", script.GetFeeReceivers, s.Chain)
s.scriptGetProxyNonce = script.Compile("get_proxy_nonce", script.GetProxyNonce, s.Chain)
s.scriptGetProxyPublicKey = script.Compile("get_proxy_public_key", script.GetProxyPublicKey, s.Chain)
s.scriptProxyTransfer = script.Compile("proxy_transfer", script.ProxyTransfer, s.Chain)
Expand Down
8 changes: 4 additions & 4 deletions api/construction_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,13 +523,13 @@ func (s *Server) ConstructionPreprocess(ctx context.Context, r *types.Constructi
if xerr != nil {
return nil, xerr
}
// NOTE(tav): We explicitly error on transfers to the fee address so as to
// NOTE(tav): We explicitly error on transfers to a fee address so as to
// simplify our event processing logic.
if bytes.Equal(intent.receiver, s.feeAddr) {
if s.feeAddrs[string(intent.receiver)] {
return nil, wrapErrorf(
errInvalidOpsIntent,
"cannot make transfers to the fee address: 0x%s",
s.Chain.Contracts.FlowFees,
"cannot make transfers to the fee address: 0x%x",
intent.receiver,
)
}
opts := &model.ConstructOpts{
Expand Down
68 changes: 68 additions & 0 deletions api/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,79 @@ package api
import (
"context"
"os"
"strings"
"time"

"github.com/onflow/cadence"
"github.com/onflow/rosetta/log"
)

// validateFeeReceivers checks the configured fee addresses (the FlowFees
// contract account plus .contracts.fee_receivers) against the fee receiver
// accounts the FlowFees contract rotates deposits across on chain. If an
// on-chain receiver is missing from the config, fee deposits to it would be
// misclassified as ordinary transfers, so we exit with a fatal error.
// Configured addresses that are no longer on chain are fine — they may be
// needed to classify fees in historical blocks.
func (s *Server) validateFeeReceivers(ctx context.Context) {
if s.Offline {
return
}
client := s.DataAccessNodes.Client()
const attempts = 5
for attempt := 1; attempt <= attempts; attempt++ {
select {
case <-ctx.Done():
return
default:
}
if attempt > 1 {
time.Sleep(time.Duration(attempt) * time.Second)
}
latest, err := client.LatestBlockHeader(ctx)
if err != nil {
log.Errorf("Failed to get the latest block header to validate fee receivers: %s", err)
continue
}
resp, err := client.Execute(ctx, latest.Id, s.scriptGetFeeReceivers, nil)
if err != nil {
log.Errorf("Failed to execute the get_fee_receivers script: %s", err)
continue
}
arr, ok := resp.(cadence.Array)
if !ok {
log.Errorf("Failed to convert get_fee_receivers result to an array: got %T", resp)
return
}
onchain := []string{}
missing := []string{}
for _, val := range arr.Values {
addr, ok := val.(cadence.Address)
if !ok {
log.Errorf("Failed to convert get_fee_receivers element to an address: got %T", val)
return
}
onchain = append(onchain, addr.String())
if !s.feeAddrs[string(addr.Bytes())] {
missing = append(missing, addr.String())
}
}
if len(missing) > 0 {
log.Fatalf(
"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.Infof(
"Validated the configured fee addresses against the on-chain fee receivers: %s",
strings.Join(onchain, ", "),
)
return
}
log.Errorf("Giving up on fee receiver validation after %d attempts", attempts)
}

// NOTE(tav): We exit with a fatal error if the on-chain state doesn't match
// what we expect. This assumes that we can trust the data returned to us by the
// Access API servers, which may not necessarily be true.
Expand Down
27 changes: 27 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,33 @@ type Contracts struct {
FlowToken string `json:"flow_token"`
FungibleToken string `json:"fungible_token"`
FlowColdStorageProxy string `json:"flow_cold_storage_proxy"`
// FeeReceivers lists accounts, in addition to the FlowFees contract
// account, that receive transaction fee deposits. Networks may distribute
// fees across several receiver accounts: testnet does so since the
// FlowFees upgrade in transaction
// be210889dd26a320f530595bd369093e866e26c3941bf7a3d01f861db3eeda81 (the
// canonical list is returned by FlowFees.getFeeReceiverAddresses() on
// chain). Without them, fee deposits are misclassified as ordinary
// transfers.
FeeReceivers []string `json:"fee_receivers"`
}

// FeeAddresses returns the set of accounts whose FLOW deposits represent
// transaction fees: the FlowFees contract account plus any configured
// fee_receivers. The map is keyed by the raw 8-byte address string.
func (c *Contracts) FeeAddresses() map[string]bool {
addrs := map[string]bool{}
for _, src := range append([]string{c.FlowFees}, c.FeeReceivers...) {
addr, err := hex.DecodeString(src)
if err != nil {
log.Fatalf("Invalid fee address %q: %s", src, err)
}
if len(addr) != 8 {
log.Fatalf("Invalid fee address %q: expected 8 bytes, got %d", src, len(addr))
}
addrs[string(addr)] = true
}
return addrs
}

// Consensus defines the metadata needed to initialize a consensus follower for
Expand Down
41 changes: 41 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package config

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestFeeAddresses(t *testing.T) {
t.Run("defaults to the FlowFees contract account", func(t *testing.T) {
contracts := &Contracts{FlowFees: "912d5440f7e3769e"}
require.Equal(t, map[string]bool{
"\x91\x2d\x54\x40\xf7\xe3\x76\x9e": true,
}, contracts.FeeAddresses())
})

t.Run("includes configured fee receivers", func(t *testing.T) {
contracts := &Contracts{
FlowFees: "912d5440f7e3769e",
FeeReceivers: []string{
"e1ac6b2740d204c2",
"05cbd2fa5128041d",
"139fb7c9c82c0e7c",
},
}
require.Equal(t, map[string]bool{
"\x91\x2d\x54\x40\xf7\xe3\x76\x9e": true,
"\xe1\xac\x6b\x27\x40\xd2\x04\xc2": true,
"\x05\xcb\xd2\xfa\x51\x28\x04\x1d": true,
"\x13\x9f\xb7\xc9\xc8\x2c\x0e\x7c": true,
}, contracts.FeeAddresses())
})

t.Run("deduplicates a receiver equal to the FlowFees account", func(t *testing.T) {
contracts := &Contracts{
FlowFees: "912d5440f7e3769e",
FeeReceivers: []string{"912d5440f7e3769e"},
}
require.Len(t, contracts.FeeAddresses(), 1)
})
}
Loading
Loading