From 47d4895a664da1b171b1db031b84b059e6d9cbdd Mon Sep 17 00:00:00 2001 From: user Date: Thu, 14 Dec 2023 22:26:38 +1030 Subject: [PATCH] Fix a Chainlink race using a cache with goroutines --- common/ethereum/chainlink/chainlink.go | 64 +++++++------------ common/ethereum/chainlink/init.go | 2 + common/ethereum/chainlink/server.go | 86 ++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 42 deletions(-) create mode 100644 common/ethereum/chainlink/server.go diff --git a/common/ethereum/chainlink/chainlink.go b/common/ethereum/chainlink/chainlink.go index 6e25918f5..375c233f3 100644 --- a/common/ethereum/chainlink/chainlink.go +++ b/common/ethereum/chainlink/chainlink.go @@ -9,11 +9,16 @@ import ( "math/big" ethAbi "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/fluidity-money/fluidity-app/common/ethereum" + "github.com/fluidity-money/fluidity-app/lib/log" + ethCommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" - "github.com/fluidity-money/fluidity-app/common/ethereum" ) +// Context to use for logging +const Context = "CHAINLINK" + const chainlinkPriceFeedAbiString = `[ { "inputs": [], @@ -65,53 +70,28 @@ const chainlinkPriceFeedAbiString = `[ var priceFeedAbi ethAbi.ABI -var decimalsCache = make(map[ethCommon.Address]*big.Rat) - -func getFeedDecimals(client *ethclient.Client, priceFeedAddress ethCommon.Address) (*big.Rat, error) { - decimals, exists := decimalsCache[priceFeedAddress] - - if exists { - return decimals, nil - } - - decimalsRes, err := ethereum.StaticCall( - client, - priceFeedAddress, - priceFeedAbi, - "decimals", - ) - - if err != nil { - return nil, err - } - - decimals_, err := ethereum.CoerceBoundContractResultsToUint8(decimalsRes) +// GetPrice using a Chainlink feed, caching the results in an internal +// server to make it thread safe +func GetPrice(client *ethclient.Client, priceFeedAddress ethCommon.Address) (*big.Rat, error) { + log.Debug(func(k *log.Log) { + k.Context = Context - if err != nil { - return nil, fmt.Errorf( - "Failed to read decimals result! %w", - err, + k.Format( + "Using the Chainlink decimals cache to look up decimals for price feed %v", + priceFeedAddress, ) - } - - ten := big.NewRat(10, 1) + }) - decimals = ethereum.BigPow(ten, int(decimals_)) - decimalsCache[priceFeedAddress] = decimals - - return decimals, nil -} + resp := make(chan *big.Rat) -func GetPrice(client *ethclient.Client, priceFeedAddress ethCommon.Address) (*big.Rat, error) { - decimals, err := getFeedDecimals(client, priceFeedAddress) - - if err != nil { - return nil, fmt.Errorf( - "Failed to get feed decimals! %w", - err, - ) + decimalsServer <- request{ + client: client, + priceFeedAddress: priceFeedAddress, + resp: resp, } + decimals := <-resp + priceRes, err := ethereum.StaticCall( client, priceFeedAddress, diff --git a/common/ethereum/chainlink/init.go b/common/ethereum/chainlink/init.go index 75ea081f2..d0ff1555e 100644 --- a/common/ethereum/chainlink/init.go +++ b/common/ethereum/chainlink/init.go @@ -18,4 +18,6 @@ func init() { if priceFeedAbi, err = ethAbi.JSON(priceFeedReader); err != nil { panic(err) } + + go startDecimalsServer() } diff --git a/common/ethereum/chainlink/server.go b/common/ethereum/chainlink/server.go new file mode 100644 index 000000000..6ff07593a --- /dev/null +++ b/common/ethereum/chainlink/server.go @@ -0,0 +1,86 @@ +// Copyright 2022 Fluidity Money. All rights reserved. Use of this +// source code is governed by a GPL-style license that can be found in the +// LICENSE.md file. + +package chainlink + +import ( + "math/big" + + ethCommon "github.com/ethereum/go-ethereum/common" + "github.com/fluidity-money/fluidity-app/common/ethereum" + "github.com/fluidity-money/fluidity-app/lib/log" + + "github.com/ethereum/go-ethereum/ethclient" +) + +type request struct { + client *ethclient.Client + priceFeedAddress ethCommon.Address + resp chan *big.Rat +} + +// decimalsServer to respond to requests with cached (or not, looked up) +// responses +var decimalsServer = make(chan request) + +func startDecimalsServer() { + decimalsCache := make(map[ethCommon.Address]*big.Rat) + + for request := range decimalsServer { + var ( + client = request.client + priceFeedAddress = request.priceFeedAddress + respChan = request.resp + ) + + decimals, exists := decimalsCache[priceFeedAddress] + + if exists { + respChan <- decimals + continue + } + + decimalsRes, err := ethereum.StaticCall( + client, + priceFeedAddress, + priceFeedAbi, + "decimals", + ) + + if err != nil { + log.Fatal(func(k *log.Log) { + k.Context = Context + + k.Format( + "Failed to static call feed address %v", + priceFeedAddress, + ) + + k.Payload = err + }) + } + + decimals_, err := ethereum.CoerceBoundContractResultsToUint8(decimalsRes) + + if err != nil { + log.Fatal(func(k *log.Log) { + k.Context = Context + + k.Format( + "Failed to read decimals result for price feed address %v", + priceFeedAddress, + ) + + k.Payload = err + }) + } + + ten := big.NewRat(10, 1) + + decimals = ethereum.BigPow(ten, int(decimals_)) + decimalsCache[priceFeedAddress] = decimals + + respChan <- decimals + } +}