Skip to content

Commit 5eab151

Browse files
Merge pull request #100 from onflow/janezp/fee-receivers
Support multiple fee receiver accounts
2 parents 1524dda + 301ae5c commit 5eab151

14 files changed

Lines changed: 238 additions & 43 deletions

File tree

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,28 @@ config file:
357357
use the zero address `"0000000000000000"` to disable support for proxy
358358
accounts.
359359

360+
* The optional `fee_receivers` key lists accounts, in addition to the
361+
FlowFees contract account, that receive transaction fee deposits. Networks
362+
may distribute fees across several receiver accounts (testnet does so
363+
since the concurrent fee collection upgrade), and without them configured,
364+
fee deposits to those accounts would be misclassified as ordinary
365+
transfers, e.g.
366+
367+
```json
368+
{
369+
"fee_receivers": [
370+
"e1ac6b2740d204c2",
371+
"05cbd2fa5128041d",
372+
"139fb7c9c82c0e7c"
373+
]
374+
}
375+
```
376+
377+
* The canonical list is returned by `FlowFees.getFeeReceiverAddresses()` on
378+
chain. On startup, the server validates the configured addresses against
379+
that list and exits with a fatal error if any on-chain receiver is
380+
missing from the config.
381+
360382
* `data_dir: string`
361383

362384
* This defines the path to the data directory where the server stores data,

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: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,81 @@ 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+
const attempts = 5
25+
for attempt := 1; attempt <= attempts; attempt++ {
26+
select {
27+
case <-ctx.Done():
28+
return
29+
default:
30+
}
31+
if attempt > 1 {
32+
time.Sleep(time.Duration(attempt) * time.Second)
33+
}
34+
// Pick a client on each attempt so a retry can land on a different
35+
// access node if the previously selected one is unavailable.
36+
client := s.DataAccessNodes.Client()
37+
latest, err := client.LatestBlockHeader(ctx)
38+
if err != nil {
39+
log.Errorf("Failed to get the latest block header to validate fee receivers: %s", err)
40+
continue
41+
}
42+
resp, err := client.Execute(ctx, latest.Id, s.scriptGetFeeReceivers, nil)
43+
if err != nil {
44+
log.Errorf("Failed to execute the get_fee_receivers script: %s", err)
45+
continue
46+
}
47+
arr, ok := resp.(cadence.Array)
48+
if !ok {
49+
log.Errorf("Failed to convert get_fee_receivers result to an array: got %T", resp)
50+
return
51+
}
52+
onchain := []string{}
53+
missing := []string{}
54+
for _, val := range arr.Values {
55+
addr, ok := val.(cadence.Address)
56+
if !ok {
57+
log.Errorf("Failed to convert get_fee_receivers element to an address: got %T", val)
58+
return
59+
}
60+
onchain = append(onchain, addr.String())
61+
if !s.feeAddrs[string(addr.Bytes())] {
62+
missing = append(missing, addr.String())
63+
}
64+
}
65+
if len(missing) > 0 {
66+
log.Fatalf(
67+
"On-chain fee receiver account(s) %s are missing from the configured fee addresses: "+
68+
"fee deposits to them would be misclassified as transfers; add them to .contracts.fee_receivers",
69+
strings.Join(missing, ", "),
70+
)
71+
}
72+
log.Infof(
73+
"Validated the configured fee addresses against the on-chain fee receivers: %s",
74+
strings.Join(onchain, ", "),
75+
)
76+
return
77+
}
78+
log.Errorf("Giving up on fee receiver validation after %d attempts", attempts)
79+
}
80+
1181
// NOTE(tav): We exit with a fatal error if the on-chain state doesn't match
1282
// what we expect. This assumes that we can trust the data returned to us by the
1383
// Access API servers, which may not necessarily be true.

config/config.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,33 @@ 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 returned by FlowFees.getFeeReceiverAddresses() on
93+
// chain). Without them, fee deposits are misclassified as ordinary
94+
// transfers.
95+
FeeReceivers []string `json:"fee_receivers"`
96+
}
97+
98+
// FeeAddresses returns the set of accounts whose FLOW deposits represent
99+
// transaction fees: the FlowFees contract account plus any configured
100+
// fee_receivers. The map is keyed by the raw 8-byte address string.
101+
func (c *Contracts) FeeAddresses() map[string]bool {
102+
addrs := map[string]bool{}
103+
for _, src := range append([]string{c.FlowFees}, c.FeeReceivers...) {
104+
addr, err := hex.DecodeString(src)
105+
if err != nil {
106+
log.Fatalf("Invalid fee address %q: %s", src, err)
107+
}
108+
if len(addr) != 8 {
109+
log.Fatalf("Invalid fee address %q: expected 8 bytes, got %d", src, len(addr))
110+
}
111+
addrs[string(addr)] = true
112+
}
113+
return addrs
87114
}
88115

89116
// 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+
}

go.mod

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ require (
1111
github.com/golang/protobuf v1.5.4
1212
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
1313
github.com/libp2p/go-libp2p v0.38.2
14-
github.com/onflow/cadence v1.10.3
14+
github.com/onflow/cadence v1.10.5
1515
github.com/onflow/crypto v0.25.4
16-
github.com/onflow/flow-go v0.48.1-evm-cache-block.0.20260518173711-5b9fa9c8352e
16+
github.com/onflow/flow-go v0.50.1-0.20260804214725-b73fea20b252
1717
github.com/onflow/flow/protobuf/go/flow v0.4.20
1818
github.com/rs/zerolog v1.29.0
1919
github.com/stretchr/testify v1.11.1
@@ -267,11 +267,11 @@ require (
267267
github.com/multiformats/go-multistream v0.6.0 // indirect
268268
github.com/multiformats/go-varint v0.0.7 // indirect
269269
github.com/olekukonko/tablewriter v0.0.5 // indirect
270-
github.com/onflow/atree v0.16.0 // indirect
271-
github.com/onflow/flow-core-contracts/lib/go/contracts v1.10.2 // indirect; v1.2.4-0.20230703193002-53362441b57d // indirect
272-
github.com/onflow/flow-core-contracts/lib/go/templates v1.10.2 // indirect; v1.2.3 // indirect
270+
github.com/onflow/atree v0.16.1 // indirect
271+
github.com/onflow/flow-core-contracts/lib/go/contracts v1.10.4 // indirect; v1.2.4-0.20230703193002-53362441b57d // indirect
272+
github.com/onflow/flow-core-contracts/lib/go/templates v1.10.4 // indirect; v1.2.3 // indirect
273273
github.com/onflow/flow-ft/lib/go/contracts v1.1.1 // indirect
274-
github.com/onflow/flow-go-sdk v1.10.3 // indirect
274+
github.com/onflow/flow-go-sdk v1.10.5 // indirect
275275
github.com/onflow/flow-nft/lib/go/contracts v1.4.1 // indirect
276276
github.com/onflow/go-ethereum v1.16.2 // indirect
277277
github.com/onflow/sdks v0.6.0-preview.1 // indirect

go.sum

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -732,30 +732,30 @@ github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JX
732732
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
733733
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
734734
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
735-
github.com/onflow/atree v0.16.0 h1:b+f/suzcnnr1Lx1KdJEjpn2CX+AKSAz1yIB30NQDutU=
736-
github.com/onflow/atree v0.16.0/go.mod h1:hiOT/vKK/Zyw34Ru9OFbfEemC5NnQ7SHFB43bN9/4qI=
735+
github.com/onflow/atree v0.16.1 h1:EmlaIz/GwQ39o5agAb2KT2ynt4SHRBkgMMWU5bp6iTs=
736+
github.com/onflow/atree v0.16.1/go.mod h1:hiOT/vKK/Zyw34Ru9OFbfEemC5NnQ7SHFB43bN9/4qI=
737737
github.com/onflow/boxo v0.0.0-20240201202436-f2477b92f483 h1:LpiQhTAfM9CAmNVEs0n//cBBgCg+vJSiIxTHYUklZ84=
738738
github.com/onflow/boxo v0.0.0-20240201202436-f2477b92f483/go.mod h1:pIZgTWdm3k3pLF9Uq6MB8JEcW07UDwNJjlXW1HELW80=
739-
github.com/onflow/cadence v1.10.3 h1:PJIIYKbaOT2DcZBnSO4O8ZF/Xc/fKV9vvOFLChgy85c=
740-
github.com/onflow/cadence v1.10.3/go.mod h1:tyUNaYlAgeQVgfR2C38MI1dtFFjKay+yGGPMrCRc068=
739+
github.com/onflow/cadence v1.10.5 h1:Y5kk4aY70SpxJtG/Wd05+xvkUL6tvodeQjSxnXNq65A=
740+
github.com/onflow/cadence v1.10.5/go.mod h1:axaADpRs+qTlq5cdHBawCiJ7dgqusRbBqOPkyWUwUOo=
741741
github.com/onflow/crypto v0.25.4 h1:R615PWPdSoA5RATNb/j3cYaloBIZlSXVNgS7BjwHiwM=
742742
github.com/onflow/crypto v0.25.4/go.mod h1:DlkW/1SPUvLHYvUcjWa9PkLIRgSBKR4EDc3i+ATQKW4=
743743
github.com/onflow/fixed-point v0.1.1 h1:j0jYZVO8VGyk1476alGudEg7XqCkeTVxb5ElRJRKS90=
744744
github.com/onflow/fixed-point v0.1.1/go.mod h1:gJdoHqKtToKdOZbvryJvDZfcpzC7d2fyWuo3ZmLtcGY=
745-
github.com/onflow/flow-core-contracts/lib/go/contracts v1.10.2 h1:OQr7LyoAzk9kVfVjPcHXoFtWIBSqbB7ksNb6wIJlFE8=
746-
github.com/onflow/flow-core-contracts/lib/go/contracts v1.10.2/go.mod h1:fn0eOOINlOdQSOWptENC92MpPorB7dHzZaC3VTAmiQY=
747-
github.com/onflow/flow-core-contracts/lib/go/templates v1.10.2 h1:qq16IwoT+xAh45GfmC9lR+gFCixJWx9NxKgkkP/W4Nw=
748-
github.com/onflow/flow-core-contracts/lib/go/templates v1.10.2/go.mod h1:bXe+VkZmvM3QYGjfizprStRfasLA/7ii7l6+LHP5V1U=
745+
github.com/onflow/flow-core-contracts/lib/go/contracts v1.10.4 h1:tUmbvKlApxfLfguNj9UCvgIB9kxiYqdPbK/IGdKzINQ=
746+
github.com/onflow/flow-core-contracts/lib/go/contracts v1.10.4/go.mod h1:fn0eOOINlOdQSOWptENC92MpPorB7dHzZaC3VTAmiQY=
747+
github.com/onflow/flow-core-contracts/lib/go/templates v1.10.4 h1:CifICeJM0FpOVzmGM328VT7OSJOQ7GRioZTRmSBrMgQ=
748+
github.com/onflow/flow-core-contracts/lib/go/templates v1.10.4/go.mod h1:bXe+VkZmvM3QYGjfizprStRfasLA/7ii7l6+LHP5V1U=
749749
github.com/onflow/flow-evm-bridge v0.2.1 h1:S32kk+UV7/COdQZakIMsJw6vxShel0s8lI3FyGFl9jM=
750750
github.com/onflow/flow-evm-bridge v0.2.1/go.mod h1:ExhTZax2F+boo13dzT/uAI7rvwewAoz9v+dEXhhFjYg=
751751
github.com/onflow/flow-ft/lib/go/contracts v1.1.1 h1:BNbP3CrTIgScpx2NS9snq9XDESFjgXrMXTrwk5H4iSs=
752752
github.com/onflow/flow-ft/lib/go/contracts v1.1.1/go.mod h1:PwsL8fC81cjnUnTfmyL/HOIyHnyaw/JA474Wfj2tl6A=
753753
github.com/onflow/flow-ft/lib/go/templates v1.1.1 h1:X+EGTWKeVlsF33JD5QBFZLr8KW2apl6Oh1AXRWHmzLI=
754754
github.com/onflow/flow-ft/lib/go/templates v1.1.1/go.mod h1:uQ8XFqmMK2jxyBSVrmyuwdWjTEb+6zGjRYotfDJ5pAE=
755-
github.com/onflow/flow-go v0.48.1-evm-cache-block.0.20260518173711-5b9fa9c8352e h1:n8yp4pz72O0CqHwzCmTgpeYyxwOVf8Vth1jEn2/4wIk=
756-
github.com/onflow/flow-go v0.48.1-evm-cache-block.0.20260518173711-5b9fa9c8352e/go.mod h1:x+/B1Ki53/TbRtdd317T6wmQ2QRMF4YKhkSE+3fy76A=
757-
github.com/onflow/flow-go-sdk v1.10.3 h1:4zJYkdDNqeQqUJmdQJXlHIZuEjOLp8lsu8dRz5GZ/Cc=
758-
github.com/onflow/flow-go-sdk v1.10.3/go.mod h1:cnpuCUvKLGqVrhz6yPEv0+LdsT9ib+cbn0YxfAJxHEI=
755+
github.com/onflow/flow-go v0.50.1-0.20260804214725-b73fea20b252 h1:XUvRo0Zt8GQtgSHKzcQ/EpCm1HdSthNEgcNci2wJzMU=
756+
github.com/onflow/flow-go v0.50.1-0.20260804214725-b73fea20b252/go.mod h1:NPiMixDFGz4/IQjdeV4phjZfbfQgxtmNJ9zU9DWkcYg=
757+
github.com/onflow/flow-go-sdk v1.10.5 h1:aE9E2xXW2AiR/7KzZApZ8HpmEzsLdNQWso5y/RHADsQ=
758+
github.com/onflow/flow-go-sdk v1.10.5/go.mod h1:efpOBjGw/Gmdu2yKcAjVsAARUekyHL06QftCXXQebM8=
759759
github.com/onflow/flow-nft/lib/go/contracts v1.4.1 h1:iQ8s4W5HNWd92MVRZbKxYpQ6UJn9snHLKQ9hFFNCiys=
760760
github.com/onflow/flow-nft/lib/go/contracts v1.4.1/go.mod h1:XUsJjlbVoI0kebgv87xsO70U/ITGYbSEgTwbyg1RcOs=
761761
github.com/onflow/flow-nft/lib/go/templates v1.4.1 h1:P+FN51waQrACpyVeXzLl1cnlD5J8bUYiemHXgeZBM+8=
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import FlowFees from 0x{{.Contracts.FlowFees}}
2+
3+
// Returns the addresses of all accounts that may receive transaction fee
4+
// deposits: the FlowFees contract account itself, plus any child fee accounts
5+
// that FlowFees.deductTransactionFee rotates deposits across (see
6+
// onflow/flow-core-contracts#575, "Enable concurrent fee collection").
7+
access(all) fun main(): [Address] {
8+
return FlowFees.getFeeReceiverAddresses()
9+
}

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.

0 commit comments

Comments
 (0)