From 3c46825d3ff38a75c643fbf809943fa61ae55741 Mon Sep 17 00:00:00 2001 From: Sam Davies Date: Mon, 17 Mar 2025 08:53:55 -0400 Subject: [PATCH] Remove legacy Mercury plugin and associated code --- mercury/aggregate_functions.go | 168 ---- mercury/aggregate_functions_test.go | 449 ----------- mercury/cmd/chainlink-mercury/README.md | 19 - mercury/cmd/chainlink-mercury/main.go | 39 - mercury/cmd/chainlink-mercury/plugin.go | 106 --- mercury/epochround.go | 10 - mercury/fees.go | 38 - mercury/fees_test.go | 52 -- mercury/observation.go | 14 - mercury/offchain_config.go | 25 - mercury/offchain_config_test.go | 93 --- mercury/onchain_config.go | 77 -- mercury/onchain_config_test.go | 36 - mercury/package.json | 9 - mercury/v1/aggregate_functions.go | 128 ---- mercury/v1/aggregate_functions_test.go | 668 ---------------- mercury/v1/mercury.go | 454 ----------- mercury/v1/mercury_observation_v1.pb.go | 353 --------- mercury/v1/mercury_observation_v1.proto | 38 - mercury/v1/mercury_test.go | 978 ------------------------ mercury/v1/observation.go | 131 ---- mercury/v1/validation.go | 28 - mercury/v1/validation_test.go | 63 -- mercury/v2/mercury.go | 405 ---------- mercury/v2/mercury_observation_v2.pb.go | 229 ------ mercury/v2/mercury_observation_v2.proto | 19 - mercury/v2/mercury_test.go | 667 ---------------- mercury/v2/observation.go | 88 --- mercury/v3/mercury.go | 483 ------------ mercury/v3/mercury_observation_v3.pb.go | 248 ------ mercury/v3/mercury_observation_v3.proto | 21 - mercury/v3/mercury_test.go | 820 -------------------- mercury/v3/observation.go | 98 --- mercury/v4/aggregate_functions.go | 39 - mercury/v4/aggregate_functions_test.go | 252 ------ mercury/v4/mercury.go | 434 ----------- mercury/v4/mercury_observation_v4.pb.go | 249 ------ mercury/v4/mercury_observation_v4.proto | 22 - mercury/v4/mercury_test.go | 702 ----------------- mercury/v4/observation.go | 96 --- mercury/validation.go | 41 - mercury/validation_test.go | 52 -- mercury/value.go | 45 -- mercury/value_test.go | 18 - 44 files changed, 9004 deletions(-) delete mode 100644 mercury/aggregate_functions.go delete mode 100644 mercury/aggregate_functions_test.go delete mode 100644 mercury/cmd/chainlink-mercury/README.md delete mode 100644 mercury/cmd/chainlink-mercury/main.go delete mode 100644 mercury/cmd/chainlink-mercury/plugin.go delete mode 100644 mercury/epochround.go delete mode 100644 mercury/fees.go delete mode 100644 mercury/fees_test.go delete mode 100644 mercury/observation.go delete mode 100644 mercury/offchain_config.go delete mode 100644 mercury/offchain_config_test.go delete mode 100644 mercury/onchain_config.go delete mode 100644 mercury/onchain_config_test.go delete mode 100644 mercury/package.json delete mode 100644 mercury/v1/aggregate_functions.go delete mode 100644 mercury/v1/aggregate_functions_test.go delete mode 100644 mercury/v1/mercury.go delete mode 100644 mercury/v1/mercury_observation_v1.pb.go delete mode 100644 mercury/v1/mercury_observation_v1.proto delete mode 100644 mercury/v1/mercury_test.go delete mode 100644 mercury/v1/observation.go delete mode 100644 mercury/v1/validation.go delete mode 100644 mercury/v1/validation_test.go delete mode 100644 mercury/v2/mercury.go delete mode 100644 mercury/v2/mercury_observation_v2.pb.go delete mode 100644 mercury/v2/mercury_observation_v2.proto delete mode 100644 mercury/v2/mercury_test.go delete mode 100644 mercury/v2/observation.go delete mode 100644 mercury/v3/mercury.go delete mode 100644 mercury/v3/mercury_observation_v3.pb.go delete mode 100644 mercury/v3/mercury_observation_v3.proto delete mode 100644 mercury/v3/mercury_test.go delete mode 100644 mercury/v3/observation.go delete mode 100644 mercury/v4/aggregate_functions.go delete mode 100644 mercury/v4/aggregate_functions_test.go delete mode 100644 mercury/v4/mercury.go delete mode 100644 mercury/v4/mercury_observation_v4.pb.go delete mode 100644 mercury/v4/mercury_observation_v4.proto delete mode 100644 mercury/v4/mercury_test.go delete mode 100644 mercury/v4/observation.go delete mode 100644 mercury/validation.go delete mode 100644 mercury/validation_test.go delete mode 100644 mercury/value.go delete mode 100644 mercury/value_test.go diff --git a/mercury/aggregate_functions.go b/mercury/aggregate_functions.go deleted file mode 100644 index b342291..0000000 --- a/mercury/aggregate_functions.go +++ /dev/null @@ -1,168 +0,0 @@ -package mercury - -import ( - "fmt" - "math/big" - "sort" -) - -var Zero = big.NewInt(0) - -// NOTE: All aggregate functions assume at least one element in the passed slice -// The passed slice might be mutated (sorted) - -// GetConsensusTimestamp gets the median timestamp -func GetConsensusTimestamp(paos []PAO) uint32 { - sort.Slice(paos, func(i, j int) bool { - return paos[i].GetTimestamp() < paos[j].GetTimestamp() - }) - return paos[len(paos)/2].GetTimestamp() -} - -// GetConsensusBenchmarkPrice gets the median benchmark price -func GetConsensusBenchmarkPrice(paos []PAO, f int) (*big.Int, error) { - var validBenchmarkPrices []*big.Int - for _, pao := range paos { - bmPrice, valid := pao.GetBenchmarkPrice() - if valid { - validBenchmarkPrices = append(validBenchmarkPrices, bmPrice) - } - } - - if len(validBenchmarkPrices) < f+1 { - return nil, fmt.Errorf("fewer than f+1 observations have a valid price (got: %d/%d)", len(validBenchmarkPrices), len(paos)) - } - sort.Slice(validBenchmarkPrices, func(i, j int) bool { - return validBenchmarkPrices[i].Cmp(validBenchmarkPrices[j]) < 0 - }) - - return validBenchmarkPrices[len(validBenchmarkPrices)/2], nil -} - -type PAOBid interface { - GetBid() (*big.Int, bool) -} - -// GetConsensusBid gets the median bid -func GetConsensusBid(paos []PAOBid, f int) (*big.Int, error) { - var validBids []*big.Int - for _, pao := range paos { - bid, valid := pao.GetBid() - if valid { - validBids = append(validBids, bid) - } - } - if len(validBids) < f+1 { - return nil, fmt.Errorf("fewer than f+1 observations have a valid price (got: %d/%d)", len(validBids), len(paos)) - } - sort.Slice(validBids, func(i, j int) bool { - return validBids[i].Cmp(validBids[j]) < 0 - }) - - return validBids[len(validBids)/2], nil -} - -type PAOAsk interface { - GetAsk() (*big.Int, bool) -} - -// GetConsensusAsk gets the median ask -func GetConsensusAsk(paos []PAOAsk, f int) (*big.Int, error) { - var validAsks []*big.Int - for _, pao := range paos { - ask, valid := pao.GetAsk() - if valid { - validAsks = append(validAsks, ask) - } - } - if len(validAsks) < f+1 { - return nil, fmt.Errorf("fewer than f+1 observations have a valid price (got: %d/%d)", len(validAsks), len(paos)) - } - sort.Slice(validAsks, func(i, j int) bool { - return validAsks[i].Cmp(validAsks[j]) < 0 - }) - - return validAsks[len(validAsks)/2], nil -} - -type PAOMaxFinalizedTimestamp interface { - GetMaxFinalizedTimestamp() (int64, bool) -} - -// GetConsensusMaxFinalizedTimestamp returns the highest count with > f observations -func GetConsensusMaxFinalizedTimestamp(paos []PAOMaxFinalizedTimestamp, f int) (int64, error) { - var validTimestampCount int - timestampFrequencyMap := map[int64]int{} - for _, pao := range paos { - ts, valid := pao.GetMaxFinalizedTimestamp() - if valid { - validTimestampCount++ - timestampFrequencyMap[ts]++ - } - } - - // check if we have enough valid timestamps at all - if validTimestampCount < f+1 { - return 0, fmt.Errorf("fewer than f+1 observations have a valid maxFinalizedTimestamp (got: %d/%d)", validTimestampCount, len(paos)) - } - - var maxTs int64 = -2 // -1 is smallest valid amount - for ts, cnt := range timestampFrequencyMap { - // ignore any timestamps with <= f observations - if cnt > f && ts > maxTs { - maxTs = ts - } - } - - if maxTs < -1 { - return 0, fmt.Errorf("no valid maxFinalizedTimestamp with at least f+1 votes (got counts: %v)", timestampFrequencyMap) - } - - return maxTs, nil -} - -type PAOLinkFee interface { - GetLinkFee() (*big.Int, bool) -} - -// GetConsensusLinkFee gets the median link fee -func GetConsensusLinkFee(paos []PAOLinkFee, f int) (*big.Int, error) { - var validLinkFees []*big.Int - for _, pao := range paos { - fee, valid := pao.GetLinkFee() - if valid && fee.Sign() >= 0 { - validLinkFees = append(validLinkFees, fee) - } - } - if len(validLinkFees) < f+1 { - return nil, fmt.Errorf("fewer than f+1 observations have a valid linkFee (got: %d/%d)", len(validLinkFees), len(paos)) - } - sort.Slice(validLinkFees, func(i, j int) bool { - return validLinkFees[i].Cmp(validLinkFees[j]) < 0 - }) - - return validLinkFees[len(validLinkFees)/2], nil -} - -type PAONativeFee interface { - GetNativeFee() (*big.Int, bool) -} - -// GetConsensusNativeFee gets the median native fee -func GetConsensusNativeFee(paos []PAONativeFee, f int) (*big.Int, error) { - var validNativeFees []*big.Int - for _, pao := range paos { - fee, valid := pao.GetNativeFee() - if valid && fee.Sign() >= 0 { - validNativeFees = append(validNativeFees, fee) - } - } - if len(validNativeFees) < f+1 { - return nil, fmt.Errorf("fewer than f+1 observations have a valid nativeFee (got: %d/%d)", len(validNativeFees), len(paos)) - } - sort.Slice(validNativeFees, func(i, j int) bool { - return validNativeFees[i].Cmp(validNativeFees[j]) < 0 - }) - - return validNativeFees[len(validNativeFees)/2], nil -} diff --git a/mercury/aggregate_functions_test.go b/mercury/aggregate_functions_test.go deleted file mode 100644 index 150a2ef..0000000 --- a/mercury/aggregate_functions_test.go +++ /dev/null @@ -1,449 +0,0 @@ -package mercury - -import ( - "math/big" - "testing" - - "github.com/smartcontractkit/libocr/commontypes" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type testParsedAttributedObservation struct { - Timestamp uint32 - BenchmarkPrice *big.Int - BenchmarkPriceValid bool - Bid *big.Int - BidValid bool - Ask *big.Int - AskValid bool - MaxFinalizedTimestamp int64 - MaxFinalizedTimestampValid bool - LinkFee *big.Int - LinkFeeValid bool - NativeFee *big.Int - NativeFeeValid bool - MarketStatus uint32 - MarketStatusValid bool -} - -func (t testParsedAttributedObservation) GetObserver() commontypes.OracleID { return 0 } -func (t testParsedAttributedObservation) GetTimestamp() uint32 { return t.Timestamp } -func (t testParsedAttributedObservation) GetBenchmarkPrice() (*big.Int, bool) { - return t.BenchmarkPrice, t.BenchmarkPriceValid -} -func (t testParsedAttributedObservation) GetBid() (*big.Int, bool) { - return t.Bid, t.BidValid -} -func (t testParsedAttributedObservation) GetAsk() (*big.Int, bool) { - return t.Ask, t.AskValid -} -func (t testParsedAttributedObservation) GetMaxFinalizedTimestamp() (int64, bool) { - return t.MaxFinalizedTimestamp, t.MaxFinalizedTimestampValid -} -func (t testParsedAttributedObservation) GetLinkFee() (*big.Int, bool) { - return t.LinkFee, t.LinkFeeValid -} -func (t testParsedAttributedObservation) GetNativeFee() (*big.Int, bool) { - return t.NativeFee, t.NativeFeeValid -} -func (t testParsedAttributedObservation) GetMarketStatus() (uint32, bool) { - return t.MarketStatus, t.MarketStatusValid -} - -func newValidParsedAttributedObservations() []testParsedAttributedObservation { - return []testParsedAttributedObservation{ - testParsedAttributedObservation{ - Timestamp: 1689648456, - - BenchmarkPrice: big.NewInt(123), - BenchmarkPriceValid: true, - Bid: big.NewInt(120), - BidValid: true, - Ask: big.NewInt(130), - AskValid: true, - - MaxFinalizedTimestamp: 1679448456, - MaxFinalizedTimestampValid: true, - - LinkFee: big.NewInt(1), - LinkFeeValid: true, - NativeFee: big.NewInt(1), - NativeFeeValid: true, - - MarketStatus: 1, - MarketStatusValid: true, - }, - testParsedAttributedObservation{ - Timestamp: 1689648456, - - BenchmarkPrice: big.NewInt(456), - BenchmarkPriceValid: true, - Bid: big.NewInt(450), - BidValid: true, - Ask: big.NewInt(460), - AskValid: true, - - MaxFinalizedTimestamp: 1679448456, - MaxFinalizedTimestampValid: true, - - LinkFee: big.NewInt(2), - LinkFeeValid: true, - NativeFee: big.NewInt(2), - NativeFeeValid: true, - - MarketStatus: 1, - MarketStatusValid: true, - }, - testParsedAttributedObservation{ - Timestamp: 1689648789, - - BenchmarkPrice: big.NewInt(789), - BenchmarkPriceValid: true, - Bid: big.NewInt(780), - BidValid: true, - Ask: big.NewInt(800), - AskValid: true, - - MaxFinalizedTimestamp: 1679448456, - MaxFinalizedTimestampValid: true, - - LinkFee: big.NewInt(3), - LinkFeeValid: true, - NativeFee: big.NewInt(3), - NativeFeeValid: true, - - MarketStatus: 2, - MarketStatusValid: true, - }, - testParsedAttributedObservation{ - Timestamp: 1689648789, - - BenchmarkPrice: big.NewInt(456), - BenchmarkPriceValid: true, - Bid: big.NewInt(450), - BidValid: true, - Ask: big.NewInt(460), - AskValid: true, - - MaxFinalizedTimestamp: 1679513477, - MaxFinalizedTimestampValid: true, - - LinkFee: big.NewInt(4), - LinkFeeValid: true, - NativeFee: big.NewInt(4), - NativeFeeValid: true, - - MarketStatus: 3, - MarketStatusValid: true, - }, - } -} - -func NewValidParsedAttributedObservations(paos ...testParsedAttributedObservation) []testParsedAttributedObservation { - if len(paos) == 0 { - paos = newValidParsedAttributedObservations() - } - return []testParsedAttributedObservation{ - paos[0], - paos[1], - paos[2], - paos[3], - } -} - -func NewInvalidParsedAttributedObservations() []testParsedAttributedObservation { - return []testParsedAttributedObservation{ - testParsedAttributedObservation{ - Timestamp: 1, - - BenchmarkPrice: big.NewInt(123), - BenchmarkPriceValid: false, - Bid: big.NewInt(120), - BidValid: false, - Ask: big.NewInt(130), - AskValid: false, - - MaxFinalizedTimestamp: 1679648456, - MaxFinalizedTimestampValid: false, - - LinkFee: big.NewInt(1), - LinkFeeValid: false, - NativeFee: big.NewInt(1), - NativeFeeValid: false, - - MarketStatus: 1, - MarketStatusValid: false, - }, - testParsedAttributedObservation{ - Timestamp: 2, - - BenchmarkPrice: big.NewInt(456), - BenchmarkPriceValid: false, - Bid: big.NewInt(450), - BidValid: false, - Ask: big.NewInt(460), - AskValid: false, - - MaxFinalizedTimestamp: 1679648456, - MaxFinalizedTimestampValid: false, - - LinkFee: big.NewInt(2), - LinkFeeValid: false, - NativeFee: big.NewInt(2), - NativeFeeValid: false, - - MarketStatus: 1, - MarketStatusValid: false, - }, - testParsedAttributedObservation{ - Timestamp: 2, - - BenchmarkPrice: big.NewInt(789), - BenchmarkPriceValid: false, - Bid: big.NewInt(780), - BidValid: false, - Ask: big.NewInt(800), - AskValid: false, - - MaxFinalizedTimestamp: 1679648456, - MaxFinalizedTimestampValid: false, - - LinkFee: big.NewInt(3), - LinkFeeValid: false, - NativeFee: big.NewInt(3), - NativeFeeValid: false, - - MarketStatus: 2, - MarketStatusValid: false, - }, - testParsedAttributedObservation{ - Timestamp: 3, - - BenchmarkPrice: big.NewInt(456), - BenchmarkPriceValid: true, - Bid: big.NewInt(450), - BidValid: true, - Ask: big.NewInt(460), - AskValid: true, - - MaxFinalizedTimestamp: 1679513477, - MaxFinalizedTimestampValid: true, - - LinkFee: big.NewInt(4), - LinkFeeValid: true, - NativeFee: big.NewInt(4), - NativeFeeValid: true, - - MarketStatus: 3, - MarketStatusValid: false, - }, - } -} - -func Test_AggregateFunctions(t *testing.T) { - f := 1 - validPaos := NewValidParsedAttributedObservations() - invalidPaos := NewInvalidParsedAttributedObservations() - - t.Run("GetConsensusTimestamp", func(t *testing.T) { - validMPaos := convert(validPaos) - ts := GetConsensusTimestamp(validMPaos) - - assert.Equal(t, 1689648789, int(ts)) - }) - - t.Run("GetConsensusBenchmarkPrice", func(t *testing.T) { - t.Run("gets consensus price when prices are valid", func(t *testing.T) { - validMPaos := convert(validPaos) - bp, err := GetConsensusBenchmarkPrice(validMPaos, f) - require.NoError(t, err) - assert.Equal(t, "456", bp.String()) - }) - - t.Run("fails when fewer than f+1 prices are valid", func(t *testing.T) { - invalidMPaos := convert(invalidPaos) - _, err := GetConsensusBenchmarkPrice(invalidMPaos, f) - assert.EqualError(t, err, "fewer than f+1 observations have a valid price (got: 1/4)") - }) - }) - - t.Run("GetConsensusBid", func(t *testing.T) { - t.Run("gets consensus bid when prices are valid", func(t *testing.T) { - validMPaos := convertBid(validPaos) - bid, err := GetConsensusBid(validMPaos, f) - require.NoError(t, err) - assert.Equal(t, "450", bid.String()) - }) - - t.Run("fails when fewer than f+1 prices are valid", func(t *testing.T) { - invalidMPaos := convertBid(invalidPaos) - _, err := GetConsensusBid(invalidMPaos, f) - assert.EqualError(t, err, "fewer than f+1 observations have a valid price (got: 1/4)") - }) - }) - - t.Run("GetConsensusAsk", func(t *testing.T) { - t.Run("gets consensus ask when prices are valid", func(t *testing.T) { - validMPaos := convertAsk(validPaos) - bid, err := GetConsensusAsk(validMPaos, f) - require.NoError(t, err) - assert.Equal(t, "460", bid.String()) - }) - - t.Run("fails when fewer than f+1 prices are valid", func(t *testing.T) { - invalidMPaos := convertAsk(invalidPaos) - _, err := GetConsensusAsk(invalidMPaos, f) - assert.EqualError(t, err, "fewer than f+1 observations have a valid price (got: 1/4)") - }) - }) - - t.Run("GetConsensusMaxFinalizedTimestamp", func(t *testing.T) { - t.Run("gets consensus on maxFinalizedTimestamp when valid", func(t *testing.T) { - validMPaos := convertMaxFinalizedTimestamp(validPaos) - ts, err := GetConsensusMaxFinalizedTimestamp(validMPaos, f) - require.NoError(t, err) - assert.Equal(t, int64(1679448456), ts) - }) - - t.Run("uses highest value as tiebreaker", func(t *testing.T) { - paos := newValidParsedAttributedObservations() - (paos[0]).MaxFinalizedTimestamp = 1679513477 - validMPaos := convertMaxFinalizedTimestamp(NewValidParsedAttributedObservations(paos...)) - ts, err := GetConsensusMaxFinalizedTimestamp(validMPaos, f) - require.NoError(t, err) - assert.Equal(t, int64(1679513477), ts) - }) - - t.Run("fails when fewer than f+1 maxFinalizedTimestamps are valid", func(t *testing.T) { - invalidMPaos := convertMaxFinalizedTimestamp(invalidPaos) - _, err := GetConsensusMaxFinalizedTimestamp(invalidMPaos, f) - assert.EqualError(t, err, "fewer than f+1 observations have a valid maxFinalizedTimestamp (got: 1/4)") - }) - - t.Run("fails when cannot come to consensus f+1 maxFinalizedTimestamps", func(t *testing.T) { - paos := []PAOMaxFinalizedTimestamp{ - testParsedAttributedObservation{ - MaxFinalizedTimestamp: 1679648456, - MaxFinalizedTimestampValid: true, - }, - testParsedAttributedObservation{ - MaxFinalizedTimestamp: 1679648457, - MaxFinalizedTimestampValid: true, - }, - testParsedAttributedObservation{ - MaxFinalizedTimestamp: 1679648458, - MaxFinalizedTimestampValid: true, - }, - testParsedAttributedObservation{ - MaxFinalizedTimestamp: 1679513477, - MaxFinalizedTimestampValid: true, - }, - } - _, err := GetConsensusMaxFinalizedTimestamp(paos, f) - assert.EqualError(t, err, "no valid maxFinalizedTimestamp with at least f+1 votes (got counts: map[1679513477:1 1679648456:1 1679648457:1 1679648458:1])") - }) - }) - - t.Run("GetConsensusLinkFee", func(t *testing.T) { - t.Run("gets consensus on linkFee when valid", func(t *testing.T) { - validMPaos := convertLinkFee(validPaos) - linkFee, err := GetConsensusLinkFee(validMPaos, f) - require.NoError(t, err) - assert.Equal(t, big.NewInt(3), linkFee) - }) - t.Run("treats zero values as valid", func(t *testing.T) { - paos := NewValidParsedAttributedObservations() - for i := range paos { - paos[i].LinkFee = big.NewInt(0) - } - linkFee, err := GetConsensusLinkFee(convertLinkFee(paos), f) - require.NoError(t, err) - assert.Equal(t, big.NewInt(0), linkFee) - }) - t.Run("treats negative values as invalid", func(t *testing.T) { - paos := NewValidParsedAttributedObservations() - for i := range paos { - paos[i].LinkFee = big.NewInt(int64(0 - i)) - } - _, err := GetConsensusLinkFee(convertLinkFee(paos), f) - assert.EqualError(t, err, "fewer than f+1 observations have a valid linkFee (got: 1/4)") - }) - - t.Run("fails when fewer than f+1 linkFees are valid", func(t *testing.T) { - invalidMPaos := convertLinkFee(invalidPaos) - _, err := GetConsensusLinkFee(invalidMPaos, f) - assert.EqualError(t, err, "fewer than f+1 observations have a valid linkFee (got: 1/4)") - }) - }) - - t.Run("GetConsensusNativeFee", func(t *testing.T) { - t.Run("gets consensus on nativeFee when valid", func(t *testing.T) { - validMPaos := convertNativeFee(validPaos) - nativeFee, err := GetConsensusNativeFee(validMPaos, f) - require.NoError(t, err) - assert.Equal(t, big.NewInt(3), nativeFee) - }) - t.Run("treats zero values as valid", func(t *testing.T) { - paos := NewValidParsedAttributedObservations() - for i := range paos { - paos[i].NativeFee = big.NewInt(0) - } - nativeFee, err := GetConsensusNativeFee(convertNativeFee(paos), f) - require.NoError(t, err) - assert.Equal(t, big.NewInt(0), nativeFee) - }) - t.Run("treats negative values as invalid", func(t *testing.T) { - paos := NewValidParsedAttributedObservations() - for i := range paos { - paos[i].NativeFee = big.NewInt(int64(0 - i)) - } - _, err := GetConsensusNativeFee(convertNativeFee(paos), f) - assert.EqualError(t, err, "fewer than f+1 observations have a valid nativeFee (got: 1/4)") - }) - t.Run("fails when fewer than f+1 nativeFees are valid", func(t *testing.T) { - invalidMPaos := convertNativeFee(invalidPaos) - _, err := GetConsensusNativeFee(invalidMPaos, f) - assert.EqualError(t, err, "fewer than f+1 observations have a valid nativeFee (got: 1/4)") - }) - }) -} - -// convert funcs are necessary because go is not smart enough to cast -// []interface1 to []interface2 even if interface1 is a superset of interface2 -func convert(pao []testParsedAttributedObservation) (ret []PAO) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertMaxFinalizedTimestamp(pao []testParsedAttributedObservation) (ret []PAOMaxFinalizedTimestamp) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertAsk(pao []testParsedAttributedObservation) (ret []PAOAsk) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertBid(pao []testParsedAttributedObservation) (ret []PAOBid) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertLinkFee(pao []testParsedAttributedObservation) (ret []PAOLinkFee) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertNativeFee(pao []testParsedAttributedObservation) (ret []PAONativeFee) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} diff --git a/mercury/cmd/chainlink-mercury/README.md b/mercury/cmd/chainlink-mercury/README.md deleted file mode 100644 index f3601cd..0000000 --- a/mercury/cmd/chainlink-mercury/README.md +++ /dev/null @@ -1,19 +0,0 @@ -This directory houses the Mercury LOOPP - -# Running Integration Tests Locally - -Running the tests is as simple as -- building this binary -- setting the CL_MERCURY_CMD env var to the *fully resolved* binary path -- running the test(s) - - -The interesting tests are `TestIntegration_MercuryV*` in ` github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/mercury` - -In detail: -``` -sh - -go install # builds `mercury` binary in this dir -CL_MERCURY_CMD=chainlink-mercury go test -v -timeout 120s -run ^TestIntegration_MercuryV github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/mercury 2>&1 | tee /tmp/mercury_loop.log -``` \ No newline at end of file diff --git a/mercury/cmd/chainlink-mercury/main.go b/mercury/cmd/chainlink-mercury/main.go deleted file mode 100644 index d80aa8e..0000000 --- a/mercury/cmd/chainlink-mercury/main.go +++ /dev/null @@ -1,39 +0,0 @@ -package main - -import ( - "github.com/hashicorp/go-plugin" - - "github.com/smartcontractkit/chainlink-common/pkg/loop" -) - -const ( - loggerName = "PluginMercury" -) - -func main() { - s := loop.MustNewStartedServer(loggerName) - defer s.Stop() - - p := NewPlugin(s.Logger) - defer s.Logger.ErrorIfFn(p.Close, "Failed to close") - - s.MustRegister(p) - - stop := make(chan struct{}) - defer close(stop) - - plugin.Serve(&plugin.ServeConfig{ - HandshakeConfig: loop.PluginMercuryHandshakeConfig(), - Plugins: map[string]plugin.Plugin{ - loop.PluginMercuryName: &loop.GRPCPluginMercury{ - PluginServer: p, - BrokerConfig: loop.BrokerConfig{ - StopCh: stop, - Logger: s.Logger, - GRPCOpts: s.GRPCOpts, - }, - }, - }, - GRPCServer: s.GRPCOpts.NewServer, - }) -} diff --git a/mercury/cmd/chainlink-mercury/plugin.go b/mercury/cmd/chainlink-mercury/plugin.go deleted file mode 100644 index d03d3df..0000000 --- a/mercury/cmd/chainlink-mercury/plugin.go +++ /dev/null @@ -1,106 +0,0 @@ -package main - -import ( - "context" - - "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/loop" - "github.com/smartcontractkit/chainlink-common/pkg/services" - "github.com/smartcontractkit/chainlink-common/pkg/types" - v1 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v1" - v2 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v2" - v3 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v3" - v4 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v4" - - ds_v1 "github.com/smartcontractkit/chainlink-data-streams/mercury/v1" - ds_v2 "github.com/smartcontractkit/chainlink-data-streams/mercury/v2" - ds_v3 "github.com/smartcontractkit/chainlink-data-streams/mercury/v3" - ds_v4 "github.com/smartcontractkit/chainlink-data-streams/mercury/v4" -) - -type Plugin struct { - loop.Plugin - stop services.StopChan -} - -func NewPlugin(lggr logger.Logger) *Plugin { - return &Plugin{Plugin: loop.Plugin{Logger: lggr}, stop: make(services.StopChan)} -} - -func (p *Plugin) NewMercuryV1Factory(ctx context.Context, provider types.MercuryProvider, dataSource v1.DataSource) (types.MercuryPluginFactory, error) { - var ctxVals loop.ContextValues - ctxVals.SetValues(ctx) - lggr := logger.With(p.Logger, ctxVals.Args()...) - - factory := ds_v1.NewFactory(dataSource, lggr, provider.OnchainConfigCodec(), provider.ReportCodecV1()) - - s := &mercuryPluginFactoryService{lggr: logger.Named(lggr, "MercuryV1PluginFactory"), MercuryPluginFactory: factory} - - p.SubService(s) - - return s, nil -} - -func (p *Plugin) NewMercuryV2Factory(ctx context.Context, provider types.MercuryProvider, dataSource v2.DataSource) (types.MercuryPluginFactory, error) { - var ctxVals loop.ContextValues - ctxVals.SetValues(ctx) - lggr := logger.With(p.Logger, ctxVals.Args()...) - - factory := ds_v2.NewFactory(dataSource, lggr, provider.OnchainConfigCodec(), provider.ReportCodecV2()) - - s := &mercuryPluginFactoryService{lggr: logger.Named(lggr, "MercuryV2PluginFactory"), MercuryPluginFactory: factory} - - p.SubService(s) - - return s, nil -} - -func (p *Plugin) NewMercuryV3Factory(ctx context.Context, provider types.MercuryProvider, dataSource v3.DataSource) (types.MercuryPluginFactory, error) { - var ctxVals loop.ContextValues - ctxVals.SetValues(ctx) - lggr := logger.With(p.Logger, ctxVals.Args()...) - - factory := ds_v3.NewFactory(dataSource, lggr, provider.OnchainConfigCodec(), provider.ReportCodecV3()) - - s := &mercuryPluginFactoryService{lggr: logger.Named(lggr, "MercuryV3PluginFactory"), MercuryPluginFactory: factory} - - p.SubService(s) - - return s, nil -} - -func (p *Plugin) NewMercuryV4Factory(ctx context.Context, provider types.MercuryProvider, dataSource v4.DataSource) (types.MercuryPluginFactory, error) { - var ctxVals loop.ContextValues - ctxVals.SetValues(ctx) - lggr := logger.With(p.Logger, ctxVals.Args()...) - - factory := ds_v4.NewFactory(dataSource, lggr, provider.OnchainConfigCodec(), provider.ReportCodecV4()) - - s := &mercuryPluginFactoryService{lggr: logger.Named(lggr, "MercuryV4PluginFactory"), MercuryPluginFactory: factory} - - p.SubService(s) - - return s, nil -} - -type mercuryPluginFactoryService struct { - services.StateMachine - lggr logger.Logger - ocr3types.MercuryPluginFactory -} - -func (r *mercuryPluginFactoryService) Name() string { return r.lggr.Name() } - -func (r *mercuryPluginFactoryService) Start(ctx context.Context) error { - return r.StartOnce("ReportingPluginFactory", func() error { return nil }) -} - -func (r *mercuryPluginFactoryService) Close() error { - return r.StopOnce("ReportingPluginFactory", func() error { return nil }) -} - -func (r *mercuryPluginFactoryService) HealthReport() map[string]error { - return map[string]error{r.Name(): r.Healthy()} -} diff --git a/mercury/epochround.go b/mercury/epochround.go deleted file mode 100644 index 65a0396..0000000 --- a/mercury/epochround.go +++ /dev/null @@ -1,10 +0,0 @@ -package mercury - -type EpochRound struct { - Epoch uint32 - Round uint8 -} - -func (x EpochRound) Less(y EpochRound) bool { - return x.Epoch < y.Epoch || (x.Epoch == y.Epoch && x.Round < y.Round) -} diff --git a/mercury/fees.go b/mercury/fees.go deleted file mode 100644 index 5d54d44..0000000 --- a/mercury/fees.go +++ /dev/null @@ -1,38 +0,0 @@ -package mercury - -import ( - "math/big" - - "github.com/shopspring/decimal" -) - -// PriceScalingFactor indicates the multiplier applied to token prices that we expect from data source -// e.g. for a 1e8 multiplier, a LINK/USD value of 7.42 will be derived from a data source value of 742000000 -var PriceScalingFactor = decimal.NewFromInt(1e18) //nolint:revive - -// FeeScalingFactor indicates the multiplier applied to fees. -// e.g. for a 1e18 multiplier, a LINK fee of 7.42 will be represented as 7.42e18 -// This is what will be baked into the report for use on-chain. -var FeeScalingFactor = decimal.NewFromInt(1e18) - -// CalculateFee outputs a fee in wei according to the formula: baseUSDFee * scaleFactor / tokenPriceInUSD -func CalculateFee(tokenPriceInUSD *big.Int, baseUSDFee decimal.Decimal) *big.Int { - if tokenPriceInUSD.Cmp(big.NewInt(0)) == 0 || baseUSDFee.IsZero() { - // zero fee if token price or base fee is zero - return big.NewInt(0) - } - - // scale baseFee in USD - baseFeeScaled := baseUSDFee.Mul(PriceScalingFactor) - - tokenPrice := decimal.NewFromBigInt(tokenPriceInUSD, 0) - - // fee denominated in token - fee := baseFeeScaled.Div(tokenPrice) - - // scale fee to the expected format - fee = fee.Mul(FeeScalingFactor) - - // convert to big.Int - return fee.BigInt() -} diff --git a/mercury/fees_test.go b/mercury/fees_test.go deleted file mode 100644 index b66e1f9..0000000 --- a/mercury/fees_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package mercury - -import ( - "math/big" - "testing" - - "github.com/shopspring/decimal" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// simulate price from DP -func scalePrice(usdPrice float64) *big.Int { - scaledPrice := new(big.Float).Mul(big.NewFloat(usdPrice), big.NewFloat(1e18)) - scaledPriceInt, _ := scaledPrice.Int(nil) - return scaledPriceInt -} - -func Test_Fees(t *testing.T) { - BaseUSDFee, err := decimal.NewFromString("0.70") - require.NoError(t, err) - t.Run("with token price > 1", func(t *testing.T) { - tokenPriceInUSD := scalePrice(1630) - fee := CalculateFee(tokenPriceInUSD, BaseUSDFee) - expectedFee := big.NewInt(429447852760700) // 0.0004294478527607 18 decimals - if fee.Cmp(expectedFee) != 0 { - t.Errorf("Expected fee to be %v, got %v", expectedFee, fee) - } - }) - - t.Run("with token price < 1", func(t *testing.T) { - tokenPriceInUSD := scalePrice(0.4) - fee := CalculateFee(tokenPriceInUSD, BaseUSDFee) - expectedFee := big.NewInt(1750000000000000000) // 1.75 18 decimals - if fee.Cmp(expectedFee) != 0 { - t.Errorf("Expected fee to be %v, got %v", expectedFee, fee) - } - }) - - t.Run("with token price == 0", func(t *testing.T) { - tokenPriceInUSD := scalePrice(0) - fee := CalculateFee(tokenPriceInUSD, BaseUSDFee) - assert.Equal(t, big.NewInt(0), fee) - }) - - t.Run("with base fee == 0", func(t *testing.T) { - tokenPriceInUSD := scalePrice(123) - BaseUSDFee = decimal.NewFromInt32(0) - fee := CalculateFee(tokenPriceInUSD, BaseUSDFee) - assert.Equal(t, big.NewInt(0), fee) - }) -} diff --git a/mercury/observation.go b/mercury/observation.go deleted file mode 100644 index 5617f48..0000000 --- a/mercury/observation.go +++ /dev/null @@ -1,14 +0,0 @@ -package mercury - -import ( - "math/big" - - "github.com/smartcontractkit/libocr/commontypes" -) - -type PAO interface { - // These fields are common to all observations - GetTimestamp() uint32 - GetObserver() commontypes.OracleID - GetBenchmarkPrice() (*big.Int, bool) -} diff --git a/mercury/offchain_config.go b/mercury/offchain_config.go deleted file mode 100644 index 9fd0554..0000000 --- a/mercury/offchain_config.go +++ /dev/null @@ -1,25 +0,0 @@ -package mercury - -import ( - "encoding/json" - "fmt" - - "github.com/shopspring/decimal" -) - -type OffchainConfig struct { - ExpirationWindow uint32 `json:"expirationWindow"` // Integer number of seconds - BaseUSDFee decimal.Decimal `json:"baseUSDFee"` // Base USD fee -} - -func DecodeOffchainConfig(b []byte) (o OffchainConfig, err error) { - err = json.Unmarshal(b, &o) - if err != nil { - return o, fmt.Errorf("failed to decode offchain config: must be valid JSON (got: 0x%x); %w", b, err) - } - return -} - -func (c OffchainConfig) Encode() ([]byte, error) { - return json.Marshal(c) -} diff --git a/mercury/offchain_config_test.go b/mercury/offchain_config_test.go deleted file mode 100644 index db06f89..0000000 --- a/mercury/offchain_config_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package mercury - -import ( - "testing" - - "github.com/shopspring/decimal" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Test_OffchainConfig(t *testing.T) { - t.Run("decoding", func(t *testing.T) { - t.Run("with number type for USD fee", func(t *testing.T) { - json := ` -{ - "expirationWindow": 42, - "baseUSDFee": 123.456 -} -` - c, err := DecodeOffchainConfig([]byte(json)) - require.NoError(t, err) - - assert.Equal(t, decimal.NewFromFloat32(123.456), c.BaseUSDFee) - - json = ` -{ - "expirationWindow": 42, - "baseUSDFee": 123 -} -` - c, err = DecodeOffchainConfig([]byte(json)) - require.NoError(t, err) - - assert.Equal(t, decimal.NewFromInt32(123), c.BaseUSDFee) - - json = ` -{ - "expirationWindow": 42, - "baseUSDFee": 0.12 -} -` - c, err = DecodeOffchainConfig([]byte(json)) - require.NoError(t, err) - - assert.Equal(t, decimal.NewFromFloat32(0.12), c.BaseUSDFee) - }) - t.Run("with string type for USD fee", func(t *testing.T) { - json := ` -{ - "expirationWindow": 42, - "baseUSDFee": "123.456" -} -` - c, err := DecodeOffchainConfig([]byte(json)) - require.NoError(t, err) - - assert.Equal(t, decimal.NewFromFloat32(123.456), c.BaseUSDFee) - - json = ` -{ - "expirationWindow": 42, - "baseUSDFee": "123" -} -` - c, err = DecodeOffchainConfig([]byte(json)) - require.NoError(t, err) - - assert.Equal(t, decimal.NewFromInt32(123), c.BaseUSDFee) - - json = ` -{ - "expirationWindow": 42, - "baseUSDFee": "0.12" -} -` - c, err = DecodeOffchainConfig([]byte(json)) - require.NoError(t, err) - - assert.Equal(t, decimal.NewFromFloat32(0.12), c.BaseUSDFee) - }) - }) - t.Run("serialize/deserialize", func(t *testing.T) { - c := OffchainConfig{32, decimal.NewFromFloat32(1.23)} - - serialized, err := c.Encode() - require.NoError(t, err) - - deserialized, err := DecodeOffchainConfig(serialized) - require.NoError(t, err) - - assert.Equal(t, c, deserialized) - }) -} diff --git a/mercury/onchain_config.go b/mercury/onchain_config.go deleted file mode 100644 index d52ff5a..0000000 --- a/mercury/onchain_config.go +++ /dev/null @@ -1,77 +0,0 @@ -package mercury - -import ( - "context" - "fmt" - "math/big" - - "github.com/smartcontractkit/libocr/bigbigendian" - - "github.com/smartcontractkit/chainlink-common/pkg/types/mercury" -) - -const onchainConfigVersion = 1 - -var onchainConfigVersionBig = big.NewInt(onchainConfigVersion) - -const onchainConfigEncodedLength = 96 // 3x 32bit evm words, version + min + max - -var _ mercury.OnchainConfigCodec = StandardOnchainConfigCodec{} - -// StandardOnchainConfigCodec provides a mercury-specific implementation of -// OnchainConfigCodec. -// -// An encoded onchain config is expected to be in the format -// -// where version is a uint8 and min and max are in the format -// returned by EncodeValueInt192. -type StandardOnchainConfigCodec struct{} - -func (StandardOnchainConfigCodec) Decode(ctx context.Context, b []byte) (mercury.OnchainConfig, error) { - if len(b) != onchainConfigEncodedLength { - return mercury.OnchainConfig{}, fmt.Errorf("unexpected length of OnchainConfig, expected %v, got %v", onchainConfigEncodedLength, len(b)) - } - - v, err := bigbigendian.DeserializeSigned(32, b[:32]) - if err != nil { - return mercury.OnchainConfig{}, err - } - if v.Cmp(onchainConfigVersionBig) != 0 { - return mercury.OnchainConfig{}, fmt.Errorf("unexpected version of OnchainConfig, expected %v, got %v", onchainConfigVersion, v) - } - - min, err := bigbigendian.DeserializeSigned(32, b[32:64]) - if err != nil { - return mercury.OnchainConfig{}, err - } - max, err := bigbigendian.DeserializeSigned(32, b[64:96]) - if err != nil { - return mercury.OnchainConfig{}, err - } - - if !(min.Cmp(max) <= 0) { - return mercury.OnchainConfig{}, fmt.Errorf("OnchainConfig min (%v) should not be greater than max(%v)", min, max) - } - - return mercury.OnchainConfig{Min: min, Max: max}, nil -} - -func (StandardOnchainConfigCodec) Encode(ctx context.Context, c mercury.OnchainConfig) ([]byte, error) { - verBytes, err := bigbigendian.SerializeSigned(32, onchainConfigVersionBig) - if err != nil { - return nil, err - } - minBytes, err := bigbigendian.SerializeSigned(32, c.Min) - if err != nil { - return nil, err - } - maxBytes, err := bigbigendian.SerializeSigned(32, c.Max) - if err != nil { - return nil, err - } - result := make([]byte, 0, onchainConfigEncodedLength) - result = append(result, verBytes...) - result = append(result, minBytes...) - result = append(result, maxBytes...) - return result, nil -} diff --git a/mercury/onchain_config_test.go b/mercury/onchain_config_test.go deleted file mode 100644 index 6f11cc0..0000000 --- a/mercury/onchain_config_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package mercury - -import ( - "bytes" - "math/big" - "testing" - - "github.com/smartcontractkit/chainlink-common/pkg/types/mercury" - "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" -) - -func FuzzDecodeOnchainConfig(f *testing.F) { - valid, err := StandardOnchainConfigCodec{}.Encode(tests.Context(f), mercury.OnchainConfig{Min: big.NewInt(1), Max: big.NewInt(1000)}) - if err != nil { - f.Fatalf("failed to construct valid OnchainConfig: %s", err) - } - - f.Add([]byte{}) - f.Add(valid) - f.Fuzz(func(t *testing.T, encoded []byte) { - ctx := tests.Context(t) - decoded, err := StandardOnchainConfigCodec{}.Decode(ctx, encoded) - if err != nil { - return - } - - encoded2, err := StandardOnchainConfigCodec{}.Encode(ctx, decoded) - if err != nil { - t.Fatalf("failed to re-encode decoded input: %s", err) - } - - if !bytes.Equal(encoded, encoded2) { - t.Fatalf("re-encoding of decoded input %x did not match original input %x", encoded2, encoded) - } - }) -} diff --git a/mercury/package.json b/mercury/package.json deleted file mode 100644 index 90add75..0000000 --- a/mercury/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "mercury", - "version": "0.1.0", - "description": "", - "private": true, - "main": "index.js", - "author": "", - "license": "UNLICENSED" -} diff --git a/mercury/v1/aggregate_functions.go b/mercury/v1/aggregate_functions.go deleted file mode 100644 index c03da99..0000000 --- a/mercury/v1/aggregate_functions.go +++ /dev/null @@ -1,128 +0,0 @@ -package v1 - -import ( - "fmt" - "sort" - - v1 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v1" -) - -// GetConsensusLatestBlock gets the latest block that has at least f+1 votes -// Assumes that LatestBlocks are ordered by block number desc -func GetConsensusLatestBlock(paos []PAO, f int) (hash []byte, num int64, ts uint64, err error) { - // observed blocks grouped by their block number - groupingsM := make(map[int64][]v1.Block) - for _, pao := range paos { - if blocks := pao.GetLatestBlocks(); len(blocks) > 0 { - for _, block := range blocks { - groupingsM[block.Num] = append(groupingsM[block.Num], block) - } - } else { // DEPRECATED - // TODO: Remove this handling after deployment (https://smartcontract-it.atlassian.net/browse/MERC-2272) - blockHash, valid := pao.GetCurrentBlockHash() - if !valid { - continue - } - blockNum, valid := pao.GetCurrentBlockNum() - if !valid { - continue - } - blockTs, valid := pao.GetCurrentBlockTimestamp() - if !valid { - continue - } - groupingsM[blockNum] = append(groupingsM[blockNum], v1.NewBlock(blockNum, blockHash, blockTs)) - } - } - - // sort by latest block number desc - groupings := make([][]v1.Block, len(groupingsM)) - { - i := 0 - for _, blocks := range groupingsM { - groupings[i] = blocks - i++ - } - } - - // each grouping will have all blocks with the same block number, sorted desc - sort.Slice(groupings, func(i, j int) bool { - return groupings[i][0].Num > groupings[j][0].Num - }) - - // take highest block number with at least f+1 in agreement on everything - for _, blocks := range groupings { - m := map[v1.Block]int{} - maxCnt := 0 - // count unique blocks - for _, b := range blocks { - m[b]++ - if cnt := m[b]; cnt > maxCnt { - maxCnt = cnt - } - } - if maxCnt >= f+1 { - // at least one set of blocks has f+1 in agreement - - // take the blocks with highest count - var usableBlocks []v1.Block - for b, cnt := range m { - if cnt == maxCnt { - usableBlocks = append(usableBlocks, b) - } - } - sort.Slice(usableBlocks, func(i, j int) bool { - return usableBlocks[j].Less(usableBlocks[i]) - }) - - return usableBlocks[0].HashBytes(), usableBlocks[0].Num, usableBlocks[0].Ts, nil - } - // this grouping does not have any identical blocks with at least f+1 in agreement, try next block number down - } - - return nil, 0, 0, fmt.Errorf("cannot come to consensus on latest block number, got observations: %#v", paos) -} - -// GetConsensusMaxFinalizedBlockNum gets the most common (mode) -// ConsensusMaxFinalizedBlockNum In the event of a tie, the lower number is -// chosen -func GetConsensusMaxFinalizedBlockNum(paos []PAO, f int) (int64, error) { - var validPaos []PAO - for _, pao := range paos { - _, valid := pao.GetMaxFinalizedBlockNumber() - if valid { - validPaos = append(validPaos, pao) - } - } - if len(validPaos) < f+1 { - return 0, fmt.Errorf("fewer than f+1 observations have a valid maxFinalizedBlockNumber (got: %d/%d, f=%d)", len(validPaos), len(paos), f) - } - // pick the most common block number with at least f+1 votes - m := map[int64]int{} - maxCnt := 0 - for _, pao := range validPaos { - n, _ := pao.GetMaxFinalizedBlockNumber() - m[n]++ - if cnt := m[n]; cnt > maxCnt { - maxCnt = cnt - } - } - - var nums []int64 - for num, cnt := range m { - if cnt == maxCnt { - nums = append(nums, num) - } - } - - if maxCnt < f+1 { - return 0, fmt.Errorf("no valid maxFinalizedBlockNumber with at least f+1 votes (got counts: %v, f=%d)", m, f) - } - // guaranteed to be at least one num after this - - // determistic tie-break for number - sort.Slice(nums, func(i, j int) bool { - return nums[i] < nums[j] - }) - return nums[0], nil -} diff --git a/mercury/v1/aggregate_functions_test.go b/mercury/v1/aggregate_functions_test.go deleted file mode 100644 index 92b5ebc..0000000 --- a/mercury/v1/aggregate_functions_test.go +++ /dev/null @@ -1,668 +0,0 @@ -package v1 - -import ( - "encoding/hex" - "math/big" - "testing" - - "github.com/smartcontractkit/libocr/commontypes" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - v1 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v1" -) - -func mustDecodeHex(s string) []byte { - b, err := hex.DecodeString(s) - if err != nil { - panic(err) - } - return b -} - -var ChainViewBase = []v1.Block{ - v1.NewBlock(16634362, mustDecodeHex("6f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908172), - v1.NewBlock(16634361, mustDecodeHex("5f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908170), -} - -// ChainView1 vs ChainView2 simulates a re-org based off of a common block 16634362 -func MakeChainView1() []v1.Block { - return append([]v1.Block{ - v1.NewBlock(16634365, mustDecodeHex("9f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908181), - v1.NewBlock(16634364, mustDecodeHex("8f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908180), - v1.NewBlock(16634363, mustDecodeHex("7f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908176), - }, ChainViewBase...) -} - -var ChainView2 = append([]v1.Block{ - v1.NewBlock(16634365, mustDecodeHex("8e30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908180), - v1.NewBlock(16634364, mustDecodeHex("7e30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908177), - v1.NewBlock(16634363, mustDecodeHex("6e30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908173), -}, ChainViewBase...) - -var ChainView3 = append([]v1.Block{ - v1.NewBlock(16634366, mustDecodeHex("9e30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908181), - v1.NewBlock(16634365, mustDecodeHex("8e30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908180), - v1.NewBlock(16634364, mustDecodeHex("7e30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908177), - v1.NewBlock(16634363, mustDecodeHex("6e30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), 1682908173), -}, ChainViewBase...) - -func NewRawPAOS() []parsedAttributedObservation { - return []parsedAttributedObservation{ - parsedAttributedObservation{ - Timestamp: 1676484822, - Observer: commontypes.OracleID(1), - - BenchmarkPrice: big.NewInt(345), - Bid: big.NewInt(343), - Ask: big.NewInt(347), - PricesValid: true, - - LatestBlocks: []v1.Block{}, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: true, - }, - parsedAttributedObservation{ - Timestamp: 1676484826, - Observer: commontypes.OracleID(2), - - BenchmarkPrice: big.NewInt(335), - Bid: big.NewInt(332), - Ask: big.NewInt(336), - PricesValid: true, - - CurrentBlockNum: 16634364, - CurrentBlockHash: mustDecodeHex("8f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), - CurrentBlockTimestamp: 1682908180, - CurrentBlockValid: true, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: true, - }, - parsedAttributedObservation{ - Timestamp: 1676484828, - Observer: commontypes.OracleID(3), - - BenchmarkPrice: big.NewInt(347), - Bid: big.NewInt(345), - Ask: big.NewInt(350), - PricesValid: true, - - CurrentBlockNum: 16634365, - CurrentBlockHash: mustDecodeHex("40044147503a81e9f2a225f4717bf5faf5dc574f69943bdcd305d5ed97504a7e"), - CurrentBlockTimestamp: 1682591344, - CurrentBlockValid: true, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: true, - }, - parsedAttributedObservation{ - Timestamp: 1676484830, - Observer: commontypes.OracleID(4), - - BenchmarkPrice: big.NewInt(346), - Bid: big.NewInt(347), - Ask: big.NewInt(350), - PricesValid: true, - - CurrentBlockNum: 16634365, - CurrentBlockHash: mustDecodeHex("40044147503a81e9f2a225f4717bf5faf5dc574f69943bdcd305d5ed97504a7e"), - CurrentBlockTimestamp: 1682591344, - CurrentBlockValid: true, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: true, - }, - } -} - -func NewValidLegacyParsedAttributedObservations() []PAO { - return []PAO{ - parsedAttributedObservation{ - Timestamp: 1676484822, - Observer: commontypes.OracleID(1), - - BenchmarkPrice: big.NewInt(345), - Bid: big.NewInt(343), - Ask: big.NewInt(347), - PricesValid: true, - - CurrentBlockNum: 16634364, - CurrentBlockHash: mustDecodeHex("8f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), - CurrentBlockTimestamp: 1682908180, - CurrentBlockValid: true, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: true, - }, - parsedAttributedObservation{ - Timestamp: 1676484826, - Observer: commontypes.OracleID(2), - - BenchmarkPrice: big.NewInt(335), - Bid: big.NewInt(332), - Ask: big.NewInt(336), - PricesValid: true, - - CurrentBlockNum: 16634364, - CurrentBlockHash: mustDecodeHex("8f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), - CurrentBlockTimestamp: 1682908180, - CurrentBlockValid: true, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: true, - }, - parsedAttributedObservation{ - Timestamp: 1676484828, - Observer: commontypes.OracleID(3), - - BenchmarkPrice: big.NewInt(347), - Bid: big.NewInt(345), - Ask: big.NewInt(350), - PricesValid: true, - - CurrentBlockNum: 16634365, - CurrentBlockHash: mustDecodeHex("40044147503a81e9f2a225f4717bf5faf5dc574f69943bdcd305d5ed97504a7e"), - CurrentBlockTimestamp: 1682591344, - CurrentBlockValid: true, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: true, - }, - parsedAttributedObservation{ - Timestamp: 1676484830, - Observer: commontypes.OracleID(4), - - BenchmarkPrice: big.NewInt(346), - Bid: big.NewInt(347), - Ask: big.NewInt(350), - PricesValid: true, - - CurrentBlockNum: 16634365, - CurrentBlockHash: mustDecodeHex("40044147503a81e9f2a225f4717bf5faf5dc574f69943bdcd305d5ed97504a7e"), - CurrentBlockTimestamp: 1682591344, - CurrentBlockValid: true, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: true, - }, - } -} - -func NewInvalidParsedAttributedObservations() []PAO { - return []PAO{ - parsedAttributedObservation{ - Timestamp: 1676484822, - Observer: commontypes.OracleID(1), - - BenchmarkPrice: big.NewInt(345), - Bid: big.NewInt(343), - Ask: big.NewInt(347), - PricesValid: false, - - CurrentBlockNum: 16634364, - CurrentBlockHash: mustDecodeHex("8f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), - CurrentBlockTimestamp: 1682908180, - CurrentBlockValid: false, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: false, - }, - parsedAttributedObservation{ - Timestamp: 1676484826, - Observer: commontypes.OracleID(2), - - BenchmarkPrice: big.NewInt(335), - Bid: big.NewInt(332), - Ask: big.NewInt(336), - PricesValid: false, - - CurrentBlockNum: 16634364, - CurrentBlockHash: mustDecodeHex("8f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), - CurrentBlockTimestamp: 1682908180, - CurrentBlockValid: false, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: false, - }, - parsedAttributedObservation{ - Timestamp: 1676484828, - Observer: commontypes.OracleID(3), - - BenchmarkPrice: big.NewInt(347), - Bid: big.NewInt(345), - Ask: big.NewInt(350), - PricesValid: false, - - CurrentBlockNum: 16634365, - CurrentBlockHash: mustDecodeHex("40044147503a81e9f2a225f4717bf5faf5dc574f69943bdcd305d5ed97504a7e"), - CurrentBlockTimestamp: 1682591344, - CurrentBlockValid: false, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: false, - }, - parsedAttributedObservation{ - Timestamp: 1676484830, - Observer: commontypes.OracleID(4), - - BenchmarkPrice: big.NewInt(346), - Bid: big.NewInt(347), - Ask: big.NewInt(350), - PricesValid: false, - - CurrentBlockNum: 16634365, - CurrentBlockHash: mustDecodeHex("40044147503a81e9f2a225f4717bf5faf5dc574f69943bdcd305d5ed97504a7e"), - CurrentBlockTimestamp: 1682591344, - CurrentBlockValid: false, - - MaxFinalizedBlockNumber: 16634355, - MaxFinalizedBlockNumberValid: false, - }, - } -} - -func Test_AggregateFunctions(t *testing.T) { - f := 1 - invalidPaos := NewInvalidParsedAttributedObservations() - validLegacyPaos := NewValidLegacyParsedAttributedObservations() - - t.Run("GetConsensusLatestBlock", func(t *testing.T) { - makePAO := func(blocks []v1.Block) PAO { - return parsedAttributedObservation{LatestBlocks: blocks} - } - - makeLegacyPAO := func(num int64, hash string, ts uint64) PAO { - return parsedAttributedObservation{CurrentBlockNum: num, CurrentBlockHash: mustDecodeHex(hash), CurrentBlockTimestamp: ts, CurrentBlockValid: true} - } - - t.Run("when all paos are using legacy 'current block'", func(t *testing.T) { - t.Run("succeeds in the valid case", func(t *testing.T) { - hash, num, ts, err := GetConsensusLatestBlock(validLegacyPaos, f) - - require.NoError(t, err) - assert.Equal(t, mustDecodeHex("40044147503a81e9f2a225f4717bf5faf5dc574f69943bdcd305d5ed97504a7e"), hash) - assert.Equal(t, 16634365, int(num)) - assert.Equal(t, uint64(1682591344), ts) - }) - - t.Run("if invalid, fails", func(t *testing.T) { - _, _, _, err := GetConsensusLatestBlock(invalidPaos, f) - require.Error(t, err) - assert.Contains(t, err.Error(), "cannot come to consensus on latest block number") - }) - t.Run("if there are not at least f+1 in consensus about hash", func(t *testing.T) { - _, _, _, err := GetConsensusLatestBlock(validLegacyPaos, 2) - require.Error(t, err) - assert.Contains(t, err.Error(), "cannot come to consensus on latest block number") - _, _, _, err = GetConsensusLatestBlock(validLegacyPaos, 3) - require.Error(t, err) - assert.Contains(t, err.Error(), "cannot come to consensus on latest block number") - }) - t.Run("if there are not at least f+1 in consensus about block number", func(t *testing.T) { - badPaos := []PAO{ - parsedAttributedObservation{ - CurrentBlockNum: 100, - CurrentBlockValid: true, - }, - parsedAttributedObservation{ - CurrentBlockNum: 200, - CurrentBlockValid: true, - }, - parsedAttributedObservation{ - CurrentBlockNum: 300, - CurrentBlockValid: true, - }, - parsedAttributedObservation{ - CurrentBlockNum: 400, - CurrentBlockValid: true, - }, - } - _, _, _, err := GetConsensusLatestBlock(badPaos, f) - require.Error(t, err) - assert.Contains(t, err.Error(), "cannot come to consensus on latest block number") - }) - t.Run("if there are not at least f+1 in consensus about timestamp", func(t *testing.T) { - badPaos := []PAO{ - parsedAttributedObservation{ - CurrentBlockTimestamp: 100, - CurrentBlockValid: true, - }, - parsedAttributedObservation{ - CurrentBlockTimestamp: 200, - CurrentBlockValid: true, - }, - parsedAttributedObservation{ - CurrentBlockTimestamp: 300, - CurrentBlockValid: true, - }, - parsedAttributedObservation{ - CurrentBlockTimestamp: 400, - CurrentBlockValid: true, - }, - } - _, _, _, err := GetConsensusLatestBlock(badPaos, f) - require.Error(t, err) - assert.Contains(t, err.Error(), "cannot come to consensus on latest block number") - }) - t.Run("in the event of an even split for block number/hash, take the higher block number", func(t *testing.T) { - validFrom := int64(26014056) - // values below are from a real observed case of this happening in the wild - paos := []PAO{ - parsedAttributedObservation{ - Timestamp: 1686759784, - Observer: commontypes.OracleID(2), - BenchmarkPrice: big.NewInt(90700), - Bid: big.NewInt(26200), - Ask: big.NewInt(17500), - PricesValid: true, - CurrentBlockNum: 26014055, - CurrentBlockHash: mustDecodeHex("1a2b96ef9a29614c9fc4341a5ca6690ed8ee1a2cd6b232c90ba8bea65a4b93b5"), - CurrentBlockTimestamp: 1686759784, - CurrentBlockValid: true, - MaxFinalizedBlockNumber: 0, - MaxFinalizedBlockNumberValid: false, - }, - parsedAttributedObservation{ - Timestamp: 1686759784, - Observer: commontypes.OracleID(3), - BenchmarkPrice: big.NewInt(92000), - Bid: big.NewInt(21300), - Ask: big.NewInt(74700), - PricesValid: true, - CurrentBlockNum: 26014056, - CurrentBlockHash: mustDecodeHex("bdeb0181416f88812028c4e1ee9e049296c909c1ee15d57cf67d4ce869ed6518"), - CurrentBlockTimestamp: 1686759784, - CurrentBlockValid: true, - MaxFinalizedBlockNumber: 0, - MaxFinalizedBlockNumberValid: false, - }, - parsedAttributedObservation{ - Timestamp: 1686759784, - Observer: commontypes.OracleID(1), - BenchmarkPrice: big.NewInt(67300), - Bid: big.NewInt(70100), - Ask: big.NewInt(83200), - PricesValid: true, - CurrentBlockNum: 26014056, - CurrentBlockHash: mustDecodeHex("bdeb0181416f88812028c4e1ee9e049296c909c1ee15d57cf67d4ce869ed6518"), - CurrentBlockTimestamp: 1686759784, - CurrentBlockValid: true, - MaxFinalizedBlockNumber: 0, - MaxFinalizedBlockNumberValid: false, - }, - parsedAttributedObservation{ - Timestamp: 1686759784, - Observer: commontypes.OracleID(0), - BenchmarkPrice: big.NewInt(8600), - Bid: big.NewInt(89100), - Ask: big.NewInt(53300), - PricesValid: true, - CurrentBlockNum: 26014055, - CurrentBlockHash: mustDecodeHex("1a2b96ef9a29614c9fc4341a5ca6690ed8ee1a2cd6b232c90ba8bea65a4b93b5"), - CurrentBlockTimestamp: 1686759784, - CurrentBlockValid: true, - MaxFinalizedBlockNumber: 0, - MaxFinalizedBlockNumberValid: false, - }, - } - hash, num, _, err := GetConsensusLatestBlock(paos, f) - assert.NoError(t, err) - assert.Equal(t, mustDecodeHex("bdeb0181416f88812028c4e1ee9e049296c909c1ee15d57cf67d4ce869ed6518"), hash) - assert.Equal(t, int64(26014056), num) - assert.GreaterOrEqual(t, num, validFrom) - }) - t.Run("when there are multiple possible blocks meeting > f+1 hashes, takes the hash with the most block numbers in agreement", func(t *testing.T) { - paos := []PAO{ - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 1), - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 1), - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 1), - makeLegacyPAO(41, "3333333333333333333333333333333333333333333333333333333333333333", 0), - makeLegacyPAO(41, "3333333333333333333333333333333333333333333333333333333333333333", 0), - makeLegacyPAO(41, "3333333333333333333333333333333333333333333333333333333333333333", 0), - makeLegacyPAO(42, "1111111111111111111111111111111111111111111111111111111111111111", 1), - makeLegacyPAO(42, "1111111111111111111111111111111111111111111111111111111111111111", 1), - makeLegacyPAO(41, "1111111111111111111111111111111111111111111111111111111111111111", 1), - makeLegacyPAO(43, "2222222222222222222222222222222222222222222222222222222222222222", 1), - makeLegacyPAO(42, "2222222222222222222222222222222222222222222222222222222222222222", 1), - makeLegacyPAO(42, "2222222222222222222222222222222222222222222222222222222222222222", 1), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, f) - assert.NoError(t, err) - assert.Equal(t, mustDecodeHex("3333333333333333333333333333333333333333333333333333333333333333"), hash) - assert.Equal(t, int64(42), num) - assert.Equal(t, uint64(1), ts) - }) - t.Run("in the event of an even split of numbers/hashes, takes the hash with the highest block number", func(t *testing.T) { - paos := []PAO{ - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 1), - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 1), - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 1), - makeLegacyPAO(41, "2222222222222222222222222222222222222222222222222222222222222222", 1), - makeLegacyPAO(41, "2222222222222222222222222222222222222222222222222222222222222222", 1), - makeLegacyPAO(41, "2222222222222222222222222222222222222222222222222222222222222222", 1), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, f) - assert.NoError(t, err) - assert.Equal(t, mustDecodeHex("3333333333333333333333333333333333333333333333333333333333333333"), hash) - assert.Equal(t, int64(42), num) - assert.Equal(t, uint64(1), ts) - }) - t.Run("in the case where all block numbers are equal but timestamps differ, tie-breaks on latest timestamp", func(t *testing.T) { - paos := []PAO{ - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 2), - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 2), - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 2), - makeLegacyPAO(42, "2222222222222222222222222222222222222222222222222222222222222222", 1), - makeLegacyPAO(42, "2222222222222222222222222222222222222222222222222222222222222222", 1), - makeLegacyPAO(42, "2222222222222222222222222222222222222222222222222222222222222222", 1), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, f) - assert.NoError(t, err) - assert.Equal(t, mustDecodeHex("3333333333333333333333333333333333333333333333333333333333333333"), hash) - assert.Equal(t, int64(42), num) - assert.Equal(t, uint64(2), ts) - }) - t.Run("in the case where all block numbers and timestamps are equal, tie-breaks by taking the 'lowest' hash", func(t *testing.T) { - paos := []PAO{ - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 1), - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 1), - makeLegacyPAO(42, "3333333333333333333333333333333333333333333333333333333333333333", 1), - makeLegacyPAO(42, "2222222222222222222222222222222222222222222222222222222222222222", 1), - makeLegacyPAO(42, "2222222222222222222222222222222222222222222222222222222222222222", 1), - makeLegacyPAO(42, "2222222222222222222222222222222222222222222222222222222222222222", 1), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, f) - assert.NoError(t, err) - assert.Equal(t, mustDecodeHex("2222222222222222222222222222222222222222222222222222222222222222"), hash) - assert.Equal(t, int64(42), num) - assert.Equal(t, uint64(1), ts) - }) - }) - - t.Run("when there is a mix of PAOS, some with legacy 'current block' and some with LatestBlocks", func(t *testing.T) { - t.Run("succeeds in the valid case where all agree", func(t *testing.T) { - cv := MakeChainView1() - paos := []PAO{ - makePAO(cv), - makePAO(cv), - makeLegacyPAO(cv[0].Num, hex.EncodeToString([]byte(cv[0].Hash)), cv[0].Ts), - makeLegacyPAO(cv[0].Num, hex.EncodeToString([]byte(cv[0].Hash)), cv[0].Ts), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, f) - - require.NoError(t, err) - assert.Equal(t, mustDecodeHex("9f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), hash) - assert.Equal(t, 16634365, int(num)) - assert.Equal(t, 1682908181, int(ts)) - }) - - t.Run("succeeds in the valid case with two different chain views, and returns the highest common block with f+1 observations", func(t *testing.T) { - cv := MakeChainView1() - cv2 := ChainView2 - paos := []PAO{ - makePAO(cv[1:]), - makePAO(cv2), - makeLegacyPAO(cv[3].Num, hex.EncodeToString([]byte(cv[3].Hash)), cv[3].Ts), - makeLegacyPAO(cv2[0].Num, hex.EncodeToString([]byte(cv2[0].Hash)), cv2[0].Ts), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, 1) - - require.NoError(t, err) - assert.Equal(t, mustDecodeHex("8e30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), hash) - assert.Equal(t, 16634365, int(num)) - assert.Equal(t, 1682908180, int(ts)) - - hash, num, ts, err = GetConsensusLatestBlock(paos, 2) - - require.NoError(t, err) - assert.Equal(t, mustDecodeHex("6f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), hash) - assert.Equal(t, 16634362, int(num)) - assert.Equal(t, 1682908172, int(ts)) - }) - }) - - t.Run("when all PAOS are using LatestBlocks", func(t *testing.T) { - t.Run("succeeds in the valid case where all agree", func(t *testing.T) { - paos := []PAO{ - makePAO(MakeChainView1()), - makePAO(MakeChainView1()), - makePAO(MakeChainView1()), - makePAO(MakeChainView1()), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, f) - - require.NoError(t, err) - assert.Equal(t, mustDecodeHex("9f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), hash) - assert.Equal(t, 16634365, int(num)) - assert.Equal(t, 1682908181, int(ts)) - }) - - t.Run("succeeds in the valid case with two different chain views, and returns the highest common block with f+1 observations", func(t *testing.T) { - paos := []PAO{ - makePAO(ChainView2), - makePAO(ChainView2), - makePAO(MakeChainView1()), - makePAO(MakeChainView1()), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, 3) - - require.NoError(t, err) - assert.Equal(t, mustDecodeHex("6f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), hash) - assert.Equal(t, 16634362, int(num)) - assert.Equal(t, 1682908172, int(ts)) - }) - - t.Run("succeeds in the case with many different chain views, and returns the highest common block with f+1 observations", func(t *testing.T) { - paos := []PAO{ - makePAO(ChainView3), - makePAO(ChainView2), - makePAO(MakeChainView1()), - makePAO(MakeChainView1()), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, 1) - - require.NoError(t, err) - assert.Equal(t, mustDecodeHex("9f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), hash) - assert.Equal(t, 16634365, int(num)) - assert.Equal(t, 1682908181, int(ts)) - }) - - t.Run("takes highest with at least f+1 when some observations are behind", func(t *testing.T) { - paos := []PAO{ - makePAO(MakeChainView1()[2:]), - makePAO(MakeChainView1()[1:]), - makePAO(MakeChainView1()), - makePAO(MakeChainView1()), - } - hash, num, ts, err := GetConsensusLatestBlock(paos, 3) - - require.NoError(t, err) - assert.Equal(t, mustDecodeHex("7f30cda279821c5bb6f72f7ab900aa5118215ce59fcf8835b12d0cdbadc9d7b0"), hash) - assert.Equal(t, 16634363, int(num)) - assert.Equal(t, 1682908176, int(ts)) - }) - - t.Run("tie-breaks using smaller hash", func(t *testing.T) { - cv1 := MakeChainView1()[0:2] - cv2 := MakeChainView1()[0:1] - cv3 := MakeChainView1()[0:3] - cv4 := MakeChainView1()[0:3] - - cv1[0].Hash = string(mustDecodeHex("0000000000000000000000000000000000000000000000000000000000000000")) - cv4[0].Hash = string(mustDecodeHex("0000000000000000000000000000000000000000000000000000000000000000")) - - paos := []PAO{ - makePAO(cv1), - makePAO(cv2), - makePAO(cv3), - makePAO(cv4), - } - - hash, num, ts, err := GetConsensusLatestBlock(paos, 1) - - require.NoError(t, err) - assert.Equal(t, mustDecodeHex("0000000000000000000000000000000000000000000000000000000000000000"), hash) - assert.Equal(t, 16634365, int(num)) - assert.Equal(t, 1682908181, int(ts)) - }) - - t.Run("fails in the case where there is no common block with at least f+1 observations", func(t *testing.T) { - paos := []PAO{ - makePAO(ChainView2[0:3]), - makePAO(ChainView2[0:3]), - makePAO(MakeChainView1()[0:3]), - makePAO(MakeChainView1()[0:3]), - } - _, _, _, err := GetConsensusLatestBlock(paos, 3) - require.Error(t, err) - assert.Contains(t, err.Error(), "cannot come to consensus on latest block number") - }) - - t.Run("if invalid, fails", func(t *testing.T) { - _, _, _, err := GetConsensusLatestBlock(invalidPaos, f) - require.Error(t, err) - assert.Contains(t, err.Error(), "cannot come to consensus on latest block number") - }) - }) - }) - - t.Run("GetConsensusMaxFinalizedBlockNum", func(t *testing.T) { - t.Run("in the valid case", func(t *testing.T) { - num, err := GetConsensusMaxFinalizedBlockNum(validLegacyPaos, f) - - require.NoError(t, err) - assert.Equal(t, 16634355, int(num)) - }) - - t.Run("errors if there are not at least f+1 valid", func(t *testing.T) { - _, err := GetConsensusMaxFinalizedBlockNum(invalidPaos, f) - assert.EqualError(t, err, "fewer than f+1 observations have a valid maxFinalizedBlockNumber (got: 0/4, f=1)") - }) - - t.Run("errors if there are not at least f+1 in consensus about number", func(t *testing.T) { - badPaos := []PAO{ - parsedAttributedObservation{ - MaxFinalizedBlockNumber: 100, - MaxFinalizedBlockNumberValid: true, - }, - parsedAttributedObservation{ - MaxFinalizedBlockNumber: 200, - MaxFinalizedBlockNumberValid: true, - }, - parsedAttributedObservation{ - MaxFinalizedBlockNumber: 300, - MaxFinalizedBlockNumberValid: true, - }, - parsedAttributedObservation{ - MaxFinalizedBlockNumber: 400, - MaxFinalizedBlockNumberValid: true, - }, - } - - _, err := GetConsensusMaxFinalizedBlockNum(badPaos, f) - assert.EqualError(t, err, "no valid maxFinalizedBlockNumber with at least f+1 votes (got counts: map[100:1 200:1 300:1 400:1], f=1)") - }) - }) -} diff --git a/mercury/v1/mercury.go b/mercury/v1/mercury.go deleted file mode 100644 index 433869c..0000000 --- a/mercury/v1/mercury.go +++ /dev/null @@ -1,454 +0,0 @@ -package v1 - -import ( - "context" - "errors" - "fmt" - "sort" - "time" - - "google.golang.org/protobuf/proto" - - "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - mercurytypes "github.com/smartcontractkit/chainlink-common/pkg/types/mercury" - v1 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v1" - - "github.com/smartcontractkit/chainlink-data-streams/mercury" -) - -// MaxAllowedBlocks indicates the maximum len of LatestBlocks in any given observation. -// observations that violate this will be discarded -const MaxAllowedBlocks = 10 - -// DataSource implementations must be thread-safe. Observe may be called by many -// different threads concurrently. -type DataSource interface { - // Observe queries the data source. Returns a value or an error. Once the - // context is expires, Observe may still do cheap computations and return a - // result, but should return as quickly as possible. - // - // More details: In the current implementation, the context passed to - // Observe will time out after MaxDurationObservation. However, Observe - // should *not* make any assumptions about context timeout behavior. Once - // the context times out, Observe should prioritize returning as quickly as - // possible, but may still perform fast computations to return a result - // rather than error. For example, if Observe medianizes a number of data - // sources, some of which already returned a result to Observe prior to the - // context's expiry, Observe might still compute their median, and return it - // instead of an error. - // - // Important: Observe should not perform any potentially time-consuming - // actions like database access, once the context passed has expired. - Observe(ctx context.Context, repts types.ReportTimestamp, fetchMaxFinalizedBlockNum bool) (v1.Observation, error) -} - -var _ ocr3types.MercuryPluginFactory = Factory{} - -// Maximum length in bytes of Observation, Report returned by the -// MercuryPlugin. Used for defending against spam attacks. -const maxObservationLength = 4 + // timestamp - mercury.ByteWidthInt192 + // benchmarkPrice - mercury.ByteWidthInt192 + // bid - mercury.ByteWidthInt192 + // ask - 1 + // pricesValid - 8 + // currentBlockNum - 32 + // currentBlockHash - 8 + // currentBlockTimestamp - 1 + // currentBlockValid - 8 + // maxFinalizedBlockNumber - 1 + // maxFinalizedBlockNumberValid - 32 + // [> overapprox. of protobuf overhead <] - MaxAllowedBlocks*(8+ // num - 32+ // hash - 8+ // ts - 32) // [> overapprox. of protobuf overhead <] - -type Factory struct { - dataSource DataSource - logger logger.Logger - onchainConfigCodec mercurytypes.OnchainConfigCodec - reportCodec v1.ReportCodec -} - -func NewFactory(ds DataSource, lggr logger.Logger, occ mercurytypes.OnchainConfigCodec, rc v1.ReportCodec) Factory { - return Factory{ds, lggr, occ, rc} -} - -func (fac Factory) NewMercuryPlugin(ctx context.Context, configuration ocr3types.MercuryPluginConfig) (ocr3types.MercuryPlugin, ocr3types.MercuryPluginInfo, error) { - offchainConfig, err := mercury.DecodeOffchainConfig(configuration.OffchainConfig) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - onchainConfig, err := fac.onchainConfigCodec.Decode(ctx, configuration.OnchainConfig) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - maxReportLength, err := fac.reportCodec.MaxReportLength(ctx, configuration.N) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - r := &reportingPlugin{ - offchainConfig, - onchainConfig, - fac.dataSource, - fac.logger, - fac.reportCodec, - configuration.ConfigDigest, - configuration.F, - mercury.EpochRound{}, - maxReportLength, - } - - return r, ocr3types.MercuryPluginInfo{ - Name: "Mercury", - Limits: ocr3types.MercuryPluginLimits{ - MaxObservationLength: maxObservationLength, - MaxReportLength: maxReportLength, - }, - }, nil -} - -var _ ocr3types.MercuryPlugin = (*reportingPlugin)(nil) - -type reportingPlugin struct { - offchainConfig mercury.OffchainConfig - onchainConfig mercurytypes.OnchainConfig - dataSource DataSource - logger logger.Logger - reportCodec v1.ReportCodec - - configDigest types.ConfigDigest - f int - latestAcceptedEpochRound mercury.EpochRound - maxReportLength int -} - -func (rp *reportingPlugin) Observation(ctx context.Context, repts types.ReportTimestamp, previousReport types.Report) (types.Observation, error) { - obs, err := rp.dataSource.Observe(ctx, repts, previousReport == nil) - if err != nil { - return nil, fmt.Errorf("DataSource.Observe returned an error: %s", err) - } - - p := MercuryObservationProto{Timestamp: uint32(time.Now().Unix())} - - var obsErrors []error - if previousReport == nil { - // if previousReport we fall back to the observed MaxFinalizedBlockNumber - if obs.MaxFinalizedBlockNumber.Err != nil { - obsErrors = append(obsErrors, err) - } else if obs.CurrentBlockNum.Err == nil && obs.CurrentBlockNum.Val < obs.MaxFinalizedBlockNumber.Val { - obsErrors = append(obsErrors, fmt.Errorf("failed to observe ValidFromBlockNum; current block number %d (hash: 0x%x) < max finalized block number %d; ignoring observation for out-of-date RPC", obs.CurrentBlockNum.Val, obs.CurrentBlockHash.Val, obs.MaxFinalizedBlockNumber.Val)) - } else { - p.MaxFinalizedBlockNumber = obs.MaxFinalizedBlockNumber.Val // MaxFinalizedBlockNumber comes as -1 if unset - p.MaxFinalizedBlockNumberValid = true - } - } - - var bpErr, bidErr, askErr error - if obs.BenchmarkPrice.Err != nil { - bpErr = fmt.Errorf("failed to observe BenchmarkPrice: %w", obs.BenchmarkPrice.Err) - obsErrors = append(obsErrors, bpErr) - } else if benchmarkPrice, err := mercury.EncodeValueInt192(obs.BenchmarkPrice.Val); err != nil { - bpErr = fmt.Errorf("failed to observe BenchmarkPrice; encoding failed: %w", err) - obsErrors = append(obsErrors, bpErr) - } else { - p.BenchmarkPrice = benchmarkPrice - } - - if obs.Bid.Err != nil { - bidErr = fmt.Errorf("failed to observe Bid: %w", obs.Bid.Err) - obsErrors = append(obsErrors, bidErr) - } else if bid, err := mercury.EncodeValueInt192(obs.Bid.Val); err != nil { - bidErr = fmt.Errorf("failed to observe Bid; encoding failed: %w", err) - obsErrors = append(obsErrors, bidErr) - } else { - p.Bid = bid - } - - if obs.Ask.Err != nil { - askErr = fmt.Errorf("failed to observe Ask: %w", obs.Ask.Err) - obsErrors = append(obsErrors, askErr) - } else if ask, err := mercury.EncodeValueInt192(obs.Ask.Val); err != nil { - askErr = fmt.Errorf("failed to observe Ask; encoding failed: %w", err) - obsErrors = append(obsErrors, askErr) - } else { - p.Ask = ask - } - - if bpErr == nil && bidErr == nil && askErr == nil { - p.PricesValid = true - } - - if obs.CurrentBlockNum.Err != nil { - obsErrors = append(obsErrors, fmt.Errorf("failed to observe CurrentBlockNum: %w", obs.CurrentBlockNum.Err)) - } else { - p.CurrentBlockNum = obs.CurrentBlockNum.Val - } - - if obs.CurrentBlockHash.Err != nil { - obsErrors = append(obsErrors, fmt.Errorf("failed to observe CurrentBlockHash: %w", obs.CurrentBlockHash.Err)) - } else { - p.CurrentBlockHash = obs.CurrentBlockHash.Val - } - - if obs.CurrentBlockTimestamp.Err != nil { - obsErrors = append(obsErrors, fmt.Errorf("failed to observe CurrentBlockTimestamp: %w", obs.CurrentBlockTimestamp.Err)) - } else { - p.CurrentBlockTimestamp = obs.CurrentBlockTimestamp.Val - } - - if obs.CurrentBlockNum.Err == nil && obs.CurrentBlockHash.Err == nil && obs.CurrentBlockTimestamp.Err == nil { - p.CurrentBlockValid = true - } - - if len(obsErrors) > 0 { - rp.logger.Warnw(fmt.Sprintf("Observe failed %d/6 observations", len(obsErrors)), "err", errors.Join(obsErrors...)) - } - - p.LatestBlocks = make([]*BlockProto, len(obs.LatestBlocks)) - for i, b := range obs.LatestBlocks { - p.LatestBlocks[i] = &BlockProto{Num: b.Num, Hash: []byte(b.Hash), Ts: b.Ts} - } - if len(p.LatestBlocks) == 0 { - rp.logger.Warn("Observation had no LatestBlocks") - } - - return proto.Marshal(&p) -} - -func parseAttributedObservation(ao types.AttributedObservation) (PAO, error) { - var pao parsedAttributedObservation - var obs MercuryObservationProto - if err := proto.Unmarshal(ao.Observation, &obs); err != nil { - return parsedAttributedObservation{}, fmt.Errorf("attributed observation cannot be unmarshaled: %s", err) - } - - pao.Timestamp = obs.Timestamp - pao.Observer = ao.Observer - - if obs.PricesValid { - var err error - pao.BenchmarkPrice, err = mercury.DecodeValueInt192(obs.BenchmarkPrice) - if err != nil { - return parsedAttributedObservation{}, fmt.Errorf("benchmarkPrice cannot be converted to big.Int: %s", err) - } - pao.Bid, err = mercury.DecodeValueInt192(obs.Bid) - if err != nil { - return parsedAttributedObservation{}, fmt.Errorf("bid cannot be converted to big.Int: %s", err) - } - pao.Ask, err = mercury.DecodeValueInt192(obs.Ask) - if err != nil { - return parsedAttributedObservation{}, fmt.Errorf("ask cannot be converted to big.Int: %s", err) - } - pao.PricesValid = true - } - - if len(obs.LatestBlocks) > 0 { - if len(obs.LatestBlocks) > MaxAllowedBlocks { - return parsedAttributedObservation{}, fmt.Errorf("LatestBlocks too large; got: %d, max: %d", len(obs.LatestBlocks), MaxAllowedBlocks) - } - for _, b := range obs.LatestBlocks { - pao.LatestBlocks = append(pao.LatestBlocks, v1.NewBlock(b.Num, b.Hash, b.Ts)) - - // Ignore observation if it has duplicate blocks by number or hash - // for security to avoid the case where one node can "throw" block - // numbers by including a bunch of duplicates - nums := make(map[int64]struct{}, len(pao.LatestBlocks)) - hashes := make(map[string]struct{}, len(pao.LatestBlocks)) - for _, block := range pao.LatestBlocks { - if _, exists := nums[block.Num]; exists { - return parsedAttributedObservation{}, fmt.Errorf("observation invalid for observer %d; got duplicate block number: %d", ao.Observer, block.Num) - } - if _, exists := hashes[block.Hash]; exists { - return parsedAttributedObservation{}, fmt.Errorf("observation invalid for observer %d; got duplicate block hash: 0x%x", ao.Observer, block.HashBytes()) - } - nums[block.Num] = struct{}{} - hashes[block.Hash] = struct{}{} - - if len(block.Hash) != mercury.EvmHashLen { - return parsedAttributedObservation{}, fmt.Errorf("wrong len for hash: %d (expected: %d)", len(block.Hash), mercury.EvmHashLen) - } - if block.Num < 0 { - return parsedAttributedObservation{}, fmt.Errorf("negative block number: %d", block.Num) - } - } - - // sort desc - sort.SliceStable(pao.LatestBlocks, func(i, j int) bool { - // NOTE: This ought to be redundant since observing nodes - // should give us the blocks pre-sorted, but is included here - // for safety - return pao.LatestBlocks[j].Less(pao.LatestBlocks[i]) - }) - } - } else if obs.CurrentBlockValid { - // DEPRECATED - // TODO: Remove this handling after deployment (https://smartcontract-it.atlassian.net/browse/MERC-2272) - if len(obs.CurrentBlockHash) != mercury.EvmHashLen { - return parsedAttributedObservation{}, fmt.Errorf("wrong len for hash: %d (expected: %d)", len(obs.CurrentBlockHash), mercury.EvmHashLen) - } - pao.CurrentBlockHash = obs.CurrentBlockHash - if obs.CurrentBlockNum < 0 { - return parsedAttributedObservation{}, fmt.Errorf("negative block number: %d", obs.CurrentBlockNum) - } - pao.CurrentBlockNum = obs.CurrentBlockNum - pao.CurrentBlockTimestamp = obs.CurrentBlockTimestamp - pao.CurrentBlockValid = true - } - - if obs.MaxFinalizedBlockNumberValid { - pao.MaxFinalizedBlockNumber = obs.MaxFinalizedBlockNumber - pao.MaxFinalizedBlockNumberValid = true - } - - return pao, nil -} - -func parseAttributedObservations(lggr logger.Logger, aos []types.AttributedObservation) []PAO { - paos := make([]PAO, 0, len(aos)) - for i, ao := range aos { - pao, err := parseAttributedObservation(ao) - if err != nil { - lggr.Warnw("parseAttributedObservations: dropping invalid observation", - "observer", ao.Observer, - "error", err, - "i", i, - ) - continue - } - paos = append(paos, pao) - } - return paos -} - -func (rp *reportingPlugin) Report(ctx context.Context, repts types.ReportTimestamp, previousReport types.Report, aos []types.AttributedObservation) (shouldReport bool, report types.Report, err error) { - paos := parseAttributedObservations(rp.logger, aos) - - if len(paos) == 0 { - return false, nil, errors.New("got zero valid attributed observations") - } - - // By assumption, we have at most f malicious oracles, so there should be at least f+1 valid paos - if !(rp.f+1 <= len(paos)) { - return false, nil, fmt.Errorf("only received %v valid attributed observations, but need at least f+1 (%v)", len(paos), rp.f+1) - } - - rf, err := rp.buildReportFields(ctx, previousReport, paos) - if err != nil { - rp.logger.Errorw("failed to build report fields", "paos", paos, "f", rp.f, "reportFields", rf, "repts", repts, "err", err) - return false, nil, err - } - - if rf.CurrentBlockNum < rf.ValidFromBlockNum { - rp.logger.Debugw("shouldReport: no (overlap)", "currentBlockNum", rf.CurrentBlockNum, "validFromBlockNum", rf.ValidFromBlockNum, "repts", repts) - return false, nil, nil - } - - if err = rp.validateReport(rf); err != nil { - rp.logger.Errorw("shouldReport: no (validation error)", "reportFields", rf, "err", err, "repts", repts, "paos", paos) - return false, nil, err - } - rp.logger.Debugw("shouldReport: yes", - "timestamp", repts, - ) - - report, err = rp.reportCodec.BuildReport(ctx, rf) - if err != nil { - rp.logger.Debugw("failed to BuildReport", "paos", paos, "f", rp.f, "reportFields", rf, "repts", repts) - return false, nil, err - } - if !(len(report) <= rp.maxReportLength) { - return false, nil, fmt.Errorf("report with len %d violates MaxReportLength limit set by ReportCodec (%d)", len(report), rp.maxReportLength) - } else if len(report) == 0 { - return false, nil, errors.New("report may not have zero length (invariant violation)") - } - - return true, report, nil -} - -func (rp *reportingPlugin) buildReportFields(ctx context.Context, previousReport types.Report, paos []PAO) (rf v1.ReportFields, merr error) { - var err error - if previousReport != nil { - var maxFinalizedBlockNumber int64 - maxFinalizedBlockNumber, err = rp.reportCodec.CurrentBlockNumFromReport(ctx, previousReport) - if err != nil { - merr = errors.Join(merr, err) - } else { - rf.ValidFromBlockNum = maxFinalizedBlockNumber + 1 - } - } else { - var maxFinalizedBlockNumber int64 - maxFinalizedBlockNumber, err = GetConsensusMaxFinalizedBlockNum(paos, rp.f) - if err != nil { - merr = errors.Join(merr, err) - } else { - rf.ValidFromBlockNum = maxFinalizedBlockNumber + 1 - } - } - - mPaos := convert(paos) - - rf.Timestamp = mercury.GetConsensusTimestamp(mPaos) - - rf.BenchmarkPrice, err = mercury.GetConsensusBenchmarkPrice(mPaos, rp.f) - if err != nil { - merr = errors.Join(merr, fmt.Errorf("GetConsensusBenchmarkPrice failed: %w", err)) - } - - rf.Bid, err = mercury.GetConsensusBid(convertBid(paos), rp.f) - if err != nil { - merr = errors.Join(merr, fmt.Errorf("GetConsensusBid failed: %w", err)) - } - - rf.Ask, err = mercury.GetConsensusAsk(convertAsk(paos), rp.f) - if err != nil { - merr = errors.Join(merr, fmt.Errorf("GetConsensusAsk failed: %w", err)) - } - - rf.CurrentBlockHash, rf.CurrentBlockNum, rf.CurrentBlockTimestamp, err = GetConsensusLatestBlock(paos, rp.f) - if err != nil { - merr = errors.Join(merr, fmt.Errorf("GetConsensusCurrentBlock failed: %w", err)) - } - - return rf, merr -} - -func (rp *reportingPlugin) validateReport(rf v1.ReportFields) error { - return errors.Join( - mercury.ValidateBetween("median benchmark price", rf.BenchmarkPrice, rp.onchainConfig.Min, rp.onchainConfig.Max), - mercury.ValidateBetween("median bid", rf.Bid, rp.onchainConfig.Min, rp.onchainConfig.Max), - mercury.ValidateBetween("median ask", rf.Ask, rp.onchainConfig.Min, rp.onchainConfig.Max), - ValidateCurrentBlock(rf), - ) -} - -func (rp *reportingPlugin) Close() error { - return nil -} - -// convert funcs are necessary because go is not smart enough to cast -// []interface1 to []interface2 even if interface1 is a superset of interface2 -func convert(pao []PAO) (ret []mercury.PAO) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertBid(pao []PAO) (ret []mercury.PAOBid) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertAsk(pao []PAO) (ret []mercury.PAOAsk) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} diff --git a/mercury/v1/mercury_observation_v1.pb.go b/mercury/v1/mercury_observation_v1.pb.go deleted file mode 100644 index 385c1e5..0000000 --- a/mercury/v1/mercury_observation_v1.pb.go +++ /dev/null @@ -1,353 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.31.0 -// protoc v4.24.2 -// source: mercury_observation_v1.proto - -package v1 - -import ( - "reflect" - "sync" - - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/runtime/protoimpl" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type MercuryObservationProto struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Timestamp uint32 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - // Prices - BenchmarkPrice []byte `protobuf:"bytes,2,opt,name=benchmarkPrice,proto3" json:"benchmarkPrice,omitempty"` - Bid []byte `protobuf:"bytes,3,opt,name=bid,proto3" json:"bid,omitempty"` - Ask []byte `protobuf:"bytes,4,opt,name=ask,proto3" json:"ask,omitempty"` - // All three prices must be valid, or none are (they all should come from one API query and hold invariant bid <= bm <= ask) - PricesValid bool `protobuf:"varint,5,opt,name=pricesValid,proto3" json:"pricesValid,omitempty"` - // DEPRECATED: replaced by "latestBlocks" - // Current block - CurrentBlockNum int64 `protobuf:"varint,6,opt,name=currentBlockNum,proto3" json:"currentBlockNum,omitempty"` - CurrentBlockHash []byte `protobuf:"bytes,7,opt,name=currentBlockHash,proto3" json:"currentBlockHash,omitempty"` - CurrentBlockTimestamp uint64 `protobuf:"varint,8,opt,name=currentBlockTimestamp,proto3" json:"currentBlockTimestamp,omitempty"` - // All three block observations must be valid, or none are (they all come from the same block) - CurrentBlockValid bool `protobuf:"varint,9,opt,name=currentBlockValid,proto3" json:"currentBlockValid,omitempty"` - // MaxFinalizedBlockNumber comes from previous report when present and is - // only observed from mercury server when previous report is nil - MaxFinalizedBlockNumber int64 `protobuf:"varint,10,opt,name=maxFinalizedBlockNumber,proto3" json:"maxFinalizedBlockNumber,omitempty"` - MaxFinalizedBlockNumberValid bool `protobuf:"varint,11,opt,name=maxFinalizedBlockNumberValid,proto3" json:"maxFinalizedBlockNumberValid,omitempty"` - // Latest blocks - LatestBlocks []*BlockProto `protobuf:"bytes,12,rep,name=latestBlocks,proto3" json:"latestBlocks,omitempty"` -} - -func (x *MercuryObservationProto) Reset() { - *x = MercuryObservationProto{} - if protoimpl.UnsafeEnabled { - mi := &file_mercury_observation_v1_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *MercuryObservationProto) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*MercuryObservationProto) ProtoMessage() {} - -func (x *MercuryObservationProto) ProtoReflect() protoreflect.Message { - mi := &file_mercury_observation_v1_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use MercuryObservationProto.ProtoReflect.Descriptor instead. -func (*MercuryObservationProto) Descriptor() ([]byte, []int) { - return file_mercury_observation_v1_proto_rawDescGZIP(), []int{0} -} - -func (x *MercuryObservationProto) GetTimestamp() uint32 { - if x != nil { - return x.Timestamp - } - return 0 -} - -func (x *MercuryObservationProto) GetBenchmarkPrice() []byte { - if x != nil { - return x.BenchmarkPrice - } - return nil -} - -func (x *MercuryObservationProto) GetBid() []byte { - if x != nil { - return x.Bid - } - return nil -} - -func (x *MercuryObservationProto) GetAsk() []byte { - if x != nil { - return x.Ask - } - return nil -} - -func (x *MercuryObservationProto) GetPricesValid() bool { - if x != nil { - return x.PricesValid - } - return false -} - -func (x *MercuryObservationProto) GetCurrentBlockNum() int64 { - if x != nil { - return x.CurrentBlockNum - } - return 0 -} - -func (x *MercuryObservationProto) GetCurrentBlockHash() []byte { - if x != nil { - return x.CurrentBlockHash - } - return nil -} - -func (x *MercuryObservationProto) GetCurrentBlockTimestamp() uint64 { - if x != nil { - return x.CurrentBlockTimestamp - } - return 0 -} - -func (x *MercuryObservationProto) GetCurrentBlockValid() bool { - if x != nil { - return x.CurrentBlockValid - } - return false -} - -func (x *MercuryObservationProto) GetMaxFinalizedBlockNumber() int64 { - if x != nil { - return x.MaxFinalizedBlockNumber - } - return 0 -} - -func (x *MercuryObservationProto) GetMaxFinalizedBlockNumberValid() bool { - if x != nil { - return x.MaxFinalizedBlockNumberValid - } - return false -} - -func (x *MercuryObservationProto) GetLatestBlocks() []*BlockProto { - if x != nil { - return x.LatestBlocks - } - return nil -} - -type BlockProto struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Num int64 `protobuf:"varint,1,opt,name=num,proto3" json:"num,omitempty"` - Hash []byte `protobuf:"bytes,2,opt,name=hash,proto3" json:"hash,omitempty"` - Ts uint64 `protobuf:"varint,3,opt,name=ts,proto3" json:"ts,omitempty"` -} - -func (x *BlockProto) Reset() { - *x = BlockProto{} - if protoimpl.UnsafeEnabled { - mi := &file_mercury_observation_v1_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *BlockProto) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*BlockProto) ProtoMessage() {} - -func (x *BlockProto) ProtoReflect() protoreflect.Message { - mi := &file_mercury_observation_v1_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use BlockProto.ProtoReflect.Descriptor instead. -func (*BlockProto) Descriptor() ([]byte, []int) { - return file_mercury_observation_v1_proto_rawDescGZIP(), []int{1} -} - -func (x *BlockProto) GetNum() int64 { - if x != nil { - return x.Num - } - return 0 -} - -func (x *BlockProto) GetHash() []byte { - if x != nil { - return x.Hash - } - return nil -} - -func (x *BlockProto) GetTs() uint64 { - if x != nil { - return x.Ts - } - return 0 -} - -var File_mercury_observation_v1_proto protoreflect.FileDescriptor - -var file_mercury_observation_v1_proto_rawDesc = []byte{ - 0x0a, 0x1c, 0x6d, 0x65, 0x72, 0x63, 0x75, 0x72, 0x79, 0x5f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x76, 0x31, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, - 0x6d, 0x65, 0x72, 0x63, 0x75, 0x72, 0x79, 0x5f, 0x76, 0x31, 0x22, 0x99, 0x04, 0x0a, 0x17, 0x4d, - 0x65, 0x72, 0x63, 0x75, 0x72, 0x79, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x62, 0x65, 0x6e, 0x63, 0x68, 0x6d, 0x61, 0x72, - 0x6b, 0x50, 0x72, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x62, 0x65, - 0x6e, 0x63, 0x68, 0x6d, 0x61, 0x72, 0x6b, 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, - 0x62, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x62, 0x69, 0x64, 0x12, 0x10, - 0x0a, 0x03, 0x61, 0x73, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x61, 0x73, 0x6b, - 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x63, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x70, 0x72, 0x69, 0x63, 0x65, 0x73, 0x56, 0x61, 0x6c, - 0x69, 0x64, 0x12, 0x28, 0x0a, 0x0f, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x42, 0x6c, 0x6f, - 0x63, 0x6b, 0x4e, 0x75, 0x6d, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x63, 0x75, 0x72, - 0x72, 0x65, 0x6e, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x4e, 0x75, 0x6d, 0x12, 0x2a, 0x0a, 0x10, - 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x61, 0x73, 0x68, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x42, - 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x61, 0x73, 0x68, 0x12, 0x34, 0x0a, 0x15, 0x63, 0x75, 0x72, 0x72, - 0x65, 0x6e, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x52, 0x15, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, - 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x2c, - 0x0a, 0x11, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x56, 0x61, - 0x6c, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x63, 0x75, 0x72, 0x72, 0x65, - 0x6e, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x12, 0x38, 0x0a, 0x17, - 0x6d, 0x61, 0x78, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x42, 0x6c, 0x6f, 0x63, - 0x6b, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x17, 0x6d, - 0x61, 0x78, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, - 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x1c, 0x6d, 0x61, 0x78, 0x46, 0x69, 0x6e, - 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x4e, 0x75, 0x6d, 0x62, 0x65, - 0x72, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1c, 0x6d, 0x61, - 0x78, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x4e, - 0x75, 0x6d, 0x62, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x12, 0x3a, 0x0a, 0x0c, 0x6c, 0x61, - 0x74, 0x65, 0x73, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x6d, 0x65, 0x72, 0x63, 0x75, 0x72, 0x79, 0x5f, 0x76, 0x31, 0x2e, 0x42, 0x6c, - 0x6f, 0x63, 0x6b, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x52, 0x0c, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, - 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x22, 0x42, 0x0a, 0x0a, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x0a, 0x03, 0x6e, 0x75, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x03, 0x6e, 0x75, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x73, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x74, 0x73, 0x42, 0x0e, 0x5a, 0x0c, 0x2e, 0x3b, - 0x6d, 0x65, 0x72, 0x63, 0x75, 0x72, 0x79, 0x5f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, -} - -var ( - file_mercury_observation_v1_proto_rawDescOnce sync.Once - file_mercury_observation_v1_proto_rawDescData = file_mercury_observation_v1_proto_rawDesc -) - -func file_mercury_observation_v1_proto_rawDescGZIP() []byte { - file_mercury_observation_v1_proto_rawDescOnce.Do(func() { - file_mercury_observation_v1_proto_rawDescData = protoimpl.X.CompressGZIP(file_mercury_observation_v1_proto_rawDescData) - }) - return file_mercury_observation_v1_proto_rawDescData -} - -var file_mercury_observation_v1_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_mercury_observation_v1_proto_goTypes = []interface{}{ - (*MercuryObservationProto)(nil), // 0: mercury_v1.MercuryObservationProto - (*BlockProto)(nil), // 1: mercury_v1.BlockProto -} -var file_mercury_observation_v1_proto_depIdxs = []int32{ - 1, // 0: mercury_v1.MercuryObservationProto.latestBlocks:type_name -> mercury_v1.BlockProto - 1, // [1:1] is the sub-list for method output_type - 1, // [1:1] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name -} - -func init() { file_mercury_observation_v1_proto_init() } -func file_mercury_observation_v1_proto_init() { - if File_mercury_observation_v1_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_mercury_observation_v1_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MercuryObservationProto); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_mercury_observation_v1_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BlockProto); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_mercury_observation_v1_proto_rawDesc, - NumEnums: 0, - NumMessages: 2, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_mercury_observation_v1_proto_goTypes, - DependencyIndexes: file_mercury_observation_v1_proto_depIdxs, - MessageInfos: file_mercury_observation_v1_proto_msgTypes, - }.Build() - File_mercury_observation_v1_proto = out.File - file_mercury_observation_v1_proto_rawDesc = nil - file_mercury_observation_v1_proto_goTypes = nil - file_mercury_observation_v1_proto_depIdxs = nil -} diff --git a/mercury/v1/mercury_observation_v1.proto b/mercury/v1/mercury_observation_v1.proto deleted file mode 100644 index 9198e97..0000000 --- a/mercury/v1/mercury_observation_v1.proto +++ /dev/null @@ -1,38 +0,0 @@ -syntax="proto3"; - -package v1; -option go_package = ".;mercury_v1"; - -message MercuryObservationProto { - uint32 timestamp = 1; - - // Prices - bytes benchmarkPrice = 2; - bytes bid = 3; - bytes ask = 4; - // All three prices must be valid, or none are (they all should come from one API query and hold invariant bid <= bm <= ask) - bool pricesValid = 5; - - // DEPRECATED: replaced by "latestBlocks" - // https://smartcontract-it.atlassian.net/browse/MERC-2272 - // Current block - int64 currentBlockNum = 6; - bytes currentBlockHash = 7; - uint64 currentBlockTimestamp = 8; - // All three block observations must be valid, or none are (they all come from the same block) - bool currentBlockValid = 9; - - // MaxFinalizedBlockNumber comes from previous report when present and is - // only observed from mercury server when previous report is nil - int64 maxFinalizedBlockNumber = 10; - bool maxFinalizedBlockNumberValid = 11; - - // Latest blocks - repeated BlockProto latestBlocks = 12; -} - -message BlockProto { - int64 num = 1; - bytes hash = 2; - uint64 ts = 3; -} diff --git a/mercury/v1/mercury_test.go b/mercury/v1/mercury_test.go deleted file mode 100644 index 3dcb714..0000000 --- a/mercury/v1/mercury_test.go +++ /dev/null @@ -1,978 +0,0 @@ -package v1 - -import ( - "context" - crand "crypto/rand" - "errors" - "fmt" - "math" - "math/big" - "math/rand" - "reflect" - "slices" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - "github.com/smartcontractkit/libocr/commontypes" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - mercurytypes "github.com/smartcontractkit/chainlink-common/pkg/types/mercury" - v1 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v1" - "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - - "github.com/smartcontractkit/chainlink-data-streams/mercury" -) - -type testReportCodec struct { - currentBlock int64 - currentBlockErr error - builtReport types.Report - buildReportShouldFail bool - - builtReportFields *v1.ReportFields -} - -func (trc *testReportCodec) reset() { - trc.currentBlockErr = nil - trc.buildReportShouldFail = false - trc.builtReportFields = nil -} - -func (trc *testReportCodec) BuildReport(ctx context.Context, rf v1.ReportFields) (types.Report, error) { - if trc.buildReportShouldFail { - return nil, errors.New("buildReportShouldFail=true") - } - trc.builtReportFields = &rf - return trc.builtReport, nil -} - -func (trc *testReportCodec) MaxReportLength(ctx context.Context, n int) (int, error) { - return 8*32 + // feed ID - 32 + // timestamp - 192 + // benchmarkPrice - 192 + // bid - 192 + // ask - 64 + //currentBlockNum - 8*32 + // currentBlockHash - 64, // validFromBlockNum - nil -} - -func (trc *testReportCodec) CurrentBlockNumFromReport(context.Context, types.Report) (int64, error) { - return trc.currentBlock, trc.currentBlockErr -} - -func newReportingPlugin(t *testing.T, codec *testReportCodec) *reportingPlugin { - maxReportLength, err := codec.MaxReportLength(tests.Context(t), 4) - require.NoError(t, err) - return &reportingPlugin{ - f: 1, - onchainConfig: mercurytypes.OnchainConfig{Min: big.NewInt(0), Max: big.NewInt(1000)}, - logger: logger.Test(t), - reportCodec: codec, - maxReportLength: maxReportLength, - } -} - -func newValidReportFields() v1.ReportFields { - return v1.ReportFields{ - BenchmarkPrice: big.NewInt(42), - Bid: big.NewInt(42), - Ask: big.NewInt(42), - CurrentBlockNum: 42, - ValidFromBlockNum: 42, - CurrentBlockHash: make([]byte, 32), - } -} - -func Test_ReportingPlugin_validateReport(t *testing.T) { - rp := newReportingPlugin(t, &testReportCodec{}) - rf := newValidReportFields() - - t.Run("reports if currentBlockNum > validFromBlockNum", func(t *testing.T) { - rf.CurrentBlockNum = 500 - rf.ValidFromBlockNum = 499 - err := rp.validateReport(rf) - require.NoError(t, err) - }) - t.Run("reports if currentBlockNum == validFromBlockNum", func(t *testing.T) { - rf.CurrentBlockNum = 500 - rf.ValidFromBlockNum = 500 - err := rp.validateReport(rf) - require.NoError(t, err) - }) - t.Run("does not report if currentBlockNum < validFromBlockNum", func(t *testing.T) { - rf.CurrentBlockNum = 499 - rf.ValidFromBlockNum = 500 - err := rp.validateReport(rf) - require.Error(t, err) - assert.Contains(t, err.Error(), "validFromBlockNum (Value: 500) must be less than or equal to CurrentBlockNum (Value: 499)") - }) -} - -var _ DataSource = &mockDataSource{} - -type mockDataSource struct{ obs v1.Observation } - -func (m mockDataSource) Observe(context.Context, types.ReportTimestamp, bool) (v1.Observation, error) { - return m.obs, nil -} - -func randBigInt() *big.Int { - return big.NewInt(rand.Int63()) -} - -func randBytes(n int) []byte { - b := make([]byte, n) - _, err := crand.Read(b) - if err != nil { - panic(err) - } - return b -} - -func mustDecodeBigInt(b []byte) *big.Int { - n, err := mercury.DecodeValueInt192(b) - if err != nil { - panic(err) - } - return n -} - -func Test_Plugin_Observation(t *testing.T) { - ctx := context.Background() - repts := types.ReportTimestamp{} - codec := &testReportCodec{ - currentBlock: int64(rand.Int31()), - builtReport: []byte{}, - } - - rp := newReportingPlugin(t, codec) - - t.Run("with previous report", func(t *testing.T) { - // content of previousReport is irrelevant, the only thing that matters - // for this test is that it's not nil - previousReport := types.Report{} - - t.Run("when all observations are successful", func(t *testing.T) { - obs := v1.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - Bid: mercurytypes.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - Ask: mercurytypes.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - CurrentBlockNum: mercurytypes.ObsResult[int64]{ - Val: rand.Int63(), - }, - CurrentBlockHash: mercurytypes.ObsResult[[]byte]{ - Val: randBytes(32), - }, - CurrentBlockTimestamp: mercurytypes.ObsResult[uint64]{ - Val: rand.Uint64(), - }, - LatestBlocks: []v1.Block{ - v1.Block{Num: rand.Int63(), Hash: string(randBytes(32)), Ts: rand.Uint64()}, - v1.Block{Num: rand.Int63(), Hash: string(randBytes(32)), Ts: rand.Uint64()}, - v1.Block{Num: rand.Int63(), Hash: string(randBytes(32)), Ts: rand.Uint64()}, - }, - } - - rp.dataSource = mockDataSource{obs} - - pbObs, err := rp.Observation(ctx, repts, previousReport) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(pbObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Equal(t, obs.BenchmarkPrice.Val, mustDecodeBigInt(p.BenchmarkPrice)) - assert.Equal(t, obs.Bid.Val, mustDecodeBigInt(p.Bid)) - assert.Equal(t, obs.Ask.Val, mustDecodeBigInt(p.Ask)) - assert.Equal(t, obs.CurrentBlockNum.Val, p.CurrentBlockNum) - assert.Equal(t, obs.CurrentBlockHash.Val, p.CurrentBlockHash) - assert.Equal(t, obs.CurrentBlockTimestamp.Val, p.CurrentBlockTimestamp) - assert.Equal(t, len(obs.LatestBlocks), len(p.LatestBlocks)) - for i := range obs.LatestBlocks { - assert.Equal(t, obs.LatestBlocks[i].Num, p.LatestBlocks[i].Num) - assert.Equal(t, []byte(obs.LatestBlocks[i].Hash), p.LatestBlocks[i].Hash) - assert.Equal(t, obs.LatestBlocks[i].Ts, p.LatestBlocks[i].Ts) - } - // since previousReport is not nil, maxFinalizedBlockNumber is skipped - assert.Zero(t, p.MaxFinalizedBlockNumber) - - assert.True(t, p.PricesValid) - assert.True(t, p.CurrentBlockValid) - // since previousReport is not nil, maxFinalizedBlockNumber is skipped - assert.False(t, p.MaxFinalizedBlockNumberValid) - }) - - t.Run("when all observations have failed", func(t *testing.T) { - obs := v1.Observation{ - // Vals should be ignored, this is asserted with .Zero below - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: randBigInt(), - Err: errors.New("benchmarkPrice exploded"), - }, - Bid: mercurytypes.ObsResult[*big.Int]{ - Val: randBigInt(), - Err: errors.New("bid exploded"), - }, - Ask: mercurytypes.ObsResult[*big.Int]{ - Val: randBigInt(), - Err: errors.New("ask exploded"), - }, - CurrentBlockNum: mercurytypes.ObsResult[int64]{ - Err: errors.New("currentBlockNum exploded"), - Val: rand.Int63(), - }, - CurrentBlockHash: mercurytypes.ObsResult[[]byte]{ - Err: errors.New("currentBlockHash exploded"), - Val: randBytes(32), - }, - CurrentBlockTimestamp: mercurytypes.ObsResult[uint64]{ - Err: errors.New("currentBlockTimestamp exploded"), - Val: rand.Uint64(), - }, - LatestBlocks: ([]v1.Block)(nil), - } - rp.dataSource = mockDataSource{obs} - - pbObs, err := rp.Observation(ctx, repts, previousReport) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(pbObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Zero(t, p.BenchmarkPrice) - assert.Zero(t, p.Bid) - assert.Zero(t, p.Ask) - assert.Zero(t, p.CurrentBlockNum) - assert.Zero(t, p.CurrentBlockHash) - assert.Zero(t, p.CurrentBlockTimestamp) - // since previousReport is not nil, maxFinalizedBlockNumber is skipped - assert.Zero(t, p.MaxFinalizedBlockNumber) - assert.Len(t, p.LatestBlocks, 0) - - assert.False(t, p.PricesValid) - assert.False(t, p.CurrentBlockValid) - // since previousReport is not nil, maxFinalizedBlockNumber is skipped - assert.False(t, p.MaxFinalizedBlockNumberValid) - }) - - t.Run("when some observations have failed", func(t *testing.T) { - obs := v1.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - Bid: mercurytypes.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - Ask: mercurytypes.ObsResult[*big.Int]{ - Err: errors.New("ask exploded"), - }, - CurrentBlockNum: mercurytypes.ObsResult[int64]{ - Err: errors.New("currentBlockNum exploded"), - }, - CurrentBlockHash: mercurytypes.ObsResult[[]byte]{ - Val: randBytes(32), - }, - CurrentBlockTimestamp: mercurytypes.ObsResult[uint64]{ - Val: rand.Uint64(), - }, - LatestBlocks: []v1.Block{ - v1.Block{Num: rand.Int63(), Hash: string(randBytes(32)), Ts: rand.Uint64()}, - }, - } - rp.dataSource = mockDataSource{obs} - - pbObs, err := rp.Observation(ctx, repts, previousReport) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(pbObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Equal(t, obs.BenchmarkPrice.Val, mustDecodeBigInt(p.BenchmarkPrice)) - assert.Equal(t, obs.Bid.Val, mustDecodeBigInt(p.Bid)) - assert.Zero(t, p.Ask) - assert.Zero(t, p.CurrentBlockNum) - assert.Equal(t, obs.CurrentBlockHash.Val, p.CurrentBlockHash) - assert.Equal(t, obs.CurrentBlockTimestamp.Val, p.CurrentBlockTimestamp) - assert.Equal(t, len(obs.LatestBlocks), len(p.LatestBlocks)) - for i := range obs.LatestBlocks { - assert.Equal(t, obs.LatestBlocks[i].Num, p.LatestBlocks[i].Num) - assert.Equal(t, []byte(obs.LatestBlocks[i].Hash), p.LatestBlocks[i].Hash) - assert.Equal(t, obs.LatestBlocks[i].Ts, p.LatestBlocks[i].Ts) - } - // since previousReport is not nil, maxFinalizedBlockNumber is skipped - assert.Zero(t, p.MaxFinalizedBlockNumber) - - assert.False(t, p.PricesValid) - assert.False(t, p.CurrentBlockValid) - // since previousReport is not nil, maxFinalizedBlockNumber is skipped - assert.False(t, p.MaxFinalizedBlockNumberValid) - }) - - t.Run("when encoding fails on some price observations", func(t *testing.T) { - obs := v1.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - // too large to encode - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - Bid: mercurytypes.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - Ask: mercurytypes.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - CurrentBlockNum: mercurytypes.ObsResult[int64]{ - Val: rand.Int63(), - }, - CurrentBlockHash: mercurytypes.ObsResult[[]byte]{ - Val: randBytes(32), - }, - CurrentBlockTimestamp: mercurytypes.ObsResult[uint64]{ - Val: rand.Uint64(), - }, - } - rp.dataSource = mockDataSource{obs} - - pbObs, err := rp.Observation(ctx, repts, previousReport) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(pbObs, &p)) - - assert.False(t, p.PricesValid) - assert.Zero(t, p.BenchmarkPrice) - assert.NotZero(t, p.Bid) - assert.NotZero(t, p.Ask) - }) - t.Run("when encoding fails on all price observations", func(t *testing.T) { - obs := v1.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - // too large to encode - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - Bid: mercurytypes.ObsResult[*big.Int]{ - // too large to encode - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - Ask: mercurytypes.ObsResult[*big.Int]{ - // too large to encode - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - CurrentBlockNum: mercurytypes.ObsResult[int64]{ - Val: rand.Int63(), - }, - CurrentBlockHash: mercurytypes.ObsResult[[]byte]{ - Val: randBytes(32), - }, - CurrentBlockTimestamp: mercurytypes.ObsResult[uint64]{ - Val: rand.Uint64(), - }, - } - rp.dataSource = mockDataSource{obs} - - pbObs, err := rp.Observation(ctx, repts, previousReport) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(pbObs, &p)) - - assert.False(t, p.PricesValid) - assert.Zero(t, p.BenchmarkPrice) - assert.Zero(t, p.Bid) - assert.Zero(t, p.Ask) - }) - }) - - t.Run("without previous report, includes maxFinalizedBlockNumber observation", func(t *testing.T) { - currentBlockNum := int64(rand.Int31()) - obs := v1.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - Bid: mercurytypes.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - Ask: mercurytypes.ObsResult[*big.Int]{ - Val: randBigInt(), - }, - CurrentBlockNum: mercurytypes.ObsResult[int64]{ - Val: currentBlockNum, - }, - CurrentBlockHash: mercurytypes.ObsResult[[]byte]{ - Val: randBytes(32), - }, - CurrentBlockTimestamp: mercurytypes.ObsResult[uint64]{ - Val: rand.Uint64(), - }, - MaxFinalizedBlockNumber: mercurytypes.ObsResult[int64]{ - Val: currentBlockNum - 42, - }, - } - rp.dataSource = mockDataSource{obs} - - pbObs, err := rp.Observation(ctx, repts, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(pbObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Equal(t, obs.BenchmarkPrice.Val, mustDecodeBigInt(p.BenchmarkPrice)) - assert.Equal(t, obs.Bid.Val, mustDecodeBigInt(p.Bid)) - assert.Equal(t, obs.Ask.Val, mustDecodeBigInt(p.Ask)) - assert.Equal(t, obs.CurrentBlockNum.Val, p.CurrentBlockNum) - assert.Equal(t, obs.CurrentBlockHash.Val, p.CurrentBlockHash) - assert.Equal(t, obs.CurrentBlockTimestamp.Val, p.CurrentBlockTimestamp) - assert.Equal(t, obs.MaxFinalizedBlockNumber.Val, p.MaxFinalizedBlockNumber) - - assert.True(t, p.PricesValid) - assert.True(t, p.CurrentBlockValid) - assert.True(t, p.MaxFinalizedBlockNumberValid) - }) -} - -func newAttributedObservation(t *testing.T, p *MercuryObservationProto) types.AttributedObservation { - marshalledObs, err := proto.Marshal(p) - require.NoError(t, err) - return types.AttributedObservation{ - Observation: types.Observation(marshalledObs), - Observer: commontypes.OracleID(42), - } -} - -func newUnparseableAttributedObservation() types.AttributedObservation { - return types.AttributedObservation{ - Observation: []byte{1, 2}, - Observer: commontypes.OracleID(42), - } -} - -func genRandHash(seed int64) []byte { - r := rand.New(rand.NewSource(seed)) - - b := make([]byte, 32) - _, err := r.Read(b) - if err != nil { - panic(err) - } - return b -} - -func newValidMercuryObservationProto() *MercuryObservationProto { - var latestBlocks []*BlockProto - for i := 0; i < MaxAllowedBlocks; i++ { - latestBlocks = append(latestBlocks, &BlockProto{Num: int64(49 - i), Hash: genRandHash(int64(i)), Ts: uint64(46 - i)}) - } - - return &MercuryObservationProto{ - Timestamp: 42, - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(43)), - Bid: mercury.MustEncodeValueInt192(big.NewInt(44)), - Ask: mercury.MustEncodeValueInt192(big.NewInt(45)), - PricesValid: true, - CurrentBlockNum: latestBlocks[0].Num, - CurrentBlockHash: latestBlocks[0].Hash, - CurrentBlockTimestamp: latestBlocks[0].Ts, - CurrentBlockValid: true, - LatestBlocks: latestBlocks, - MaxFinalizedBlockNumber: 47, - MaxFinalizedBlockNumberValid: true, - } -} - -func newInvalidMercuryObservationProto() *MercuryObservationProto { - return &MercuryObservationProto{ - PricesValid: false, - CurrentBlockValid: false, - MaxFinalizedBlockNumberValid: false, - } -} - -func Test_Plugin_parseAttributedObservation(t *testing.T) { - t.Run("with all valid values, and > 0 LatestBlocks", func(t *testing.T) { - obs := newValidMercuryObservationProto() - ao := newAttributedObservation(t, obs) - expectedLatestBlocks := make([]v1.Block, len(obs.LatestBlocks)) - for i, b := range obs.LatestBlocks { - expectedLatestBlocks[i] = v1.NewBlock(b.Num, b.Hash, b.Ts) - } - - pao, err := parseAttributedObservation(ao) - require.NoError(t, err) - - assert.Equal(t, - parsedAttributedObservation{ - Timestamp: 0x2a, - Observer: 0x2a, - BenchmarkPrice: big.NewInt(43), - Bid: big.NewInt(44), - Ask: big.NewInt(45), - PricesValid: true, - MaxFinalizedBlockNumber: 47, - MaxFinalizedBlockNumberValid: true, - LatestBlocks: expectedLatestBlocks, - }, - pao, - ) - t.Run("with 0 LatestBlocks", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.LatestBlocks = nil - ao := newAttributedObservation(t, obs) - - pao, err := parseAttributedObservation(ao) - require.NoError(t, err) - - assert.Equal(t, - parsedAttributedObservation{ - Timestamp: 0x2a, - Observer: 0x2a, - BenchmarkPrice: big.NewInt(43), - Bid: big.NewInt(44), - Ask: big.NewInt(45), - PricesValid: true, - CurrentBlockNum: 49, - CurrentBlockHash: obs.CurrentBlockHash, - CurrentBlockTimestamp: 46, - CurrentBlockValid: true, - MaxFinalizedBlockNumber: 47, - MaxFinalizedBlockNumberValid: true, - }, - pao, - ) - }) - }) - - t.Run("with all invalid values", func(t *testing.T) { - obs := newInvalidMercuryObservationProto() - ao := newAttributedObservation(t, obs) - - pao, err := parseAttributedObservation(ao) - assert.NoError(t, err) - - assert.Equal(t, - parsedAttributedObservation{ - Observer: 0x2a, - PricesValid: false, - CurrentBlockValid: false, - MaxFinalizedBlockNumberValid: false, - LatestBlocks: ([]v1.Block)(nil), - }, - pao, - ) - }) - - t.Run("when LatestBlocks is valid", func(t *testing.T) { - t.Run("sorts blocks if they are out of order", func(t *testing.T) { - obs := newValidMercuryObservationProto() - slices.Reverse(obs.LatestBlocks) - - ao := newAttributedObservation(t, obs) - - pao, err := parseAttributedObservation(ao) - assert.NoError(t, err) - - assert.Len(t, pao.GetLatestBlocks(), MaxAllowedBlocks) - assert.Equal(t, 49, int(pao.GetLatestBlocks()[0].Num)) - assert.Equal(t, 48, int(pao.GetLatestBlocks()[1].Num)) - assert.Equal(t, 47, int(pao.GetLatestBlocks()[2].Num)) - assert.Equal(t, 46, int(pao.GetLatestBlocks()[3].Num)) - assert.Equal(t, 45, int(pao.GetLatestBlocks()[4].Num)) - }) - }) - - t.Run("when LatestBlocks is invalid", func(t *testing.T) { - t.Run("contains duplicate block numbers", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.LatestBlocks = []*BlockProto{&BlockProto{Num: 32, Hash: randBytes(32)}, &BlockProto{Num: 32, Hash: randBytes(32)}} - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, "observation invalid for observer 42; got duplicate block number: 32") - }) - t.Run("contains duplicate block hashes", func(t *testing.T) { - obs := newValidMercuryObservationProto() - h := randBytes(32) - obs.LatestBlocks = []*BlockProto{&BlockProto{Num: 1, Hash: h}, &BlockProto{Num: 2, Hash: h}} - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, fmt.Sprintf("observation invalid for observer 42; got duplicate block hash: 0x%x", h)) - }) - t.Run("contains too many blocks", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.LatestBlocks = nil - for i := 0; i < MaxAllowedBlocks+1; i++ { - obs.LatestBlocks = append(obs.LatestBlocks, &BlockProto{Num: int64(i)}) - } - - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, fmt.Sprintf("LatestBlocks too large; got: %d, max: %d", MaxAllowedBlocks+1, MaxAllowedBlocks)) - }) - }) - - t.Run("with unparseable values", func(t *testing.T) { - t.Run("ao cannot be unmarshalled", func(t *testing.T) { - ao := newUnparseableAttributedObservation() - - _, err := parseAttributedObservation(ao) - require.Error(t, err) - assert.Contains(t, err.Error(), "attributed observation cannot be unmarshaled") - }) - t.Run("bad benchmark price", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.BenchmarkPrice = randBytes(16) - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, "benchmarkPrice cannot be converted to big.Int: expected b to have length 24, but got length 16") - }) - t.Run("bad bid", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.Bid = []byte{1} - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, "bid cannot be converted to big.Int: expected b to have length 24, but got length 1") - }) - t.Run("bad ask", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.Ask = []byte{1} - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, "ask cannot be converted to big.Int: expected b to have length 24, but got length 1") - }) - t.Run("bad block hash", func(t *testing.T) { - t.Run("CurrentBlockHash", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.LatestBlocks = nil - obs.CurrentBlockHash = []byte{1} - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, "wrong len for hash: 1 (expected: 32)") - }) - - t.Run("LatestBlocks", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.LatestBlocks[0].Hash = []byte{1} - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, "wrong len for hash: 1 (expected: 32)") - }) - }) - t.Run("negative block number", func(t *testing.T) { - t.Run("CurrentBlockNum", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.LatestBlocks = nil - obs.CurrentBlockNum = -1 - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, "negative block number: -1") - }) - t.Run("LatestBlocks", func(t *testing.T) { - obs := newValidMercuryObservationProto() - obs.LatestBlocks[0].Num = -1 - ao := newAttributedObservation(t, obs) - - _, err := parseAttributedObservation(ao) - assert.EqualError(t, err, "negative block number: -1") - }) - }) - }) -} - -func Test_Plugin_Report(t *testing.T) { - repts := types.ReportTimestamp{} - - t.Run("when previous report is nil", func(t *testing.T) { - codec := &testReportCodec{ - currentBlock: int64(rand.Int31()), - builtReport: []byte{1, 2, 3, 4}, - } - rp := newReportingPlugin(t, codec) - - t.Run("errors if not enough attributed observations", func(t *testing.T) { - _, _, err := rp.Report(tests.Context(t), repts, nil, []types.AttributedObservation{}) - assert.EqualError(t, err, "got zero valid attributed observations") - }) - t.Run("succeeds, ignoring unparseable attributed observations", func(t *testing.T) { - aos := []types.AttributedObservation{ - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newUnparseableAttributedObservation(), - } - should, report, err := rp.Report(tests.Context(t), repts, nil, aos) - - assert.NoError(t, err) - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - }) - t.Run("succeeds and generates validFromBlockNum from maxFinalizedBlockNumber", func(t *testing.T) { - codec.reset() - - aos := []types.AttributedObservation{ - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - } - should, report, err := rp.Report(tests.Context(t), repts, nil, aos) - - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - assert.NoError(t, err) - - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, 48, int(codec.builtReportFields.ValidFromBlockNum)) - }) - t.Run("errors if cannot get consensus maxFinalizedBlockNumber", func(t *testing.T) { - obs := []*MercuryObservationProto{ - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - } - for i := range obs { - obs[i].MaxFinalizedBlockNumber = int64(i) - } - aos := []types.AttributedObservation{ - newAttributedObservation(t, obs[0]), - newAttributedObservation(t, obs[1]), - newAttributedObservation(t, obs[2]), - newAttributedObservation(t, obs[3]), - } - _, _, err := rp.Report(tests.Context(t), repts, nil, aos) - - assert.EqualError(t, err, "no valid maxFinalizedBlockNumber with at least f+1 votes (got counts: map[0:1 1:1 2:1 3:1], f=1)") - }) - t.Run("errors if it cannot come to consensus about currentBlockNum", func(t *testing.T) { - obs := []*MercuryObservationProto{ - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - } - for i := range obs { - obs[i].LatestBlocks = nil - obs[i].CurrentBlockNum = int64(i) - } - aos := []types.AttributedObservation{ - newAttributedObservation(t, obs[0]), - newAttributedObservation(t, obs[1]), - newAttributedObservation(t, obs[2]), - newAttributedObservation(t, obs[3]), - } - _, _, err := rp.Report(tests.Context(t), repts, nil, aos) - - require.Error(t, err) - assert.Contains(t, err.Error(), "GetConsensusCurrentBlock failed: cannot come to consensus on latest block number") - }) - t.Run("errors if it cannot come to consensus on LatestBlocks", func(t *testing.T) { - obs := []*MercuryObservationProto{ - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - } - for i := range obs { - for j := range obs[i].LatestBlocks { - obs[i].LatestBlocks[j].Hash = randBytes(32) - } - } - aos := []types.AttributedObservation{ - newAttributedObservation(t, obs[0]), - newAttributedObservation(t, obs[1]), - newAttributedObservation(t, obs[2]), - newAttributedObservation(t, obs[3]), - } - _, _, err := rp.Report(tests.Context(t), repts, nil, aos) - - require.Error(t, err) - assert.Contains(t, err.Error(), "GetConsensusCurrentBlock failed: cannot come to consensus on latest block number") - }) - t.Run("errors if price is invalid", func(t *testing.T) { - obs := []*MercuryObservationProto{ - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - newValidMercuryObservationProto(), - } - for i := range obs { - obs[i].BenchmarkPrice = mercury.MustEncodeValueInt192(big.NewInt(-1)) // benchmark price below min of 0, cannot report - } - aos := []types.AttributedObservation{ - newAttributedObservation(t, obs[0]), - newAttributedObservation(t, obs[1]), - newAttributedObservation(t, obs[2]), - newAttributedObservation(t, obs[3]), - } - should, report, err := rp.Report(tests.Context(t), repts, nil, aos) - - assert.False(t, should) - assert.Nil(t, report) - assert.EqualError(t, err, "median benchmark price (Value: -1) is outside of allowable range (Min: 0, Max: 1000)") - }) - t.Run("BuildReport failures", func(t *testing.T) { - t.Run("errors if BuildReport returns error", func(t *testing.T) { - codec.buildReportShouldFail = true - defer codec.reset() - - aos := []types.AttributedObservation{ - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newUnparseableAttributedObservation(), - } - _, _, err := rp.Report(tests.Context(t), repts, nil, aos) - - assert.EqualError(t, err, "buildReportShouldFail=true") - }) - t.Run("errors if BuildReport returns a report that is too long", func(t *testing.T) { - codec.builtReport = randBytes(9999) - aos := []types.AttributedObservation{ - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newUnparseableAttributedObservation(), - } - _, _, err := rp.Report(tests.Context(t), repts, nil, aos) - - assert.EqualError(t, err, "report with len 9999 violates MaxReportLength limit set by ReportCodec (1248)") - }) - t.Run("errors if BuildReport returns a report that is too short", func(t *testing.T) { - codec.builtReport = []byte{} - aos := []types.AttributedObservation{ - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newUnparseableAttributedObservation(), - } - _, _, err := rp.Report(tests.Context(t), repts, nil, aos) - - assert.EqualError(t, err, "report may not have zero length (invariant violation)") - }) - }) - }) - t.Run("when previous report is present", func(t *testing.T) { - codec := &testReportCodec{ - currentBlock: int64(rand.Int31()), - builtReport: []byte{1, 2, 3, 4}, - } - rp := newReportingPlugin(t, codec) - previousReport := types.Report{} - - t.Run("succeeds and uses block number in previous report if valid", func(t *testing.T) { - currentBlock := int64(32) - codec.currentBlock = currentBlock - - aos := []types.AttributedObservation{ - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - } - should, report, err := rp.Report(tests.Context(t), repts, previousReport, aos) - - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - assert.NoError(t, err) - - require.NotNil(t, codec.builtReportFields) - // current block of previous report + 1 is the validFromBlockNum of current report - assert.Equal(t, 33, int(codec.builtReportFields.ValidFromBlockNum)) - }) - t.Run("errors if cannot extract block number from previous report", func(t *testing.T) { - codec.currentBlockErr = errors.New("test error current block fail") - - aos := []types.AttributedObservation{ - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - } - should, _, err := rp.Report(tests.Context(t), repts, previousReport, aos) - - assert.False(t, should) - assert.EqualError(t, err, "test error current block fail") - }) - t.Run("does not report if currentBlockNum < validFromBlockNum", func(t *testing.T) { - codec.currentBlock = 49 // means that validFromBlockNum=50 which is > currentBlockNum of 49 - codec.currentBlockErr = nil - - aos := []types.AttributedObservation{ - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - newAttributedObservation(t, newValidMercuryObservationProto()), - } - should, _, err := rp.Report(tests.Context(t), repts, previousReport, aos) - - assert.False(t, should) - assert.NoError(t, err) - }) - }) -} - -func Test_MaxObservationLength(t *testing.T) { - t.Run("maximally sized pbuf does not exceed maxObservationLength", func(t *testing.T) { - maxInt192Bytes := make([]byte, 24) - for i := 0; i < 24; i++ { - maxInt192Bytes[i] = 255 - } - maxHash := make([]byte, 32) - for i := 0; i < 32; i++ { - maxHash[i] = 255 - } - maxLatestBlocks := []*BlockProto{} - for i := 0; i < MaxAllowedBlocks; i++ { - maxLatestBlocks = append(maxLatestBlocks, &BlockProto{Num: math.MaxInt64, Hash: maxHash, Ts: math.MaxUint64}) - } - obs := MercuryObservationProto{ - Timestamp: math.MaxUint32, - BenchmarkPrice: maxInt192Bytes, - Bid: maxInt192Bytes, - Ask: maxInt192Bytes, - PricesValid: true, - CurrentBlockNum: math.MaxInt64, - CurrentBlockHash: maxHash, - CurrentBlockTimestamp: math.MaxUint64, - CurrentBlockValid: true, - MaxFinalizedBlockNumber: math.MaxInt64, - MaxFinalizedBlockNumberValid: true, - LatestBlocks: maxLatestBlocks, - } - // This assertion is here to force this test to fail if a new field is - // added to the protobuf. In this case, you must add the max value of - // the field to the MercuryObservationProto in the test and only after - // that increment the count below - numFields := reflect.TypeOf(obs).NumField() //nolint:all - // 3 fields internal to pbuf struct - require.Equal(t, 12, numFields-3) - - // the actual test - b, err := proto.Marshal(&obs) - require.NoError(t, err) - assert.LessOrEqual(t, len(b), maxObservationLength) - }) -} diff --git a/mercury/v1/observation.go b/mercury/v1/observation.go deleted file mode 100644 index 3f8c064..0000000 --- a/mercury/v1/observation.go +++ /dev/null @@ -1,131 +0,0 @@ -package v1 - -import ( - "math/big" - - "github.com/smartcontractkit/libocr/commontypes" - - v1 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v1" - - "github.com/smartcontractkit/chainlink-data-streams/mercury" -) - -type PAO interface { - mercury.PAO - - GetBid() (*big.Int, bool) - GetAsk() (*big.Int, bool) - - // DEPRECATED - // TODO: Remove this handling after deployment (https://smartcontract-it.atlassian.net/browse/MERC-2272) - GetCurrentBlockNum() (int64, bool) - GetCurrentBlockHash() ([]byte, bool) - GetCurrentBlockTimestamp() (uint64, bool) - - GetLatestBlocks() []v1.Block - GetMaxFinalizedBlockNumber() (int64, bool) -} - -var _ PAO = parsedAttributedObservation{} - -type parsedAttributedObservation struct { - Timestamp uint32 - Observer commontypes.OracleID - - BenchmarkPrice *big.Int - Bid *big.Int - Ask *big.Int - // All three prices must be valid, or none are (they all should come from one API query and hold invariant bid <= bm <= ask) - PricesValid bool - - // DEPRECATED - // TODO: Remove this handling after deployment (https://smartcontract-it.atlassian.net/browse/MERC-2272) - CurrentBlockNum int64 // inclusive; current block - CurrentBlockHash []byte - CurrentBlockTimestamp uint64 - // All three block observations must be valid, or none are (they all come from the same block) - CurrentBlockValid bool - - LatestBlocks []v1.Block - - // MaxFinalizedBlockNumber comes from previous report when present and is - // only observed from mercury server when previous report is nil - // - // MaxFinalizedBlockNumber will be -1 if there is none - MaxFinalizedBlockNumber int64 - MaxFinalizedBlockNumberValid bool -} - -func NewParsedAttributedObservation(ts uint32, observer commontypes.OracleID, bp *big.Int, bid *big.Int, ask *big.Int, pricesValid bool, - currentBlockNum int64, currentBlockHash []byte, currentBlockTimestamp uint64, currentBlockValid bool, - maxFinalizedBlockNumber int64, maxFinalizedBlockNumberValid bool) PAO { - return parsedAttributedObservation{ - Timestamp: ts, - Observer: observer, - - BenchmarkPrice: bp, - Bid: bid, - Ask: ask, - PricesValid: pricesValid, - - CurrentBlockNum: currentBlockNum, - CurrentBlockHash: currentBlockHash, - CurrentBlockTimestamp: currentBlockTimestamp, - CurrentBlockValid: currentBlockValid, - - MaxFinalizedBlockNumber: maxFinalizedBlockNumber, - MaxFinalizedBlockNumberValid: maxFinalizedBlockNumberValid, - } -} - -func (pao parsedAttributedObservation) GetTimestamp() uint32 { - return pao.Timestamp -} - -func (pao parsedAttributedObservation) GetObserver() commontypes.OracleID { - return pao.Observer -} - -func (pao parsedAttributedObservation) GetBenchmarkPrice() (*big.Int, bool) { - return pao.BenchmarkPrice, pao.PricesValid -} - -func (pao parsedAttributedObservation) GetBid() (*big.Int, bool) { - return pao.Bid, pao.PricesValid -} - -func (pao parsedAttributedObservation) GetAsk() (*big.Int, bool) { - return pao.Ask, pao.PricesValid -} - -func (pao parsedAttributedObservation) GetCurrentBlockNum() (int64, bool) { - return pao.CurrentBlockNum, pao.CurrentBlockValid -} - -func (pao parsedAttributedObservation) GetCurrentBlockHash() ([]byte, bool) { - return pao.CurrentBlockHash, pao.CurrentBlockValid -} - -func (pao parsedAttributedObservation) GetCurrentBlockTimestamp() (uint64, bool) { - return pao.CurrentBlockTimestamp, pao.CurrentBlockValid -} - -func (pao parsedAttributedObservation) GetLatestBlocks() []v1.Block { - return pao.LatestBlocks -} - -func (pao parsedAttributedObservation) GetMaxFinalizedBlockNumber() (int64, bool) { - return pao.MaxFinalizedBlockNumber, pao.MaxFinalizedBlockNumberValid -} - -func (pao parsedAttributedObservation) GetMaxFinalizedTimestamp() (uint32, bool) { - panic("current observation doesn't contain the field") -} - -func (pao parsedAttributedObservation) GetLinkFee() (*big.Int, bool) { - panic("current observation doesn't contain the field") -} - -func (pao parsedAttributedObservation) GetNativeFee() (*big.Int, bool) { - panic("current observation doesn't contain the field") -} diff --git a/mercury/v1/validation.go b/mercury/v1/validation.go deleted file mode 100644 index 79574b1..0000000 --- a/mercury/v1/validation.go +++ /dev/null @@ -1,28 +0,0 @@ -package v1 - -import ( - "fmt" - - v1 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v1" - - "github.com/smartcontractkit/chainlink-data-streams/mercury" -) - -// ValidateCurrentBlock sanity checks number and hash -func ValidateCurrentBlock(rf v1.ReportFields) error { - if rf.ValidFromBlockNum < 0 { - return fmt.Errorf("validFromBlockNum must be >= 0 (got: %d)", rf.ValidFromBlockNum) - } - if rf.CurrentBlockNum < 0 { - return fmt.Errorf("currentBlockNum must be >= 0 (got: %d)", rf.ValidFromBlockNum) - } - if rf.ValidFromBlockNum > rf.CurrentBlockNum { - return fmt.Errorf("validFromBlockNum (Value: %d) must be less than or equal to CurrentBlockNum (Value: %d)", rf.ValidFromBlockNum, rf.CurrentBlockNum) - } - // NOTE: hardcoded ethereum hash - if len(rf.CurrentBlockHash) != mercury.EvmHashLen { - return fmt.Errorf("invalid length for hash; expected %d (got: %d)", mercury.EvmHashLen, len(rf.CurrentBlockHash)) - } - - return nil -} diff --git a/mercury/v1/validation_test.go b/mercury/v1/validation_test.go deleted file mode 100644 index 68c4b89..0000000 --- a/mercury/v1/validation_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package v1 - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - v1 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v1" -) - -func TestValidation(t *testing.T) { - rf := v1.ReportFields{ - CurrentBlockHash: make([]byte, 32), - } - - t.Run("ValidateCurrentBlock", func(t *testing.T) { - t.Run("succeeds when validFromBlockNum < current block num", func(t *testing.T) { - rf.ValidFromBlockNum = 16634363 - rf.CurrentBlockNum = 16634364 - err := ValidateCurrentBlock(rf) - assert.NoError(t, err) - }) - t.Run("succeeds when validFromBlockNum is equal to current block number", func(t *testing.T) { - rf.ValidFromBlockNum = 16634364 - rf.CurrentBlockNum = 16634364 - err := ValidateCurrentBlock(rf) - assert.NoError(t, err) - }) - t.Run("zero is ok", func(t *testing.T) { - rf.ValidFromBlockNum = 0 - rf.CurrentBlockNum = 0 - err := ValidateCurrentBlock(rf) - assert.NoError(t, err) - }) - t.Run("errors when validFromBlockNum number < 0", func(t *testing.T) { - rf.ValidFromBlockNum = -1 - rf.CurrentBlockNum = -1 - err := ValidateCurrentBlock(rf) - assert.EqualError(t, err, "validFromBlockNum must be >= 0 (got: -1)") - }) - t.Run("errors when validFrom > block number", func(t *testing.T) { - rf.CurrentBlockNum = 1 - rf.ValidFromBlockNum = 16634366 - err := ValidateCurrentBlock(rf) - assert.EqualError(t, err, "validFromBlockNum (Value: 16634366) must be less than or equal to CurrentBlockNum (Value: 1)") - }) - t.Run("errors when validFrom < 0", func(t *testing.T) { - rf.ValidFromBlockNum = -1 - err := ValidateCurrentBlock(rf) - assert.EqualError(t, err, "validFromBlockNum must be >= 0 (got: -1)") - }) - t.Run("errors when hash has incorrect length", func(t *testing.T) { - rf.ValidFromBlockNum = 16634363 - rf.CurrentBlockNum = 16634364 - rf.CurrentBlockHash = []byte{} - err := ValidateCurrentBlock(rf) - assert.EqualError(t, err, "invalid length for hash; expected 32 (got: 0)") - rf.CurrentBlockHash = make([]byte, 64) - err = ValidateCurrentBlock(rf) - assert.EqualError(t, err, "invalid length for hash; expected 32 (got: 64)") - }) - }) -} diff --git a/mercury/v2/mercury.go b/mercury/v2/mercury.go deleted file mode 100644 index 8eb2e1e..0000000 --- a/mercury/v2/mercury.go +++ /dev/null @@ -1,405 +0,0 @@ -package v2 - -import ( - "context" - "errors" - "fmt" - "math" - "math/big" - "time" - - "google.golang.org/protobuf/proto" - - "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - mercurytypes "github.com/smartcontractkit/chainlink-common/pkg/types/mercury" - v2 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v2" - - "github.com/smartcontractkit/chainlink-data-streams/mercury" -) - -// DataSource implementations must be thread-safe. Observe may be called by many -// different threads concurrently. -type DataSource interface { - // Observe queries the data source. Returns a value or an error. Once the - // context is expires, Observe may still do cheap computations and return a - // result, but should return as quickly as possible. - // - // More details: In the current implementation, the context passed to - // Observe will time out after MaxDurationObservation. However, Observe - // should *not* make any assumptions about context timeout behavior. Once - // the context times out, Observe should prioritize returning as quickly as - // possible, but may still perform fast computations to return a result - // rather than error. For example, if Observe medianizes a number of data - // sources, some of which already returned a result to Observe prior to the - // context's expiry, Observe might still compute their median, and return it - // instead of an error. - // - // Important: Observe should not perform any potentially time-consuming - // actions like database access, once the context passed has expired. - Observe(ctx context.Context, repts types.ReportTimestamp, fetchMaxFinalizedTimestamp bool) (v2.Observation, error) -} - -var _ ocr3types.MercuryPluginFactory = Factory{} - -const maxObservationLength = 32 + // feedID - 4 + // timestamp - mercury.ByteWidthInt192 + // benchmarkPrice - 4 + // validFromTimestamp - mercury.ByteWidthInt192 + // linkFee - mercury.ByteWidthInt192 + // nativeFee - 16 /* overapprox. of protobuf overhead */ - -type Factory struct { - dataSource DataSource - logger logger.Logger - onchainConfigCodec mercurytypes.OnchainConfigCodec - reportCodec v2.ReportCodec -} - -func NewFactory(ds DataSource, lggr logger.Logger, occ mercurytypes.OnchainConfigCodec, rc v2.ReportCodec) Factory { - return Factory{ds, lggr, occ, rc} -} - -func (fac Factory) NewMercuryPlugin(ctx context.Context, configuration ocr3types.MercuryPluginConfig) (ocr3types.MercuryPlugin, ocr3types.MercuryPluginInfo, error) { - offchainConfig, err := mercury.DecodeOffchainConfig(configuration.OffchainConfig) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - onchainConfig, err := fac.onchainConfigCodec.Decode(ctx, configuration.OnchainConfig) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - maxReportLength, err := fac.reportCodec.MaxReportLength(ctx, configuration.N) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - r := &reportingPlugin{ - offchainConfig, - onchainConfig, - fac.dataSource, - fac.logger, - fac.reportCodec, - configuration.ConfigDigest, - configuration.F, - mercury.EpochRound{}, - new(big.Int), - maxReportLength, - } - - return r, ocr3types.MercuryPluginInfo{ - Name: "Mercury", - Limits: ocr3types.MercuryPluginLimits{ - MaxObservationLength: maxObservationLength, - MaxReportLength: maxReportLength, - }, - }, nil -} - -var _ ocr3types.MercuryPlugin = (*reportingPlugin)(nil) - -type reportingPlugin struct { - offchainConfig mercury.OffchainConfig - onchainConfig mercurytypes.OnchainConfig - dataSource DataSource - logger logger.Logger - reportCodec v2.ReportCodec - - configDigest types.ConfigDigest - f int - latestAcceptedEpochRound mercury.EpochRound - latestAcceptedMedian *big.Int - maxReportLength int -} - -var MissingPrice = big.NewInt(-1) - -func (rp *reportingPlugin) Observation(ctx context.Context, repts types.ReportTimestamp, previousReport types.Report) (types.Observation, error) { - obs, err := rp.dataSource.Observe(ctx, repts, previousReport == nil) - if err != nil { - return nil, fmt.Errorf("DataSource.Observe returned an error: %s", err) - } - - observationTimestamp := time.Now() - if observationTimestamp.Unix() > math.MaxUint32 { - return nil, fmt.Errorf("current unix epoch %d exceeds max uint32", observationTimestamp.Unix()) - } - p := MercuryObservationProto{Timestamp: uint32(observationTimestamp.Unix())} - var obsErrors []error - - var bpErr error - if obs.BenchmarkPrice.Err != nil { - bpErr = fmt.Errorf("failed to observe BenchmarkPrice: %w", obs.BenchmarkPrice.Err) - obsErrors = append(obsErrors, bpErr) - } else if benchmarkPrice, err := mercury.EncodeValueInt192(obs.BenchmarkPrice.Val); err != nil { - bpErr = fmt.Errorf("failed to encode BenchmarkPrice; val=%s: %w", obs.BenchmarkPrice.Val, err) - obsErrors = append(obsErrors, bpErr) - } else { - p.BenchmarkPrice = benchmarkPrice - p.PricesValid = true - } - - var maxFinalizedTimestampErr error - if obs.MaxFinalizedTimestamp.Err != nil { - maxFinalizedTimestampErr = fmt.Errorf("failed to observe MaxFinalizedTimestamp: %w", obs.MaxFinalizedTimestamp.Err) - obsErrors = append(obsErrors, maxFinalizedTimestampErr) - } else { - p.MaxFinalizedTimestamp = obs.MaxFinalizedTimestamp.Val - p.MaxFinalizedTimestampValid = true - } - - var linkErr error - if obs.LinkPrice.Err != nil { - linkErr = fmt.Errorf("failed to observe LINK price: %w", obs.LinkPrice.Err) - obsErrors = append(obsErrors, linkErr) - } else if obs.LinkPrice.Val.Cmp(MissingPrice) <= 0 { - p.LinkFee = mercury.MaxInt192Enc - } else { - linkFee := mercury.CalculateFee(obs.LinkPrice.Val, rp.offchainConfig.BaseUSDFee) - if linkFeeEncoded, err := mercury.EncodeValueInt192(linkFee); err != nil { - linkErr = fmt.Errorf("failed to encode LINK fee; val=%s: %w", linkFee, err) - obsErrors = append(obsErrors, linkErr) - } else { - p.LinkFee = linkFeeEncoded - } - } - - if linkErr == nil { - p.LinkFeeValid = true - } - - var nativeErr error - if obs.NativePrice.Err != nil { - nativeErr = fmt.Errorf("failed to observe native price: %w", obs.NativePrice.Err) - obsErrors = append(obsErrors, nativeErr) - } else if obs.NativePrice.Val.Cmp(MissingPrice) <= 0 { - p.NativeFee = mercury.MaxInt192Enc - } else { - nativeFee := mercury.CalculateFee(obs.NativePrice.Val, rp.offchainConfig.BaseUSDFee) - if nativeFeeEncoded, err := mercury.EncodeValueInt192(nativeFee); err != nil { - nativeErr = fmt.Errorf("failed to encode native fee; val=%s: %w", nativeFee, err) - obsErrors = append(obsErrors, nativeErr) - } else { - p.NativeFee = nativeFeeEncoded - } - } - - if nativeErr == nil { - p.NativeFeeValid = true - } - - if len(obsErrors) > 0 { - rp.logger.Warnw(fmt.Sprintf("Observe failed %d/4 observations", len(obsErrors)), "err", errors.Join(obsErrors...)) - } - - return proto.Marshal(&p) -} - -func parseAttributedObservation(ao types.AttributedObservation) (PAO, error) { - var pao parsedAttributedObservation - var obs MercuryObservationProto - if err := proto.Unmarshal(ao.Observation, &obs); err != nil { - return parsedAttributedObservation{}, fmt.Errorf("attributed observation cannot be unmarshaled: %s", err) - } - - pao.Timestamp = obs.Timestamp - pao.Observer = ao.Observer - - if obs.PricesValid { - var err error - pao.BenchmarkPrice, err = mercury.DecodeValueInt192(obs.BenchmarkPrice) - if err != nil { - return parsedAttributedObservation{}, fmt.Errorf("benchmarkPrice cannot be converted to big.Int: %s", err) - } - pao.PricesValid = true - } - - if obs.MaxFinalizedTimestampValid { - pao.MaxFinalizedTimestamp = obs.MaxFinalizedTimestamp - pao.MaxFinalizedTimestampValid = true - } - - if obs.LinkFeeValid { - var err error - pao.LinkFee, err = mercury.DecodeValueInt192(obs.LinkFee) - if err != nil { - return parsedAttributedObservation{}, fmt.Errorf("link price cannot be converted to big.Int: %s", err) - } - pao.LinkFeeValid = true - } - if obs.NativeFeeValid { - var err error - pao.NativeFee, err = mercury.DecodeValueInt192(obs.NativeFee) - if err != nil { - return parsedAttributedObservation{}, fmt.Errorf("native price cannot be converted to big.Int: %s", err) - } - pao.NativeFeeValid = true - } - - return pao, nil -} - -func parseAttributedObservations(lggr logger.Logger, aos []types.AttributedObservation) []PAO { - paos := make([]PAO, 0, len(aos)) - for i, ao := range aos { - pao, err := parseAttributedObservation(ao) - if err != nil { - lggr.Warnw("parseAttributedObservations: dropping invalid observation", - "observer", ao.Observer, - "error", err, - "i", i, - ) - continue - } - paos = append(paos, pao) - } - return paos -} - -func (rp *reportingPlugin) Report(ctx context.Context, repts types.ReportTimestamp, previousReport types.Report, aos []types.AttributedObservation) (shouldReport bool, report types.Report, err error) { - paos := parseAttributedObservations(rp.logger, aos) - - if len(paos) == 0 { - return false, nil, errors.New("got zero valid attributed observations") - } - - // By assumption, we have at most f malicious oracles, so there should be at least f+1 valid paos - if !(rp.f+1 <= len(paos)) { - return false, nil, fmt.Errorf("only received %v valid attributed observations, but need at least f+1 (%v)", len(paos), rp.f+1) - } - - rf, err := rp.buildReportFields(ctx, previousReport, paos) - if err != nil { - rp.logger.Errorw("failed to build report fields", "paos", paos, "f", rp.f, "reportFields", rf, "repts", repts, "err", err) - return false, nil, err - } - - if rf.Timestamp < rf.ValidFromTimestamp { - rp.logger.Debugw("shouldReport: no (overlap)", "observationTimestamp", rf.Timestamp, "validFromTimestamp", rf.ValidFromTimestamp, "repts", repts) - return false, nil, nil - } - - if err = rp.validateReport(rf); err != nil { - rp.logger.Errorw("shouldReport: no (validation error)", "reportFields", rf, "err", err, "repts", repts, "paos", paos) - return false, nil, err - } - rp.logger.Debugw("shouldReport: yes", "repts", repts) - - report, err = rp.reportCodec.BuildReport(ctx, rf) - if err != nil { - rp.logger.Debugw("failed to BuildReport", "paos", paos, "f", rp.f, "reportFields", rf, "repts", repts) - return false, nil, err - } - - if !(len(report) <= rp.maxReportLength) { - return false, nil, fmt.Errorf("report with len %d violates MaxReportLength limit set by ReportCodec (%d)", len(report), rp.maxReportLength) - } else if len(report) == 0 { - return false, nil, errors.New("report may not have zero length (invariant violation)") - } - - return true, report, nil -} - -func (rp *reportingPlugin) buildReportFields(ctx context.Context, previousReport types.Report, paos []PAO) (rf v2.ReportFields, merr error) { - mPaos := convert(paos) - rf.Timestamp = mercury.GetConsensusTimestamp(mPaos) - - var err error - if previousReport != nil { - var maxFinalizedTimestamp uint32 - maxFinalizedTimestamp, err = rp.reportCodec.ObservationTimestampFromReport(ctx, previousReport) - merr = errors.Join(merr, err) - rf.ValidFromTimestamp = maxFinalizedTimestamp + 1 - } else { - var maxFinalizedTimestamp int64 - maxFinalizedTimestamp, err = mercury.GetConsensusMaxFinalizedTimestamp(convertMaxFinalizedTimestamp(paos), rp.f) - if err != nil { - merr = errors.Join(merr, err) - } else if maxFinalizedTimestamp < 0 { - // no previous observation timestamp available, e.g. in case of new - // feed; use current timestamp as start of range - rf.ValidFromTimestamp = rf.Timestamp - } else if maxFinalizedTimestamp+1 > math.MaxUint32 { - merr = errors.Join(err, fmt.Errorf("maxFinalizedTimestamp is too large, got: %d", maxFinalizedTimestamp)) - } else { - rf.ValidFromTimestamp = uint32(maxFinalizedTimestamp + 1) - } - } - - rf.BenchmarkPrice, err = mercury.GetConsensusBenchmarkPrice(mPaos, rp.f) - if err != nil { - merr = errors.Join(merr, fmt.Errorf("GetConsensusBenchmarkPrice failed: %w", err)) - } - - rf.LinkFee, err = mercury.GetConsensusLinkFee(convertLinkFee(paos), rp.f) - if err != nil { - // It is better to generate a report that will validate for free, - // rather than no report at all, if we cannot come to consensus on a - // valid fee. - rp.logger.Errorw("Cannot come to consensus on LINK fee, falling back to 0", "err", err, "paos", paos) - rf.LinkFee = big.NewInt(0) - } - - rf.NativeFee, err = mercury.GetConsensusNativeFee(convertNativeFee(paos), rp.f) - if err != nil { - // It is better to generate a report that will validate for free, - // rather than no report at all, if we cannot come to consensus on a - // valid fee. - rp.logger.Errorw("Cannot come to consensus on Native fee, falling back to 0", "err", err, "paos", paos) - rf.NativeFee = big.NewInt(0) - } - - if int64(rf.Timestamp)+int64(rp.offchainConfig.ExpirationWindow) > math.MaxUint32 { - merr = errors.Join(merr, fmt.Errorf("timestamp %d + expiration window %d overflows uint32", rf.Timestamp, rp.offchainConfig.ExpirationWindow)) - } else { - rf.ExpiresAt = rf.Timestamp + rp.offchainConfig.ExpirationWindow - } - - return rf, merr -} - -func (rp *reportingPlugin) validateReport(rf v2.ReportFields) error { - return errors.Join( - mercury.ValidateBetween("median benchmark price", rf.BenchmarkPrice, rp.onchainConfig.Min, rp.onchainConfig.Max), - mercury.ValidateFee("median link fee", rf.LinkFee), - mercury.ValidateFee("median native fee", rf.NativeFee), - mercury.ValidateValidFromTimestamp(rf.Timestamp, rf.ValidFromTimestamp), - mercury.ValidateExpiresAt(rf.Timestamp, rf.ExpiresAt), - ) -} - -func (rp *reportingPlugin) Close() error { - return nil -} - -// convert funcs are necessary because go is not smart enough to cast -// []interface1 to []interface2 even if interface1 is a superset of interface2 -func convert(pao []PAO) (ret []mercury.PAO) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertMaxFinalizedTimestamp(pao []PAO) (ret []mercury.PAOMaxFinalizedTimestamp) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertLinkFee(pao []PAO) (ret []mercury.PAOLinkFee) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertNativeFee(pao []PAO) (ret []mercury.PAONativeFee) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} diff --git a/mercury/v2/mercury_observation_v2.pb.go b/mercury/v2/mercury_observation_v2.pb.go deleted file mode 100644 index 985b69f..0000000 --- a/mercury/v2/mercury_observation_v2.pb.go +++ /dev/null @@ -1,229 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.31.0 -// protoc v4.23.4 -// source: mercury_observation_v2.proto - -package v2 - -import ( - "reflect" - "sync" - - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/runtime/protoimpl" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type MercuryObservationProto struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Timestamp uint32 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - BenchmarkPrice []byte `protobuf:"bytes,2,opt,name=benchmarkPrice,proto3" json:"benchmarkPrice,omitempty"` - PricesValid bool `protobuf:"varint,3,opt,name=pricesValid,proto3" json:"pricesValid,omitempty"` - MaxFinalizedTimestamp int64 `protobuf:"varint,4,opt,name=maxFinalizedTimestamp,proto3" json:"maxFinalizedTimestamp,omitempty"` - MaxFinalizedTimestampValid bool `protobuf:"varint,5,opt,name=maxFinalizedTimestampValid,proto3" json:"maxFinalizedTimestampValid,omitempty"` - LinkFee []byte `protobuf:"bytes,6,opt,name=linkFee,proto3" json:"linkFee,omitempty"` - LinkFeeValid bool `protobuf:"varint,7,opt,name=linkFeeValid,proto3" json:"linkFeeValid,omitempty"` - NativeFee []byte `protobuf:"bytes,8,opt,name=nativeFee,proto3" json:"nativeFee,omitempty"` - NativeFeeValid bool `protobuf:"varint,9,opt,name=nativeFeeValid,proto3" json:"nativeFeeValid,omitempty"` -} - -func (x *MercuryObservationProto) Reset() { - *x = MercuryObservationProto{} - if protoimpl.UnsafeEnabled { - mi := &file_mercury_observation_v2_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *MercuryObservationProto) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*MercuryObservationProto) ProtoMessage() {} - -func (x *MercuryObservationProto) ProtoReflect() protoreflect.Message { - mi := &file_mercury_observation_v2_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use MercuryObservationProto.ProtoReflect.Descriptor instead. -func (*MercuryObservationProto) Descriptor() ([]byte, []int) { - return file_mercury_observation_v2_proto_rawDescGZIP(), []int{0} -} - -func (x *MercuryObservationProto) GetTimestamp() uint32 { - if x != nil { - return x.Timestamp - } - return 0 -} - -func (x *MercuryObservationProto) GetBenchmarkPrice() []byte { - if x != nil { - return x.BenchmarkPrice - } - return nil -} - -func (x *MercuryObservationProto) GetPricesValid() bool { - if x != nil { - return x.PricesValid - } - return false -} - -func (x *MercuryObservationProto) GetMaxFinalizedTimestamp() int64 { - if x != nil { - return x.MaxFinalizedTimestamp - } - return 0 -} - -func (x *MercuryObservationProto) GetMaxFinalizedTimestampValid() bool { - if x != nil { - return x.MaxFinalizedTimestampValid - } - return false -} - -func (x *MercuryObservationProto) GetLinkFee() []byte { - if x != nil { - return x.LinkFee - } - return nil -} - -func (x *MercuryObservationProto) GetLinkFeeValid() bool { - if x != nil { - return x.LinkFeeValid - } - return false -} - -func (x *MercuryObservationProto) GetNativeFee() []byte { - if x != nil { - return x.NativeFee - } - return nil -} - -func (x *MercuryObservationProto) GetNativeFeeValid() bool { - if x != nil { - return x.NativeFeeValid - } - return false -} - -var File_mercury_observation_v2_proto protoreflect.FileDescriptor - -var file_mercury_observation_v2_proto_rawDesc = []byte{ - 0x0a, 0x1c, 0x6d, 0x65, 0x72, 0x63, 0x75, 0x72, 0x79, 0x5f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x76, 0x32, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, - 0x6d, 0x65, 0x72, 0x63, 0x75, 0x72, 0x79, 0x5f, 0x76, 0x32, 0x22, 0xfb, 0x02, 0x0a, 0x17, 0x4d, - 0x65, 0x72, 0x63, 0x75, 0x72, 0x79, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x62, 0x65, 0x6e, 0x63, 0x68, 0x6d, 0x61, 0x72, - 0x6b, 0x50, 0x72, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x62, 0x65, - 0x6e, 0x63, 0x68, 0x6d, 0x61, 0x72, 0x6b, 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x0b, - 0x70, 0x72, 0x69, 0x63, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x0b, 0x70, 0x72, 0x69, 0x63, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x12, 0x34, - 0x0a, 0x15, 0x6d, 0x61, 0x78, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x54, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x6d, - 0x61, 0x78, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3e, 0x0a, 0x1a, 0x6d, 0x61, 0x78, 0x46, 0x69, 0x6e, 0x61, 0x6c, - 0x69, 0x7a, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x56, 0x61, 0x6c, - 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x6d, 0x61, 0x78, 0x46, 0x69, 0x6e, - 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x56, - 0x61, 0x6c, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x69, 0x6e, 0x6b, 0x46, 0x65, 0x65, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6c, 0x69, 0x6e, 0x6b, 0x46, 0x65, 0x65, 0x12, 0x22, - 0x0a, 0x0c, 0x6c, 0x69, 0x6e, 0x6b, 0x46, 0x65, 0x65, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x6c, 0x69, 0x6e, 0x6b, 0x46, 0x65, 0x65, 0x56, 0x61, 0x6c, - 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x46, 0x65, 0x65, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x46, 0x65, 0x65, - 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x46, 0x65, 0x65, 0x56, 0x61, 0x6c, - 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, - 0x46, 0x65, 0x65, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x42, 0x0e, 0x5a, 0x0c, 0x2e, 0x3b, 0x6d, 0x65, - 0x72, 0x63, 0x75, 0x72, 0x79, 0x5f, 0x76, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_mercury_observation_v2_proto_rawDescOnce sync.Once - file_mercury_observation_v2_proto_rawDescData = file_mercury_observation_v2_proto_rawDesc -) - -func file_mercury_observation_v2_proto_rawDescGZIP() []byte { - file_mercury_observation_v2_proto_rawDescOnce.Do(func() { - file_mercury_observation_v2_proto_rawDescData = protoimpl.X.CompressGZIP(file_mercury_observation_v2_proto_rawDescData) - }) - return file_mercury_observation_v2_proto_rawDescData -} - -var file_mercury_observation_v2_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_mercury_observation_v2_proto_goTypes = []interface{}{ - (*MercuryObservationProto)(nil), // 0: mercury_v2.MercuryObservationProto -} -var file_mercury_observation_v2_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_mercury_observation_v2_proto_init() } -func file_mercury_observation_v2_proto_init() { - if File_mercury_observation_v2_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_mercury_observation_v2_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MercuryObservationProto); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_mercury_observation_v2_proto_rawDesc, - NumEnums: 0, - NumMessages: 1, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_mercury_observation_v2_proto_goTypes, - DependencyIndexes: file_mercury_observation_v2_proto_depIdxs, - MessageInfos: file_mercury_observation_v2_proto_msgTypes, - }.Build() - File_mercury_observation_v2_proto = out.File - file_mercury_observation_v2_proto_rawDesc = nil - file_mercury_observation_v2_proto_goTypes = nil - file_mercury_observation_v2_proto_depIdxs = nil -} diff --git a/mercury/v2/mercury_observation_v2.proto b/mercury/v2/mercury_observation_v2.proto deleted file mode 100644 index bce202a..0000000 --- a/mercury/v2/mercury_observation_v2.proto +++ /dev/null @@ -1,19 +0,0 @@ -syntax="proto3"; - -package v2; -option go_package = ".;v2"; - -message MercuryObservationProto { - uint32 timestamp = 1; - - bytes benchmarkPrice = 2; - bool pricesValid = 3; - - int64 maxFinalizedTimestamp = 4; - bool maxFinalizedTimestampValid = 5; - - bytes linkFee = 6; - bool linkFeeValid = 7; - bytes nativeFee = 8; - bool nativeFeeValid = 9; -} diff --git a/mercury/v2/mercury_test.go b/mercury/v2/mercury_test.go deleted file mode 100644 index 60271af..0000000 --- a/mercury/v2/mercury_test.go +++ /dev/null @@ -1,667 +0,0 @@ -package v2 - -import ( - "context" - "errors" - "math" - "math/big" - "math/rand" - "reflect" - "testing" - "time" - - "github.com/shopspring/decimal" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - "github.com/smartcontractkit/libocr/commontypes" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - mercurytypes "github.com/smartcontractkit/chainlink-common/pkg/types/mercury" - v2 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v2" - "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - - "github.com/smartcontractkit/chainlink-data-streams/mercury" -) - -type testDataSource struct { - Obs v2.Observation -} - -func (ds testDataSource) Observe(ctx context.Context, repts types.ReportTimestamp, fetchMaxFinalizedTimestamp bool) (v2.Observation, error) { - return ds.Obs, nil -} - -type testReportCodec struct { - observationTimestamp uint32 - builtReport types.Report - - builtReportFields *v2.ReportFields - err error -} - -func (rc *testReportCodec) BuildReport(ctx context.Context, rf v2.ReportFields) (types.Report, error) { - rc.builtReportFields = &rf - - return rc.builtReport, nil -} - -func (rc testReportCodec) MaxReportLength(ctx context.Context, n int) (int, error) { - return 123, nil -} - -func (rc testReportCodec) ObservationTimestampFromReport(context.Context, types.Report) (uint32, error) { - return rc.observationTimestamp, rc.err -} - -func newTestReportPlugin(t *testing.T, codec *testReportCodec, ds *testDataSource) *reportingPlugin { - offchainConfig := mercury.OffchainConfig{ - ExpirationWindow: 1, - BaseUSDFee: decimal.NewFromInt32(1), - } - onchainConfig := mercurytypes.OnchainConfig{ - Min: big.NewInt(1), - Max: big.NewInt(1000), - } - maxReportLength, _ := codec.MaxReportLength(tests.Context(t), 4) - return &reportingPlugin{ - offchainConfig: offchainConfig, - onchainConfig: onchainConfig, - dataSource: ds, - logger: logger.Test(t), - reportCodec: codec, - configDigest: types.ConfigDigest{}, - f: 1, - latestAcceptedEpochRound: mercury.EpochRound{}, - latestAcceptedMedian: big.NewInt(0), - maxReportLength: maxReportLength, - } -} - -func newValidProtos() []*MercuryObservationProto { - return []*MercuryObservationProto{ - &MercuryObservationProto{ - Timestamp: 42, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(123)), - PricesValid: true, - - MaxFinalizedTimestamp: 40, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.1e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.1e18)), - NativeFeeValid: true, - }, - &MercuryObservationProto{ - Timestamp: 45, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(234)), - PricesValid: true, - - MaxFinalizedTimestamp: 40, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.2e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.2e18)), - NativeFeeValid: true, - }, - &MercuryObservationProto{ - Timestamp: 47, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(345)), - PricesValid: true, - - MaxFinalizedTimestamp: 39, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.3e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.3e18)), - NativeFeeValid: true, - }, - &MercuryObservationProto{ - Timestamp: 39, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(456)), - PricesValid: true, - - MaxFinalizedTimestamp: 39, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.4e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.4e18)), - NativeFeeValid: true, - }, - } -} - -func newValidAos(t *testing.T, protos ...*MercuryObservationProto) (aos []types.AttributedObservation) { - if len(protos) == 0 { - protos = newValidProtos() - } - aos = make([]types.AttributedObservation, len(protos)) - for i := range aos { - marshalledObs, err := proto.Marshal(protos[i]) - require.NoError(t, err) - aos[i] = types.AttributedObservation{ - Observation: marshalledObs, - Observer: commontypes.OracleID(i), - } - } - return -} - -func Test_Plugin_Report(t *testing.T) { - dataSource := &testDataSource{} - codec := &testReportCodec{ - builtReport: []byte{1, 2, 3, 4}, - } - rp := newTestReportPlugin(t, codec, dataSource) - repts := types.ReportTimestamp{} - - t.Run("when previous report is nil", func(t *testing.T) { - t.Run("errors if not enough attributed observations", func(t *testing.T) { - _, _, err := rp.Report(tests.Context(t), repts, nil, newValidAos(t)[0:1]) - assert.EqualError(t, err, "only received 1 valid attributed observations, but need at least f+1 (2)") - }) - t.Run("errors if too many maxFinalizedTimestamp observations are invalid", func(t *testing.T) { - ps := newValidProtos() - ps[0].MaxFinalizedTimestampValid = false - ps[1].MaxFinalizedTimestampValid = false - ps[2].MaxFinalizedTimestampValid = false - aos := newValidAos(t, ps...) - - should, _, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, nil, aos) - assert.False(t, should) - assert.EqualError(t, err, "fewer than f+1 observations have a valid maxFinalizedTimestamp (got: 1/4)") - }) - t.Run("errors if maxFinalizedTimestamp is too large", func(t *testing.T) { - ps := newValidProtos() - ps[0].MaxFinalizedTimestamp = math.MaxUint32 - ps[1].MaxFinalizedTimestamp = math.MaxUint32 - ps[2].MaxFinalizedTimestamp = math.MaxUint32 - ps[3].MaxFinalizedTimestamp = math.MaxUint32 - aos := newValidAos(t, ps...) - - should, _, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, nil, aos) - assert.False(t, should) - assert.EqualError(t, err, "maxFinalizedTimestamp is too large, got: 4294967295") - }) - - t.Run("succeeds and generates validFromTimestamp from maxFinalizedTimestamp when maxFinalizedTimestamp is positive", func(t *testing.T) { - aos := newValidAos(t) - - should, report, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, nil, aos) - assert.True(t, should) - assert.NoError(t, err) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, v2.ReportFields{ - ValidFromTimestamp: 41, // consensus maxFinalizedTimestamp is 40, so validFrom should be 40+1 - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - }, *codec.builtReportFields) - }) - t.Run("succeeds and generates validFromTimestamp from maxFinalizedTimestamp when maxFinalizedTimestamp is zero", func(t *testing.T) { - protos := newValidProtos() - for i := range protos { - protos[i].MaxFinalizedTimestamp = 0 - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, nil, aos) - assert.True(t, should) - assert.NoError(t, err) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, v2.ReportFields{ - ValidFromTimestamp: 1, - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - }, *codec.builtReportFields) - }) - t.Run("succeeds and generates validFromTimestamp from maxFinalizedTimestamp when maxFinalizedTimestamp is -1 (missing feed)", func(t *testing.T) { - protos := newValidProtos() - for i := range protos { - protos[i].MaxFinalizedTimestamp = -1 - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, nil, aos) - assert.True(t, should) - assert.NoError(t, err) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, v2.ReportFields{ - ValidFromTimestamp: 45, // in case of missing feed, ValidFromTimestamp=Timestamp for first report - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - }, *codec.builtReportFields) - }) - - t.Run("succeeds, ignoring unparseable attributed observation", func(t *testing.T) { - aos := newValidAos(t) - aos[0] = newUnparseableAttributedObservation() - - should, report, err := rp.Report(tests.Context(t), repts, nil, aos) - require.NoError(t, err) - - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, v2.ReportFields{ - ValidFromTimestamp: 40, // consensus maxFinalizedTimestamp is 39, so validFrom should be 39+1 - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - }, *codec.builtReportFields) - }) - }) - - t.Run("when previous report is present", func(t *testing.T) { - *codec = testReportCodec{ - observationTimestamp: uint32(rand.Int31n(math.MaxInt16)), - builtReport: []byte{1, 2, 3, 4}, - } - previousReport := types.Report{} - - t.Run("succeeds and uses timestamp from previous report if valid", func(t *testing.T) { - protos := newValidProtos() - ts := codec.observationTimestamp + 1 - for i := range protos { - protos[i].Timestamp = ts - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(tests.Context(t), repts, previousReport, aos) - require.NoError(t, err) - - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, v2.ReportFields{ - ValidFromTimestamp: codec.observationTimestamp + 1, // previous observation timestamp +1 second - Timestamp: ts, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: ts + 1, - BenchmarkPrice: big.NewInt(345), - }, *codec.builtReportFields) - }) - t.Run("errors if cannot extract timestamp from previous report", func(t *testing.T) { - codec.err = errors.New("something exploded trying to extract timestamp") - aos := newValidAos(t) - - should, _, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, previousReport, aos) - assert.False(t, should) - assert.EqualError(t, err, "something exploded trying to extract timestamp") - }) - t.Run("does not report if observationTimestamp < validFromTimestamp", func(t *testing.T) { - codec.observationTimestamp = 43 - codec.err = nil - - protos := newValidProtos() - for i := range protos { - protos[i].Timestamp = 42 - } - aos := newValidAos(t, protos...) - - should, _, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, previousReport, aos) - assert.False(t, should) - assert.NoError(t, err) - }) - t.Run("uses 0 values for link/native if they are invalid", func(t *testing.T) { - codec.observationTimestamp = 42 - codec.err = nil - - protos := newValidProtos() - for i := range protos { - protos[i].LinkFeeValid = false - protos[i].NativeFeeValid = false - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, previousReport, aos) - assert.True(t, should) - assert.NoError(t, err) - - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, "0", codec.builtReportFields.LinkFee.String()) - assert.Equal(t, "0", codec.builtReportFields.NativeFee.String()) - }) - }) - - t.Run("buildReport failures", func(t *testing.T) { - t.Run("Report errors when the report is too large", func(t *testing.T) { - aos := newValidAos(t) - codec.builtReport = make([]byte, 1<<16) - - _, _, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, nil, aos) - - assert.EqualError(t, err, "report with len 65536 violates MaxReportLength limit set by ReportCodec (123)") - }) - - t.Run("Report errors when the report length is 0", func(t *testing.T) { - aos := newValidAos(t) - codec.builtReport = []byte{} - _, _, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, nil, aos) - - assert.EqualError(t, err, "report may not have zero length (invariant violation)") - }) - }) -} - -func Test_Plugin_validateReport(t *testing.T) { - dataSource := &testDataSource{} - codec := &testReportCodec{} - rp := newTestReportPlugin(t, codec, dataSource) - - t.Run("valid reports", func(t *testing.T) { - rf := v2.ReportFields{ - ValidFromTimestamp: 42, - Timestamp: 43, - NativeFee: big.NewInt(100), - LinkFee: big.NewInt(50), - ExpiresAt: 44, - BenchmarkPrice: big.NewInt(150), - } - err := rp.validateReport(rf) - require.NoError(t, err) - - rf = v2.ReportFields{ - ValidFromTimestamp: 42, - Timestamp: 42, - NativeFee: big.NewInt(0), - LinkFee: big.NewInt(0), - ExpiresAt: 42, - BenchmarkPrice: big.NewInt(1), - } - err = rp.validateReport(rf) - require.NoError(t, err) - }) - t.Run("fails validation", func(t *testing.T) { - rf := v2.ReportFields{ - ValidFromTimestamp: 44, // later than timestamp not allowed - Timestamp: 43, - NativeFee: big.NewInt(-1), // negative value not allowed - LinkFee: big.NewInt(-1), // negative value not allowed - ExpiresAt: 42, // before timestamp - BenchmarkPrice: big.NewInt(150000), // exceeds max - } - err := rp.validateReport(rf) - require.Error(t, err) - - assert.Contains(t, err.Error(), "median benchmark price (Value: 150000) is outside of allowable range (Min: 1, Max: 1000)") - assert.Contains(t, err.Error(), "median link fee (Value: -1) is outside of allowable range (Min: 0, Max: 3138550867693340381917894711603833208051177722232017256447)") - assert.Contains(t, err.Error(), "median native fee (Value: -1) is outside of allowable range (Min: 0, Max: 3138550867693340381917894711603833208051177722232017256447)") - assert.Contains(t, err.Error(), "observationTimestamp (Value: 43) must be >= validFromTimestamp (Value: 44)") - assert.Contains(t, err.Error(), "expiresAt (Value: 42) must be ahead of observation timestamp (Value: 43)") - }) - - t.Run("zero values", func(t *testing.T) { - rf := v2.ReportFields{} - err := rp.validateReport(rf) - require.Error(t, err) - - assert.Contains(t, err.Error(), "median benchmark price: got nil value") - assert.Contains(t, err.Error(), "median native fee: got nil value") - assert.Contains(t, err.Error(), "median link fee: got nil value") - }) -} - -func mustDecodeBigInt(b []byte) *big.Int { - n, err := mercury.DecodeValueInt192(b) - if err != nil { - panic(err) - } - return n -} - -func Test_Plugin_Observation(t *testing.T) { - dataSource := &testDataSource{} - codec := &testReportCodec{} - rp := newTestReportPlugin(t, codec, dataSource) - t.Run("Observation protobuf doesn't exceed maxObservationLength", func(t *testing.T) { - obs := MercuryObservationProto{ - Timestamp: math.MaxUint32, - BenchmarkPrice: make([]byte, 24), - PricesValid: true, - MaxFinalizedTimestamp: math.MaxUint32, - MaxFinalizedTimestampValid: true, - LinkFee: make([]byte, 24), - LinkFeeValid: true, - NativeFee: make([]byte, 24), - NativeFeeValid: true, - } - // This assertion is here to force this test to fail if a new field is - // added to the protobuf. In this case, you must add the max value of - // the field to the MercuryObservationProto in the test and only after - // that increment the count below - numFields := reflect.TypeOf(obs).NumField() //nolint:all - // 3 fields internal to pbuf struct - require.Equal(t, 9, numFields-3) - - b, err := proto.Marshal(&obs) - require.NoError(t, err) - assert.LessOrEqual(t, len(b), maxObservationLength) - }) - - t.Run("all observations succeeded", func(t *testing.T) { - obs := v2.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - MaxFinalizedTimestamp: mercurytypes.ObsResult[int64]{ - Val: rand.Int63(), - }, - LinkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - NativePrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - } - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Equal(t, obs.BenchmarkPrice.Val, mustDecodeBigInt(p.BenchmarkPrice)) - assert.True(t, p.PricesValid) - assert.Equal(t, obs.MaxFinalizedTimestamp.Val, p.MaxFinalizedTimestamp) - assert.True(t, p.MaxFinalizedTimestampValid) - - fee := mercury.CalculateFee(obs.LinkPrice.Val, decimal.NewFromInt32(1)) - assert.Equal(t, fee, mustDecodeBigInt(p.LinkFee)) - assert.True(t, p.LinkFeeValid) - - fee = mercury.CalculateFee(obs.NativePrice.Val, decimal.NewFromInt32(1)) - assert.Equal(t, fee, mustDecodeBigInt(p.NativeFee)) - assert.True(t, p.NativeFeeValid) - }) - - t.Run("negative link/native prices set fee to max int192", func(t *testing.T) { - obs := v2.Observation{ - LinkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(-1), - }, - NativePrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(-1), - }, - } - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.Equal(t, mercury.MaxInt192, mustDecodeBigInt(p.LinkFee)) - assert.True(t, p.LinkFeeValid) - assert.Equal(t, mercury.MaxInt192, mustDecodeBigInt(p.NativeFee)) - assert.True(t, p.NativeFeeValid) - }) - - t.Run("some observations failed", func(t *testing.T) { - obs := v2.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - Err: errors.New("bechmarkPrice error"), - }, - MaxFinalizedTimestamp: mercurytypes.ObsResult[int64]{ - Val: rand.Int63(), - Err: errors.New("maxFinalizedTimestamp error"), - }, - LinkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - Err: errors.New("linkPrice error"), - }, - NativePrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Zero(t, p.BenchmarkPrice) - assert.False(t, p.PricesValid) - assert.Zero(t, p.MaxFinalizedTimestamp) - assert.False(t, p.MaxFinalizedTimestampValid) - assert.Zero(t, p.LinkFee) - assert.False(t, p.LinkFeeValid) - - fee := mercury.CalculateFee(obs.NativePrice.Val, decimal.NewFromInt32(1)) - assert.Equal(t, fee, mustDecodeBigInt(p.NativeFee)) - assert.True(t, p.NativeFeeValid) - }) - - t.Run("all observations failed", func(t *testing.T) { - obs := v2.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Err: errors.New("bechmarkPrice error"), - }, - MaxFinalizedTimestamp: mercurytypes.ObsResult[int64]{ - Err: errors.New("maxFinalizedTimestamp error"), - }, - LinkPrice: mercurytypes.ObsResult[*big.Int]{ - Err: errors.New("linkPrice error"), - }, - NativePrice: mercurytypes.ObsResult[*big.Int]{ - Err: errors.New("nativePrice error"), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Zero(t, p.BenchmarkPrice) - assert.False(t, p.PricesValid) - assert.Zero(t, p.MaxFinalizedTimestamp) - assert.False(t, p.MaxFinalizedTimestampValid) - assert.Zero(t, p.LinkFee) - assert.False(t, p.LinkFeeValid) - assert.Zero(t, p.NativeFee) - assert.False(t, p.NativeFeeValid) - }) - - t.Run("encoding fails on some observations", func(t *testing.T) { - obs := v2.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - MaxFinalizedTimestamp: mercurytypes.ObsResult[int64]{ - Val: rand.Int63(), - }, - LinkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - NativePrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.Zero(t, p.BenchmarkPrice) - assert.False(t, p.PricesValid) - }) - - t.Run("encoding fails on all observations", func(t *testing.T) { - obs := v2.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - MaxFinalizedTimestamp: mercurytypes.ObsResult[int64]{ - Val: rand.Int63(), - }, - // encoding never fails on calculated fees - LinkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - NativePrice: mercurytypes.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.Zero(t, p.BenchmarkPrice) - assert.False(t, p.PricesValid) - }) -} - -func newUnparseableAttributedObservation() types.AttributedObservation { - return types.AttributedObservation{ - Observation: []byte{1, 2}, - Observer: commontypes.OracleID(42), - } -} diff --git a/mercury/v2/observation.go b/mercury/v2/observation.go deleted file mode 100644 index 86b41d0..0000000 --- a/mercury/v2/observation.go +++ /dev/null @@ -1,88 +0,0 @@ -package v2 - -import ( - "math/big" - - "github.com/smartcontractkit/libocr/commontypes" - - "github.com/smartcontractkit/chainlink-data-streams/mercury" -) - -type PAO interface { - mercury.PAO - GetMaxFinalizedTimestamp() (int64, bool) - GetLinkFee() (*big.Int, bool) - GetNativeFee() (*big.Int, bool) -} - -var _ PAO = parsedAttributedObservation{} - -type parsedAttributedObservation struct { - Timestamp uint32 - Observer commontypes.OracleID - - BenchmarkPrice *big.Int - PricesValid bool - - MaxFinalizedTimestamp int64 - MaxFinalizedTimestampValid bool - - LinkFee *big.Int - LinkFeeValid bool - - NativeFee *big.Int - NativeFeeValid bool -} - -func NewParsedAttributedObservation(ts uint32, observer commontypes.OracleID, - bp *big.Int, pricesValid bool, mfts int64, - mftsValid bool, linkFee *big.Int, linkFeeValid bool, nativeFee *big.Int, nativeFeeValid bool) PAO { - return parsedAttributedObservation{ - Timestamp: ts, - Observer: observer, - - BenchmarkPrice: bp, - PricesValid: pricesValid, - - MaxFinalizedTimestamp: mfts, - MaxFinalizedTimestampValid: mftsValid, - - LinkFee: linkFee, - LinkFeeValid: linkFeeValid, - - NativeFee: nativeFee, - NativeFeeValid: nativeFeeValid, - } -} - -func (pao parsedAttributedObservation) GetTimestamp() uint32 { - return pao.Timestamp -} - -func (pao parsedAttributedObservation) GetObserver() commontypes.OracleID { - return pao.Observer -} - -func (pao parsedAttributedObservation) GetBenchmarkPrice() (*big.Int, bool) { - return pao.BenchmarkPrice, pao.PricesValid -} - -func (pao parsedAttributedObservation) GetBid() (*big.Int, bool) { - panic("current observation doesn't contain the field") -} - -func (pao parsedAttributedObservation) GetAsk() (*big.Int, bool) { - panic("current observation doesn't contain the field") -} - -func (pao parsedAttributedObservation) GetMaxFinalizedTimestamp() (int64, bool) { - return pao.MaxFinalizedTimestamp, pao.MaxFinalizedTimestampValid -} - -func (pao parsedAttributedObservation) GetLinkFee() (*big.Int, bool) { - return pao.LinkFee, pao.LinkFeeValid -} - -func (pao parsedAttributedObservation) GetNativeFee() (*big.Int, bool) { - return pao.NativeFee, pao.NativeFeeValid -} diff --git a/mercury/v3/mercury.go b/mercury/v3/mercury.go deleted file mode 100644 index 29279f2..0000000 --- a/mercury/v3/mercury.go +++ /dev/null @@ -1,483 +0,0 @@ -package v3 - -import ( - "context" - "errors" - "fmt" - "math" - "math/big" - "time" - - "google.golang.org/protobuf/proto" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - mercurytypes "github.com/smartcontractkit/chainlink-common/pkg/types/mercury" - v3 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v3" - - "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - "github.com/smartcontractkit/chainlink-data-streams/mercury" -) - -// DataSource implementations must be thread-safe. Observe may be called by many -// different threads concurrently. -type DataSource interface { - // Observe queries the data source. Returns a value or an error. Once the - // context is expires, Observe may still do cheap computations and return a - // result, but should return as quickly as possible. - // - // More details: In the current implementation, the context passed to - // Observe will time out after MaxDurationObservation. However, Observe - // should *not* make any assumptions about context timeout behavior. Once - // the context times out, Observe should prioritize returning as quickly as - // possible, but may still perform fast computations to return a result - // rather than error. For example, if Observe medianizes a number of data - // sources, some of which already returned a result to Observe prior to the - // context's expiry, Observe might still compute their median, and return it - // instead of an error. - // - // Important: Observe should not perform any potentially time-consuming - // actions like database access, once the context passed has expired. - Observe(ctx context.Context, repts types.ReportTimestamp, fetchMaxFinalizedTimestamp bool) (v3.Observation, error) -} - -var _ ocr3types.MercuryPluginFactory = Factory{} - -const maxObservationLength = 32 + // feedID - 4 + // timestamp - mercury.ByteWidthInt192 + // benchmarkPrice - mercury.ByteWidthInt192 + // bid - mercury.ByteWidthInt192 + // ask - 4 + // validFromTimestamp - mercury.ByteWidthInt192 + // linkFee - mercury.ByteWidthInt192 + // nativeFee - 16 /* overapprox. of protobuf overhead */ - -type Factory struct { - dataSource DataSource - logger logger.Logger - onchainConfigCodec mercurytypes.OnchainConfigCodec - reportCodec v3.ReportCodec -} - -func NewFactory(ds DataSource, lggr logger.Logger, occ mercurytypes.OnchainConfigCodec, rc v3.ReportCodec) Factory { - return Factory{ds, lggr, occ, rc} -} - -func (fac Factory) NewMercuryPlugin(ctx context.Context, configuration ocr3types.MercuryPluginConfig) (ocr3types.MercuryPlugin, ocr3types.MercuryPluginInfo, error) { - offchainConfig, err := mercury.DecodeOffchainConfig(configuration.OffchainConfig) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - onchainConfig, err := fac.onchainConfigCodec.Decode(ctx, configuration.OnchainConfig) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - maxReportLength, err := fac.reportCodec.MaxReportLength(ctx, configuration.N) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - r := &reportingPlugin{ - offchainConfig, - onchainConfig, - fac.dataSource, - fac.logger, - fac.reportCodec, - configuration.ConfigDigest, - configuration.F, - mercury.EpochRound{}, - new(big.Int), - maxReportLength, - } - - return r, ocr3types.MercuryPluginInfo{ - Name: "Mercury", - Limits: ocr3types.MercuryPluginLimits{ - MaxObservationLength: maxObservationLength, - MaxReportLength: maxReportLength, - }, - }, nil -} - -var _ ocr3types.MercuryPlugin = (*reportingPlugin)(nil) - -type reportingPlugin struct { - offchainConfig mercury.OffchainConfig - onchainConfig mercurytypes.OnchainConfig - dataSource DataSource - logger logger.Logger - reportCodec v3.ReportCodec - - configDigest types.ConfigDigest - f int - latestAcceptedEpochRound mercury.EpochRound - latestAcceptedMedian *big.Int - maxReportLength int -} - -var MissingPrice = big.NewInt(-1) - -func (rp *reportingPlugin) Observation(ctx context.Context, repts types.ReportTimestamp, previousReport types.Report) (types.Observation, error) { - obs, err := rp.dataSource.Observe(ctx, repts, previousReport == nil) - if err != nil { - return nil, fmt.Errorf("DataSource.Observe returned an error: %s", err) - } - - observationTimestamp := time.Now() - if observationTimestamp.Unix() > math.MaxUint32 { - return nil, fmt.Errorf("current unix epoch %d exceeds max uint32", observationTimestamp.Unix()) - } - p := MercuryObservationProto{Timestamp: uint32(observationTimestamp.Unix())} - var obsErrors []error - - var bpErr, bidErr, askErr error - if obs.BenchmarkPrice.Err != nil { - bpErr = fmt.Errorf("failed to observe BenchmarkPrice: %w", obs.BenchmarkPrice.Err) - obsErrors = append(obsErrors, bpErr) - } else if benchmarkPrice, err := mercury.EncodeValueInt192(obs.BenchmarkPrice.Val); err != nil { - bpErr = fmt.Errorf("failed to encode BenchmarkPrice; val=%s: %w", obs.BenchmarkPrice.Val, err) - obsErrors = append(obsErrors, bpErr) - } else { - p.BenchmarkPrice = benchmarkPrice - } - - if obs.Bid.Err != nil { - bidErr = fmt.Errorf("failed to observe Bid: %w", obs.Bid.Err) - obsErrors = append(obsErrors, bidErr) - } else if bid, err := mercury.EncodeValueInt192(obs.Bid.Val); err != nil { - bidErr = fmt.Errorf("failed to encode Bid; val=%s: %w", obs.Bid.Val, err) - obsErrors = append(obsErrors, bidErr) - } else { - p.Bid = bid - } - - if obs.Ask.Err != nil { - askErr = fmt.Errorf("failed to observe Ask: %w", obs.Ask.Err) - obsErrors = append(obsErrors, askErr) - } else if ask, err := mercury.EncodeValueInt192(obs.Ask.Val); err != nil { - askErr = fmt.Errorf("failed to encode Ask; val=%s: %w", obs.Ask.Val, err) - obsErrors = append(obsErrors, askErr) - } else { - p.Ask = ask - } - - if bpErr == nil && bidErr == nil && askErr == nil { - if err := validatePrices(obs.Bid.Val, obs.BenchmarkPrice.Val, obs.Ask.Val); err != nil { - rp.logger.Errorw("Cannot generate price observation: invalid bid/mid/ask", "err", err) - p.PricesValid = false - } else { - p.PricesValid = true - } - } - - var maxFinalizedTimestampErr error - if obs.MaxFinalizedTimestamp.Err != nil { - maxFinalizedTimestampErr = fmt.Errorf("failed to observe MaxFinalizedTimestamp: %w", obs.MaxFinalizedTimestamp.Err) - obsErrors = append(obsErrors, maxFinalizedTimestampErr) - } else { - p.MaxFinalizedTimestamp = obs.MaxFinalizedTimestamp.Val - p.MaxFinalizedTimestampValid = true - } - - var linkErr error - if obs.LinkPrice.Err != nil { - linkErr = fmt.Errorf("failed to observe LINK price: %w", obs.LinkPrice.Err) - obsErrors = append(obsErrors, linkErr) - } else if obs.LinkPrice.Val.Cmp(MissingPrice) <= 0 { - p.LinkFee = mercury.MaxInt192Enc - } else { - linkFee := mercury.CalculateFee(obs.LinkPrice.Val, rp.offchainConfig.BaseUSDFee) - if linkFeeEncoded, err := mercury.EncodeValueInt192(linkFee); err != nil { - linkErr = fmt.Errorf("failed to encode LINK fee; val=%s: %w", linkFee, err) - obsErrors = append(obsErrors, linkErr) - } else { - p.LinkFee = linkFeeEncoded - } - } - - if linkErr == nil { - p.LinkFeeValid = true - } - - var nativeErr error - if obs.NativePrice.Err != nil { - nativeErr = fmt.Errorf("failed to observe native price: %w", obs.NativePrice.Err) - obsErrors = append(obsErrors, nativeErr) - } else if obs.NativePrice.Val.Cmp(MissingPrice) <= 0 { - p.NativeFee = mercury.MaxInt192Enc - } else { - nativeFee := mercury.CalculateFee(obs.NativePrice.Val, rp.offchainConfig.BaseUSDFee) - if nativeFeeEncoded, err := mercury.EncodeValueInt192(nativeFee); err != nil { - nativeErr = fmt.Errorf("failed to encode native fee; val=%s: %w", nativeFee, err) - obsErrors = append(obsErrors, nativeErr) - } else { - p.NativeFee = nativeFeeEncoded - } - } - - if nativeErr == nil { - p.NativeFeeValid = true - } - - if len(obsErrors) > 0 { - rp.logger.Warnw(fmt.Sprintf("Observe failed %d/6 observations", len(obsErrors)), "err", errors.Join(obsErrors...)) - } - - return proto.Marshal(&p) -} - -func validatePrices(bid, benchmarkPrice, ask *big.Int) error { - if bid.Cmp(benchmarkPrice) > 0 || benchmarkPrice.Cmp(ask) > 0 { - return fmt.Errorf("invariant violated: expected bid<=mid<=ask, got bid: %s, mid: %s, ask: %s", bid, benchmarkPrice, ask) - } - return nil -} - -func parseAttributedObservation(ao types.AttributedObservation) (PAO, error) { - var pao parsedAttributedObservation - var obs MercuryObservationProto - if err := proto.Unmarshal(ao.Observation, &obs); err != nil { - return parsedAttributedObservation{}, fmt.Errorf("attributed observation cannot be unmarshaled: %s", err) - } - - pao.Timestamp = obs.Timestamp - pao.Observer = ao.Observer - - if obs.PricesValid { - var err error - pao.BenchmarkPrice, err = mercury.DecodeValueInt192(obs.BenchmarkPrice) - if err != nil { - return parsedAttributedObservation{}, fmt.Errorf("benchmarkPrice cannot be converted to big.Int: %s", err) - } - pao.Bid, err = mercury.DecodeValueInt192(obs.Bid) - if err != nil { - return parsedAttributedObservation{}, fmt.Errorf("bid cannot be converted to big.Int: %s", err) - } - pao.Ask, err = mercury.DecodeValueInt192(obs.Ask) - if err != nil { - return parsedAttributedObservation{}, fmt.Errorf("ask cannot be converted to big.Int: %s", err) - } - if err := validatePrices(pao.Bid, pao.BenchmarkPrice, pao.Ask); err != nil { - // NOTE: since nodes themselves are not supposed to set - // PricesValid=true if this invariant is violated, this indicates a - // faulty/misbehaving node and the entire observation should be - // ignored - return parsedAttributedObservation{}, fmt.Errorf("observation claimed to be valid, but contains invalid prices: %w", err) - } - pao.PricesValid = true - } - - if obs.MaxFinalizedTimestampValid { - pao.MaxFinalizedTimestamp = obs.MaxFinalizedTimestamp - pao.MaxFinalizedTimestampValid = true - } - - if obs.LinkFeeValid { - var err error - pao.LinkFee, err = mercury.DecodeValueInt192(obs.LinkFee) - if err != nil { - return parsedAttributedObservation{}, fmt.Errorf("link price cannot be converted to big.Int: %s", err) - } - pao.LinkFeeValid = true - } - if obs.NativeFeeValid { - var err error - pao.NativeFee, err = mercury.DecodeValueInt192(obs.NativeFee) - if err != nil { - return parsedAttributedObservation{}, fmt.Errorf("native price cannot be converted to big.Int: %s", err) - } - pao.NativeFeeValid = true - } - - return pao, nil -} - -func parseAttributedObservations(lggr logger.Logger, aos []types.AttributedObservation) []PAO { - paos := make([]PAO, 0, len(aos)) - for i, ao := range aos { - pao, err := parseAttributedObservation(ao) - if err != nil { - lggr.Warnw("parseAttributedObservations: dropping invalid observation", - "observer", ao.Observer, - "error", err, - "i", i, - ) - continue - } - paos = append(paos, pao) - } - return paos -} - -func (rp *reportingPlugin) Report(ctx context.Context, repts types.ReportTimestamp, previousReport types.Report, aos []types.AttributedObservation) (shouldReport bool, report types.Report, err error) { - paos := parseAttributedObservations(rp.logger, aos) - - if len(paos) == 0 { - return false, nil, errors.New("got zero valid attributed observations") - } - - // By assumption, we have at most f malicious oracles, so there should be at least f+1 valid paos - if !(rp.f+1 <= len(paos)) { - return false, nil, fmt.Errorf("only received %v valid attributed observations, but need at least f+1 (%v)", len(paos), rp.f+1) - } - - rf, err := rp.buildReportFields(ctx, previousReport, paos) - if err != nil { - rp.logger.Errorw("failed to build report fields", "paos", paos, "f", rp.f, "reportFields", rf, "repts", repts, "err", err) - return false, nil, err - } - - if rf.Timestamp < rf.ValidFromTimestamp { - rp.logger.Debugw("shouldReport: no (overlap)", "observationTimestamp", rf.Timestamp, "validFromTimestamp", rf.ValidFromTimestamp, "repts", repts) - return false, nil, nil - } - - if err = rp.validateReport(rf); err != nil { - rp.logger.Errorw("shouldReport: no (validation error)", "reportFields", rf, "err", err, "repts", repts, "paos", paos) - return false, nil, err - } - rp.logger.Debugw("shouldReport: yes", "repts", repts) - - report, err = rp.reportCodec.BuildReport(ctx, rf) - if err != nil { - rp.logger.Debugw("failed to BuildReport", "paos", paos, "f", rp.f, "reportFields", rf, "repts", repts) - return false, nil, err - } - - if !(len(report) <= rp.maxReportLength) { - return false, nil, fmt.Errorf("report with len %d violates MaxReportLength limit set by ReportCodec (%d)", len(report), rp.maxReportLength) - } else if len(report) == 0 { - return false, nil, errors.New("report may not have zero length (invariant violation)") - } - - return true, report, nil -} - -func (rp *reportingPlugin) buildReportFields(ctx context.Context, previousReport types.Report, paos []PAO) (rf v3.ReportFields, merr error) { - mPaos := convert(paos) - rf.Timestamp = mercury.GetConsensusTimestamp(mPaos) - - var err error - if previousReport != nil { - var maxFinalizedTimestamp uint32 - maxFinalizedTimestamp, err = rp.reportCodec.ObservationTimestampFromReport(ctx, previousReport) - merr = errors.Join(merr, err) - rf.ValidFromTimestamp = maxFinalizedTimestamp + 1 - } else { - var maxFinalizedTimestamp int64 - maxFinalizedTimestamp, err = mercury.GetConsensusMaxFinalizedTimestamp(convertMaxFinalizedTimestamp(paos), rp.f) - if err != nil { - merr = errors.Join(merr, err) - } else if maxFinalizedTimestamp < 0 { - // no previous observation timestamp available, e.g. in case of new - // feed; use current timestamp as start of range - rf.ValidFromTimestamp = rf.Timestamp - } else if maxFinalizedTimestamp+1 > math.MaxUint32 { - merr = errors.Join(err, fmt.Errorf("maxFinalizedTimestamp is too large, got: %d", maxFinalizedTimestamp)) - } else { - rf.ValidFromTimestamp = uint32(maxFinalizedTimestamp + 1) - } - } - - rf.BenchmarkPrice, err = mercury.GetConsensusBenchmarkPrice(mPaos, rp.f) - if err != nil { - merr = errors.Join(merr, fmt.Errorf("GetConsensusBenchmarkPrice failed: %w", err)) - } - - rf.Bid, err = mercury.GetConsensusBid(convertBid(paos), rp.f) - if err != nil { - merr = errors.Join(merr, fmt.Errorf("GetConsensusBid failed: %w", err)) - } - - rf.Ask, err = mercury.GetConsensusAsk(convertAsk(paos), rp.f) - if err != nil { - merr = errors.Join(merr, fmt.Errorf("GetConsensusAsk failed: %w", err)) - } - - rf.LinkFee, err = mercury.GetConsensusLinkFee(convertLinkFee(paos), rp.f) - if err != nil { - // It is better to generate a report that will validate for free, - // rather than no report at all, if we cannot come to consensus on a - // valid fee. - rp.logger.Errorw("Cannot come to consensus on LINK fee, falling back to 0", "err", err, "paos", paos) - rf.LinkFee = big.NewInt(0) - } - - rf.NativeFee, err = mercury.GetConsensusNativeFee(convertNativeFee(paos), rp.f) - if err != nil { - // It is better to generate a report that will validate for free, - // rather than no report at all, if we cannot come to consensus on a - // valid fee. - rp.logger.Errorw("Cannot come to consensus on Native fee, falling back to 0", "err", err, "paos", paos) - rf.NativeFee = big.NewInt(0) - } - - if int64(rf.Timestamp)+int64(rp.offchainConfig.ExpirationWindow) > math.MaxUint32 { - merr = errors.Join(merr, fmt.Errorf("timestamp %d + expiration window %d overflows uint32", rf.Timestamp, rp.offchainConfig.ExpirationWindow)) - } else { - rf.ExpiresAt = rf.Timestamp + rp.offchainConfig.ExpirationWindow - } - - return rf, merr -} - -func (rp *reportingPlugin) validateReport(rf v3.ReportFields) error { - return errors.Join( - mercury.ValidateBetween("median benchmark price", rf.BenchmarkPrice, rp.onchainConfig.Min, rp.onchainConfig.Max), - mercury.ValidateBetween("median bid invariant", rf.Bid, rp.onchainConfig.Min, rf.BenchmarkPrice), - mercury.ValidateBetween("median ask invariant", rf.Ask, rf.BenchmarkPrice, rp.onchainConfig.Max), - mercury.ValidateBetween("median bid", rf.Bid, rp.onchainConfig.Min, rp.onchainConfig.Max), - mercury.ValidateBetween("median ask", rf.Ask, rp.onchainConfig.Min, rp.onchainConfig.Max), - mercury.ValidateFee("median link fee", rf.LinkFee), - mercury.ValidateFee("median native fee", rf.NativeFee), - mercury.ValidateValidFromTimestamp(rf.Timestamp, rf.ValidFromTimestamp), - mercury.ValidateExpiresAt(rf.Timestamp, rf.ExpiresAt), - ) -} - -func (rp *reportingPlugin) Close() error { - return nil -} - -// convert funcs are necessary because go is not smart enough to cast -// []interface1 to []interface2 even if interface1 is a superset of interface2 -func convert(pao []PAO) (ret []mercury.PAO) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertMaxFinalizedTimestamp(pao []PAO) (ret []mercury.PAOMaxFinalizedTimestamp) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertBid(pao []PAO) (ret []mercury.PAOBid) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertAsk(pao []PAO) (ret []mercury.PAOAsk) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertLinkFee(pao []PAO) (ret []mercury.PAOLinkFee) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertNativeFee(pao []PAO) (ret []mercury.PAONativeFee) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} diff --git a/mercury/v3/mercury_observation_v3.pb.go b/mercury/v3/mercury_observation_v3.pb.go deleted file mode 100644 index d8162d6..0000000 --- a/mercury/v3/mercury_observation_v3.pb.go +++ /dev/null @@ -1,248 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.31.0 -// protoc v4.23.4 -// source: mercury_observation_v3.proto - -package v3 - -import ( - "reflect" - "sync" - - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/runtime/protoimpl" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type MercuryObservationProto struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Timestamp uint32 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - BenchmarkPrice []byte `protobuf:"bytes,2,opt,name=benchmarkPrice,proto3" json:"benchmarkPrice,omitempty"` - Bid []byte `protobuf:"bytes,3,opt,name=bid,proto3" json:"bid,omitempty"` - Ask []byte `protobuf:"bytes,4,opt,name=ask,proto3" json:"ask,omitempty"` - PricesValid bool `protobuf:"varint,5,opt,name=pricesValid,proto3" json:"pricesValid,omitempty"` - MaxFinalizedTimestamp int64 `protobuf:"varint,6,opt,name=maxFinalizedTimestamp,proto3" json:"maxFinalizedTimestamp,omitempty"` - MaxFinalizedTimestampValid bool `protobuf:"varint,7,opt,name=maxFinalizedTimestampValid,proto3" json:"maxFinalizedTimestampValid,omitempty"` - LinkFee []byte `protobuf:"bytes,8,opt,name=linkFee,proto3" json:"linkFee,omitempty"` - LinkFeeValid bool `protobuf:"varint,9,opt,name=linkFeeValid,proto3" json:"linkFeeValid,omitempty"` - NativeFee []byte `protobuf:"bytes,10,opt,name=nativeFee,proto3" json:"nativeFee,omitempty"` - NativeFeeValid bool `protobuf:"varint,11,opt,name=nativeFeeValid,proto3" json:"nativeFeeValid,omitempty"` -} - -func (x *MercuryObservationProto) Reset() { - *x = MercuryObservationProto{} - if protoimpl.UnsafeEnabled { - mi := &file_mercury_observation_v3_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *MercuryObservationProto) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*MercuryObservationProto) ProtoMessage() {} - -func (x *MercuryObservationProto) ProtoReflect() protoreflect.Message { - mi := &file_mercury_observation_v3_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use MercuryObservationProto.ProtoReflect.Descriptor instead. -func (*MercuryObservationProto) Descriptor() ([]byte, []int) { - return file_mercury_observation_v3_proto_rawDescGZIP(), []int{0} -} - -func (x *MercuryObservationProto) GetTimestamp() uint32 { - if x != nil { - return x.Timestamp - } - return 0 -} - -func (x *MercuryObservationProto) GetBenchmarkPrice() []byte { - if x != nil { - return x.BenchmarkPrice - } - return nil -} - -func (x *MercuryObservationProto) GetBid() []byte { - if x != nil { - return x.Bid - } - return nil -} - -func (x *MercuryObservationProto) GetAsk() []byte { - if x != nil { - return x.Ask - } - return nil -} - -func (x *MercuryObservationProto) GetPricesValid() bool { - if x != nil { - return x.PricesValid - } - return false -} - -func (x *MercuryObservationProto) GetMaxFinalizedTimestamp() int64 { - if x != nil { - return x.MaxFinalizedTimestamp - } - return 0 -} - -func (x *MercuryObservationProto) GetMaxFinalizedTimestampValid() bool { - if x != nil { - return x.MaxFinalizedTimestampValid - } - return false -} - -func (x *MercuryObservationProto) GetLinkFee() []byte { - if x != nil { - return x.LinkFee - } - return nil -} - -func (x *MercuryObservationProto) GetLinkFeeValid() bool { - if x != nil { - return x.LinkFeeValid - } - return false -} - -func (x *MercuryObservationProto) GetNativeFee() []byte { - if x != nil { - return x.NativeFee - } - return nil -} - -func (x *MercuryObservationProto) GetNativeFeeValid() bool { - if x != nil { - return x.NativeFeeValid - } - return false -} - -var File_mercury_observation_v3_proto protoreflect.FileDescriptor - -var file_mercury_observation_v3_proto_rawDesc = []byte{ - 0x0a, 0x1c, 0x6d, 0x65, 0x72, 0x63, 0x75, 0x72, 0x79, 0x5f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x76, 0x33, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, - 0x6d, 0x65, 0x72, 0x63, 0x75, 0x72, 0x79, 0x5f, 0x76, 0x33, 0x22, 0x9f, 0x03, 0x0a, 0x17, 0x4d, - 0x65, 0x72, 0x63, 0x75, 0x72, 0x79, 0x4f, 0x62, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x12, 0x26, 0x0a, 0x0e, 0x62, 0x65, 0x6e, 0x63, 0x68, 0x6d, 0x61, 0x72, - 0x6b, 0x50, 0x72, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x62, 0x65, - 0x6e, 0x63, 0x68, 0x6d, 0x61, 0x72, 0x6b, 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, - 0x62, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x62, 0x69, 0x64, 0x12, 0x10, - 0x0a, 0x03, 0x61, 0x73, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x61, 0x73, 0x6b, - 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x63, 0x65, 0x73, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x70, 0x72, 0x69, 0x63, 0x65, 0x73, 0x56, 0x61, 0x6c, - 0x69, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6d, 0x61, 0x78, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, - 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x15, 0x6d, 0x61, 0x78, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x54, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3e, 0x0a, 0x1a, 0x6d, 0x61, 0x78, 0x46, - 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x6d, 0x61, - 0x78, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x69, 0x6e, 0x6b, - 0x46, 0x65, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6c, 0x69, 0x6e, 0x6b, 0x46, - 0x65, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x6c, 0x69, 0x6e, 0x6b, 0x46, 0x65, 0x65, 0x56, 0x61, 0x6c, - 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x6c, 0x69, 0x6e, 0x6b, 0x46, 0x65, - 0x65, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, - 0x46, 0x65, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x6e, 0x61, 0x74, 0x69, 0x76, - 0x65, 0x46, 0x65, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x46, 0x65, - 0x65, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x6e, 0x61, - 0x74, 0x69, 0x76, 0x65, 0x46, 0x65, 0x65, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x42, 0x0e, 0x5a, 0x0c, - 0x2e, 0x3b, 0x6d, 0x65, 0x72, 0x63, 0x75, 0x72, 0x79, 0x5f, 0x76, 0x33, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_mercury_observation_v3_proto_rawDescOnce sync.Once - file_mercury_observation_v3_proto_rawDescData = file_mercury_observation_v3_proto_rawDesc -) - -func file_mercury_observation_v3_proto_rawDescGZIP() []byte { - file_mercury_observation_v3_proto_rawDescOnce.Do(func() { - file_mercury_observation_v3_proto_rawDescData = protoimpl.X.CompressGZIP(file_mercury_observation_v3_proto_rawDescData) - }) - return file_mercury_observation_v3_proto_rawDescData -} - -var file_mercury_observation_v3_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_mercury_observation_v3_proto_goTypes = []interface{}{ - (*MercuryObservationProto)(nil), // 0: mercury_v3.MercuryObservationProto -} -var file_mercury_observation_v3_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_mercury_observation_v3_proto_init() } -func file_mercury_observation_v3_proto_init() { - if File_mercury_observation_v3_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_mercury_observation_v3_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MercuryObservationProto); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_mercury_observation_v3_proto_rawDesc, - NumEnums: 0, - NumMessages: 1, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_mercury_observation_v3_proto_goTypes, - DependencyIndexes: file_mercury_observation_v3_proto_depIdxs, - MessageInfos: file_mercury_observation_v3_proto_msgTypes, - }.Build() - File_mercury_observation_v3_proto = out.File - file_mercury_observation_v3_proto_rawDesc = nil - file_mercury_observation_v3_proto_goTypes = nil - file_mercury_observation_v3_proto_depIdxs = nil -} diff --git a/mercury/v3/mercury_observation_v3.proto b/mercury/v3/mercury_observation_v3.proto deleted file mode 100644 index 5a6625c..0000000 --- a/mercury/v3/mercury_observation_v3.proto +++ /dev/null @@ -1,21 +0,0 @@ -syntax="proto3"; - -package v3; -option go_package = ".;mercury_v3"; - -message MercuryObservationProto { - uint32 timestamp = 1; - - bytes benchmarkPrice = 2; - bytes bid = 3; - bytes ask = 4; - bool pricesValid = 5; - - int64 maxFinalizedTimestamp = 6; - bool maxFinalizedTimestampValid = 7; - - bytes linkFee = 8; - bool linkFeeValid = 9; - bytes nativeFee = 10; - bool nativeFeeValid = 11; -} diff --git a/mercury/v3/mercury_test.go b/mercury/v3/mercury_test.go deleted file mode 100644 index 87be4ac..0000000 --- a/mercury/v3/mercury_test.go +++ /dev/null @@ -1,820 +0,0 @@ -package v3 - -import ( - "context" - "errors" - "math" - "math/big" - "math/rand" - "reflect" - "testing" - "time" - - "github.com/shopspring/decimal" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - "github.com/smartcontractkit/libocr/commontypes" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - mercurytypes "github.com/smartcontractkit/chainlink-common/pkg/types/mercury" - v3 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v3" - "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - - "github.com/smartcontractkit/chainlink-data-streams/mercury" -) - -type testDataSource struct { - Obs v3.Observation -} - -func (ds testDataSource) Observe(ctx context.Context, repts types.ReportTimestamp, fetchMaxFinalizedTimestamp bool) (v3.Observation, error) { - return ds.Obs, nil -} - -type testReportCodec struct { - observationTimestamp uint32 - builtReport types.Report - - builtReportFields *v3.ReportFields - err error -} - -func (rc *testReportCodec) BuildReport(ctx context.Context, rf v3.ReportFields) (types.Report, error) { - rc.builtReportFields = &rf - - return rc.builtReport, nil -} - -func (rc testReportCodec) MaxReportLength(ctx context.Context, n int) (int, error) { - return 123, nil -} - -func (rc testReportCodec) ObservationTimestampFromReport(context.Context, types.Report) (uint32, error) { - return rc.observationTimestamp, rc.err -} - -func newTestReportPlugin(t *testing.T, codec *testReportCodec, ds *testDataSource) *reportingPlugin { - offchainConfig := mercury.OffchainConfig{ - ExpirationWindow: 1, - BaseUSDFee: decimal.NewFromInt32(1), - } - onchainConfig := mercurytypes.OnchainConfig{ - Min: big.NewInt(1), - Max: big.NewInt(1000), - } - maxReportLength, _ := codec.MaxReportLength(tests.Context(t), 4) - return &reportingPlugin{ - offchainConfig: offchainConfig, - onchainConfig: onchainConfig, - dataSource: ds, - logger: logger.Test(t), - reportCodec: codec, - configDigest: types.ConfigDigest{}, - f: 1, - latestAcceptedEpochRound: mercury.EpochRound{}, - latestAcceptedMedian: big.NewInt(0), - maxReportLength: maxReportLength, - } -} - -func newValidProtos() []*MercuryObservationProto { - return []*MercuryObservationProto{ - &MercuryObservationProto{ - Timestamp: 42, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(123)), - Bid: mercury.MustEncodeValueInt192(big.NewInt(120)), - Ask: mercury.MustEncodeValueInt192(big.NewInt(130)), - PricesValid: true, - - MaxFinalizedTimestamp: 40, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.1e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.1e18)), - NativeFeeValid: true, - }, - &MercuryObservationProto{ - Timestamp: 45, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(234)), - Bid: mercury.MustEncodeValueInt192(big.NewInt(230)), - Ask: mercury.MustEncodeValueInt192(big.NewInt(240)), - PricesValid: true, - - MaxFinalizedTimestamp: 40, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.2e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.2e18)), - NativeFeeValid: true, - }, - &MercuryObservationProto{ - Timestamp: 47, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(345)), - Bid: mercury.MustEncodeValueInt192(big.NewInt(340)), - Ask: mercury.MustEncodeValueInt192(big.NewInt(350)), - PricesValid: true, - - MaxFinalizedTimestamp: 39, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.3e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.3e18)), - NativeFeeValid: true, - }, - &MercuryObservationProto{ - Timestamp: 39, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(456)), - Bid: mercury.MustEncodeValueInt192(big.NewInt(450)), - Ask: mercury.MustEncodeValueInt192(big.NewInt(460)), - PricesValid: true, - - MaxFinalizedTimestamp: 39, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.4e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.4e18)), - NativeFeeValid: true, - }, - } -} - -func newValidAos(t *testing.T, protos ...*MercuryObservationProto) (aos []types.AttributedObservation) { - if len(protos) == 0 { - protos = newValidProtos() - } - aos = make([]types.AttributedObservation, len(protos)) - for i := range aos { - marshalledObs, err := proto.Marshal(protos[i]) - require.NoError(t, err) - aos[i] = types.AttributedObservation{ - Observation: marshalledObs, - Observer: commontypes.OracleID(i), - } - } - return -} - -func Test_parseAttributedObservation(t *testing.T) { - t.Run("returns error if bid<=mid<=ask is violated, even if observation claims itself to be valid", func(t *testing.T) { - obs := &MercuryObservationProto{ - Timestamp: 42, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(123)), - Bid: mercury.MustEncodeValueInt192(big.NewInt(130)), - Ask: mercury.MustEncodeValueInt192(big.NewInt(120)), - PricesValid: true, - - MaxFinalizedTimestamp: 40, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.1e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.1e18)), - NativeFeeValid: true, - } - - serialized, err := proto.Marshal(obs) - require.NoError(t, err) - - _, err = parseAttributedObservation(types.AttributedObservation{Observation: serialized, Observer: commontypes.OracleID(42)}) - require.Error(t, err) - assert.Equal(t, "observation claimed to be valid, but contains invalid prices: invariant violated: expected bid<=mid<=ask, got bid: 130, mid: 123, ask: 120", err.Error()) - }) -} - -func Test_Plugin_Report(t *testing.T) { - dataSource := &testDataSource{} - codec := &testReportCodec{ - builtReport: []byte{1, 2, 3, 4}, - } - rp := newTestReportPlugin(t, codec, dataSource) - repts := types.ReportTimestamp{} - - t.Run("when previous report is nil", func(t *testing.T) { - t.Run("errors if not enough attributed observations", func(t *testing.T) { - _, _, err := rp.Report(tests.Context(t), repts, nil, newValidAos(t)[0:1]) - assert.EqualError(t, err, "only received 1 valid attributed observations, but need at least f+1 (2)") - }) - - t.Run("errors if too many maxFinalizedTimestamp observations are invalid", func(t *testing.T) { - ps := newValidProtos() - ps[0].MaxFinalizedTimestampValid = false - ps[1].MaxFinalizedTimestampValid = false - ps[2].MaxFinalizedTimestampValid = false - aos := newValidAos(t, ps...) - - should, _, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, nil, aos) - assert.False(t, should) - assert.EqualError(t, err, "fewer than f+1 observations have a valid maxFinalizedTimestamp (got: 1/4)") - }) - t.Run("errors if maxFinalizedTimestamp is too large", func(t *testing.T) { - ps := newValidProtos() - ps[0].MaxFinalizedTimestamp = math.MaxUint32 - ps[1].MaxFinalizedTimestamp = math.MaxUint32 - ps[2].MaxFinalizedTimestamp = math.MaxUint32 - ps[3].MaxFinalizedTimestamp = math.MaxUint32 - aos := newValidAos(t, ps...) - - should, _, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, nil, aos) - assert.False(t, should) - assert.EqualError(t, err, "maxFinalizedTimestamp is too large, got: 4294967295") - }) - - t.Run("succeeds and generates validFromTimestamp from maxFinalizedTimestamp when maxFinalizedTimestamp is positive", func(t *testing.T) { - aos := newValidAos(t) - - should, report, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, nil, aos) - assert.True(t, should) - assert.NoError(t, err) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, v3.ReportFields{ - ValidFromTimestamp: 41, // consensus maxFinalizedTimestamp is 40, so validFrom should be 40+1 - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - Bid: big.NewInt(340), - Ask: big.NewInt(350), - }, *codec.builtReportFields) - }) - t.Run("succeeds and generates validFromTimestamp from maxFinalizedTimestamp when maxFinalizedTimestamp is zero", func(t *testing.T) { - protos := newValidProtos() - for i := range protos { - protos[i].MaxFinalizedTimestamp = 0 - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, nil, aos) - assert.True(t, should) - assert.NoError(t, err) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, v3.ReportFields{ - ValidFromTimestamp: 1, - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - Bid: big.NewInt(340), - Ask: big.NewInt(350), - }, *codec.builtReportFields) - }) - t.Run("succeeds and generates validFromTimestamp from maxFinalizedTimestamp when maxFinalizedTimestamp is -1 (missing feed)", func(t *testing.T) { - protos := newValidProtos() - for i := range protos { - protos[i].MaxFinalizedTimestamp = -1 - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, nil, aos) - assert.True(t, should) - assert.NoError(t, err) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, v3.ReportFields{ - ValidFromTimestamp: 45, // in case of missing feed, ValidFromTimestamp=Timestamp for first report - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - Bid: big.NewInt(340), - Ask: big.NewInt(350), - }, *codec.builtReportFields) - }) - - t.Run("succeeds, ignoring unparseable attributed observation", func(t *testing.T) { - aos := newValidAos(t) - aos[0] = newUnparseableAttributedObservation() - - should, report, err := rp.Report(tests.Context(t), repts, nil, aos) - require.NoError(t, err) - - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, v3.ReportFields{ - ValidFromTimestamp: 40, // consensus maxFinalizedTimestamp is 39, so validFrom should be 39+1 - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - Bid: big.NewInt(340), - Ask: big.NewInt(350), - }, *codec.builtReportFields) - }) - }) - - t.Run("when previous report is present", func(t *testing.T) { - *codec = testReportCodec{ - observationTimestamp: uint32(rand.Int31n(math.MaxInt16)), - builtReport: []byte{1, 2, 3, 4}, - } - previousReport := types.Report{} - - t.Run("succeeds and uses timestamp from previous report if valid", func(t *testing.T) { - protos := newValidProtos() - ts := codec.observationTimestamp + 1 - for i := range protos { - protos[i].Timestamp = ts - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(tests.Context(t), repts, previousReport, aos) - require.NoError(t, err) - - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, v3.ReportFields{ - ValidFromTimestamp: codec.observationTimestamp + 1, // previous observation timestamp +1 second - Timestamp: ts, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: ts + 1, - BenchmarkPrice: big.NewInt(345), - Bid: big.NewInt(340), - Ask: big.NewInt(350), - }, *codec.builtReportFields) - }) - t.Run("errors if cannot extract timestamp from previous report", func(t *testing.T) { - codec.err = errors.New("something exploded trying to extract timestamp") - aos := newValidAos(t) - - should, _, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, previousReport, aos) - assert.False(t, should) - assert.EqualError(t, err, "something exploded trying to extract timestamp") - }) - t.Run("does not report if observationTimestamp < validFromTimestamp", func(t *testing.T) { - codec.observationTimestamp = 43 - codec.err = nil - - protos := newValidProtos() - for i := range protos { - protos[i].Timestamp = 42 - } - aos := newValidAos(t, protos...) - - should, _, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, previousReport, aos) - assert.False(t, should) - assert.NoError(t, err) - }) - t.Run("uses 0 values for link/native if they are invalid", func(t *testing.T) { - codec.observationTimestamp = 42 - codec.err = nil - - protos := newValidProtos() - for i := range protos { - protos[i].LinkFeeValid = false - protos[i].NativeFeeValid = false - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, previousReport, aos) - assert.True(t, should) - assert.NoError(t, err) - - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, "0", codec.builtReportFields.LinkFee.String()) - assert.Equal(t, "0", codec.builtReportFields.NativeFee.String()) - }) - }) - - t.Run("buildReport failures", func(t *testing.T) { - t.Run("Report errors when the report is too large", func(t *testing.T) { - aos := newValidAos(t) - codec.builtReport = make([]byte, 1<<16) - - _, _, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, nil, aos) - - assert.EqualError(t, err, "report with len 65536 violates MaxReportLength limit set by ReportCodec (123)") - }) - - t.Run("Report errors when the report length is 0", func(t *testing.T) { - aos := newValidAos(t) - codec.builtReport = []byte{} - _, _, err := rp.Report(tests.Context(t), types.ReportTimestamp{}, nil, aos) - - assert.EqualError(t, err, "report may not have zero length (invariant violation)") - }) - }) -} - -func Test_Plugin_validateReport(t *testing.T) { - dataSource := &testDataSource{} - codec := &testReportCodec{} - rp := newTestReportPlugin(t, codec, dataSource) - - t.Run("valid reports", func(t *testing.T) { - rf := v3.ReportFields{ - ValidFromTimestamp: 42, - Timestamp: 43, - NativeFee: big.NewInt(100), - LinkFee: big.NewInt(50), - ExpiresAt: 44, - BenchmarkPrice: big.NewInt(150), - Bid: big.NewInt(140), - Ask: big.NewInt(160), - } - err := rp.validateReport(rf) - require.NoError(t, err) - - rf = v3.ReportFields{ - ValidFromTimestamp: 42, - Timestamp: 42, - NativeFee: big.NewInt(0), - LinkFee: big.NewInt(0), - ExpiresAt: 42, - BenchmarkPrice: big.NewInt(1), - Bid: big.NewInt(1), - Ask: big.NewInt(1), - } - err = rp.validateReport(rf) - require.NoError(t, err) - }) - t.Run("fails validation", func(t *testing.T) { - rf := v3.ReportFields{ - ValidFromTimestamp: 44, // later than timestamp not allowed - Timestamp: 43, - NativeFee: big.NewInt(-1), // negative value not allowed - LinkFee: big.NewInt(-1), // negative value not allowed - ExpiresAt: 42, // before timestamp - BenchmarkPrice: big.NewInt(150000), // exceeds max - Bid: big.NewInt(150000), // exceeds max - Ask: big.NewInt(150000), // exceeds max - } - err := rp.validateReport(rf) - require.Error(t, err) - - assert.Contains(t, err.Error(), "median benchmark price (Value: 150000) is outside of allowable range (Min: 1, Max: 1000)") - assert.Contains(t, err.Error(), "median bid (Value: 150000) is outside of allowable range (Min: 1, Max: 1000)") - assert.Contains(t, err.Error(), "median ask (Value: 150000) is outside of allowable range (Min: 1, Max: 1000)") - assert.Contains(t, err.Error(), "median link fee (Value: -1) is outside of allowable range (Min: 0, Max: 3138550867693340381917894711603833208051177722232017256447)") - assert.Contains(t, err.Error(), "median native fee (Value: -1) is outside of allowable range (Min: 0, Max: 3138550867693340381917894711603833208051177722232017256447)") - assert.Contains(t, err.Error(), "observationTimestamp (Value: 43) must be >= validFromTimestamp (Value: 44)") - assert.Contains(t, err.Error(), "median link fee (Value: -1) is outside of allowable range (Min: 0, Max: 3138550867693340381917894711603833208051177722232017256447)") - assert.Contains(t, err.Error(), "expiresAt (Value: 42) must be ahead of observation timestamp (Value: 43)") - }) - t.Run("bid/ask invariant violation", func(t *testing.T) { - rf := v3.ReportFields{ - BenchmarkPrice: big.NewInt(500), - Bid: big.NewInt(501), - Ask: big.NewInt(499), - } - err := rp.validateReport(rf) - require.Error(t, err) - - assert.Contains(t, err.Error(), "median bid invariant (Value: 501) is outside of allowable range (Min: 1, Max: 500)") - assert.Contains(t, err.Error(), "median ask invariant (Value: 499) is outside of allowable range (Min: 500, Max: 1000)") - }) - - t.Run("zero values", func(t *testing.T) { - rf := v3.ReportFields{} - err := rp.validateReport(rf) - require.Error(t, err) - - assert.Contains(t, err.Error(), "median benchmark price: got nil value") - assert.Contains(t, err.Error(), "median bid: got nil value") - assert.Contains(t, err.Error(), "median ask: got nil value") - assert.Contains(t, err.Error(), "median native fee: got nil value") - assert.Contains(t, err.Error(), "median link fee: got nil value") - }) -} - -func mustDecodeBigInt(b []byte) *big.Int { - n, err := mercury.DecodeValueInt192(b) - if err != nil { - panic(err) - } - return n -} - -func Test_Plugin_Observation(t *testing.T) { - dataSource := &testDataSource{} - codec := &testReportCodec{} - rp := newTestReportPlugin(t, codec, dataSource) - t.Run("Observation protobuf doesn't exceed maxObservationLength", func(t *testing.T) { - obs := MercuryObservationProto{ - Timestamp: math.MaxUint32, - BenchmarkPrice: make([]byte, 24), - Bid: make([]byte, 24), - Ask: make([]byte, 24), - PricesValid: true, - MaxFinalizedTimestamp: math.MaxUint32, - MaxFinalizedTimestampValid: true, - LinkFee: make([]byte, 24), - LinkFeeValid: true, - NativeFee: make([]byte, 24), - NativeFeeValid: true, - } - // This assertion is here to force this test to fail if a new field is - // added to the protobuf. In this case, you must add the max value of - // the field to the MercuryObservationProto in the test and only after - // that increment the count below - numFields := reflect.TypeOf(obs).NumField() //nolint:all - // 3 fields internal to pbuf struct - require.Equal(t, 11, numFields-3) - - b, err := proto.Marshal(&obs) - require.NoError(t, err) - assert.LessOrEqual(t, len(b), maxObservationLength) - }) - - validBid := big.NewInt(rand.Int63() - 2) - validBenchmarkPrice := new(big.Int).Add(validBid, big.NewInt(1)) - validAsk := new(big.Int).Add(validBid, big.NewInt(2)) - - t.Run("all observations succeeded", func(t *testing.T) { - obs := v3.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: validBenchmarkPrice, - }, - Bid: mercurytypes.ObsResult[*big.Int]{ - Val: validBid, - }, - Ask: mercurytypes.ObsResult[*big.Int]{ - Val: validAsk, - }, - MaxFinalizedTimestamp: mercurytypes.ObsResult[int64]{ - Val: rand.Int63(), - }, - LinkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - NativePrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - } - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Equal(t, obs.BenchmarkPrice.Val, mustDecodeBigInt(p.BenchmarkPrice)) - assert.True(t, p.PricesValid) - assert.Equal(t, obs.MaxFinalizedTimestamp.Val, p.MaxFinalizedTimestamp) - assert.True(t, p.MaxFinalizedTimestampValid) - - fee := mercury.CalculateFee(obs.LinkPrice.Val, decimal.NewFromInt32(1)) - assert.Equal(t, fee, mustDecodeBigInt(p.LinkFee)) - assert.True(t, p.LinkFeeValid) - - fee = mercury.CalculateFee(obs.NativePrice.Val, decimal.NewFromInt32(1)) - assert.Equal(t, fee, mustDecodeBigInt(p.NativeFee)) - assert.True(t, p.NativeFeeValid) - }) - - t.Run("negative link/native prices set fee to max int192", func(t *testing.T) { - obs := v3.Observation{ - LinkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(-1), - }, - NativePrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(-1), - }, - } - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.Equal(t, mercury.MaxInt192, mustDecodeBigInt(p.LinkFee)) - assert.True(t, p.LinkFeeValid) - assert.Equal(t, mercury.MaxInt192, mustDecodeBigInt(p.NativeFee)) - assert.True(t, p.NativeFeeValid) - }) - - t.Run("some observations failed", func(t *testing.T) { - obs := v3.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - Err: errors.New("bechmarkPrice error"), - }, - MaxFinalizedTimestamp: mercurytypes.ObsResult[int64]{ - Val: rand.Int63(), - Err: errors.New("maxFinalizedTimestamp error"), - }, - LinkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - Err: errors.New("linkPrice error"), - }, - NativePrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Zero(t, p.BenchmarkPrice) - assert.False(t, p.PricesValid) - assert.Zero(t, p.MaxFinalizedTimestamp) - assert.False(t, p.MaxFinalizedTimestampValid) - assert.Zero(t, p.LinkFee) - assert.False(t, p.LinkFeeValid) - - fee := mercury.CalculateFee(obs.NativePrice.Val, decimal.NewFromInt32(1)) - assert.Equal(t, fee, mustDecodeBigInt(p.NativeFee)) - assert.True(t, p.NativeFeeValid) - }) - - t.Run("all observations failed", func(t *testing.T) { - obs := v3.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Err: errors.New("bechmarkPrice error"), - }, - Bid: mercurytypes.ObsResult[*big.Int]{ - Err: errors.New("bid error"), - }, - Ask: mercurytypes.ObsResult[*big.Int]{ - Err: errors.New("ask error"), - }, - MaxFinalizedTimestamp: mercurytypes.ObsResult[int64]{ - Err: errors.New("maxFinalizedTimestamp error"), - }, - LinkPrice: mercurytypes.ObsResult[*big.Int]{ - Err: errors.New("linkPrice error"), - }, - NativePrice: mercurytypes.ObsResult[*big.Int]{ - Err: errors.New("nativePrice error"), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Zero(t, p.BenchmarkPrice) - assert.Zero(t, p.Bid) - assert.Zero(t, p.Ask) - assert.False(t, p.PricesValid) - assert.Zero(t, p.MaxFinalizedTimestamp) - assert.False(t, p.MaxFinalizedTimestampValid) - assert.Zero(t, p.LinkFee) - assert.False(t, p.LinkFeeValid) - assert.Zero(t, p.NativeFee) - assert.False(t, p.NativeFeeValid) - }) - - t.Run("encoding fails on some observations", func(t *testing.T) { - obs := v3.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - MaxFinalizedTimestamp: mercurytypes.ObsResult[int64]{ - Val: rand.Int63(), - }, - LinkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - NativePrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.Zero(t, p.BenchmarkPrice) - assert.False(t, p.PricesValid) - }) - - t.Run("encoding fails on all observations", func(t *testing.T) { - obs := v3.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - Bid: mercurytypes.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - Ask: mercurytypes.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - MaxFinalizedTimestamp: mercurytypes.ObsResult[int64]{ - Val: rand.Int63(), - }, - // encoding never fails on calculated fees - LinkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - NativePrice: mercurytypes.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.Zero(t, p.BenchmarkPrice) - assert.Zero(t, p.Bid) - assert.Zero(t, p.Ask) - assert.False(t, p.PricesValid) - }) - - t.Run("bid<=mid<=ask violation", func(t *testing.T) { - obs := v3.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(10), - }, - Bid: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(11), - }, - Ask: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(12), - }, - MaxFinalizedTimestamp: mercurytypes.ObsResult[int64]{ - Val: rand.Int63(), - }, - LinkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - NativePrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - } - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Equal(t, obs.BenchmarkPrice.Val, mustDecodeBigInt(p.BenchmarkPrice)) - assert.False(t, p.PricesValid) // not valid! - - // other values passed through ok - assert.Equal(t, obs.MaxFinalizedTimestamp.Val, p.MaxFinalizedTimestamp) - assert.True(t, p.MaxFinalizedTimestampValid) - - fee := mercury.CalculateFee(obs.LinkPrice.Val, decimal.NewFromInt32(1)) - assert.Equal(t, fee, mustDecodeBigInt(p.LinkFee)) - assert.True(t, p.LinkFeeValid) - - fee = mercury.CalculateFee(obs.NativePrice.Val, decimal.NewFromInt32(1)) - assert.Equal(t, fee, mustDecodeBigInt(p.NativeFee)) - assert.True(t, p.NativeFeeValid) - - // test benchmark price higher than ask - obs.BenchmarkPrice.Val = big.NewInt(13) - dataSource.Obs = obs - - parsedObs, err = rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - assert.False(t, p.PricesValid) // not valid! - }) -} - -func newUnparseableAttributedObservation() types.AttributedObservation { - return types.AttributedObservation{ - Observation: []byte{1, 2}, - Observer: commontypes.OracleID(42), - } -} diff --git a/mercury/v3/observation.go b/mercury/v3/observation.go deleted file mode 100644 index 2b1708c..0000000 --- a/mercury/v3/observation.go +++ /dev/null @@ -1,98 +0,0 @@ -package v3 - -import ( - "math/big" - - "github.com/smartcontractkit/libocr/commontypes" - - "github.com/smartcontractkit/chainlink-data-streams/mercury" -) - -type PAO interface { - mercury.PAO - GetBid() (*big.Int, bool) - GetAsk() (*big.Int, bool) - GetMaxFinalizedTimestamp() (int64, bool) - GetLinkFee() (*big.Int, bool) - GetNativeFee() (*big.Int, bool) -} - -var _ PAO = parsedAttributedObservation{} - -type parsedAttributedObservation struct { - Timestamp uint32 - Observer commontypes.OracleID - - BenchmarkPrice *big.Int - Bid *big.Int - Ask *big.Int - PricesValid bool - - MaxFinalizedTimestamp int64 - MaxFinalizedTimestampValid bool - - LinkFee *big.Int - LinkFeeValid bool - - NativeFee *big.Int - NativeFeeValid bool -} - -func NewParsedAttributedObservation(ts uint32, observer commontypes.OracleID, - bp *big.Int, bid *big.Int, ask *big.Int, pricesValid bool, mfts int64, - mftsValid bool, linkFee *big.Int, linkFeeValid bool, nativeFee *big.Int, nativeFeeValid bool) PAO { - return parsedAttributedObservation{ - Timestamp: ts, - Observer: observer, - - BenchmarkPrice: bp, - Bid: bid, - Ask: ask, - PricesValid: pricesValid, - - MaxFinalizedTimestamp: mfts, - MaxFinalizedTimestampValid: mftsValid, - - LinkFee: linkFee, - LinkFeeValid: linkFeeValid, - - NativeFee: nativeFee, - NativeFeeValid: nativeFeeValid, - } -} - -func (pao parsedAttributedObservation) GetTimestamp() uint32 { - return pao.Timestamp -} - -func (pao parsedAttributedObservation) GetObserver() commontypes.OracleID { - return pao.Observer -} - -func (pao parsedAttributedObservation) GetBenchmarkPrice() (*big.Int, bool) { - return pao.BenchmarkPrice, pao.PricesValid -} - -func (pao parsedAttributedObservation) GetBid() (*big.Int, bool) { - return pao.Bid, pao.PricesValid -} - -func (pao parsedAttributedObservation) GetAsk() (*big.Int, bool) { - return pao.Ask, pao.PricesValid -} - -func (pao parsedAttributedObservation) GetMaxFinalizedTimestamp() (int64, bool) { - if pao.MaxFinalizedTimestamp < -1 { - // values below -1 are not valid - return 0, false - } - return pao.MaxFinalizedTimestamp, pao.MaxFinalizedTimestampValid -} - -func (pao parsedAttributedObservation) GetLinkFee() (*big.Int, bool) { - return pao.LinkFee, pao.LinkFeeValid -} - -func (pao parsedAttributedObservation) GetNativeFee() (*big.Int, bool) { - return pao.NativeFee, pao.NativeFeeValid -} diff --git a/mercury/v4/aggregate_functions.go b/mercury/v4/aggregate_functions.go deleted file mode 100644 index 617faa8..0000000 --- a/mercury/v4/aggregate_functions.go +++ /dev/null @@ -1,39 +0,0 @@ -package v4 - -import "fmt" - -type PAOMarketStatus interface { - GetMarketStatus() (uint32, bool) -} - -// GetConsensusMarketStatus gets the most common status, provided that it is at least F+1. -func GetConsensusMarketStatus(paos []PAOMarketStatus, f int) (uint32, error) { - marketStatusCounts := make(map[uint32]int) - for _, pao := range paos { - marketStatus, valid := pao.GetMarketStatus() - if valid { - marketStatusCounts[marketStatus]++ - } - } - - var mostCommonMarketStatus uint32 - var mostCommonCount int - for marketStatus, count := range marketStatusCounts { - if count > mostCommonCount { - mostCommonMarketStatus = marketStatus - mostCommonCount = count - } else if count == mostCommonCount { - // For stability, always prefer the smaller enum value in case of ties. - // In practice this will prefer CLOSED over OPEN. - if marketStatus < mostCommonMarketStatus { - mostCommonMarketStatus = marketStatus - } - } - } - - if mostCommonCount < f+1 { - return 0, fmt.Errorf("market status has fewer than f+1 observations (status %d got %d/%d)", mostCommonMarketStatus, mostCommonCount, len(paos)) - } - - return mostCommonMarketStatus, nil -} diff --git a/mercury/v4/aggregate_functions_test.go b/mercury/v4/aggregate_functions_test.go deleted file mode 100644 index ba18a29..0000000 --- a/mercury/v4/aggregate_functions_test.go +++ /dev/null @@ -1,252 +0,0 @@ -package v4 - -import ( - "math/big" - "testing" - - "github.com/smartcontractkit/libocr/commontypes" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type testParsedAttributedObservation struct { - Timestamp uint32 - BenchmarkPrice *big.Int - BenchmarkPriceValid bool - MaxFinalizedTimestamp int64 - MaxFinalizedTimestampValid bool - LinkFee *big.Int - LinkFeeValid bool - NativeFee *big.Int - NativeFeeValid bool - MarketStatus uint32 - MarketStatusValid bool -} - -func (t testParsedAttributedObservation) GetObserver() commontypes.OracleID { return 0 } -func (t testParsedAttributedObservation) GetTimestamp() uint32 { return t.Timestamp } -func (t testParsedAttributedObservation) GetBenchmarkPrice() (*big.Int, bool) { - return t.BenchmarkPrice, t.BenchmarkPriceValid -} -func (t testParsedAttributedObservation) GetMaxFinalizedTimestamp() (int64, bool) { - return t.MaxFinalizedTimestamp, t.MaxFinalizedTimestampValid -} -func (t testParsedAttributedObservation) GetLinkFee() (*big.Int, bool) { - return t.LinkFee, t.LinkFeeValid -} -func (t testParsedAttributedObservation) GetNativeFee() (*big.Int, bool) { - return t.NativeFee, t.NativeFeeValid -} -func (t testParsedAttributedObservation) GetMarketStatus() (uint32, bool) { - return t.MarketStatus, t.MarketStatusValid -} - -func convertTestPAOsToPAOs(testPAOs []testParsedAttributedObservation) []PAO { - var paos []PAO - for _, testPAO := range testPAOs { - paos = append(paos, testPAO) - } - return paos -} - -func newValidParsedAttributedObservations() []testParsedAttributedObservation { - return []testParsedAttributedObservation{ - testParsedAttributedObservation{ - Timestamp: 1689648456, - - BenchmarkPrice: big.NewInt(123), - BenchmarkPriceValid: true, - - MaxFinalizedTimestamp: 1679448456, - MaxFinalizedTimestampValid: true, - - LinkFee: big.NewInt(1), - LinkFeeValid: true, - NativeFee: big.NewInt(1), - NativeFeeValid: true, - - MarketStatus: 1, - MarketStatusValid: true, - }, - testParsedAttributedObservation{ - Timestamp: 1689648456, - - BenchmarkPrice: big.NewInt(456), - BenchmarkPriceValid: true, - - MaxFinalizedTimestamp: 1679448456, - MaxFinalizedTimestampValid: true, - - LinkFee: big.NewInt(2), - LinkFeeValid: true, - NativeFee: big.NewInt(2), - NativeFeeValid: true, - - MarketStatus: 1, - MarketStatusValid: true, - }, - testParsedAttributedObservation{ - Timestamp: 1689648789, - - BenchmarkPrice: big.NewInt(789), - BenchmarkPriceValid: true, - - MaxFinalizedTimestamp: 1679448456, - MaxFinalizedTimestampValid: true, - - LinkFee: big.NewInt(3), - LinkFeeValid: true, - NativeFee: big.NewInt(3), - NativeFeeValid: true, - - MarketStatus: 2, - MarketStatusValid: true, - }, - testParsedAttributedObservation{ - Timestamp: 1689648789, - - BenchmarkPrice: big.NewInt(456), - BenchmarkPriceValid: true, - - MaxFinalizedTimestamp: 1679513477, - MaxFinalizedTimestampValid: true, - - LinkFee: big.NewInt(4), - LinkFeeValid: true, - NativeFee: big.NewInt(4), - NativeFeeValid: true, - - MarketStatus: 3, - MarketStatusValid: true, - }, - } -} - -func NewValidParsedAttributedObservations(paos ...testParsedAttributedObservation) []testParsedAttributedObservation { - if len(paos) == 0 { - paos = newValidParsedAttributedObservations() - } - return []testParsedAttributedObservation{ - paos[0], - paos[1], - paos[2], - paos[3], - } -} - -func NewInvalidParsedAttributedObservations() []testParsedAttributedObservation { - return []testParsedAttributedObservation{ - testParsedAttributedObservation{ - Timestamp: 1, - - BenchmarkPrice: big.NewInt(123), - BenchmarkPriceValid: false, - - MaxFinalizedTimestamp: 1679648456, - MaxFinalizedTimestampValid: false, - - LinkFee: big.NewInt(1), - LinkFeeValid: false, - NativeFee: big.NewInt(1), - NativeFeeValid: false, - - MarketStatus: 1, - MarketStatusValid: false, - }, - testParsedAttributedObservation{ - Timestamp: 2, - - BenchmarkPrice: big.NewInt(456), - BenchmarkPriceValid: false, - - MaxFinalizedTimestamp: 1679648456, - MaxFinalizedTimestampValid: false, - - LinkFee: big.NewInt(2), - LinkFeeValid: false, - NativeFee: big.NewInt(2), - NativeFeeValid: false, - - MarketStatus: 1, - MarketStatusValid: false, - }, - testParsedAttributedObservation{ - Timestamp: 2, - - BenchmarkPrice: big.NewInt(789), - BenchmarkPriceValid: false, - - MaxFinalizedTimestamp: 1679648456, - MaxFinalizedTimestampValid: false, - - LinkFee: big.NewInt(3), - LinkFeeValid: false, - NativeFee: big.NewInt(3), - NativeFeeValid: false, - - MarketStatus: 2, - MarketStatusValid: false, - }, - testParsedAttributedObservation{ - Timestamp: 3, - - BenchmarkPrice: big.NewInt(456), - BenchmarkPriceValid: true, - - MaxFinalizedTimestamp: 1679513477, - MaxFinalizedTimestampValid: true, - - LinkFee: big.NewInt(4), - LinkFeeValid: true, - NativeFee: big.NewInt(4), - NativeFeeValid: true, - - MarketStatus: 3, - MarketStatusValid: false, - }, - } -} - -func Test_AggregateFunctions(t *testing.T) { - f := 1 - validPaos := NewValidParsedAttributedObservations() - invalidPaos := NewInvalidParsedAttributedObservations() - - t.Run("GetConsensusMarketStatus", func(t *testing.T) { - t.Run("gets consensus on market status when valid", func(t *testing.T) { - marketStatus, err := GetConsensusMarketStatus(convertMarketStatus(convertTestPAOsToPAOs(validPaos)), f) - require.NoError(t, err) - assert.Equal(t, uint32(1), marketStatus) - }) - t.Run("treats zero values as valid", func(t *testing.T) { - paos := NewValidParsedAttributedObservations() - for i := range paos { - paos[i].MarketStatus = 0 - } - marketStatus, err := GetConsensusMarketStatus(convertMarketStatus(convertTestPAOsToPAOs(paos)), f) - require.NoError(t, err) - assert.Equal(t, uint32(0), marketStatus) - }) - t.Run("is stable during ties", func(t *testing.T) { - paos := NewValidParsedAttributedObservations() - for i := range paos { - paos[i].MarketStatus = uint32(i % 2) - } - marketStatus, err := GetConsensusMarketStatus(convertMarketStatus(convertTestPAOsToPAOs(paos)), f) - require.NoError(t, err) - assert.Equal(t, uint32(0), marketStatus) - }) - t.Run("fails when the mode is less than f+1", func(t *testing.T) { - paos := NewValidParsedAttributedObservations() - for i := range paos { - paos[i].MarketStatus = uint32(i) - } - _, err := GetConsensusMarketStatus(convertMarketStatus(convertTestPAOsToPAOs(paos)), f) - assert.EqualError(t, err, "market status has fewer than f+1 observations (status 0 got 1/4)") - }) - t.Run("fails when all observations are invalid", func(t *testing.T) { - _, err := GetConsensusMarketStatus(convertMarketStatus(convertTestPAOsToPAOs(invalidPaos)), f) - assert.EqualError(t, err, "market status has fewer than f+1 observations (status 0 got 0/4)") - }) - }) -} diff --git a/mercury/v4/mercury.go b/mercury/v4/mercury.go deleted file mode 100644 index e396886..0000000 --- a/mercury/v4/mercury.go +++ /dev/null @@ -1,434 +0,0 @@ -package v4 - -import ( - "context" - "errors" - "fmt" - "math" - "math/big" - "time" - - "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - "google.golang.org/protobuf/proto" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - mercurytypes "github.com/smartcontractkit/chainlink-common/pkg/types/mercury" - v4 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v4" - - "github.com/smartcontractkit/chainlink-data-streams/mercury" -) - -//go:generate protoc -I=. --go_out=. mercury_observation_v4.proto - -// DataSource implementations must be thread-safe. Observe may be called by many -// different threads concurrently. -type DataSource interface { - // Observe queries the data source. Returns a value or an error. Once the - // context is expires, Observe may still do cheap computations and return a - // result, but should return as quickly as possible. - // - // More details: In the current implementation, the context passed to - // Observe will time out after MaxDurationObservation. However, Observe - // should *not* make any assumptions about context timeout behavior. Once - // the context times out, Observe should prioritize returning as quickly as - // possible, but may still perform fast computations to return a result - // rather than error. For example, if Observe medianizes a number of data - // sources, some of which already returned a result to Observe prior to the - // context's expiry, Observe might still compute their median, and return it - // instead of an error. - // - // Important: Observe should not perform any potentially time-consuming - // actions like database access, once the context passed has expired. - Observe(ctx context.Context, repts types.ReportTimestamp, fetchMaxFinalizedTimestamp bool) (v4.Observation, error) -} - -var _ ocr3types.MercuryPluginFactory = Factory{} - -const maxObservationLength = 32 + // feedID - 4 + // timestamp - mercury.ByteWidthInt192 + // benchmarkPrice - mercury.ByteWidthInt192 + // bid - mercury.ByteWidthInt192 + // ask - 4 + // validFromTimestamp - mercury.ByteWidthInt192 + // linkFee - mercury.ByteWidthInt192 + // nativeFee - 4 + // marketStatus (enum is int32) - 18 /* overapprox. of protobuf overhead */ - -type Factory struct { - dataSource DataSource - logger logger.Logger - onchainConfigCodec mercurytypes.OnchainConfigCodec - reportCodec v4.ReportCodec -} - -func NewFactory(ds DataSource, lggr logger.Logger, occ mercurytypes.OnchainConfigCodec, rc v4.ReportCodec) Factory { - return Factory{ds, lggr, occ, rc} -} - -func (fac Factory) NewMercuryPlugin(ctx context.Context, configuration ocr3types.MercuryPluginConfig) (ocr3types.MercuryPlugin, ocr3types.MercuryPluginInfo, error) { - offchainConfig, err := mercury.DecodeOffchainConfig(configuration.OffchainConfig) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - onchainConfig, err := fac.onchainConfigCodec.Decode(ctx, configuration.OnchainConfig) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - maxReportLength, err := fac.reportCodec.MaxReportLength(ctx, configuration.N) - if err != nil { - return nil, ocr3types.MercuryPluginInfo{}, err - } - - r := &reportingPlugin{ - offchainConfig, - onchainConfig, - fac.dataSource, - fac.logger, - fac.reportCodec, - configuration.ConfigDigest, - configuration.F, - mercury.EpochRound{}, - new(big.Int), - maxReportLength, - } - - return r, ocr3types.MercuryPluginInfo{ - Name: "Mercury", - Limits: ocr3types.MercuryPluginLimits{ - MaxObservationLength: maxObservationLength, - MaxReportLength: maxReportLength, - }, - }, nil -} - -var _ ocr3types.MercuryPlugin = (*reportingPlugin)(nil) - -type reportingPlugin struct { - offchainConfig mercury.OffchainConfig - onchainConfig mercurytypes.OnchainConfig - dataSource DataSource - logger logger.Logger - reportCodec v4.ReportCodec - - configDigest types.ConfigDigest - f int - latestAcceptedEpochRound mercury.EpochRound - latestAcceptedMedian *big.Int - maxReportLength int -} - -var MissingPrice = big.NewInt(-1) - -func (rp *reportingPlugin) Observation(ctx context.Context, repts types.ReportTimestamp, previousReport types.Report) (types.Observation, error) { - obs, err := rp.dataSource.Observe(ctx, repts, previousReport == nil) - if err != nil { - return nil, fmt.Errorf("DataSource.Observe returned an error: %s", err) - } - - observationTimestamp := time.Now() - if observationTimestamp.Unix() > math.MaxUint32 { - return nil, fmt.Errorf("current unix epoch %d exceeds max uint32", observationTimestamp.Unix()) - } - p := MercuryObservationProto{Timestamp: uint32(observationTimestamp.Unix())} - var obsErrors []error - - var bpErr error - if obs.BenchmarkPrice.Err != nil { - bpErr = fmt.Errorf("failed to observe BenchmarkPrice: %w", obs.BenchmarkPrice.Err) - obsErrors = append(obsErrors, bpErr) - } else if benchmarkPrice, err := mercury.EncodeValueInt192(obs.BenchmarkPrice.Val); err != nil { - bpErr = fmt.Errorf("failed to encode BenchmarkPrice; val=%s: %w", obs.BenchmarkPrice.Val, err) - obsErrors = append(obsErrors, bpErr) - } else { - p.BenchmarkPrice = benchmarkPrice - p.PricesValid = true - } - - var maxFinalizedTimestampErr error - if obs.MaxFinalizedTimestamp.Err != nil { - maxFinalizedTimestampErr = fmt.Errorf("failed to observe MaxFinalizedTimestamp: %w", obs.MaxFinalizedTimestamp.Err) - obsErrors = append(obsErrors, maxFinalizedTimestampErr) - } else { - p.MaxFinalizedTimestamp = obs.MaxFinalizedTimestamp.Val - p.MaxFinalizedTimestampValid = true - } - - var linkErr error - if obs.LinkPrice.Err != nil { - linkErr = fmt.Errorf("failed to observe LINK price: %w", obs.LinkPrice.Err) - obsErrors = append(obsErrors, linkErr) - } else if obs.LinkPrice.Val.Cmp(MissingPrice) <= 0 { - p.LinkFee = mercury.MaxInt192Enc - } else { - linkFee := mercury.CalculateFee(obs.LinkPrice.Val, rp.offchainConfig.BaseUSDFee) - if linkFeeEncoded, err := mercury.EncodeValueInt192(linkFee); err != nil { - linkErr = fmt.Errorf("failed to encode LINK fee; val=%s: %w", linkFee, err) - obsErrors = append(obsErrors, linkErr) - } else { - p.LinkFee = linkFeeEncoded - } - } - - if linkErr == nil { - p.LinkFeeValid = true - } - - var nativeErr error - if obs.NativePrice.Err != nil { - nativeErr = fmt.Errorf("failed to observe native price: %w", obs.NativePrice.Err) - obsErrors = append(obsErrors, nativeErr) - } else if obs.NativePrice.Val.Cmp(MissingPrice) <= 0 { - p.NativeFee = mercury.MaxInt192Enc - } else { - nativeFee := mercury.CalculateFee(obs.NativePrice.Val, rp.offchainConfig.BaseUSDFee) - if nativeFeeEncoded, err := mercury.EncodeValueInt192(nativeFee); err != nil { - nativeErr = fmt.Errorf("failed to encode native fee; val=%s: %w", nativeFee, err) - obsErrors = append(obsErrors, nativeErr) - } else { - p.NativeFee = nativeFeeEncoded - } - } - - if nativeErr == nil { - p.NativeFeeValid = true - } - - var marketStatusErr error - if obs.MarketStatus.Err != nil { - marketStatusErr = fmt.Errorf("failed to observe market status: %w", obs.MarketStatus.Err) - obsErrors = append(obsErrors, marketStatusErr) - } else { - p.MarketStatus = obs.MarketStatus.Val - p.MarketStatusValid = true - } - - if len(obsErrors) > 0 { - rp.logger.Warnw(fmt.Sprintf("Observe failed %d/7 observations", len(obsErrors)), "err", errors.Join(obsErrors...)) - } - - return proto.Marshal(&p) -} - -func parseAttributedObservation(ao types.AttributedObservation) (PAO, error) { - var pao parsedAttributedObservation - var obs MercuryObservationProto - if err := proto.Unmarshal(ao.Observation, &obs); err != nil { - return parsedAttributedObservation{}, fmt.Errorf("attributed observation cannot be unmarshaled: %s", err) - } - - pao.Timestamp = obs.Timestamp - pao.Observer = ao.Observer - - if obs.PricesValid { - var err error - pao.BenchmarkPrice, err = mercury.DecodeValueInt192(obs.BenchmarkPrice) - if err != nil { - return parsedAttributedObservation{}, fmt.Errorf("benchmarkPrice cannot be converted to big.Int: %s", err) - } - pao.PricesValid = true - } - - if obs.MaxFinalizedTimestampValid { - pao.MaxFinalizedTimestamp = obs.MaxFinalizedTimestamp - pao.MaxFinalizedTimestampValid = true - } - - if obs.LinkFeeValid { - var err error - pao.LinkFee, err = mercury.DecodeValueInt192(obs.LinkFee) - if err != nil { - return parsedAttributedObservation{}, fmt.Errorf("link price cannot be converted to big.Int: %s", err) - } - pao.LinkFeeValid = true - } - if obs.NativeFeeValid { - var err error - pao.NativeFee, err = mercury.DecodeValueInt192(obs.NativeFee) - if err != nil { - return parsedAttributedObservation{}, fmt.Errorf("native price cannot be converted to big.Int: %s", err) - } - pao.NativeFeeValid = true - } - - if obs.MarketStatusValid { - pao.MarketStatus = obs.MarketStatus - pao.MarketStatusValid = true - } - - return pao, nil -} - -func parseAttributedObservations(lggr logger.Logger, aos []types.AttributedObservation) []PAO { - paos := make([]PAO, 0, len(aos)) - for i, ao := range aos { - pao, err := parseAttributedObservation(ao) - if err != nil { - lggr.Warnw("parseAttributedObservations: dropping invalid observation", - "observer", ao.Observer, - "error", err, - "i", i, - ) - continue - } - paos = append(paos, pao) - } - return paos -} - -func (rp *reportingPlugin) Report(ctx context.Context, repts types.ReportTimestamp, previousReport types.Report, aos []types.AttributedObservation) (shouldReport bool, report types.Report, err error) { - paos := parseAttributedObservations(rp.logger, aos) - - if len(paos) == 0 { - return false, nil, errors.New("got zero valid attributed observations") - } - - // By assumption, we have at most f malicious oracles, so there should be at least f+1 valid paos - if !(rp.f+1 <= len(paos)) { - return false, nil, fmt.Errorf("only received %v valid attributed observations, but need at least f+1 (%v)", len(paos), rp.f+1) - } - - rf, err := rp.buildReportFields(ctx, previousReport, paos) - if err != nil { - rp.logger.Errorw("failed to build report fields", "paos", paos, "f", rp.f, "reportFields", rf, "repts", repts, "err", err) - return false, nil, err - } - - if rf.Timestamp < rf.ValidFromTimestamp { - rp.logger.Debugw("shouldReport: no (overlap)", "observationTimestamp", rf.Timestamp, "validFromTimestamp", rf.ValidFromTimestamp, "repts", repts) - return false, nil, nil - } - - if err = rp.validateReport(rf); err != nil { - rp.logger.Errorw("shouldReport: no (validation error)", "reportFields", rf, "err", err, "repts", repts, "paos", paos) - return false, nil, err - } - rp.logger.Debugw("shouldReport: yes", "repts", repts) - - report, err = rp.reportCodec.BuildReport(ctx, rf) - if err != nil { - rp.logger.Debugw("failed to BuildReport", "paos", paos, "f", rp.f, "reportFields", rf, "repts", repts) - return false, nil, err - } - - if !(len(report) <= rp.maxReportLength) { - return false, nil, fmt.Errorf("report with len %d violates MaxReportLength limit set by ReportCodec (%d)", len(report), rp.maxReportLength) - } else if len(report) == 0 { - return false, nil, errors.New("report may not have zero length (invariant violation)") - } - - return true, report, nil -} - -func (rp *reportingPlugin) buildReportFields(ctx context.Context, previousReport types.Report, paos []PAO) (rf v4.ReportFields, merr error) { - mPaos := convert(paos) - rf.Timestamp = mercury.GetConsensusTimestamp(mPaos) - - var err error - if previousReport != nil { - var maxFinalizedTimestamp uint32 - maxFinalizedTimestamp, err = rp.reportCodec.ObservationTimestampFromReport(ctx, previousReport) - merr = errors.Join(merr, err) - rf.ValidFromTimestamp = maxFinalizedTimestamp + 1 - } else { - var maxFinalizedTimestamp int64 - maxFinalizedTimestamp, err = mercury.GetConsensusMaxFinalizedTimestamp(convertMaxFinalizedTimestamp(paos), rp.f) - if err != nil { - merr = errors.Join(merr, err) - } else if maxFinalizedTimestamp < 0 { - // no previous observation timestamp available, e.g. in case of new - // feed; use current timestamp as start of range - rf.ValidFromTimestamp = rf.Timestamp - } else if maxFinalizedTimestamp+1 > math.MaxUint32 { - merr = errors.Join(err, fmt.Errorf("maxFinalizedTimestamp is too large, got: %d", maxFinalizedTimestamp)) - } else { - rf.ValidFromTimestamp = uint32(maxFinalizedTimestamp + 1) - } - } - - rf.BenchmarkPrice, err = mercury.GetConsensusBenchmarkPrice(mPaos, rp.f) - if err != nil { - merr = errors.Join(merr, fmt.Errorf("GetConsensusBenchmarkPrice failed: %w", err)) - } - - rf.LinkFee, err = mercury.GetConsensusLinkFee(convertLinkFee(paos), rp.f) - if err != nil { - // It is better to generate a report that will validate for free, - // rather than no report at all, if we cannot come to consensus on a - // valid fee. - rp.logger.Errorw("Cannot come to consensus on LINK fee, falling back to 0", "err", err, "paos", paos) - rf.LinkFee = big.NewInt(0) - } - - rf.NativeFee, err = mercury.GetConsensusNativeFee(convertNativeFee(paos), rp.f) - if err != nil { - // It is better to generate a report that will validate for free, - // rather than no report at all, if we cannot come to consensus on a - // valid fee. - rp.logger.Errorw("Cannot come to consensus on Native fee, falling back to 0", "err", err, "paos", paos) - rf.NativeFee = big.NewInt(0) - } - - rf.MarketStatus, err = GetConsensusMarketStatus(convertMarketStatus(paos), rp.f) - if err != nil { - merr = errors.Join(merr, fmt.Errorf("GetConsensusMarketStatus failed: %w", err)) - } - - if int64(rf.Timestamp)+int64(rp.offchainConfig.ExpirationWindow) > math.MaxUint32 { - merr = errors.Join(merr, fmt.Errorf("timestamp %d + expiration window %d overflows uint32", rf.Timestamp, rp.offchainConfig.ExpirationWindow)) - } else { - rf.ExpiresAt = rf.Timestamp + rp.offchainConfig.ExpirationWindow - } - - return rf, merr -} - -func (rp *reportingPlugin) validateReport(rf v4.ReportFields) error { - return errors.Join( - mercury.ValidateBetween("median benchmark price", rf.BenchmarkPrice, rp.onchainConfig.Min, rp.onchainConfig.Max), - mercury.ValidateFee("median link fee", rf.LinkFee), - mercury.ValidateFee("median native fee", rf.NativeFee), - mercury.ValidateValidFromTimestamp(rf.Timestamp, rf.ValidFromTimestamp), - mercury.ValidateExpiresAt(rf.Timestamp, rf.ExpiresAt), - ) -} - -func (rp *reportingPlugin) Close() error { - return nil -} - -// convert funcs are necessary because go is not smart enough to cast -// []interface1 to []interface2 even if interface1 is a superset of interface2 -func convert(pao []PAO) (ret []mercury.PAO) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertMaxFinalizedTimestamp(pao []PAO) (ret []mercury.PAOMaxFinalizedTimestamp) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertLinkFee(pao []PAO) (ret []mercury.PAOLinkFee) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertNativeFee(pao []PAO) (ret []mercury.PAONativeFee) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} -func convertMarketStatus(pao []PAO) (ret []PAOMarketStatus) { - for _, v := range pao { - ret = append(ret, v) - } - return ret -} diff --git a/mercury/v4/mercury_observation_v4.pb.go b/mercury/v4/mercury_observation_v4.pb.go deleted file mode 100644 index 4cc042f..0000000 --- a/mercury/v4/mercury_observation_v4.pb.go +++ /dev/null @@ -1,249 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.31.0 -// protoc v4.23.2 -// source: mercury_observation_v4.proto - -package v4 - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type MercuryObservationProto struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Timestamp uint32 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` - BenchmarkPrice []byte `protobuf:"bytes,2,opt,name=benchmarkPrice,proto3" json:"benchmarkPrice,omitempty"` - PricesValid bool `protobuf:"varint,5,opt,name=pricesValid,proto3" json:"pricesValid,omitempty"` - MaxFinalizedTimestamp int64 `protobuf:"varint,6,opt,name=maxFinalizedTimestamp,proto3" json:"maxFinalizedTimestamp,omitempty"` - MaxFinalizedTimestampValid bool `protobuf:"varint,7,opt,name=maxFinalizedTimestampValid,proto3" json:"maxFinalizedTimestampValid,omitempty"` - LinkFee []byte `protobuf:"bytes,8,opt,name=linkFee,proto3" json:"linkFee,omitempty"` - LinkFeeValid bool `protobuf:"varint,9,opt,name=linkFeeValid,proto3" json:"linkFeeValid,omitempty"` - NativeFee []byte `protobuf:"bytes,10,opt,name=nativeFee,proto3" json:"nativeFee,omitempty"` - NativeFeeValid bool `protobuf:"varint,11,opt,name=nativeFeeValid,proto3" json:"nativeFeeValid,omitempty"` - MarketStatus uint32 `protobuf:"varint,12,opt,name=marketStatus,proto3" json:"marketStatus,omitempty"` - MarketStatusValid bool `protobuf:"varint,13,opt,name=marketStatusValid,proto3" json:"marketStatusValid,omitempty"` -} - -func (x *MercuryObservationProto) Reset() { - *x = MercuryObservationProto{} - if protoimpl.UnsafeEnabled { - mi := &file_mercury_observation_v4_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *MercuryObservationProto) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*MercuryObservationProto) ProtoMessage() {} - -func (x *MercuryObservationProto) ProtoReflect() protoreflect.Message { - mi := &file_mercury_observation_v4_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use MercuryObservationProto.ProtoReflect.Descriptor instead. -func (*MercuryObservationProto) Descriptor() ([]byte, []int) { - return file_mercury_observation_v4_proto_rawDescGZIP(), []int{0} -} - -func (x *MercuryObservationProto) GetTimestamp() uint32 { - if x != nil { - return x.Timestamp - } - return 0 -} - -func (x *MercuryObservationProto) GetBenchmarkPrice() []byte { - if x != nil { - return x.BenchmarkPrice - } - return nil -} - -func (x *MercuryObservationProto) GetPricesValid() bool { - if x != nil { - return x.PricesValid - } - return false -} - -func (x *MercuryObservationProto) GetMaxFinalizedTimestamp() int64 { - if x != nil { - return x.MaxFinalizedTimestamp - } - return 0 -} - -func (x *MercuryObservationProto) GetMaxFinalizedTimestampValid() bool { - if x != nil { - return x.MaxFinalizedTimestampValid - } - return false -} - -func (x *MercuryObservationProto) GetLinkFee() []byte { - if x != nil { - return x.LinkFee - } - return nil -} - -func (x *MercuryObservationProto) GetLinkFeeValid() bool { - if x != nil { - return x.LinkFeeValid - } - return false -} - -func (x *MercuryObservationProto) GetNativeFee() []byte { - if x != nil { - return x.NativeFee - } - return nil -} - -func (x *MercuryObservationProto) GetNativeFeeValid() bool { - if x != nil { - return x.NativeFeeValid - } - return false -} - -func (x *MercuryObservationProto) GetMarketStatus() uint32 { - if x != nil { - return x.MarketStatus - } - return 0 -} - -func (x *MercuryObservationProto) GetMarketStatusValid() bool { - if x != nil { - return x.MarketStatusValid - } - return false -} - -var File_mercury_observation_v4_proto protoreflect.FileDescriptor - -var file_mercury_observation_v4_proto_rawDesc = []byte{ - 0x0a, 0x1c, 0x6d, 0x65, 0x72, 0x63, 0x75, 0x72, 0x79, 0x5f, 0x6f, 0x62, 0x73, 0x65, 0x72, 0x76, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x76, 0x34, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, - 0x76, 0x34, 0x22, 0xcd, 0x03, 0x0a, 0x17, 0x4d, 0x65, 0x72, 0x63, 0x75, 0x72, 0x79, 0x4f, 0x62, - 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1c, - 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0d, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x26, 0x0a, 0x0e, - 0x62, 0x65, 0x6e, 0x63, 0x68, 0x6d, 0x61, 0x72, 0x6b, 0x50, 0x72, 0x69, 0x63, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x62, 0x65, 0x6e, 0x63, 0x68, 0x6d, 0x61, 0x72, 0x6b, 0x50, - 0x72, 0x69, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x63, 0x65, 0x73, 0x56, 0x61, - 0x6c, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x70, 0x72, 0x69, 0x63, 0x65, - 0x73, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6d, 0x61, 0x78, 0x46, 0x69, 0x6e, - 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x6d, 0x61, 0x78, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, - 0x7a, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x3e, 0x0a, 0x1a, - 0x6d, 0x61, 0x78, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x1a, 0x6d, 0x61, 0x78, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x64, 0x54, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, - 0x6c, 0x69, 0x6e, 0x6b, 0x46, 0x65, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6c, - 0x69, 0x6e, 0x6b, 0x46, 0x65, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x6c, 0x69, 0x6e, 0x6b, 0x46, 0x65, - 0x65, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x6c, 0x69, - 0x6e, 0x6b, 0x46, 0x65, 0x65, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, - 0x74, 0x69, 0x76, 0x65, 0x46, 0x65, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x6e, - 0x61, 0x74, 0x69, 0x76, 0x65, 0x46, 0x65, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x61, 0x74, 0x69, - 0x76, 0x65, 0x46, 0x65, 0x65, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0e, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x46, 0x65, 0x65, 0x56, 0x61, 0x6c, 0x69, 0x64, - 0x12, 0x22, 0x0a, 0x0c, 0x6d, 0x61, 0x72, 0x6b, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x6d, 0x61, 0x72, 0x6b, 0x65, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x2c, 0x0a, 0x11, 0x6d, 0x61, 0x72, 0x6b, 0x65, 0x74, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x11, 0x6d, 0x61, 0x72, 0x6b, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x56, 0x61, 0x6c, - 0x69, 0x64, 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x3b, 0x76, 0x34, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, -} - -var ( - file_mercury_observation_v4_proto_rawDescOnce sync.Once - file_mercury_observation_v4_proto_rawDescData = file_mercury_observation_v4_proto_rawDesc -) - -func file_mercury_observation_v4_proto_rawDescGZIP() []byte { - file_mercury_observation_v4_proto_rawDescOnce.Do(func() { - file_mercury_observation_v4_proto_rawDescData = protoimpl.X.CompressGZIP(file_mercury_observation_v4_proto_rawDescData) - }) - return file_mercury_observation_v4_proto_rawDescData -} - -var file_mercury_observation_v4_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_mercury_observation_v4_proto_goTypes = []interface{}{ - (*MercuryObservationProto)(nil), // 0: v4.MercuryObservationProto -} -var file_mercury_observation_v4_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_mercury_observation_v4_proto_init() } -func file_mercury_observation_v4_proto_init() { - if File_mercury_observation_v4_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_mercury_observation_v4_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MercuryObservationProto); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_mercury_observation_v4_proto_rawDesc, - NumEnums: 0, - NumMessages: 1, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_mercury_observation_v4_proto_goTypes, - DependencyIndexes: file_mercury_observation_v4_proto_depIdxs, - MessageInfos: file_mercury_observation_v4_proto_msgTypes, - }.Build() - File_mercury_observation_v4_proto = out.File - file_mercury_observation_v4_proto_rawDesc = nil - file_mercury_observation_v4_proto_goTypes = nil - file_mercury_observation_v4_proto_depIdxs = nil -} diff --git a/mercury/v4/mercury_observation_v4.proto b/mercury/v4/mercury_observation_v4.proto deleted file mode 100644 index 598bcad..0000000 --- a/mercury/v4/mercury_observation_v4.proto +++ /dev/null @@ -1,22 +0,0 @@ -syntax="proto3"; - -package v4; -option go_package = ".;v4"; - -message MercuryObservationProto { - uint32 timestamp = 1; - - bytes benchmarkPrice = 2; - bool pricesValid = 5; - - int64 maxFinalizedTimestamp = 6; - bool maxFinalizedTimestampValid = 7; - - bytes linkFee = 8; - bool linkFeeValid = 9; - bytes nativeFee = 10; - bool nativeFeeValid = 11; - - uint32 marketStatus = 12; - bool marketStatusValid = 13; -} diff --git a/mercury/v4/mercury_test.go b/mercury/v4/mercury_test.go deleted file mode 100644 index 253beab..0000000 --- a/mercury/v4/mercury_test.go +++ /dev/null @@ -1,702 +0,0 @@ -package v4 - -import ( - "context" - "errors" - "math" - "math/big" - "math/rand" - "reflect" - "testing" - "time" - - "github.com/shopspring/decimal" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - "github.com/smartcontractkit/libocr/commontypes" - "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - mercurytypes "github.com/smartcontractkit/chainlink-common/pkg/types/mercury" - v4 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v4" - "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - - "github.com/smartcontractkit/chainlink-data-streams/mercury" -) - -type testDataSource struct { - Obs v4.Observation -} - -func (ds testDataSource) Observe(ctx context.Context, repts types.ReportTimestamp, fetchMaxFinalizedTimestamp bool) (v4.Observation, error) { - return ds.Obs, nil -} - -type testReportCodec struct { - observationTimestamp uint32 - builtReport types.Report - - builtReportFields *v4.ReportFields - err error -} - -func (rc *testReportCodec) BuildReport(ctx context.Context, rf v4.ReportFields) (types.Report, error) { - rc.builtReportFields = &rf - - return rc.builtReport, nil -} - -func (rc testReportCodec) MaxReportLength(ctx context.Context, n int) (int, error) { - return 123, nil -} - -func (rc testReportCodec) ObservationTimestampFromReport(context.Context, types.Report) (uint32, error) { - return rc.observationTimestamp, rc.err -} - -func newTestReportPlugin(t *testing.T, codec *testReportCodec, ds *testDataSource) *reportingPlugin { - ctx := tests.Context(t) - offchainConfig := mercury.OffchainConfig{ - ExpirationWindow: 1, - BaseUSDFee: decimal.NewFromInt32(1), - } - onchainConfig := mercurytypes.OnchainConfig{ - Min: big.NewInt(1), - Max: big.NewInt(1000), - } - maxReportLength, _ := codec.MaxReportLength(ctx, 4) - return &reportingPlugin{ - offchainConfig: offchainConfig, - onchainConfig: onchainConfig, - dataSource: ds, - logger: logger.Test(t), - reportCodec: codec, - configDigest: types.ConfigDigest{}, - f: 1, - latestAcceptedEpochRound: mercury.EpochRound{}, - latestAcceptedMedian: big.NewInt(0), - maxReportLength: maxReportLength, - } -} - -func newValidProtos() []*MercuryObservationProto { - return []*MercuryObservationProto{ - &MercuryObservationProto{ - Timestamp: 42, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(123)), - PricesValid: true, - - MaxFinalizedTimestamp: 40, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.1e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.1e18)), - NativeFeeValid: true, - - MarketStatus: 1, - MarketStatusValid: true, - }, - &MercuryObservationProto{ - Timestamp: 45, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(234)), - PricesValid: true, - - MaxFinalizedTimestamp: 40, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.2e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.2e18)), - NativeFeeValid: true, - - MarketStatus: 1, - MarketStatusValid: true, - }, - &MercuryObservationProto{ - Timestamp: 47, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(345)), - PricesValid: true, - - MaxFinalizedTimestamp: 39, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.3e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.3e18)), - NativeFeeValid: true, - - MarketStatus: 1, - MarketStatusValid: true, - }, - &MercuryObservationProto{ - Timestamp: 39, - - BenchmarkPrice: mercury.MustEncodeValueInt192(big.NewInt(456)), - PricesValid: true, - - MaxFinalizedTimestamp: 39, - MaxFinalizedTimestampValid: true, - - LinkFee: mercury.MustEncodeValueInt192(big.NewInt(1.4e18)), - LinkFeeValid: true, - NativeFee: mercury.MustEncodeValueInt192(big.NewInt(2.4e18)), - NativeFeeValid: true, - - MarketStatus: 1, - MarketStatusValid: true, - }, - } -} - -func newValidAos(t *testing.T, protos ...*MercuryObservationProto) (aos []types.AttributedObservation) { - if len(protos) == 0 { - protos = newValidProtos() - } - aos = make([]types.AttributedObservation, len(protos)) - for i := range aos { - marshalledObs, err := proto.Marshal(protos[i]) - require.NoError(t, err) - aos[i] = types.AttributedObservation{ - Observation: marshalledObs, - Observer: commontypes.OracleID(i), - } - } - return -} - -func Test_Plugin_Report(t *testing.T) { - dataSource := &testDataSource{} - codec := &testReportCodec{ - builtReport: []byte{1, 2, 3, 4}, - } - rp := newTestReportPlugin(t, codec, dataSource) - repts := types.ReportTimestamp{} - - t.Run("when previous report is nil", func(t *testing.T) { - t.Run("errors if not enough attributed observations", func(t *testing.T) { - ctx := tests.Context(t) - _, _, err := rp.Report(ctx, repts, nil, newValidAos(t)[0:1]) - assert.EqualError(t, err, "only received 1 valid attributed observations, but need at least f+1 (2)") - }) - - t.Run("errors if too many maxFinalizedTimestamp observations are invalid", func(t *testing.T) { - ctx := tests.Context(t) - ps := newValidProtos() - ps[0].MaxFinalizedTimestampValid = false - ps[1].MaxFinalizedTimestampValid = false - ps[2].MaxFinalizedTimestampValid = false - aos := newValidAos(t, ps...) - - should, _, err := rp.Report(ctx, types.ReportTimestamp{}, nil, aos) - assert.False(t, should) - assert.EqualError(t, err, "fewer than f+1 observations have a valid maxFinalizedTimestamp (got: 1/4)") - }) - t.Run("errors if maxFinalizedTimestamp is too large", func(t *testing.T) { - ctx := tests.Context(t) - ps := newValidProtos() - ps[0].MaxFinalizedTimestamp = math.MaxUint32 - ps[1].MaxFinalizedTimestamp = math.MaxUint32 - ps[2].MaxFinalizedTimestamp = math.MaxUint32 - ps[3].MaxFinalizedTimestamp = math.MaxUint32 - aos := newValidAos(t, ps...) - - should, _, err := rp.Report(ctx, types.ReportTimestamp{}, nil, aos) - assert.False(t, should) - assert.EqualError(t, err, "maxFinalizedTimestamp is too large, got: 4294967295") - }) - - t.Run("succeeds and generates validFromTimestamp from maxFinalizedTimestamp when maxFinalizedTimestamp is positive", func(t *testing.T) { - ctx := tests.Context(t) - aos := newValidAos(t) - - should, report, err := rp.Report(ctx, types.ReportTimestamp{}, nil, aos) - assert.True(t, should) - assert.NoError(t, err) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, v4.ReportFields{ - ValidFromTimestamp: 41, // consensus maxFinalizedTimestamp is 40, so validFrom should be 40+1 - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - MarketStatus: 1, - }, *codec.builtReportFields) - }) - t.Run("succeeds and generates validFromTimestamp from maxFinalizedTimestamp when maxFinalizedTimestamp is zero", func(t *testing.T) { - ctx := tests.Context(t) - protos := newValidProtos() - for i := range protos { - protos[i].MaxFinalizedTimestamp = 0 - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(ctx, types.ReportTimestamp{}, nil, aos) - assert.True(t, should) - assert.NoError(t, err) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, v4.ReportFields{ - ValidFromTimestamp: 1, - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - MarketStatus: 1, - }, *codec.builtReportFields) - }) - t.Run("succeeds and generates validFromTimestamp from maxFinalizedTimestamp when maxFinalizedTimestamp is -1 (missing feed)", func(t *testing.T) { - ctx := tests.Context(t) - protos := newValidProtos() - for i := range protos { - protos[i].MaxFinalizedTimestamp = -1 - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(ctx, types.ReportTimestamp{}, nil, aos) - assert.True(t, should) - assert.NoError(t, err) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, v4.ReportFields{ - ValidFromTimestamp: 45, // in case of missing feed, ValidFromTimestamp=Timestamp for first report - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - MarketStatus: 1, - }, *codec.builtReportFields) - }) - - t.Run("succeeds, ignoring unparseable attributed observation", func(t *testing.T) { - ctx := tests.Context(t) - aos := newValidAos(t) - aos[0] = newUnparseableAttributedObservation() - - should, report, err := rp.Report(ctx, repts, nil, aos) - require.NoError(t, err) - - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, v4.ReportFields{ - ValidFromTimestamp: 40, // consensus maxFinalizedTimestamp is 39, so validFrom should be 39+1 - Timestamp: 45, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: 46, - BenchmarkPrice: big.NewInt(345), - MarketStatus: 1, - }, *codec.builtReportFields) - }) - }) - - t.Run("when previous report is present", func(t *testing.T) { - *codec = testReportCodec{ - observationTimestamp: uint32(rand.Int31n(math.MaxInt16)), - builtReport: []byte{1, 2, 3, 4}, - } - previousReport := types.Report{} - - t.Run("succeeds and uses timestamp from previous report if valid", func(t *testing.T) { - ctx := tests.Context(t) - protos := newValidProtos() - ts := codec.observationTimestamp + 1 - for i := range protos { - protos[i].Timestamp = ts - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(ctx, repts, previousReport, aos) - require.NoError(t, err) - - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, v4.ReportFields{ - ValidFromTimestamp: codec.observationTimestamp + 1, // previous observation timestamp +1 second - Timestamp: ts, - NativeFee: big.NewInt(2300000000000000000), // 2.3e18 - LinkFee: big.NewInt(1300000000000000000), // 1.3e18 - ExpiresAt: ts + 1, - BenchmarkPrice: big.NewInt(345), - MarketStatus: 1, - }, *codec.builtReportFields) - }) - t.Run("errors if cannot extract timestamp from previous report", func(t *testing.T) { - ctx := tests.Context(t) - codec.err = errors.New("something exploded trying to extract timestamp") - aos := newValidAos(t) - - should, _, err := rp.Report(ctx, types.ReportTimestamp{}, previousReport, aos) - assert.False(t, should) - assert.EqualError(t, err, "something exploded trying to extract timestamp") - }) - t.Run("does not report if observationTimestamp < validFromTimestamp", func(t *testing.T) { - ctx := tests.Context(t) - codec.observationTimestamp = 43 - codec.err = nil - - protos := newValidProtos() - for i := range protos { - protos[i].Timestamp = 42 - } - aos := newValidAos(t, protos...) - - should, _, err := rp.Report(ctx, types.ReportTimestamp{}, previousReport, aos) - assert.False(t, should) - assert.NoError(t, err) - }) - t.Run("uses 0 values for link/native if they are invalid", func(t *testing.T) { - ctx := tests.Context(t) - codec.observationTimestamp = 42 - codec.err = nil - - protos := newValidProtos() - for i := range protos { - protos[i].LinkFeeValid = false - protos[i].NativeFeeValid = false - } - aos := newValidAos(t, protos...) - - should, report, err := rp.Report(ctx, types.ReportTimestamp{}, previousReport, aos) - assert.True(t, should) - assert.NoError(t, err) - - assert.True(t, should) - assert.Equal(t, codec.builtReport, report) - require.NotNil(t, codec.builtReportFields) - assert.Equal(t, "0", codec.builtReportFields.LinkFee.String()) - assert.Equal(t, "0", codec.builtReportFields.NativeFee.String()) - }) - }) - - t.Run("buildReport failures", func(t *testing.T) { - t.Run("Report errors when the report is too large", func(t *testing.T) { - ctx := tests.Context(t) - aos := newValidAos(t) - codec.builtReport = make([]byte, 1<<16) - - _, _, err := rp.Report(ctx, types.ReportTimestamp{}, nil, aos) - - assert.EqualError(t, err, "report with len 65536 violates MaxReportLength limit set by ReportCodec (123)") - }) - - t.Run("Report errors when the report length is 0", func(t *testing.T) { - ctx := tests.Context(t) - aos := newValidAos(t) - codec.builtReport = []byte{} - _, _, err := rp.Report(ctx, types.ReportTimestamp{}, nil, aos) - - assert.EqualError(t, err, "report may not have zero length (invariant violation)") - }) - }) -} - -func Test_Plugin_validateReport(t *testing.T) { - dataSource := &testDataSource{} - codec := &testReportCodec{} - rp := newTestReportPlugin(t, codec, dataSource) - - t.Run("valid reports", func(t *testing.T) { - rf := v4.ReportFields{ - ValidFromTimestamp: 42, - Timestamp: 43, - NativeFee: big.NewInt(100), - LinkFee: big.NewInt(50), - ExpiresAt: 44, - BenchmarkPrice: big.NewInt(150), - } - err := rp.validateReport(rf) - require.NoError(t, err) - - rf = v4.ReportFields{ - ValidFromTimestamp: 42, - Timestamp: 42, - NativeFee: big.NewInt(0), - LinkFee: big.NewInt(0), - ExpiresAt: 42, - BenchmarkPrice: big.NewInt(1), - } - err = rp.validateReport(rf) - require.NoError(t, err) - }) - - t.Run("fails validation", func(t *testing.T) { - rf := v4.ReportFields{ - ValidFromTimestamp: 44, // later than timestamp not allowed - Timestamp: 43, - NativeFee: big.NewInt(-1), // negative value not allowed - LinkFee: big.NewInt(-1), // negative value not allowed - ExpiresAt: 42, // before timestamp - BenchmarkPrice: big.NewInt(150000), // exceeds max - } - err := rp.validateReport(rf) - require.Error(t, err) - - assert.Contains(t, err.Error(), "median benchmark price (Value: 150000) is outside of allowable range (Min: 1, Max: 1000)") - assert.Contains(t, err.Error(), "median link fee (Value: -1) is outside of allowable range (Min: 0, Max: 3138550867693340381917894711603833208051177722232017256447)") - assert.Contains(t, err.Error(), "median native fee (Value: -1) is outside of allowable range (Min: 0, Max: 3138550867693340381917894711603833208051177722232017256447)") - assert.Contains(t, err.Error(), "observationTimestamp (Value: 43) must be >= validFromTimestamp (Value: 44)") - assert.Contains(t, err.Error(), "expiresAt (Value: 42) must be ahead of observation timestamp (Value: 43)") - }) - - t.Run("zero values", func(t *testing.T) { - rf := v4.ReportFields{} - err := rp.validateReport(rf) - require.Error(t, err) - - assert.Contains(t, err.Error(), "median benchmark price: got nil value") - assert.Contains(t, err.Error(), "median native fee: got nil value") - assert.Contains(t, err.Error(), "median link fee: got nil value") - }) -} - -func mustDecodeBigInt(b []byte) *big.Int { - n, err := mercury.DecodeValueInt192(b) - if err != nil { - panic(err) - } - return n -} - -func Test_Plugin_Observation(t *testing.T) { - dataSource := &testDataSource{} - codec := &testReportCodec{} - rp := newTestReportPlugin(t, codec, dataSource) - t.Run("Observation protobuf doesn't exceed maxObservationLength", func(t *testing.T) { - obs := MercuryObservationProto{ - Timestamp: math.MaxUint32, - BenchmarkPrice: make([]byte, 24), - PricesValid: true, - MaxFinalizedTimestamp: math.MaxUint32, - MaxFinalizedTimestampValid: true, - LinkFee: make([]byte, 24), - LinkFeeValid: true, - NativeFee: make([]byte, 24), - NativeFeeValid: true, - } - // This assertion is here to force this test to fail if a new field is - // added to the protobuf. In this case, you must add the max value of - // the field to the MercuryObservationProto in the test and only after - // that increment the count below - numFields := reflect.TypeOf(obs).NumField() //nolint:all - // 3 fields internal to pbuf struct - require.Equal(t, 11, numFields-3) - - b, err := proto.Marshal(&obs) - require.NoError(t, err) - assert.LessOrEqual(t, len(b), maxObservationLength) - }) - - validBenchmarkPrice := big.NewInt(rand.Int63() - 2) - - t.Run("all observations succeeded", func(t *testing.T) { - obs := v4.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: validBenchmarkPrice, - }, - MaxFinalizedTimestamp: mercurytypes.ObsResult[int64]{ - Val: rand.Int63(), - }, - LinkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - NativePrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - } - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Equal(t, obs.BenchmarkPrice.Val, mustDecodeBigInt(p.BenchmarkPrice)) - assert.True(t, p.PricesValid) - assert.Equal(t, obs.MaxFinalizedTimestamp.Val, p.MaxFinalizedTimestamp) - assert.True(t, p.MaxFinalizedTimestampValid) - - fee := mercury.CalculateFee(obs.LinkPrice.Val, decimal.NewFromInt32(1)) - assert.Equal(t, fee, mustDecodeBigInt(p.LinkFee)) - assert.True(t, p.LinkFeeValid) - - fee = mercury.CalculateFee(obs.NativePrice.Val, decimal.NewFromInt32(1)) - assert.Equal(t, fee, mustDecodeBigInt(p.NativeFee)) - assert.True(t, p.NativeFeeValid) - }) - - t.Run("negative link/native prices set fee to max int192", func(t *testing.T) { - obs := v4.Observation{ - LinkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(-1), - }, - NativePrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(-1), - }, - } - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.Equal(t, mercury.MaxInt192, mustDecodeBigInt(p.LinkFee)) - assert.True(t, p.LinkFeeValid) - assert.Equal(t, mercury.MaxInt192, mustDecodeBigInt(p.NativeFee)) - assert.True(t, p.NativeFeeValid) - }) - - t.Run("some observations failed", func(t *testing.T) { - obs := v4.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - Err: errors.New("bechmarkPrice error"), - }, - MaxFinalizedTimestamp: mercurytypes.ObsResult[int64]{ - Val: rand.Int63(), - Err: errors.New("maxFinalizedTimestamp error"), - }, - LinkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - Err: errors.New("linkPrice error"), - }, - NativePrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Zero(t, p.BenchmarkPrice) - assert.False(t, p.PricesValid) - assert.Zero(t, p.MaxFinalizedTimestamp) - assert.False(t, p.MaxFinalizedTimestampValid) - assert.Zero(t, p.LinkFee) - assert.False(t, p.LinkFeeValid) - - fee := mercury.CalculateFee(obs.NativePrice.Val, decimal.NewFromInt32(1)) - assert.Equal(t, fee, mustDecodeBigInt(p.NativeFee)) - assert.True(t, p.NativeFeeValid) - }) - - t.Run("all observations failed", func(t *testing.T) { - obs := v4.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Err: errors.New("benchmarkPrice error"), - }, - MaxFinalizedTimestamp: mercurytypes.ObsResult[int64]{ - Err: errors.New("maxFinalizedTimestamp error"), - }, - LinkPrice: mercurytypes.ObsResult[*big.Int]{ - Err: errors.New("linkPrice error"), - }, - NativePrice: mercurytypes.ObsResult[*big.Int]{ - Err: errors.New("nativePrice error"), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.LessOrEqual(t, p.Timestamp, uint32(time.Now().Unix())) - assert.Zero(t, p.BenchmarkPrice) - assert.False(t, p.PricesValid) - assert.Zero(t, p.MaxFinalizedTimestamp) - assert.False(t, p.MaxFinalizedTimestampValid) - assert.Zero(t, p.LinkFee) - assert.False(t, p.LinkFeeValid) - assert.Zero(t, p.NativeFee) - assert.False(t, p.NativeFeeValid) - }) - - t.Run("encoding fails on some observations", func(t *testing.T) { - obs := v4.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - MaxFinalizedTimestamp: mercurytypes.ObsResult[int64]{ - Val: rand.Int63(), - }, - LinkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - NativePrice: mercurytypes.ObsResult[*big.Int]{ - Val: big.NewInt(rand.Int63()), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.Zero(t, p.BenchmarkPrice) - assert.False(t, p.PricesValid) - }) - - t.Run("encoding fails on all observations", func(t *testing.T) { - obs := v4.Observation{ - BenchmarkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - MaxFinalizedTimestamp: mercurytypes.ObsResult[int64]{ - Val: rand.Int63(), - }, - // encoding never fails on calculated fees - LinkPrice: mercurytypes.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - NativePrice: mercurytypes.ObsResult[*big.Int]{ - Val: new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), - }, - } - - dataSource.Obs = obs - - parsedObs, err := rp.Observation(context.Background(), types.ReportTimestamp{}, nil) - require.NoError(t, err) - - var p MercuryObservationProto - require.NoError(t, proto.Unmarshal(parsedObs, &p)) - - assert.Zero(t, p.BenchmarkPrice) - assert.False(t, p.PricesValid) - }) -} - -func newUnparseableAttributedObservation() types.AttributedObservation { - return types.AttributedObservation{ - Observation: []byte{1, 2}, - Observer: commontypes.OracleID(42), - } -} diff --git a/mercury/v4/observation.go b/mercury/v4/observation.go deleted file mode 100644 index 4ec9f79..0000000 --- a/mercury/v4/observation.go +++ /dev/null @@ -1,96 +0,0 @@ -package v4 - -import ( - "math/big" - - "github.com/smartcontractkit/libocr/commontypes" - - "github.com/smartcontractkit/chainlink-data-streams/mercury" -) - -type PAO interface { - mercury.PAO - GetMaxFinalizedTimestamp() (int64, bool) - GetLinkFee() (*big.Int, bool) - GetNativeFee() (*big.Int, bool) - GetMarketStatus() (uint32, bool) -} - -var _ PAO = parsedAttributedObservation{} - -type parsedAttributedObservation struct { - Timestamp uint32 - Observer commontypes.OracleID - - BenchmarkPrice *big.Int - PricesValid bool - - MaxFinalizedTimestamp int64 - MaxFinalizedTimestampValid bool - - LinkFee *big.Int - LinkFeeValid bool - - NativeFee *big.Int - NativeFeeValid bool - - MarketStatus uint32 - MarketStatusValid bool -} - -func NewParsedAttributedObservation(ts uint32, observer commontypes.OracleID, - bp *big.Int, pricesValid bool, mfts int64, - mftsValid bool, linkFee *big.Int, linkFeeValid bool, nativeFee *big.Int, nativeFeeValid bool, - marketStatus uint32, marketStatusValid bool) PAO { - return parsedAttributedObservation{ - Timestamp: ts, - Observer: observer, - - BenchmarkPrice: bp, - PricesValid: pricesValid, - - MaxFinalizedTimestamp: mfts, - MaxFinalizedTimestampValid: mftsValid, - - LinkFee: linkFee, - LinkFeeValid: linkFeeValid, - - NativeFee: nativeFee, - NativeFeeValid: nativeFeeValid, - - MarketStatus: marketStatus, - MarketStatusValid: marketStatusValid, - } -} - -func (pao parsedAttributedObservation) GetTimestamp() uint32 { - return pao.Timestamp -} - -func (pao parsedAttributedObservation) GetObserver() commontypes.OracleID { - return pao.Observer -} - -func (pao parsedAttributedObservation) GetBenchmarkPrice() (*big.Int, bool) { - return pao.BenchmarkPrice, pao.PricesValid -} - -func (pao parsedAttributedObservation) GetMaxFinalizedTimestamp() (int64, bool) { - if pao.MaxFinalizedTimestamp < -1 { - // values below -1 are not valid - return 0, false - } - return pao.MaxFinalizedTimestamp, pao.MaxFinalizedTimestampValid -} - -func (pao parsedAttributedObservation) GetLinkFee() (*big.Int, bool) { - return pao.LinkFee, pao.LinkFeeValid -} - -func (pao parsedAttributedObservation) GetNativeFee() (*big.Int, bool) { - return pao.NativeFee, pao.NativeFeeValid -} - -func (pao parsedAttributedObservation) GetMarketStatus() (uint32, bool) { - return pao.MarketStatus, pao.MarketStatusValid -} diff --git a/mercury/validation.go b/mercury/validation.go deleted file mode 100644 index a059630..0000000 --- a/mercury/validation.go +++ /dev/null @@ -1,41 +0,0 @@ -package mercury - -import ( - "fmt" - "math/big" -) - -// NOTE: hardcoded for now, this may need to change if we support block range on chains other than eth -const EvmHashLen = 32 - -// ValidateBetween checks that value is between min and max -func ValidateBetween(name string, answer *big.Int, min, max *big.Int) error { - if answer == nil { - return fmt.Errorf("%s: got nil value", name) - } - if !(min.Cmp(answer) <= 0 && answer.Cmp(max) <= 0) { - return fmt.Errorf("%s (Value: %s) is outside of allowable range (Min: %s, Max: %s)", name, answer, min, max) - } - - return nil -} - -func ValidateValidFromTimestamp(observationTimestamp uint32, validFromTimestamp uint32) error { - if observationTimestamp < validFromTimestamp { - return fmt.Errorf("observationTimestamp (Value: %d) must be >= validFromTimestamp (Value: %d)", observationTimestamp, validFromTimestamp) - } - - return nil -} - -func ValidateExpiresAt(observationTimestamp uint32, expiresAt uint32) error { - if observationTimestamp > expiresAt { - return fmt.Errorf("expiresAt (Value: %d) must be ahead of observation timestamp (Value: %d)", expiresAt, observationTimestamp) - } - - return nil -} - -func ValidateFee(name string, answer *big.Int) error { - return ValidateBetween(name, answer, big.NewInt(0), MaxInt192) -} diff --git a/mercury/validation_test.go b/mercury/validation_test.go deleted file mode 100644 index 87446b5..0000000 --- a/mercury/validation_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package mercury - -import ( - "math/big" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestValidation(t *testing.T) { - min := big.NewInt(0) - max := big.NewInt(10_000) - - badMin := big.NewInt(9_000) - badMax := big.NewInt(10) - - t.Run("ValidateValidFromTimestamp", func(t *testing.T) { - t.Run("succeeds when observationTimestamp is >= validFromTimestamp", func(t *testing.T) { - err := ValidateValidFromTimestamp(456, 123) - assert.NoError(t, err) - err = ValidateValidFromTimestamp(123, 123) - assert.NoError(t, err) - }) - t.Run("fails when observationTimestamp is < validFromTimestamp", func(t *testing.T) { - err := ValidateValidFromTimestamp(111, 112) - assert.EqualError(t, err, "observationTimestamp (Value: 111) must be >= validFromTimestamp (Value: 112)") - }) - }) - t.Run("ValidateExpiresAt", func(t *testing.T) { - t.Run("succeeds when observationTimestamp <= expiresAt", func(t *testing.T) { - err := ValidateExpiresAt(123, 456) - assert.NoError(t, err) - err = ValidateExpiresAt(123, 123) - assert.NoError(t, err) - }) - - t.Run("fails when observationTimestamp > expiresAt", func(t *testing.T) { - err := ValidateExpiresAt(112, 111) - assert.EqualError(t, err, "expiresAt (Value: 111) must be ahead of observation timestamp (Value: 112)") - }) - }) - t.Run("ValidateBetween", func(t *testing.T) { - bm := big.NewInt(346) - err := ValidateBetween("test foo", bm, min, max) - assert.NoError(t, err) - - err = ValidateBetween("test bar", bm, min, badMax) - assert.EqualError(t, err, "test bar (Value: 346) is outside of allowable range (Min: 0, Max: 10)") - err = ValidateBetween("test baz", bm, badMin, max) - assert.EqualError(t, err, "test baz (Value: 346) is outside of allowable range (Min: 9000, Max: 10000)") - }) -} diff --git a/mercury/value.go b/mercury/value.go deleted file mode 100644 index 50677e3..0000000 --- a/mercury/value.go +++ /dev/null @@ -1,45 +0,0 @@ -package mercury - -import ( - "math/big" - - "github.com/smartcontractkit/libocr/bigbigendian" -) - -var MaxInt192 *big.Int -var MaxInt192Enc []byte - -func init() { - one := big.NewInt(1) - // Compute the maximum value of int192 - // 1<<191 - 1 - MaxInt192 = new(big.Int).Lsh(one, 191) - MaxInt192.Sub(MaxInt192, one) - - var err error - MaxInt192Enc, err = EncodeValueInt192(MaxInt192) - if err != nil { - panic(err) - } -} - -// Bounds on an int192 -const ByteWidthInt192 = 24 - -// Encodes a value using 24-byte big endian two's complement representation. This function never panics. -func EncodeValueInt192(i *big.Int) ([]byte, error) { - return bigbigendian.SerializeSigned(ByteWidthInt192, i) -} - -// Decodes a value using 24-byte big endian two's complement representation. This function never panics. -func DecodeValueInt192(s []byte) (*big.Int, error) { - return bigbigendian.DeserializeSigned(ByteWidthInt192, s) -} - -func MustEncodeValueInt192(i *big.Int) []byte { - val, err := EncodeValueInt192(i) - if err != nil { - panic(err) - } - return val -} diff --git a/mercury/value_test.go b/mercury/value_test.go deleted file mode 100644 index 900e0d6..0000000 --- a/mercury/value_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package mercury - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Test_Values(t *testing.T) { - t.Run("serializes max int192", func(t *testing.T) { - encoded, err := EncodeValueInt192(MaxInt192) - require.NoError(t, err) - decoded, err := DecodeValueInt192(encoded) - require.NoError(t, err) - assert.Equal(t, MaxInt192, decoded) - }) -}