Skip to content

Commit b9af72e

Browse files
support multiple fee receiver accounts
1 parent 5c4e4da commit b9af72e

11 files changed

Lines changed: 205 additions & 23 deletions

File tree

api/api.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ type Server struct {
7878
Indexer *state.Indexer
7979
Offline bool
8080
Port uint16
81-
feeAddr []byte
81+
feeAddrs map[string]bool
8282
genesis *model.BlockMeta
8383
indexedStateErr *types.Error
8484
mu sync.RWMutex // protects indexedStateErr
@@ -89,6 +89,7 @@ type Server struct {
8989
scriptCreateProxyAccount []byte
9090
scriptGetBalances []byte
9191
scriptGetBalancesBasic []byte
92+
scriptGetFeeReceivers []byte
9293
scriptGetProxyNonce []byte
9394
scriptGetProxyPublicKey []byte
9495
scriptProxyTransfer []byte
@@ -104,14 +105,8 @@ func (s *Server) Run(ctx context.Context) {
104105
status: "not_started",
105106
}
106107
go s.validateBalances(ctx)
107-
feeAddr, err := hex.DecodeString(s.Chain.Contracts.FlowFees)
108-
if err != nil {
109-
log.Fatalf(
110-
"Invalid FlowFees contract address %q: %s",
111-
s.Chain.Contracts.FlowFees, err,
112-
)
113-
}
114-
s.feeAddr = feeAddr
108+
s.feeAddrs = s.Chain.Contracts.FeeAddresses()
109+
go s.validateFeeReceivers(ctx)
115110
s.genesis = s.Index.Genesis()
116111
s.networks = []*types.NetworkIdentifier{{
117112
Blockchain: "flow",
@@ -166,6 +161,7 @@ func (s *Server) compileScripts() {
166161
s.scriptCreateProxyAccount = script.Compile("create_proxy_account", script.CreateProxyAccount, s.Chain)
167162
s.scriptGetBalances = script.Compile("get_balances", script.GetBalances, s.Chain)
168163
s.scriptGetBalancesBasic = script.Compile("get_balances_basic", script.GetBalancesBasic, s.Chain)
164+
s.scriptGetFeeReceivers = script.Compile("get_fee_receivers", script.GetFeeReceivers, s.Chain)
169165
s.scriptGetProxyNonce = script.Compile("get_proxy_nonce", script.GetProxyNonce, s.Chain)
170166
s.scriptGetProxyPublicKey = script.Compile("get_proxy_public_key", script.GetProxyPublicKey, s.Chain)
171167
s.scriptProxyTransfer = script.Compile("proxy_transfer", script.ProxyTransfer, s.Chain)

api/construction_service.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -523,13 +523,13 @@ func (s *Server) ConstructionPreprocess(ctx context.Context, r *types.Constructi
523523
if xerr != nil {
524524
return nil, xerr
525525
}
526-
// NOTE(tav): We explicitly error on transfers to the fee address so as to
526+
// NOTE(tav): We explicitly error on transfers to a fee address so as to
527527
// simplify our event processing logic.
528-
if bytes.Equal(intent.receiver, s.feeAddr) {
528+
if s.feeAddrs[string(intent.receiver)] {
529529
return nil, wrapErrorf(
530530
errInvalidOpsIntent,
531-
"cannot make transfers to the fee address: 0x%s",
532-
s.Chain.Contracts.FlowFees,
531+
"cannot make transfers to the fee address: 0x%x",
532+
intent.receiver,
533533
)
534534
}
535535
opts := &model.ConstructOpts{

api/validate.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,79 @@ package api
33
import (
44
"context"
55
"os"
6+
"strings"
67
"time"
78

9+
"github.com/onflow/cadence"
810
"github.com/onflow/rosetta/log"
911
)
1012

13+
// validateFeeReceivers checks the configured fee addresses (the FlowFees
14+
// contract account plus .contracts.fee_receivers) against the fee receiver
15+
// accounts the FlowFees contract rotates deposits across on chain. If an
16+
// on-chain receiver is missing from the config, fee deposits to it would be
17+
// misclassified as ordinary transfers, so we exit with a fatal error.
18+
// Configured addresses that are no longer on chain are fine — they may be
19+
// needed to classify fees in historical blocks.
20+
func (s *Server) validateFeeReceivers(ctx context.Context) {
21+
if s.Offline {
22+
return
23+
}
24+
client := s.DataAccessNodes.Client()
25+
const attempts = 5
26+
for attempt := 1; attempt <= attempts; attempt++ {
27+
select {
28+
case <-ctx.Done():
29+
return
30+
default:
31+
}
32+
if attempt > 1 {
33+
time.Sleep(time.Duration(attempt) * time.Second)
34+
}
35+
latest, err := client.LatestBlockHeader(ctx)
36+
if err != nil {
37+
log.Errorf("Failed to get the latest block header to validate fee receivers: %s", err)
38+
continue
39+
}
40+
resp, err := client.Execute(ctx, latest.Id, s.scriptGetFeeReceivers, nil)
41+
if err != nil {
42+
log.Errorf("Failed to execute the get_fee_receivers script: %s", err)
43+
continue
44+
}
45+
arr, ok := resp.(cadence.Array)
46+
if !ok {
47+
log.Errorf("Failed to convert get_fee_receivers result to an array: got %T", resp)
48+
return
49+
}
50+
onchain := []string{}
51+
missing := []string{}
52+
for _, val := range arr.Values {
53+
addr, ok := val.(cadence.Address)
54+
if !ok {
55+
log.Errorf("Failed to convert get_fee_receivers element to an address: got %T", val)
56+
return
57+
}
58+
onchain = append(onchain, addr.String())
59+
if !s.feeAddrs[string(addr.Bytes())] {
60+
missing = append(missing, addr.String())
61+
}
62+
}
63+
if len(missing) > 0 {
64+
log.Fatalf(
65+
"On-chain fee receiver account(s) %s are missing from the configured fee addresses: "+
66+
"fee deposits to them would be misclassified as transfers; add them to .contracts.fee_receivers",
67+
strings.Join(missing, ", "),
68+
)
69+
}
70+
log.Infof(
71+
"Validated the configured fee addresses against the on-chain fee receivers: %s",
72+
strings.Join(onchain, ", "),
73+
)
74+
return
75+
}
76+
log.Errorf("Giving up on fee receiver validation after %d attempts", attempts)
77+
}
78+
1179
// NOTE(tav): We exit with a fatal error if the on-chain state doesn't match
1280
// what we expect. This assumes that we can trust the data returned to us by the
1381
// Access API servers, which may not necessarily be true.

config/config.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,32 @@ type Contracts struct {
8484
FlowToken string `json:"flow_token"`
8585
FungibleToken string `json:"fungible_token"`
8686
FlowColdStorageProxy string `json:"flow_cold_storage_proxy"`
87+
// FeeReceivers lists accounts, in addition to the FlowFees contract
88+
// account, that receive transaction fee deposits. Networks may distribute
89+
// fees across several receiver accounts: testnet does so since the
90+
// FlowFees upgrade in transaction
91+
// be210889dd26a320f530595bd369093e866e26c3941bf7a3d01f861db3eeda81 (the
92+
// canonical list is flow-go's systemcontracts.FlowFeesReceivers). Without
93+
// them, fee deposits are misclassified as ordinary transfers.
94+
FeeReceivers []string `json:"fee_receivers"`
95+
}
96+
97+
// FeeAddresses returns the set of accounts whose FLOW deposits represent
98+
// transaction fees: the FlowFees contract account plus any configured
99+
// fee_receivers. The map is keyed by the raw 8-byte address string.
100+
func (c *Contracts) FeeAddresses() map[string]bool {
101+
addrs := map[string]bool{}
102+
for _, src := range append([]string{c.FlowFees}, c.FeeReceivers...) {
103+
addr, err := hex.DecodeString(src)
104+
if err != nil {
105+
log.Fatalf("Invalid fee address %q: %s", src, err)
106+
}
107+
if len(addr) != 8 {
108+
log.Fatalf("Invalid fee address %q: expected 8 bytes, got %d", src, len(addr))
109+
}
110+
addrs[string(addr)] = true
111+
}
112+
return addrs
87113
}
88114

89115
// Consensus defines the metadata needed to initialize a consensus follower for

config/config_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package config
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestFeeAddresses(t *testing.T) {
10+
t.Run("defaults to the FlowFees contract account", func(t *testing.T) {
11+
contracts := &Contracts{FlowFees: "912d5440f7e3769e"}
12+
require.Equal(t, map[string]bool{
13+
"\x91\x2d\x54\x40\xf7\xe3\x76\x9e": true,
14+
}, contracts.FeeAddresses())
15+
})
16+
17+
t.Run("includes configured fee receivers", func(t *testing.T) {
18+
contracts := &Contracts{
19+
FlowFees: "912d5440f7e3769e",
20+
FeeReceivers: []string{
21+
"e1ac6b2740d204c2",
22+
"05cbd2fa5128041d",
23+
"139fb7c9c82c0e7c",
24+
},
25+
}
26+
require.Equal(t, map[string]bool{
27+
"\x91\x2d\x54\x40\xf7\xe3\x76\x9e": true,
28+
"\xe1\xac\x6b\x27\x40\xd2\x04\xc2": true,
29+
"\x05\xcb\xd2\xfa\x51\x28\x04\x1d": true,
30+
"\x13\x9f\xb7\xc9\xc8\x2c\x0e\x7c": true,
31+
}, contracts.FeeAddresses())
32+
})
33+
34+
t.Run("deduplicates a receiver equal to the FlowFees account", func(t *testing.T) {
35+
contracts := &Contracts{
36+
FlowFees: "912d5440f7e3769e",
37+
FeeReceivers: []string{"912d5440f7e3769e"},
38+
}
39+
require.Len(t, contracts.FeeAddresses(), 1)
40+
})
41+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Returns the addresses of all accounts that may receive transaction fee
2+
// deposits: the FlowFees contract account itself, plus any child fee accounts
3+
// that FlowFees.collectFeesOnChildAccounts rotates deposits across (see
4+
// onflow/flow-core-contracts#575, "Enable concurrent fee collection").
5+
//
6+
// The FlowFees contract exposes no getter for these addresses, so this script
7+
// reads the capability list the contract keeps at /storage/ChildFeeAccounts.
8+
// The borrowed type must spell the capability's entitlements exactly as the
9+
// contract issues them; if a contract upgrade changes them, the borrow
10+
// returns nil and this script degrades to just the FlowFees account.
11+
access(all) fun main(): [Address] {
12+
let acct = getAuthAccount<auth(Storage) &Account>(0x{{.Contracts.FlowFees}})
13+
let addresses: [Address] = [0x{{.Contracts.FlowFees}}]
14+
if let childFeeAccounts = acct.storage.borrow<&[Capability<auth(Storage, Contracts, Keys, Inbox, Capabilities) &Account>]>(from: /storage/ChildFeeAccounts) {
15+
for cap in childFeeAccounts {
16+
addresses.append(cap.address)
17+
}
18+
}
19+
return addresses
20+
}

script/script.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ var GetBalances string
5656
//go:embed cadence/scripts/get-balances-basic.cdc
5757
var GetBalancesBasic string
5858

59+
// GetFeeReceivers defines the template for the read-only transaction script
60+
// that returns the addresses of all accounts that may receive transaction fee
61+
// deposits: the FlowFees contract account plus any child fee accounts
62+
// configured on chain.
63+
//
64+
//go:embed cadence/scripts/get-fee-receivers.cdc
65+
var GetFeeReceivers string
66+
5967
// GetProxyNonce defines the template for the read-only transaction script that
6068
// returns a proxy account's sequence number, i.e. the next nonce value for its
6169
// FlowColdStorageProxy Vault.

script/script_test.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package script
22

33
import (
44
"context"
5-
"github.com/onflow/rosetta/config"
5+
"strings"
66
"testing"
7+
8+
"github.com/onflow/rosetta/config"
79
)
810

911
// TestCompile tests the Compile function
@@ -22,3 +24,25 @@ func TestCompileComputeFees(t *testing.T) {
2224
t.Errorf("Expected %q but got %q", expected, string(result))
2325
}
2426
}
27+
28+
// TestCompileGetFeeReceivers tests that the FlowFees address is rendered into
29+
// the get-fee-receivers script.
30+
//
31+
// NOTE: config.Init cannot be called a second time within the same test
32+
// binary (it locks the Badger cache database), so the chain is constructed
33+
// directly.
34+
func TestCompileGetFeeReceivers(t *testing.T) {
35+
chain := &config.Chain{Contracts: &config.Contracts{FlowFees: "912d5440f7e3769e"}}
36+
37+
result := string(Compile("get_fee_receivers", GetFeeReceivers, chain))
38+
39+
for _, expected := range []string{
40+
"getAuthAccount<auth(Storage) &Account>(0x912d5440f7e3769e)",
41+
"let addresses: [Address] = [0x912d5440f7e3769e]",
42+
"from: /storage/ChildFeeAccounts",
43+
} {
44+
if !strings.Contains(result, expected) {
45+
t.Errorf("Expected compiled script to contain %q:\n%s", expected, result)
46+
}
47+
}
48+
}

state/process.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,7 @@ outer:
803803
Receiver: receiver[:],
804804
Type: model.TransferType_DEPOSIT,
805805
})
806-
if bytes.Equal(receiver[:], i.feeAddr) {
806+
if i.feeAddrs[string(receiver[:])] {
807807
// NOTE(tav): When the deposit is to the fee
808808
// address, just increment the fee amount.
809809
fees += amount

state/state.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ type Indexer struct {
5555
Store *indexdb.Store
5656
accts map[string]bool
5757
consensus storage.DB
58-
feeAddr []byte
58+
feeAddrs map[string]bool
5959
jobs chan uint64
6060
lastIndexed *model.BlockMeta
6161
liveRoot *model.BlockMeta
@@ -541,13 +541,7 @@ func (i *Indexer) initState() {
541541
for acct, isProxy := range accts {
542542
i.accts[string(acct[:])] = isProxy
543543
}
544-
i.feeAddr, err = hex.DecodeString(i.Chain.Contracts.FlowFees)
545-
if err != nil {
546-
log.Fatalf(
547-
"Invalid FlowFees contract address %q: %s",
548-
i.Chain.Contracts.FlowFees, err,
549-
)
550-
}
544+
i.feeAddrs = i.Chain.Contracts.FeeAddresses()
551545
i.originators = map[string]bool{}
552546
for _, addr := range i.Chain.Originators {
553547
i.originators[string(addr)] = true

0 commit comments

Comments
 (0)