Skip to content

Commit ddcdd9d

Browse files
committed
record a graph on each run
1 parent 61bd560 commit ddcdd9d

File tree

7 files changed

+231
-8
lines changed

7 files changed

+231
-8
lines changed

go.mod

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/miguelmota/go-ethereum-hdwallet v0.1.1
77
github.com/multiformats/go-multiaddr v0.7.0
88
github.com/statechannels/go-nitro v0.0.0-20221009024643-ab7b1a648d10
9+
gonum.org/v1/gonum v0.12.0
910
)
1011

1112
require (
@@ -102,10 +103,11 @@ require (
102103
github.com/tklauser/go-sysconf v0.3.5 // indirect
103104
github.com/tklauser/numcpus v0.2.2 // indirect
104105
github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef // indirect
105-
golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b // indirect
106+
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 // indirect
106107
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
107108
golang.org/x/net v0.0.0-20220920183852-bf014ff85ad5 // indirect
108109
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
110+
golang.org/x/text v0.3.8 // indirect
109111
golang.org/x/tools v0.1.12 // indirect
110112
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
111113
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect

go.sum

+6-3
Original file line numberDiff line numberDiff line change
@@ -947,8 +947,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
947947
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
948948
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
949949
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
950-
golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b h1:SCE/18RnFsLrjydh/R/s5EVvHoZprqEQUuoxK8q2Pc4=
951-
golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
950+
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 h1:sBdrWpxhGDdTAYNqbgBLAR+ULAPPhfgncLr1X0lyWtg=
951+
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
952952
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
953953
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
954954
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -1145,8 +1145,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
11451145
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
11461146
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
11471147
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
1148-
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
11491148
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
1149+
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
1150+
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
11501151
golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
11511152
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
11521153
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1222,6 +1223,8 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
12221223
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
12231224
gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
12241225
gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
1226+
gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o=
1227+
gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY=
12251228
gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
12261229
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
12271230
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=

peer/peer.go

+6
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ type MyInfo struct {
5656
PrivateKey ecdsa.PrivateKey
5757
}
5858

59+
// IsGraphRecorder returns true if an instance is responsible for recording the graph to file.
60+
// We use the first payee instance as they are not too busy.
61+
func IsGraphRecorder(seq int64, c config.RunConfig) bool {
62+
return int64(c.NumHubs+c.NumPayers+c.NumPayees) == seq
63+
}
64+
5965
// GetRole determines the role an instance will play based on the run config.
6066
func GetRole(seq int64, c config.RunConfig) Role {
6167
switch {

tests/virtual-payment.go

+26-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package tests
22

33
import (
44
"context"
5+
"encoding/xml"
56
"fmt"
67
"math/big"
78
"math/rand"
@@ -95,8 +96,9 @@ func CreateVirtualPaymentTest(runEnv *runtime.RunEnv, init *run.InitContext) err
9596
client.MustBarrier(ctx, contractSetup, runEnv.TestInstanceCount)
9697

9798
nClient := nitro.New(ms, cs, store, logDestination, &engine.PermissivePolicy{}, runEnv.R())
99+
gr := utils.NewGraphRecorder(me.PeerInfo, peers, runConfig, client)
98100

99-
cm := utils.NewCompletionMonitor(&nClient, runEnv.RecordMessage)
101+
cm := utils.NewCompletionMonitor(&nClient, gr, runEnv.RecordMessage)
100102
defer cm.Close()
101103

102104
// We wait until everyone has chosen an address.
@@ -105,7 +107,7 @@ func CreateVirtualPaymentTest(runEnv *runtime.RunEnv, init *run.InitContext) err
105107
client.MustSignalAndWait(ctx, "message service connected", runEnv.TestInstanceCount)
106108

107109
// Create ledger channels with all the hubs
108-
ledgerIds := utils.CreateLedgerChannels(nClient, cm, utils.FINNEY_IN_WEI, me.PeerInfo, peers)
110+
ledgerIds := utils.CreateLedgerChannels(nClient, cm, gr, utils.FINNEY_IN_WEI, me.PeerInfo, peers)
109111

110112
client.MustSignalAndWait(ctx, sync.State("ledgerDone"), runEnv.TestInstanceCount)
111113
toSleep := runConfig.GetSleepDuration()
@@ -139,6 +141,16 @@ func CreateVirtualPaymentTest(runEnv *runtime.RunEnv, init *run.InitContext) err
139141

140142
r := nClient.CreateVirtualPaymentChannel(selectedHubs, randomPayee.Address, 0, outcome)
141143

144+
participants := []types.Address{me.Address}
145+
participants = append(participants, selectedHubs...)
146+
participants = append(participants, randomPayee.Address)
147+
gr.ObjectiveStatusUpdated(utils.ObjectiveStatusInfo{
148+
Id: r.Id,
149+
Time: time.Now(),
150+
Participants: participants,
151+
ChannelId: r.ChannelId.String(),
152+
Status: "Starting",
153+
})
142154
channelId = r.ChannelId
143155
cm.WaitForObjectivesToComplete([]protocols.ObjectiveId{r.Id})
144156

@@ -207,6 +219,18 @@ func CreateVirtualPaymentTest(runEnv *runtime.RunEnv, init *run.InitContext) err
207219
runEnv.R().RecordPoint(fmt.Sprintf("ci_mean_time_to_first_payment,me=%s", me.Address), float64(mean))
208220
}
209221
}
222+
223+
if peer.IsGraphRecorder(seq, runConfig) {
224+
data, err := xml.Marshal(gr.Graph())
225+
if err != nil {
226+
return err
227+
}
228+
err = os.WriteFile(fmt.Sprintf("./outputs/graph-%s.gexf", runEnv.TestRun), data, 0644)
229+
if err != nil {
230+
return err
231+
}
232+
233+
}
210234
client.MustSignalAndWait(ctx, "done", runEnv.TestInstanceCount)
211235

212236
return nil

utils/graph-recorder.go

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package utils
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"time"
8+
9+
"github.com/ethereum/go-ethereum/common"
10+
"github.com/statechannels/go-nitro-testground/config"
11+
"github.com/statechannels/go-nitro-testground/peer"
12+
"github.com/statechannels/go-nitro/protocols"
13+
"github.com/testground/sdk-go/sync"
14+
15+
"gonum.org/v1/gonum/graph/formats/gexf12"
16+
)
17+
18+
type objectiveStatus string
19+
20+
const (
21+
Starting objectiveStatus = "Starting"
22+
Completed objectiveStatus = "Completed"
23+
)
24+
25+
// ObjectiveStatusInfo contains information about the status change of an objective
26+
type ObjectiveStatusInfo struct {
27+
Id protocols.ObjectiveId
28+
Time time.Time
29+
Participants []common.Address
30+
ChannelId string
31+
Status objectiveStatus
32+
}
33+
34+
// GraphRecorder is a utility for recording the state of the network in GEXF file format.
35+
type GraphRecorder struct {
36+
me peer.PeerInfo
37+
peers []peer.PeerInfo
38+
config config.RunConfig
39+
graph *gexf12.Graph
40+
syncClient sync.Client
41+
}
42+
43+
// Graph returns the GEXF graph struct
44+
func (gr *GraphRecorder) Graph() *gexf12.Graph {
45+
return gr.graph
46+
}
47+
48+
// NewGraphRecorder creates a new GraphRecorder
49+
func NewGraphRecorder(me peer.PeerInfo, peers []peer.PeerInfo, config config.RunConfig, syncClient sync.Client) *GraphRecorder {
50+
var graph *gexf12.Graph
51+
if peer.IsGraphRecorder(me.Seq, config) {
52+
graph = &gexf12.Graph{}
53+
graph.TimeFormat = "dateTime"
54+
graph.Start = time.Now().Format(
55+
"2006-01-02T15:04:05-0700")
56+
graph.DefaultEdgeType = "directed"
57+
58+
// Set up attributes on the node and edges for channels and participants
59+
graph.Attributes = []gexf12.Attributes{{
60+
Class: "node",
61+
Attributes: []gexf12.Attribute{
62+
{ID: "address", Title: "address", Type: "string"}, {ID: "role", Title: "role", Type: "string"}}},
63+
{
64+
Class: "edge",
65+
Attributes: []gexf12.Attribute{
66+
{ID: "channelId", Title: "ChannelId", Type: "string"}, {ID: "channelType", Title: "ChannelType", Type: "string"}}}}
67+
68+
// Add nodes for each participant
69+
for _, p := range append(peers, me) {
70+
attValues := gexf12.AttValues{AttValues: []gexf12.AttValue{
71+
{For: "address", Value: p.Address.String()},
72+
{For: "role", Value: fmt.Sprintf("%d", p.Role)}}}
73+
74+
node := gexf12.Node{ID: p.Address.String(),
75+
Label: p.Address.String()[0:6],
76+
AttValues: &attValues,
77+
Start: time.Now().Format(
78+
"2006-01-02T15:04:05-0700")}
79+
80+
graph.Nodes.Nodes = append(graph.Nodes.Nodes, node)
81+
}
82+
83+
}
84+
85+
gr := &GraphRecorder{
86+
me: me,
87+
peers: peers,
88+
config: config,
89+
graph: graph,
90+
syncClient: syncClient,
91+
}
92+
// Start listening for changes from other participants
93+
go gr.listenForShared(context.Background())
94+
95+
return gr
96+
}
97+
98+
// objectiveStatusChangedTopic returns the topic to share objective status changes on
99+
func (gr *GraphRecorder) objectiveStatusChangedTopic() *sync.Topic {
100+
return sync.NewTopic("objective-status-change", ObjectiveStatusInfo{})
101+
}
102+
103+
// listenForShared listens for objective status changes from other participants
104+
func (gr *GraphRecorder) listenForShared(ctx context.Context) {
105+
if !peer.IsGraphRecorder(gr.me.Seq, gr.config) {
106+
return
107+
}
108+
started := make(chan ObjectiveStatusInfo)
109+
110+
gr.syncClient.MustSubscribe(ctx, gr.objectiveStatusChangedTopic(), started)
111+
for {
112+
select {
113+
case s := <-started:
114+
gr.ObjectiveStatusUpdated(s)
115+
case <-ctx.Done():
116+
return
117+
}
118+
}
119+
}
120+
121+
// ObjectiveStatusUpdated is called when an objective status changes
122+
// It uses this information to build a graph of the network
123+
func (gr *GraphRecorder) ObjectiveStatusUpdated(info ObjectiveStatusInfo) {
124+
if !peer.IsGraphRecorder(gr.me.Seq, gr.config) {
125+
gr.syncClient.MustPublish(context.Background(), gr.objectiveStatusChangedTopic(), info)
126+
}
127+
128+
isCreateChannel := (strings.Contains(string(info.Id), "VirtualFund") || strings.Contains(string(info.Id), "DirectFund")) && info.Status == Starting
129+
isCloseChannel := (strings.Contains(string(info.Id), "VirtualDefund") || strings.Contains(string(info.Id), "DirectDefund")) && info.Status == Completed
130+
131+
if isCreateChannel {
132+
channelType := "ledger"
133+
if strings.Contains(string(info.Id), "VirtualFund") {
134+
channelType = "virtual"
135+
}
136+
137+
attValues := gexf12.AttValues{
138+
AttValues: []gexf12.AttValue{
139+
{For: "channelId", Value: info.ChannelId},
140+
{For: "channelType", Value: channelType}}}
141+
142+
for i := 0; (i + 1) < len(info.Participants); i++ {
143+
gr.graph.Edges.Edges = append(gr.graph.Edges.Edges,
144+
gexf12.Edge{
145+
ID: fmt.Sprintf("%s_%s", info.ChannelId, info.Participants[i]),
146+
Label: string(info.ChannelId)[0:6],
147+
Source: info.Participants[i].String(),
148+
Target: info.Participants[i+1].String(),
149+
Start: info.Time.Format(
150+
"2006-01-02T15:04:05-0700"),
151+
AttValues: &attValues,
152+
},
153+
)
154+
}
155+
156+
}
157+
if isCloseChannel {
158+
for i := 0; i < len(gr.graph.Edges.Edges); i++ {
159+
160+
if strings.Contains(gr.graph.Edges.Edges[i].ID, info.ChannelId) {
161+
162+
gr.graph.Edges.Edges[i].End = info.Time.Format(
163+
"2006-01-02T15:04:05-0700")
164+
}
165+
}
166+
}
167+
}
168+

utils/monitor.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package utils
22

33
import (
4+
"strings"
45
"time"
56

7+
"github.com/ethereum/go-ethereum/common"
68
nitroclient "github.com/statechannels/go-nitro/client"
79
"github.com/statechannels/go-nitro/client/engine/store/safesync"
810
"github.com/statechannels/go-nitro/protocols"
@@ -16,10 +18,11 @@ type CompletionMonitor struct {
1618
client *nitroclient.Client
1719
quit chan struct{}
1820
log func(msg string, a ...interface{})
21+
gr *GraphRecorder
1922
}
2023

2124
// NewCompletionMonitor creates a new completion monitor
22-
func NewCompletionMonitor(client *nitroclient.Client, logFunc func(msg string, a ...interface{})) *CompletionMonitor {
25+
func NewCompletionMonitor(client *nitroclient.Client, gr *GraphRecorder, logFunc func(msg string, a ...interface{})) *CompletionMonitor {
2326

2427
completed := safesync.Map[bool]{}
2528

@@ -28,6 +31,7 @@ func NewCompletionMonitor(client *nitroclient.Client, logFunc func(msg string, a
2831
client: client,
2932
quit: make(chan struct{}),
3033
log: logFunc,
34+
gr: gr,
3135
}
3236
go c.watch()
3337
return c
@@ -51,6 +55,13 @@ func (c *CompletionMonitor) watch() {
5155
for {
5256
select {
5357
case id := <-c.client.CompletedObjectives():
58+
c.gr.ObjectiveStatusUpdated(ObjectiveStatusInfo{
59+
Id: id,
60+
Time: time.Now(),
61+
Participants: []common.Address{},
62+
ChannelId: strings.Split(string(id), "-")[1],
63+
Status: "Completed",
64+
})
5465
c.completed.Store(string(id), true)
5566
case <-c.quit:
5667
return

utils/testing.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"sync/atomic"
1414
"time"
1515

16+
"github.com/ethereum/go-ethereum/common"
1617
"github.com/statechannels/go-nitro-testground/config"
1718
"github.com/statechannels/go-nitro-testground/peer"
1819
"github.com/statechannels/go-nitro/channel/state/outcome"
@@ -175,7 +176,7 @@ func SelectRandomHubs(hubs []peer.PeerInfo, numHubs int) []types.Address {
175176
// The funding for each channel will be set to amount for both participants.
176177
// This function blocks until all ledger channels have successfully been created.
177178
// If the participant is a hub we use the participant's Seq to determine the initiator of the ledger channel.
178-
func CreateLedgerChannels(client nitro.Client, cm *CompletionMonitor, amount uint, me peer.PeerInfo, peers []peer.PeerInfo) []types.Destination {
179+
func CreateLedgerChannels(client nitro.Client, cm *CompletionMonitor, gr *GraphRecorder, amount uint, me peer.PeerInfo, peers []peer.PeerInfo) []types.Destination {
179180
ids := []protocols.ObjectiveId{}
180181
cIds := []types.Destination{}
181182
hubs := peer.FilterByRole(peers, peer.Hub)
@@ -200,6 +201,14 @@ func CreateLedgerChannels(client nitro.Client, cm *CompletionMonitor, amount uin
200201
},
201202
}}
202203
r := client.CreateLedgerChannel(p.Address, 0, outcome)
204+
205+
gr.ObjectiveStatusUpdated(ObjectiveStatusInfo{
206+
Id: r.Id,
207+
Time: time.Now(),
208+
Participants: []common.Address{me.Address, p.Address},
209+
ChannelId: r.ChannelId.String(),
210+
Status: "Starting",
211+
})
203212
cIds = append(cIds, r.ChannelId)
204213
ids = append(ids, r.Id)
205214
}

0 commit comments

Comments
 (0)