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
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
26 changes: 26 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,32 @@ 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 flow-go's systemcontracts.FlowFeesReceivers). 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)
})
}
20 changes: 20 additions & 0 deletions script/cadence/scripts/get-fee-receivers.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Returns the addresses of all accounts that may receive transaction fee
// deposits: the FlowFees contract account itself, plus any child fee accounts
// that FlowFees.collectFeesOnChildAccounts rotates deposits across (see
// onflow/flow-core-contracts#575, "Enable concurrent fee collection").
//
// The FlowFees contract exposes no getter for these addresses, so this script
// reads the capability list the contract keeps at /storage/ChildFeeAccounts.
// The borrowed type must spell the capability's entitlements exactly as the
// contract issues them; if a contract upgrade changes them, the borrow
// returns nil and this script degrades to just the FlowFees account.
access(all) fun main(): [Address] {
let acct = getAuthAccount<auth(Storage) &Account>(0x{{.Contracts.FlowFees}})
let addresses: [Address] = [0x{{.Contracts.FlowFees}}]
if let childFeeAccounts = acct.storage.borrow<&[Capability<auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account>]>(from: /storage/ChildFeeAccounts) {
for cap in childFeeAccounts {
addresses.append(cap.address)
}
}
return addresses
}
8 changes: 8 additions & 0 deletions script/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ var GetBalances string
//go:embed cadence/scripts/get-balances-basic.cdc
var GetBalancesBasic string

// GetFeeReceivers defines the template for the read-only transaction script
// that returns the addresses of all accounts that may receive transaction fee
// deposits: the FlowFees contract account plus any child fee accounts
// configured on chain.
//
//go:embed cadence/scripts/get-fee-receivers.cdc
var GetFeeReceivers string

// GetProxyNonce defines the template for the read-only transaction script that
// returns a proxy account's sequence number, i.e. the next nonce value for its
// FlowColdStorageProxy Vault.
Expand Down
26 changes: 25 additions & 1 deletion script/script_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package script

import (
"context"
"github.com/onflow/rosetta/config"
"strings"
"testing"

"github.com/onflow/rosetta/config"
)

// TestCompile tests the Compile function
Expand All @@ -22,3 +24,25 @@ func TestCompileComputeFees(t *testing.T) {
t.Errorf("Expected %q but got %q", expected, string(result))
}
}

// TestCompileGetFeeReceivers tests that the FlowFees address is rendered into
// the get-fee-receivers script.
//
// NOTE: config.Init cannot be called a second time within the same test
// binary (it locks the Badger cache database), so the chain is constructed
// directly.
func TestCompileGetFeeReceivers(t *testing.T) {
chain := &config.Chain{Contracts: &config.Contracts{FlowFees: "912d5440f7e3769e"}}

result := string(Compile("get_fee_receivers", GetFeeReceivers, chain))

for _, expected := range []string{
"getAuthAccount<auth(Storage) &Account>(0x912d5440f7e3769e)",
"let addresses: [Address] = [0x912d5440f7e3769e]",
"from: /storage/ChildFeeAccounts",
} {
if !strings.Contains(result, expected) {
t.Errorf("Expected compiled script to contain %q:\n%s", expected, result)
}
}
}
2 changes: 1 addition & 1 deletion state/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,7 @@ outer:
Receiver: receiver[:],
Type: model.TransferType_DEPOSIT,
})
if bytes.Equal(receiver[:], i.feeAddr) {
if i.feeAddrs[string(receiver[:])] {
// NOTE(tav): When the deposit is to the fee
// address, just increment the fee amount.
fees += amount
Expand Down
10 changes: 2 additions & 8 deletions state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ type Indexer struct {
Store *indexdb.Store
accts map[string]bool
consensus storage.DB
feeAddr []byte
feeAddrs map[string]bool
jobs chan uint64
lastIndexed *model.BlockMeta
liveRoot *model.BlockMeta
Expand Down Expand Up @@ -541,13 +541,7 @@ func (i *Indexer) initState() {
for acct, isProxy := range accts {
i.accts[string(acct[:])] = isProxy
}
i.feeAddr, err = hex.DecodeString(i.Chain.Contracts.FlowFees)
if err != nil {
log.Fatalf(
"Invalid FlowFees contract address %q: %s",
i.Chain.Contracts.FlowFees, err,
)
}
i.feeAddrs = i.Chain.Contracts.FeeAddresses()
i.originators = map[string]bool{}
for _, addr := range i.Chain.Originators {
i.originators[string(addr)] = true
Expand Down
5 changes: 5 additions & 0 deletions testnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
}
],
"contracts": {
"fee_receivers": [
"e1ac6b2740d204c2",
"05cbd2fa5128041d",
"139fb7c9c82c0e7c"
],
"flow_cold_storage_proxy": "0000000000000000",
"flow_fees": "912d5440f7e3769e",
"flow_token": "7e60df042a9c0868",
Expand Down
Loading