From d074b92729e5f2cf3325eeca484794fa15c4ce7a Mon Sep 17 00:00:00 2001 From: Sam Davies Date: Wed, 4 Dec 2024 12:37:40 -0500 Subject: [PATCH] Test for RWA --- .../report_codec_evm_abi_encode_unpacked.go | 17 +- ...port_codec_evm_abi_encode_unpacked_test.go | 156 +++++++++++++++--- .../ocr2/plugins/llo/integration_test.go | 86 ++++++++++ 3 files changed, 236 insertions(+), 23 deletions(-) diff --git a/core/services/llo/evm/report_codec_evm_abi_encode_unpacked.go b/core/services/llo/evm/report_codec_evm_abi_encode_unpacked.go index 58a8acc98f7..388717b137f 100644 --- a/core/services/llo/evm/report_codec_evm_abi_encode_unpacked.go +++ b/core/services/llo/evm/report_codec_evm_abi_encode_unpacked.go @@ -240,7 +240,7 @@ func (a ABIEncoder) Encode(value llo.StreamValue) ([]byte, error) { // TODO: How to handle overflow? // packBigInt encodes a *big.Int as a byte slice according to the given ABI type -func packBigInt(val *big.Int, t string) ([]byte, error) { +func packBigInt(val *big.Int, t string) (b []byte, err error) { abiType, err := abi.NewType(t, "", []abi.ArgumentMarshaling{}) if err != nil { return nil, fmt.Errorf("invalid ABI type %q; %w", abiType, err) @@ -253,10 +253,19 @@ func packBigInt(val *big.Int, t string) ([]byte, error) { }, } - packedData, err := arguments.Pack(val) + switch t { + case "uint32": + // packing uint32 expects uint32 as argument + if val.BitLen() > 32 { + return nil, fmt.Errorf("value %v is too large for uint32", val) + } + b, err = arguments.Pack(uint32(val.Uint64())) + default: + b, err = arguments.Pack(val) + } if err != nil { - return nil, fmt.Errorf("failed to pack value %v as %q: %w", val, abiType, err) + return nil, fmt.Errorf("failed to pack value %v as %q: %w", val, t, err) } - return packedData, nil + return b, nil } diff --git a/core/services/llo/evm/report_codec_evm_abi_encode_unpacked_test.go b/core/services/llo/evm/report_codec_evm_abi_encode_unpacked_test.go index c106e15b456..5fbcf26dbed 100644 --- a/core/services/llo/evm/report_codec_evm_abi_encode_unpacked_test.go +++ b/core/services/llo/evm/report_codec_evm_abi_encode_unpacked_test.go @@ -78,7 +78,25 @@ func TestReportCodecEVMABIEncodeUnpacked_Encode_properties(t *testing.T) { properties := gopter.NewProperties(nil) + linkQuoteStreamID := llotypes.StreamID(rand.Uint32()) + ethQuoteStreamID := llotypes.StreamID(rand.Uint32()) + dexBasedAssetDecimalStreamID := llotypes.StreamID(rand.Uint32()) + baseMarketDepthStreamID := llotypes.StreamID(rand.Uint32()) + quoteMarketDepthStreamID := llotypes.StreamID(rand.Uint32()) + marketStatusStreamID := llotypes.StreamID(rand.Uint32()) + t.Run("DEX-based asset schema example", func(t *testing.T) { + expectedDEXBasedAssetSchema := abi.Arguments([]abi.Argument{ + {Name: "feedId", Type: mustNewABIType("bytes32")}, + {Name: "validFromTimestamp", Type: mustNewABIType("uint32")}, + {Name: "observationsTimestamp", Type: mustNewABIType("uint32")}, + {Name: "nativeFee", Type: mustNewABIType("uint192")}, + {Name: "linkFee", Type: mustNewABIType("uint192")}, + {Name: "expiresAt", Type: mustNewABIType("uint32")}, + {Name: "price", Type: mustNewABIType("int192")}, + {Name: "baseMarketDepth", Type: mustNewABIType("int192")}, + {Name: "quoteMarketDepth", Type: mustNewABIType("int192")}, + }) runTest := func(sampleFeedID common.Hash, sampleObservationsTimestamp, sampleValidAfterSeconds, sampleExpirationWindow uint32, priceMultiplier, marketDepthMultiplier *ubig.Big, sampleBaseUSDFee, sampleLinkBenchmarkPrice, sampleNativeBenchmarkPrice, sampleDexBasedAssetPrice, sampleBaseMarketDepth, sampleQuoteMarketDepth decimal.Decimal) bool { report := llo.Report{ ConfigDigest: types.ConfigDigest{0x01}, @@ -96,12 +114,6 @@ func TestReportCodecEVMABIEncodeUnpacked_Encode_properties(t *testing.T) { Specimen: false, } - linkQuoteStreamID := llotypes.StreamID(rand.Uint32()) - ethQuoteStreamID := llotypes.StreamID(rand.Uint32()) - dexBasedAssetDecimalStreamID := llotypes.StreamID(rand.Uint32()) - baseMarketDepthStreamID := llotypes.StreamID(rand.Uint32()) - quoteMarketDepthStreamID := llotypes.StreamID(rand.Uint32()) - opts := ReportFormatEVMABIEncodeOpts{ BaseUSDFee: sampleBaseUSDFee, ExpirationWindow: sampleExpirationWindow, @@ -161,22 +173,10 @@ func TestReportCodecEVMABIEncodeUnpacked_Encode_properties(t *testing.T) { encoded, err := codec.Encode(ctx, report, cd) require.NoError(t, err) - expectedDEXBasedAssetSchema := abi.Arguments([]abi.Argument{ - {Name: "feedId", Type: mustNewABIType("bytes32")}, - {Name: "validFromTimestamp", Type: mustNewABIType("uint32")}, - {Name: "observationsTimestamp", Type: mustNewABIType("uint32")}, - {Name: "nativeFee", Type: mustNewABIType("uint192")}, - {Name: "linkFee", Type: mustNewABIType("uint192")}, - {Name: "expiresAt", Type: mustNewABIType("uint32")}, - {Name: "price", Type: mustNewABIType("int192")}, - {Name: "baseMarketDepth", Type: mustNewABIType("int192")}, - {Name: "quoteMarketDepth", Type: mustNewABIType("int192")}, - }) - values, err := expectedDEXBasedAssetSchema.Unpack(encoded) require.NoError(t, err) - require.Len(t, values, 9) + require.Len(t, values, len(expectedDEXBasedAssetSchema)) expectedLinkFee := CalculateFee(sampleLinkBenchmarkPrice, sampleBaseUSDFee) expectedNativeFee := CalculateFee(sampleNativeBenchmarkPrice, sampleBaseUSDFee) @@ -212,6 +212,118 @@ func TestReportCodecEVMABIEncodeUnpacked_Encode_properties(t *testing.T) { properties.TestingRun(t) }) + + t.Run("Market status schema", func(t *testing.T) { + expectedRWASchema := abi.Arguments([]abi.Argument{ + {Name: "feedId", Type: mustNewABIType("bytes32")}, + {Name: "validFromTimestamp", Type: mustNewABIType("uint32")}, + {Name: "observationsTimestamp", Type: mustNewABIType("uint32")}, + {Name: "nativeFee", Type: mustNewABIType("uint192")}, + {Name: "linkFee", Type: mustNewABIType("uint192")}, + {Name: "expiresAt", Type: mustNewABIType("uint32")}, + {Name: "marketStatus", Type: mustNewABIType("uint32")}, + }) + + runTest := func(sampleFeedID common.Hash, sampleObservationsTimestamp, sampleValidAfterSeconds, sampleExpirationWindow uint32, sampleBaseUSDFee, sampleLinkBenchmarkPrice, sampleNativeBenchmarkPrice, sampleMarketStatus decimal.Decimal) bool { + report := llo.Report{ + ConfigDigest: types.ConfigDigest{0x01}, + SeqNr: 0x02, + ChannelID: llotypes.ChannelID(0x03), + ValidAfterSeconds: sampleValidAfterSeconds, + ObservationTimestampSeconds: sampleObservationsTimestamp, + Values: []llo.StreamValue{ + &llo.Quote{Bid: decimal.NewFromFloat(6.1), Benchmark: sampleLinkBenchmarkPrice, Ask: decimal.NewFromFloat(8.2332)}, // Link price + &llo.Quote{Bid: decimal.NewFromFloat(9.4), Benchmark: sampleNativeBenchmarkPrice, Ask: decimal.NewFromFloat(11.33)}, // Native price + llo.ToDecimal(sampleMarketStatus), // DEX-based asset price + }, + Specimen: false, + } + + linkQuoteStreamID := llotypes.StreamID(rand.Uint32()) + ethQuoteStreamID := llotypes.StreamID(rand.Uint32()) + dexBasedAssetDecimalStreamID := llotypes.StreamID(rand.Uint32()) + baseMarketDepthStreamID := llotypes.StreamID(rand.Uint32()) + quoteMarketDepthStreamID := llotypes.StreamID(rand.Uint32()) + + opts := ReportFormatEVMABIEncodeOpts{ + BaseUSDFee: sampleBaseUSDFee, + ExpirationWindow: sampleExpirationWindow, + FeedID: sampleFeedID, + ABI: []ABIEncoder{ + // market status + ABIEncoder{ + StreamID: marketStatusStreamID, + Type: "uint32", + }, + }, + } + serializedOpts, err := opts.Encode() + require.NoError(t, err) + + cd := llotypes.ChannelDefinition{ + // ReportFormat: llotypes.ReportFormatEVMABIEncodeUnpacked, + ReportFormat: llotypes.ReportFormat(4), // FIXME: When chainlink-common is fixed + Streams: []llotypes.Stream{ + { + StreamID: linkQuoteStreamID, + Aggregator: llotypes.AggregatorMedian, + }, + { + StreamID: ethQuoteStreamID, + Aggregator: llotypes.AggregatorMedian, + }, + { + StreamID: dexBasedAssetDecimalStreamID, + Aggregator: llotypes.AggregatorQuote, + }, + { + StreamID: baseMarketDepthStreamID, + Aggregator: llotypes.AggregatorMedian, + }, + { + StreamID: quoteMarketDepthStreamID, + Aggregator: llotypes.AggregatorMedian, + }, + }, + Opts: serializedOpts, + } + + encoded, err := codec.Encode(ctx, report, cd) + require.NoError(t, err) + + values, err := expectedRWASchema.Unpack(encoded) + require.NoError(t, err) + + require.Len(t, values, len(expectedRWASchema)) + + expectedLinkFee := CalculateFee(sampleLinkBenchmarkPrice, sampleBaseUSDFee) + expectedNativeFee := CalculateFee(sampleNativeBenchmarkPrice, sampleBaseUSDFee) + + return AllTrue([]bool{ + assert.Equal(t, sampleFeedID, (common.Hash)(values[0].([32]byte))), // feedId + assert.Equal(t, uint32(sampleValidAfterSeconds+1), values[1].(uint32)), // validFromTimestamp + assert.Equal(t, sampleObservationsTimestamp, values[2].(uint32)), // observationsTimestamp + assert.Equal(t, expectedLinkFee.String(), values[3].(*big.Int).String()), // linkFee + assert.Equal(t, expectedNativeFee.String(), values[4].(*big.Int).String()), // nativeFee + assert.Equal(t, uint32(sampleObservationsTimestamp+sampleExpirationWindow), values[5].(uint32)), // expiresAt + assert.Equal(t, uint32(sampleMarketStatus.BigInt().Int64()), values[6].(uint32)), // market status + }) + } + + properties.Property("Encodes values", prop.ForAll( + runTest, + genFeedID(), + genObservationsTimestamp(), + genValidAfterSeconds(), + genExpirationWindow(), + genBaseUSDFee(), + genLinkBenchmarkPrice(), + genNativeBenchmarkPrice(), + genMarketStatus(), + )) + + properties.TestingRun(t) + }) } func genFeedID() gopter.Gen { @@ -276,6 +388,12 @@ func genQuoteMarketDepth() gopter.Gen { return genDecimal() } +func genMarketStatus() gopter.Gen { + return gen.UInt32().Map(func(i uint32) decimal.Decimal { + return decimal.NewFromInt(int64(i)) + }) +} + func mustNewABIType(t string) abi.Type { result, err := abi.NewType(t, "", []abi.ArgumentMarshaling{}) if err != nil { diff --git a/core/services/ocr2/plugins/llo/integration_test.go b/core/services/ocr2/plugins/llo/integration_test.go index 0491c29b39c..5f540d66453 100644 --- a/core/services/ocr2/plugins/llo/integration_test.go +++ b/core/services/ocr2/plugins/llo/integration_test.go @@ -361,6 +361,8 @@ func TestIntegration_LLO(t *testing.T) { bootstrapNode := Node{App: appBootstrap, KeyBundle: bootstrapKb} t.Run("using legacy verifier configuration contract, produces reports in v0.3 format", func(t *testing.T) { + t.Parallel() + reqs := make(chan request, 100000) serverKey := csakey.MustNewV2XXXTestingOnly(big.NewInt(-1)) serverPubKey := serverKey.PublicKey @@ -549,7 +551,91 @@ channelDefinitionsContractFromBlock = %d`, serverURL, serverPubKey, donID, confi }) }) + t.Run("generates reports using ReportFormatEVMAbiEncodeUnpacked format", func(t *testing.T) { + t.Parallel() + + t.Skip() + reqs := make(chan request, 100000) + serverKey := csakey.MustNewV2XXXTestingOnly(big.NewInt(-2)) + serverPubKey := serverKey.PublicKey + srv := NewMercuryServer(t, ed25519.PrivateKey(serverKey.Raw()), reqs) + + serverURL := startMercuryServer(t, srv, clientPubKeys) + + donID := uint32(888333) + streams := []Stream{ethStream, linkStream} + streamMap := make(map[uint32]Stream) + for _, strm := range streams { + streamMap[strm.id] = strm + } + + // Setup oracle nodes + oracles, nodes := setupNodes(t, nNodes, backend, clientCSAKeys, streams) + + chainID := testutils.SimulatedChainID + relayType := "evm" + relayConfig := fmt.Sprintf(` +chainID = "%s" +fromBlock = %d +lloDonID = %d +lloConfigMode = "bluegreen" +`, chainID, fromBlock, donID) + addBootstrapJob(t, bootstrapNode, configuratorAddress, "job-3", relayType, relayConfig) + + // Channel definitions + channelDefinitions := llotypes.ChannelDefinitions{ + 1: { + ReportFormat: llotypes.ReportFormatJSON, + Streams: []llotypes.Stream{ + { + StreamID: ethStreamID, + Aggregator: llotypes.AggregatorMedian, + }, + }, + }, + } + url, sha := newChannelDefinitionsServer(t, channelDefinitions) + + // Set channel definitions + _, err := configStore.SetChannelDefinitions(steve, donID, url, sha) + require.NoError(t, err) + backend.Commit() + + pluginConfig := fmt.Sprintf(`servers = { "%s" = "%x" } +donID = %d +channelDefinitionsContractAddress = "0x%x" +channelDefinitionsContractFromBlock = %d`, serverURL, serverPubKey, donID, configStoreAddress, fromBlock) + addOCRJobsEVMPremiumLegacy(t, streams, serverPubKey, serverURL, configuratorAddress, bootstrapPeerID, bootstrapNodePort, nodes, configStoreAddress, clientPubKeys, pluginConfig, relayType, relayConfig) + + var blueDigest ocr2types.ConfigDigest + + allReports := make(map[types.ConfigDigest][]datastreamsllo.Report) + { + // Set config on configurator + blueDigest = setProductionConfig( + t, donID, steve, backend, configurator, configuratorAddress, nodes, oracles, + ) + + // NOTE: Wait until blue produces a report + + for req := range reqs { + _, _, r, _, err := (datastreamsllo.JSONReportCodec{}).UnpackDecode(req.req.Payload) + require.NoError(t, err) + + allReports[r.ConfigDigest] = append(allReports[r.ConfigDigest], r) + + assert.Equal(t, blueDigest, r.ConfigDigest) + assert.False(t, r.Specimen) + assert.Len(t, r.Values, 1) + assert.Equal(t, "2976.39", r.Values[0].(*datastreamsllo.Decimal).String()) + break + } + } + }) + t.Run("Blue/Green lifecycle (using JSON report format)", func(t *testing.T) { + t.Parallel() + reqs := make(chan request, 100000) serverKey := csakey.MustNewV2XXXTestingOnly(big.NewInt(-2)) serverPubKey := serverKey.PublicKey