Skip to content

Commit 5258160

Browse files
authored
llo tombstone integration tests (#21830)
* llo: add channel tombstoning integration test * llo: add channel tombstoning drop integration test * llo: linter fixes
1 parent 79e8025 commit 5258160

3 files changed

Lines changed: 298 additions & 1 deletion

File tree

core/services/ocr2/plugins/llo/helpers_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"crypto"
66
"crypto/ed25519"
7+
"encoding/json"
78
"errors"
89
"fmt"
910
"io"
@@ -12,6 +13,8 @@ import (
1213
"net/http"
1314
"net/http/httptest"
1415
"net/url"
16+
"reflect"
17+
"sync/atomic"
1518
"testing"
1619
"time"
1720

@@ -419,6 +422,43 @@ func createSingleDecimalBridge(t *testing.T, name string, i int, p decimal.Decim
419422
return bridgeName
420423
}
421424

425+
// createSingleDecimalCountingBridge is like createSingleDecimalBridge but increments callCount on each bridge request.
426+
func createSingleDecimalCountingBridge(t *testing.T, name string, i int, p decimal.Decimal, borm bridges.ORM, callCount *atomic.Uint64) (bridgeName string) {
427+
ctx := testutils.Context(t)
428+
wantBody := map[string]any{"data": map[string]any{"data": "foo"}}
429+
bridge := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
430+
callCount.Add(1)
431+
b, err := io.ReadAll(req.Body)
432+
if err != nil {
433+
http.Error(res, err.Error(), http.StatusInternalServerError)
434+
return
435+
}
436+
var gotBody any
437+
if err := json.Unmarshal(b, &gotBody); err != nil {
438+
http.Error(res, "invalid json", http.StatusBadRequest)
439+
return
440+
}
441+
if !reflect.DeepEqual(gotBody, wantBody) {
442+
http.Error(res, "unexpected request body", http.StatusBadRequest)
443+
return
444+
}
445+
446+
res.WriteHeader(http.StatusOK)
447+
val := p.String()
448+
resp := fmt.Sprintf(`{"result": %s}`, val)
449+
_, _ = res.Write([]byte(resp))
450+
}))
451+
t.Cleanup(bridge.Close)
452+
u, _ := url.Parse(bridge.URL)
453+
bridgeName = fmt.Sprintf("bridge-%s-%d", name, i)
454+
require.NoError(t, borm.CreateBridgeType(ctx, &bridges.BridgeType{
455+
Name: bridges.BridgeName(bridgeName),
456+
URL: models.WebURL(*u),
457+
}))
458+
459+
return bridgeName
460+
}
461+
422462
func createBridge(t *testing.T, bridgeName string, responseJSON string, borm bridges.ORM) {
423463
ctx := testutils.Context(t)
424464
bridge := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {

core/services/ocr2/plugins/llo/integration_test.go

Lines changed: 184 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"net/http/httptest"
1212
"sort"
1313
"strings"
14+
"sync/atomic"
1415
"testing"
1516
"time"
1617

@@ -2692,6 +2693,189 @@ channelDefinitionsContractFromBlock = %d`, serverURL, serverPubKey, donID, confi
26922693
})
26932694
}
26942695

2696+
// TestIntegration_LLO_tombstone_stops_observations_and_reports checks that once a channel is
2697+
// tombstoned, the DON stops observing its streams (no bridge traffic for those stream jobs)
2698+
// and no longer transmits reports for that channel.
2699+
func TestIntegration_LLO_tombstone_stops_observations_and_reports(t *testing.T) {
2700+
t.Parallel()
2701+
2702+
const (
2703+
salt = 500
2704+
donID = uint32(777666)
2705+
streamIDActive = uint32(190)
2706+
streamIDTombstone = uint32(191)
2707+
)
2708+
2709+
offchainConfig := datastreamsllo.OffchainConfig{
2710+
ProtocolVersion: 1,
2711+
DefaultMinReportIntervalNanoseconds: uint64(1 * time.Second),
2712+
EnableObservationCompression: true,
2713+
}
2714+
2715+
clientCSAKeys := make([]csakey.KeyV2, nNodes)
2716+
clientPubKeys := make([]ed25519.PublicKey, nNodes)
2717+
for i := range nNodes {
2718+
k := big.NewInt(int64(salt + i))
2719+
key := csakey.MustNewV2XXXTestingOnly(k)
2720+
clientCSAKeys[i] = key
2721+
clientPubKeys[i] = key.PublicKey
2722+
}
2723+
2724+
steve, backend, configurator, configuratorAddress, _, _, _, _, configStore, configStoreAddress, _, _, _, _ := setupBlockchain(t)
2725+
fromBlock := 1
2726+
2727+
bootstrapCSAKey := csakey.MustNewV2XXXTestingOnly(big.NewInt(salt - 1))
2728+
bootstrapNodePort := freeport.GetOne(t)
2729+
appBootstrap, bootstrapPeerID, _, bootstrapKb, _ := setupNode(t, bootstrapNodePort, "bootstrap_llo_tombstone", backend, bootstrapCSAKey, nil)
2730+
bootstrapNode := Node{App: appBootstrap, KeyBundle: bootstrapKb}
2731+
2732+
packetCh := make(chan *packet, 100000)
2733+
serverKey := csakey.MustNewV2XXXTestingOnly(big.NewInt(salt - 2))
2734+
serverPubKey := serverKey.PublicKey
2735+
srv := NewMercuryServer(t, serverKey, packetCh)
2736+
serverURL := startMercuryServer(t, srv, clientPubKeys)
2737+
2738+
oracles, nodes := setupNodes(t, nNodes, backend, clientCSAKeys, func(c *chainlink.Config) {
2739+
c.Mercury.Transmitter.Protocol = ptr(config.MercuryTransmitterProtocolGRPC)
2740+
})
2741+
2742+
chainID := testutils.SimulatedChainID
2743+
relayType := "evm"
2744+
relayConfig := fmt.Sprintf(`
2745+
chainID = "%s"
2746+
fromBlock = %d
2747+
lloDonID = %d
2748+
lloConfigMode = "bluegreen"
2749+
`, chainID, fromBlock, donID)
2750+
addBootstrapJob(t, bootstrapNode, configuratorAddress, "job-tombstone", relayType, relayConfig)
2751+
2752+
pluginConfig := fmt.Sprintf(`servers = { "%s" = "%x" }
2753+
donID = %d
2754+
channelDefinitionsContractAddress = "0x%x"
2755+
channelDefinitionsContractFromBlock = %d`, serverURL, serverPubKey, donID, configStoreAddress, fromBlock)
2756+
2757+
var streamACalls, streamBCalls atomic.Uint64
2758+
priceA := decimal.NewFromFloat(111.1)
2759+
priceB := decimal.NewFromFloat(222.2)
2760+
for i, node := range nodes {
2761+
bridgeA := createSingleDecimalCountingBridge(t, "tomb-active", i, priceA, node.App.BridgeORM(), &streamACalls)
2762+
addSingleDecimalStreamJob(t, node, streamIDActive, bridgeA)
2763+
bridgeB := createSingleDecimalCountingBridge(t, "tomb-stone", i, priceB, node.App.BridgeORM(), &streamBCalls)
2764+
addSingleDecimalStreamJob(t, node, streamIDTombstone, bridgeB)
2765+
addLLOJob(
2766+
t,
2767+
node,
2768+
configuratorAddress,
2769+
bootstrapPeerID,
2770+
bootstrapNodePort,
2771+
clientPubKeys[i],
2772+
"tombstone-stream-test",
2773+
pluginConfig,
2774+
relayType,
2775+
relayConfig,
2776+
)
2777+
}
2778+
2779+
channelDefinitions := llotypes.ChannelDefinitions{
2780+
1: {
2781+
ReportFormat: llotypes.ReportFormatJSON,
2782+
Streams: []llotypes.Stream{
2783+
{StreamID: streamIDActive, Aggregator: llotypes.AggregatorMedian},
2784+
},
2785+
},
2786+
2: {
2787+
ReportFormat: llotypes.ReportFormatJSON,
2788+
Streams: []llotypes.Stream{
2789+
{StreamID: streamIDTombstone, Aggregator: llotypes.AggregatorMedian},
2790+
},
2791+
},
2792+
}
2793+
url, sha := newChannelDefinitionsServer(t, channelDefinitions)
2794+
_, err := configStore.SetChannelDefinitions(steve, donID, url, sha)
2795+
require.NoError(t, err)
2796+
backend.Commit()
2797+
2798+
setProductionConfig(
2799+
t, donID, steve, backend, configurator, configuratorAddress, nodes,
2800+
WithOracles(oracles), WithOffchainConfig(offchainConfig),
2801+
)
2802+
2803+
seenChannels := make(map[uint32]bool)
2804+
require.Eventually(t, func() bool {
2805+
pckt, errReceive := receiveWithTimeout(t, packetCh, 2*time.Second)
2806+
if errReceive != nil {
2807+
return false
2808+
}
2809+
req := pckt.req
2810+
if req.ReportFormat != uint32(llotypes.ReportFormatJSON) {
2811+
return len(seenChannels) == 2
2812+
}
2813+
_, _, r, _, errDecode := (datastreamsllo.JSONReportCodec{}).UnpackDecode(req.Payload)
2814+
if errDecode != nil {
2815+
return len(seenChannels) == 2
2816+
}
2817+
if r.ChannelID == 1 || r.ChannelID == 2 {
2818+
seenChannels[r.ChannelID] = true
2819+
}
2820+
return len(seenChannels) == 2
2821+
}, reportTimeout, 100*time.Millisecond, "expected reports for channel 1 and 2 before tombstone")
2822+
require.Positive(t, streamBCalls.Load(), "stream for channel 2 should be observed before tombstone")
2823+
2824+
// Tombstone channel 2 only; channel 1 keeps observing streamIDActive.
2825+
tombstonedDefs := llotypes.ChannelDefinitions{
2826+
1: {
2827+
ReportFormat: llotypes.ReportFormatJSON,
2828+
Streams: []llotypes.Stream{
2829+
{StreamID: streamIDActive, Aggregator: llotypes.AggregatorMedian},
2830+
},
2831+
},
2832+
2: {
2833+
ReportFormat: llotypes.ReportFormatJSON,
2834+
Tombstone: true,
2835+
Streams: []llotypes.Stream{
2836+
{StreamID: streamIDTombstone, Aggregator: llotypes.AggregatorMedian},
2837+
},
2838+
},
2839+
}
2840+
url2, sha2 := newChannelDefinitionsServer(t, tombstonedDefs)
2841+
_, err = configStore.SetChannelDefinitions(steve, donID, url2, sha2)
2842+
require.NoError(t, err)
2843+
backend.Commit()
2844+
2845+
tombstonedChannel := map[uint32]bool{2: true}
2846+
checkNoReportsWindow := 5 * time.Second
2847+
require.Eventually(t, func() bool {
2848+
start := time.Now()
2849+
sawTombstoned := false
2850+
for time.Since(start) < checkNoReportsWindow {
2851+
pckt, err := receiveWithTimeout(t, packetCh, 1*time.Second)
2852+
if err != nil {
2853+
continue
2854+
}
2855+
req := pckt.req
2856+
if req.ReportFormat != uint32(llotypes.ReportFormatJSON) {
2857+
continue
2858+
}
2859+
_, _, r, _, err := (datastreamsllo.JSONReportCodec{}).UnpackDecode(req.Payload)
2860+
if err == nil && tombstonedChannel[r.ChannelID] {
2861+
sawTombstoned = true
2862+
break
2863+
}
2864+
}
2865+
return !sawTombstoned
2866+
}, 45*time.Second, 200*time.Millisecond, "channel 2 should stop producing reports after tombstone")
2867+
2868+
// After reports for channel 2 have stopped, bridge traffic for streamIDTombstone should stop
2869+
// while streamIDActive continues to be observed.
2870+
bCallsAfterReportsStopped := streamBCalls.Load()
2871+
aCallsAfterReportsStopped := streamACalls.Load()
2872+
time.Sleep(1 * time.Second)
2873+
require.Equal(t, bCallsAfterReportsStopped, streamBCalls.Load(),
2874+
"tombstoned channel's stream should not be observed (no additional bridge calls)")
2875+
require.Greater(t, streamACalls.Load(), aCallsAfterReportsStopped,
2876+
"active channel's stream should still be observed")
2877+
}
2878+
26952879
func setupNodes(t *testing.T, nNodes int, backend evmtypes.Backend, clientCSAKeys []csakey.KeyV2, f func(*chainlink.Config)) (oracles []confighelper.OracleIdentityExtra, nodes []Node) {
26962880
ports := freeport.GetN(t, nNodes)
26972881
for i := range nNodes {
@@ -2730,7 +2914,6 @@ func newChannelDefinitionsServer(t *testing.T, channelDefinitions llotypes.Chann
27302914
return
27312915
}
27322916
w.Header().Set("Content-Type", "application/json")
2733-
w.WriteHeader(http.StatusOK)
27342917
}))
27352918
t.Cleanup(channelDefinitionsServer.Close)
27362919
return channelDefinitionsServer.URL, channelDefinitionsSHA

core/services/ocr2/plugins/llo/onchain_channel_definition_cache_integration_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,6 +1325,80 @@ func Test_ChannelDefinitionCache_OwnerAndAdderMerging(t *testing.T) {
13251325
"tombstoned channel 600 should be dropped, channel 601 should remain")
13261326
})
13271327

1328+
// After the owner omits a tombstoned channel from their on-chain definitions, the in-memory
1329+
// cache no longer carries that channel for the owner source. The merged map returned by
1330+
// Definitions(prevOutcome) is what the LLO plugin treats as channel definitions for the OCR
1331+
// outcome; it must stay free of the dropped channel on later rounds too.
1332+
//
1333+
// Depends on prior subtests in this test (owner tombstone + omit flow); do not run in
1334+
// isolation with go test -run matching only this subtest name.
1335+
t.Run("dropped tombstoned channel stays out of merged outcome after cache update", func(t *testing.T) {
1336+
observedLogs.TakeAll()
1337+
1338+
prevSimulatingOCROutcome := llotypes.ChannelDefinitions{
1339+
600: {
1340+
ReportFormat: llotypes.ReportFormatJSON,
1341+
Streams: []llotypes.Stream{
1342+
{StreamID: 1, Aggregator: llotypes.AggregatorMedian},
1343+
},
1344+
Source: channeldefinitions.SourceOwner,
1345+
Tombstone: true,
1346+
},
1347+
601: {
1348+
ReportFormat: llotypes.ReportFormatJSON,
1349+
Streams: []llotypes.Stream{
1350+
{StreamID: 2, Aggregator: llotypes.AggregatorMode},
1351+
},
1352+
Source: channeldefinitions.SourceOwner,
1353+
Tombstone: false,
1354+
},
1355+
602: {
1356+
ReportFormat: llotypes.ReportFormatJSON,
1357+
Streams: []llotypes.Stream{
1358+
{StreamID: 3, Aggregator: llotypes.AggregatorMedian},
1359+
},
1360+
Source: adder1ID,
1361+
Tombstone: false,
1362+
},
1363+
}
1364+
1365+
mergedOutcome := cdc.Definitions(prevSimulatingOCROutcome)
1366+
_, has600 := mergedOutcome[600]
1367+
require.False(t, has600, "merged outcome should not contain dropped tombstoned channel 600")
1368+
_, has601 := mergedOutcome[601]
1369+
require.True(t, has601, "merged outcome should still contain channel 601")
1370+
_, has602 := mergedOutcome[602]
1371+
require.True(t, has602, "merged outcome should still contain adder channel 602")
1372+
1373+
// Simulate the next observation round: prev is the prior merged channel definitions.
1374+
mergedAgain := cdc.Definitions(mergedOutcome)
1375+
_, still600 := mergedAgain[600]
1376+
require.False(t, still600, "channel 600 must not reappear in merged outcome on subsequent Definitions(prev) calls")
1377+
require.Contains(t, mergedAgain, llotypes.ChannelID(601))
1378+
require.Contains(t, mergedAgain, llotypes.ChannelID(602))
1379+
1380+
require.Eventually(t, func() bool {
1381+
loaded, err := orm.LoadChannelDefinitions(testutils.Context(t), configStoreAddress, donID)
1382+
if err != nil || loaded == nil {
1383+
return false
1384+
}
1385+
if loaded.Format != channeldefinitions.MultiChannelDefinitionsFormat {
1386+
return false
1387+
}
1388+
var sources map[uint32]llotypes2.SourceDefinition
1389+
if err = json.Unmarshal(loaded.Definitions, &sources); err != nil {
1390+
return false
1391+
}
1392+
ownerSrc, ok := sources[channeldefinitions.SourceOwner]
1393+
if !ok {
1394+
return false
1395+
}
1396+
_, ownerHas600 := ownerSrc.Definitions[600]
1397+
return !ownerHas600 && len(ownerSrc.Definitions) > 0
1398+
}, 5*time.Second, 100*time.Millisecond,
1399+
"persisted owner source definitions should not list channel 600 after owner omitted it from the cache")
1400+
})
1401+
13281402
t.Run("multiple adders can add different channels", func(t *testing.T) {
13291403
observedLogs.TakeAll()
13301404

0 commit comments

Comments
 (0)