@@ -3,6 +3,7 @@ package api
33import (
44 "context"
55 "os"
6+ "strings"
67 "time"
78
89 "github.com/onflow/cadence"
@@ -15,10 +16,11 @@ const (
1516 feeValidateRecheckInterval = 10 * time .Minute // re-check interval after a definitive result
1617)
1718
18- // validateFeeReceivers runs a background loop that checks the configured fee
19- // addresses (the FlowFees contract account plus .contracts.fee_receivers)
19+ // validateFeeReceivers runs a background loop that checks the fee addresses
20+ // used to classify fee deposits (the configured fee addresses, overridden by
21+ // the most recent indexed FlowFees.ChildFeeAccountsChanged event, if any)
2022// against the fee receiver accounts the FlowFees contract rotates deposits
21- // across on chain. If an on-chain receiver is missing from the config , fee
23+ // across on chain. If an on-chain receiver is missing from that set , fee
2224// deposits to it would be misclassified as ordinary transfers, so we log an
2325// error and surface the failure via the fee_receiver_validation_status /call
2426// method. Configured addresses that are no longer on chain are fine — they
@@ -62,25 +64,50 @@ func sleepCtx(ctx context.Context, d time.Duration) bool {
6264 }
6365}
6466
65- // checkFeeReceivers makes a single attempt at validating the configured fee
66- // addresses against the on-chain fee receivers, and records the outcome in
67- // the server's fee validation state. It returns false if the attempt failed
68- // and should be retried.
67+ // checkFeeReceivers makes a single attempt at validating the fee addresses
68+ // used for classification against the on-chain fee receivers, and records the
69+ // outcome in the server's fee validation state. It returns false if the
70+ // attempt failed and should be retried.
6971func (s * Server ) checkFeeReceivers (ctx context.Context ) bool {
70- // Pick a client on each attempt so a retry can land on a different
71- // access node if the previously selected one is unavailable.
72- client := s .DataAccessNodes .Client ()
73- latest , err := client .LatestBlockHeader (ctx )
74- if err != nil {
72+ // We validate at the latest indexed block (the genesis block if nothing
73+ // has been indexed yet), rather than the latest block available on the
74+ // Access API: the fee addresses only matter for the blocks the indexer is
75+ // currently classifying, and this keeps the check consistent with the
76+ // indexed fee receiver overrides.
77+ latest := s .Index .Latest ()
78+ if latest == nil {
79+ s .setFeeValidationRetrying (
80+ "Failed to validate fee receivers: no block has been indexed yet" ,
81+ )
82+ return false
83+ }
84+ // The latest indexed block may belong to a past spork while the indexer
85+ // is catching up, so we use the access nodes of the spork containing it —
86+ // and re-select a client on each attempt so a retry can land on a
87+ // different access node if the previously selected one is unavailable.
88+ spork := s .Chain .SporkFor (latest .Height )
89+ if spork == nil {
7590 s .setFeeValidationRetrying (
76- "Failed to get the latest block header to validate fee receivers: %s" , err ,
91+ "Failed to validate fee receivers: the latest indexed block at height %d cannot be associated with any sporks in the config" ,
92+ latest .Height ,
7793 )
7894 return false
7995 }
80- resp , err := client .Execute (ctx , latest .Id , s .scriptGetFeeReceivers , nil )
96+ client := spork .AccessNodes .Client ()
97+ resp , err := client .Execute (ctx , latest .Hash , s .scriptGetFeeReceivers , nil )
8198 if err != nil {
99+ if isMissingFeeReceiverFunc (err ) {
100+ // The FlowFees contract predates the concurrent fee collection
101+ // upgrade (onflow/flow-core-contracts#575): the FlowFees account
102+ // is the only fee receiver until the contract is upgraded. We
103+ // record this as a successful validation and keep polling so
104+ // that a later upgrade is detected.
105+ s .setFeeValidationFallback ()
106+ return true
107+ }
82108 s .setFeeValidationRetrying (
83- "Failed to execute the get_fee_receivers script: %s" , err ,
109+ "Failed to execute the get_fee_receivers script at the latest indexed block %x (%d): %s" ,
110+ latest .Hash , latest .Height , err ,
84111 )
85112 return false
86113 }
@@ -91,6 +118,7 @@ func (s *Server) checkFeeReceivers(ctx context.Context) bool {
91118 )
92119 return false
93120 }
121+ feeAddrs := s .currentFeeAddrs (latest .Height )
94122 onchain := []string {}
95123 missing := []string {}
96124 for _ , val := range arr .Values {
@@ -102,7 +130,7 @@ func (s *Server) checkFeeReceivers(ctx context.Context) bool {
102130 return false
103131 }
104132 onchain = append (onchain , addr .String ())
105- if ! s . feeAddrs [string (addr .Bytes ())] {
133+ if ! feeAddrs [string (addr .Bytes ())] {
106134 missing = append (missing , addr .String ())
107135 }
108136 }
@@ -114,6 +142,18 @@ func (s *Server) checkFeeReceivers(ctx context.Context) bool {
114142 return true
115143}
116144
145+ // isMissingFeeReceiverFunc returns whether the script execution error
146+ // indicates that the FlowFees contract predates the concurrent fee collection
147+ // upgrade (onflow/flow-core-contracts#575), i.e. it does not define
148+ // getFeeReceiverAddresses. This is a deterministic script type-checking
149+ // failure, so retrying it would never succeed — unlike transient access node
150+ // errors.
151+ func isMissingFeeReceiverFunc (err error ) bool {
152+ msg := err .Error ()
153+ return strings .Contains (msg , "getFeeReceiverAddresses" ) &&
154+ (strings .Contains (msg , "has no member" ) || strings .Contains (msg , "cannot find" ))
155+ }
156+
117157// NOTE(tav): We exit with a fatal error if the on-chain state doesn't match
118158// what we expect. This assumes that we can trust the data returned to us by the
119159// Access API servers, which may not necessarily be true.
0 commit comments