diff --git a/mercury/v3/aggregate_functions.go b/mercury/v3/aggregate_functions.go new file mode 100644 index 0000000..01cb503 --- /dev/null +++ b/mercury/v3/aggregate_functions.go @@ -0,0 +1,70 @@ +package v3 + +import ( + "fmt" + "sort" + + "github.com/shopspring/decimal" +) + +func GetConsensusPrices(paos []PAO, f int) (Prices, error) { + var validPrices []Prices + for _, pao := range paos { + prices, valid := pao.GetPrices() + if valid { + validPrices = append(validPrices, prices) + } + } + // FIXME: This should actually check for < 2*f+1, but we can't do that in + // this mercury plugin because it doesn't support ValidateObservation + if len(validPrices) < f+1 { + return Prices{}, fmt.Errorf("fewer than f+1 observations have a valid price (got: %d/%d)", len(validPrices), len(paos)) + } + + bidSpreads := make([]decimal.Decimal, len(validPrices)) + askSpreads := make([]decimal.Decimal, len(validPrices)) + for i, p := range validPrices { + bid := decimal.NewFromBigInt(p.Bid, 0) + ask := decimal.NewFromBigInt(p.Ask, 0) + benchmark := decimal.NewFromBigInt(p.Benchmark, 0) + + bidSpreads[i] = bid.Div(benchmark) + askSpreads[i] = ask.Div(benchmark) + } + + prices := Prices{} + + sort.Slice(validPrices, func(i, j int) bool { + return validPrices[i].Benchmark.Cmp(validPrices[j].Benchmark) < 0 + }) + prices.Benchmark = validPrices[len(validPrices)/2].Benchmark + benchmarkDecimal := decimal.NewFromBigInt(prices.Benchmark, 0) + + sort.Slice(bidSpreads, func(i, j int) bool { + return bidSpreads[i].Cmp(bidSpreads[j]) < 0 + }) + // We started with at least 2f+1 observations. There are at most f + // dishonest participants. Suppose with threw out m observations for + // disordered prices. Then we are left with 2f+1-m observations, f-m of + // which could still have come from dishonest participants. But + // 2f+1-m=2(f-m)+(m+1), so the median must come from an honest participant. + medianBidSpread := bidSpreads[len(bidSpreads)/2] + + prices.Bid = benchmarkDecimal.Mul(medianBidSpread).BigInt() + if prices.Bid.Cmp(prices.Benchmark) > 0 { + // Cannot happen unless > f nodes are inverted which is by assumption undefined behavior + return Prices{}, fmt.Errorf("invariant violation: bid price is greater than benchmark price (bid: %s, benchmark: %s)", prices.Bid.String(), benchmarkDecimal.String()) + } + + sort.Slice(askSpreads, func(i, j int) bool { + return askSpreads[i].Cmp(askSpreads[j]) < 0 + }) + medianAskSpread := askSpreads[len(askSpreads)/2] + prices.Ask = benchmarkDecimal.Mul(medianAskSpread).BigInt() + if prices.Ask.Cmp(prices.Benchmark) < 0 { + // Cannot happen unless > f nodes are inverted which is by assumption undefined behavior + return Prices{}, fmt.Errorf("invariant violation: ask price is less than benchmark price (ask: %s, benchmark: %s)", prices.Ask.String(), benchmarkDecimal.String()) + } + + return prices, nil +} diff --git a/mercury/v3/aggregate_functions_test.go b/mercury/v3/aggregate_functions_test.go new file mode 100644 index 0000000..1df918c --- /dev/null +++ b/mercury/v3/aggregate_functions_test.go @@ -0,0 +1,232 @@ +package v3 + +import ( + "math/big" + "testing" + + "github.com/smartcontractkit/libocr/commontypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var _ PAO = testPAO{} + +type testPAO struct { + Bid *big.Int + Benchmark *big.Int + Ask *big.Int + PricesValid bool +} + +func (t testPAO) GetPrices() (prices Prices, valid bool) { + return Prices{ + Benchmark: t.Benchmark, + Bid: t.Bid, + Ask: t.Ask, + }, t.PricesValid +} + +func (t testPAO) GetObserver() commontypes.OracleID { return 0 } +func (t testPAO) GetTimestamp() uint32 { return 0 } +func (t testPAO) GetBenchmarkPrice() (*big.Int, bool) { return nil, false } +func (t testPAO) GetMaxFinalizedTimestamp() (int64, bool) { return 0, false } +func (t testPAO) GetLinkFee() (*big.Int, bool) { return nil, false } +func (t testPAO) GetNativeFee() (*big.Int, bool) { return nil, false } + +func Test_GetConsensusPrices(t *testing.T) { + tests := []struct { + name string + paos []PAO + f int + want Prices + err string + }{ + { + name: "not enough valid observations", + paos: []PAO{ + testPAO{ + PricesValid: false, + }, + testPAO{ + PricesValid: false, + }, + testPAO{ + Bid: big.NewInt(20), + Benchmark: big.NewInt(21), + Ask: big.NewInt(23), + PricesValid: true, + }, + }, + f: 1, + want: Prices{}, + err: "fewer than f+1 observations have a valid price (got: 1/3)", + }, + { + name: "handles simple case", + paos: []PAO{ + testPAO{ + Bid: big.NewInt(1), + Benchmark: big.NewInt(2), + Ask: big.NewInt(3), + PricesValid: true, + }, + testPAO{ + Bid: big.NewInt(9), + Benchmark: big.NewInt(9), + Ask: big.NewInt(9), + PricesValid: true, + }, + testPAO{ + Bid: big.NewInt(20), + Benchmark: big.NewInt(21), + Ask: big.NewInt(23), + PricesValid: true, + }, + }, + f: 1, + want: Prices{ + Bid: big.NewInt(8), + Benchmark: big.NewInt(9), + Ask: big.NewInt(9), + }, + err: "", + }, + { + name: "handles simple inverted case", + paos: []PAO{ + testPAO{ + Bid: big.NewInt(1), + Benchmark: big.NewInt(2), + Ask: big.NewInt(3), + PricesValid: true, + }, + testPAO{ + Bid: big.NewInt(10), + Benchmark: big.NewInt(9), + Ask: big.NewInt(8), + PricesValid: true, + }, + testPAO{ + Bid: big.NewInt(20), + Benchmark: big.NewInt(21), + Ask: big.NewInt(23), + PricesValid: true, + }, + }, + f: 1, + want: Prices{ + Bid: big.NewInt(8), + Benchmark: big.NewInt(9), + Ask: big.NewInt(9), + }, + err: "", + }, + { + name: "handles complex inverted case (real world example)", + paos: []PAO{ + testPAO{big.NewInt(1315854500), big.NewInt(1316190800), big.NewInt(1316527000), true}, + testPAO{big.NewInt(1315854500), big.NewInt(1316190800), big.NewInt(1316527000), true}, + testPAO{big.NewInt(1315854500), big.NewInt(1316190800), big.NewInt(1316527000), true}, + testPAO{big.NewInt(1314633333), big.NewInt(1315131262), big.NewInt(1315629191), true}, + testPAO{big.NewInt(1315243916), big.NewInt(1316190800), big.NewInt(1316078096), true}, // inverted, ask > benchmark + testPAO{big.NewInt(1315854500), big.NewInt(1316190800), big.NewInt(1316527000), true}, + testPAO{big.NewInt(1314633333), big.NewInt(1315131262), big.NewInt(1315629191), true}, + testPAO{big.NewInt(1315854500), big.NewInt(1316190800), big.NewInt(1316527000), true}, + testPAO{big.NewInt(1314633333), big.NewInt(1315131262), big.NewInt(1315629191), true}, + testPAO{big.NewInt(1315243916), big.NewInt(1315661031), big.NewInt(1316078096), true}, + testPAO{big.NewInt(1314633333), big.NewInt(1315131262), big.NewInt(1315629191), true}, + }, + f: 5, + want: Prices{Bid: big.NewInt(1315773517), Benchmark: big.NewInt(1316190800), Ask: big.NewInt(1316526999)}, + err: "", + }, + { + name: "handles complex inverted case (f failures of various types)", + paos: []PAO{ + testPAO{big.NewInt(1315854500), big.NewInt(1316190800), big.NewInt(1316527000), true}, + testPAO{big.NewInt(1315854500), big.NewInt(1316190800), big.NewInt(1316527000), true}, + testPAO{big.NewInt(1315854500), big.NewInt(1316190800), big.NewInt(1316527000), true}, + testPAO{big.NewInt(1315131262), big.NewInt(1314633333), big.NewInt(1315629191), true}, // inverted, bid > benchmark + testPAO{big.NewInt(1315243916), big.NewInt(1316190800), big.NewInt(1316078096), true}, // inverted, ask < benchmark + testPAO{big.NewInt(2000000000), big.NewInt(1316190800), big.NewInt(1316527000), true}, // inverted, bid >>>> ask + testPAO{big.NewInt(2000000000), big.NewInt(1315131262), big.NewInt(2001000000), true}, // inverted, bid >>>> benchmark + testPAO{big.NewInt(1315854500), big.NewInt(1316190800), big.NewInt(1000000000), true}, // inverted, ask <<<< benchmark + testPAO{big.NewInt(1314633333), big.NewInt(1315131262), big.NewInt(1315629191), true}, + testPAO{big.NewInt(1315243916), big.NewInt(1315661031), big.NewInt(1316078096), true}, + testPAO{big.NewInt(1314633333), big.NewInt(1315131262), big.NewInt(1315629191), true}, + }, + f: 5, + want: Prices{Bid: big.NewInt(1315854499), Benchmark: big.NewInt(1316190800), Ask: big.NewInt(1316526999)}, + err: "", + }, + { + name: "handles complex inverted case (f failures skewed the same way)", + paos: []PAO{ + testPAO{big.NewInt(1315854500), big.NewInt(1316190800), big.NewInt(1316527000), true}, + testPAO{big.NewInt(1315854500), big.NewInt(1316190800), big.NewInt(1316527000), true}, + testPAO{big.NewInt(1315854500), big.NewInt(1316190800), big.NewInt(1316527000), true}, + testPAO{big.NewInt(1314633333), big.NewInt(1315131262), big.NewInt(1315629191), true}, + testPAO{big.NewInt(1315243916), big.NewInt(1316190800), big.NewInt(1316078096), true}, // inverted, ask < benchmark + testPAO{big.NewInt(1315243916), big.NewInt(1316190800), big.NewInt(1316078096), true}, // inverted, ask < benchmark + testPAO{big.NewInt(1315243916), big.NewInt(1316190800), big.NewInt(1316078096), true}, // inverted, ask < benchmark + testPAO{big.NewInt(1315243916), big.NewInt(1316190800), big.NewInt(1316078096), true}, // inverted, ask < benchmark + testPAO{big.NewInt(1315243916), big.NewInt(1316190800), big.NewInt(1316078096), true}, // inverted, ask < benchmark + testPAO{big.NewInt(1315243916), big.NewInt(1315661031), big.NewInt(1316078096), true}, + testPAO{big.NewInt(1314633333), big.NewInt(1315131262), big.NewInt(1315629191), true}, + }, + f: 5, + want: Prices{Bid: big.NewInt(1315692469), Benchmark: big.NewInt(1316190800), Ask: big.NewInt(1316526999)}, + err: "", + }, + { + name: "errors output in complex inverted case with f+1 failures such that ask > benchmark", + paos: []PAO{ + testPAO{big.NewInt(1315854500), big.NewInt(1316190800), big.NewInt(1316527000), true}, + testPAO{big.NewInt(1315854500), big.NewInt(1316190800), big.NewInt(1316527000), true}, + testPAO{big.NewInt(1315854500), big.NewInt(1316190800), big.NewInt(1316527000), true}, + testPAO{big.NewInt(1314633333), big.NewInt(1315131262), big.NewInt(1315629191), true}, + testPAO{big.NewInt(1315243916), big.NewInt(1316190800), big.NewInt(1316078096), true}, // inverted, ask < benchmark + testPAO{big.NewInt(1315243916), big.NewInt(1316190800), big.NewInt(1316078096), true}, // inverted, ask < benchmark + testPAO{big.NewInt(1315243916), big.NewInt(1316190800), big.NewInt(1316078096), true}, // inverted, ask < benchmark + testPAO{big.NewInt(1315243916), big.NewInt(1316190800), big.NewInt(1316078096), true}, // inverted, ask < benchmark + testPAO{big.NewInt(1315243916), big.NewInt(1316190800), big.NewInt(1316078096), true}, // inverted, ask < benchmark + testPAO{big.NewInt(1315243916), big.NewInt(1316190800), big.NewInt(1316078096), true}, // inverted, ask < benchmark + testPAO{big.NewInt(1314633333), big.NewInt(1315131262), big.NewInt(1315629191), true}, + }, + f: 5, + want: Prices{}, + err: "invariant violation: ask price is less than benchmark price (ask: 1316078096, benchmark: 1316190800)", + }, + { + name: "errors output in complex inverted case with f+1 failures such that bid < benchmark", + paos: []PAO{ + testPAO{big.NewInt(1315854500), big.NewInt(1316190800), big.NewInt(1316527000), true}, + testPAO{big.NewInt(1315854500), big.NewInt(1316190800), big.NewInt(1316527000), true}, + testPAO{big.NewInt(1315854500), big.NewInt(1316190800), big.NewInt(1316527000), true}, + testPAO{big.NewInt(1314633333), big.NewInt(1315131262), big.NewInt(1315629191), true}, + testPAO{big.NewInt(1314633333), big.NewInt(1315131262), big.NewInt(1315629191), true}, + testPAO{big.NewInt(1315131263), big.NewInt(1315131262), big.NewInt(1315629191), true}, // inverted, bid > benchmark + testPAO{big.NewInt(1315131263), big.NewInt(1315131262), big.NewInt(1315629191), true}, // inverted, bid > benchmark + testPAO{big.NewInt(1315131263), big.NewInt(1315131262), big.NewInt(1315629191), true}, // inverted, bid > benchmark + testPAO{big.NewInt(1315131263), big.NewInt(1315131262), big.NewInt(1315629191), true}, // inverted, bid > benchmark + testPAO{big.NewInt(1315131263), big.NewInt(1315131262), big.NewInt(1315629191), true}, // inverted, bid > benchmark + testPAO{big.NewInt(1315131263), big.NewInt(1315131262), big.NewInt(1315629191), true}, // inverted, bid > benchmark + }, + f: 5, + want: Prices{}, + err: "invariant violation: bid price is greater than benchmark price (bid: 1315131263, benchmark: 1315131262)", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + prices, err := GetConsensusPrices(tt.paos, tt.f) + if tt.err != "" { + require.Error(t, err) + assert.Equal(t, tt.err, err.Error()) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.want, prices) + }) + } +} diff --git a/mercury/v3/mercury.go b/mercury/v3/mercury.go index e0ebd35..a447442 100644 --- a/mercury/v3/mercury.go +++ b/mercury/v3/mercury.go @@ -166,12 +166,7 @@ func (rp *reportingPlugin) Observation(ctx context.Context, repts types.ReportTi } 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 - } + p.PricesValid = true } var maxFinalizedTimestampErr error @@ -230,13 +225,6 @@ func (rp *reportingPlugin) Observation(ctx context.Context, repts types.ReportTi 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 @@ -261,13 +249,6 @@ func parseAttributedObservation(ao types.AttributedObservation) (PAO, error) { 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 } @@ -383,20 +364,11 @@ func (rp *reportingPlugin) buildReportFields(previousReport types.Report, paos [ } } - 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) + prices, err := GetConsensusPrices(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)) + merr = errors.Join(merr, fmt.Errorf("GetConsensusPrices failed: %w", err)) } + rf.Bid, rf.BenchmarkPrice, rf.Ask = prices.Bid, prices.Benchmark, prices.Ask rf.LinkFee, err = mercury.GetConsensusLinkFee(convertLinkFee(paos), rp.f) if err != nil { @@ -455,18 +427,6 @@ func convertMaxFinalizedTimestamp(pao []PAO) (ret []mercury.PAOMaxFinalizedTimes } 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) diff --git a/mercury/v3/mercury_test.go b/mercury/v3/mercury_test.go index b2666e3..64b745e 100644 --- a/mercury/v3/mercury_test.go +++ b/mercury/v3/mercury_test.go @@ -165,7 +165,7 @@ func newValidAos(t *testing.T, protos ...*MercuryObservationProto) (aos []types. } 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) { + t.Run("returns no error if bid<=mid<=ask is violated", func(t *testing.T) { obs := &MercuryObservationProto{ Timestamp: 42, @@ -187,8 +187,7 @@ func Test_parseAttributedObservation(t *testing.T) { 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()) + require.NoError(t, err) }) } @@ -246,7 +245,7 @@ func Test_Plugin_Report(t *testing.T) { ExpiresAt: 46, BenchmarkPrice: big.NewInt(345), Bid: big.NewInt(340), - Ask: big.NewInt(350), + Ask: big.NewInt(353), }, *codec.builtReportFields) }) t.Run("succeeds and generates validFromTimestamp from maxFinalizedTimestamp when maxFinalizedTimestamp is zero", func(t *testing.T) { @@ -269,7 +268,7 @@ func Test_Plugin_Report(t *testing.T) { ExpiresAt: 46, BenchmarkPrice: big.NewInt(345), Bid: big.NewInt(340), - Ask: big.NewInt(350), + Ask: big.NewInt(353), }, *codec.builtReportFields) }) t.Run("succeeds and generates validFromTimestamp from maxFinalizedTimestamp when maxFinalizedTimestamp is -1 (missing feed)", func(t *testing.T) { @@ -292,7 +291,7 @@ func Test_Plugin_Report(t *testing.T) { ExpiresAt: 46, BenchmarkPrice: big.NewInt(345), Bid: big.NewInt(340), - Ask: big.NewInt(350), + Ask: big.NewInt(353), }, *codec.builtReportFields) }) @@ -314,7 +313,7 @@ func Test_Plugin_Report(t *testing.T) { ExpiresAt: 46, BenchmarkPrice: big.NewInt(345), Bid: big.NewInt(340), - Ask: big.NewInt(350), + Ask: big.NewInt(349), }, *codec.builtReportFields) }) }) @@ -348,7 +347,7 @@ func Test_Plugin_Report(t *testing.T) { ExpiresAt: ts + 1, BenchmarkPrice: big.NewInt(345), Bid: big.NewInt(340), - Ask: big.NewInt(350), + Ask: big.NewInt(353), }, *codec.builtReportFields) }) t.Run("errors if cannot extract timestamp from previous report", func(t *testing.T) { @@ -741,61 +740,6 @@ func Test_Plugin_Observation(t *testing.T) { 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 { diff --git a/mercury/v3/observation.go b/mercury/v3/observation.go index 2b1708c..b4a4f43 100644 --- a/mercury/v3/observation.go +++ b/mercury/v3/observation.go @@ -10,11 +10,10 @@ import ( type PAO interface { mercury.PAO - GetBid() (*big.Int, bool) - GetAsk() (*big.Int, bool) GetMaxFinalizedTimestamp() (int64, bool) GetLinkFee() (*big.Int, bool) GetNativeFee() (*big.Int, bool) + GetPrices() (prices Prices, valid bool) } var _ PAO = parsedAttributedObservation{} @@ -81,6 +80,16 @@ func (pao parsedAttributedObservation) GetAsk() (*big.Int, bool) { return pao.Ask, pao.PricesValid } +type Prices struct { + Bid *big.Int + Benchmark *big.Int + Ask *big.Int +} + +func (pao parsedAttributedObservation) GetPrices() (prices Prices, valid bool) { + return Prices{pao.Bid, pao.BenchmarkPrice, pao.Ask}, pao.PricesValid +} + func (pao parsedAttributedObservation) GetMaxFinalizedTimestamp() (int64, bool) { if pao.MaxFinalizedTimestamp < -1 { // values below -1 are not valid