diff --git a/cmd/opera/launcher/chaincmd.go b/cmd/opera/launcher/chaincmd.go index 00070b7e1..008b22b3c 100644 --- a/cmd/opera/launcher/chaincmd.go +++ b/cmd/opera/launcher/chaincmd.go @@ -78,8 +78,8 @@ The import command imports EVM storage (trie nodes, code, preimages) from files. Requires a first argument of the file to write to. Optional second and third arguments control the first and -last epoch to write. If the file ends with .gz, the output will -be gzipped +last epoch to write. If the file ends with .gz, the output will be gzipped. +End the rest of file name with .dot to export events graph as DOT `, }, { diff --git a/cmd/opera/launcher/export.go b/cmd/opera/launcher/export.go index 7681f7fdf..5751b7c08 100644 --- a/cmd/opera/launcher/export.go +++ b/cmd/opera/launcher/export.go @@ -21,6 +21,8 @@ import ( "gopkg.in/urfave/cli.v1" "github.com/Fantom-foundation/go-opera/gossip" + "github.com/Fantom-foundation/go-opera/integration" + "github.com/Fantom-foundation/go-opera/utils/dag" "github.com/Fantom-foundation/go-opera/utils/dbutil/autocompact" ) @@ -46,19 +48,14 @@ func exportEvents(ctx *cli.Context) error { fn := ctx.Args().First() - // Open the file handle and potentially wrap with a gzip stream + // Open the file handle + log.Info("Exporting events to file", "file", fn) fh, err := os.OpenFile(fn, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm) if err != nil { return err } defer fh.Close() - var writer io.Writer = fh - if strings.HasSuffix(fn, ".gz") { - writer = gzip.NewWriter(writer) - defer writer.(*gzip.Writer).Close() - } - from := idx.Epoch(1) if len(ctx.Args()) > 1 { n, err := strconv.ParseUint(ctx.Args().Get(1), 10, 32) @@ -76,22 +73,39 @@ func exportEvents(ctx *cli.Context) error { to = idx.Epoch(n) } - log.Info("Exporting events to file", "file", fn) - // Write header and version - _, err = writer.Write(append(eventsFileHeader, eventsFileVersion...)) - if err != nil { - return err + // Potentially wrap with a gzip stream + var writer io.Writer = fh + if strings.HasSuffix(fn, ".gz") { + fn = fn[:len(fn)-len(".gz")] + writer = gzip.NewWriter(writer) + defer writer.(*gzip.Writer).Close() } - err = exportTo(writer, gdb, from, to) - if err != nil { - utils.Fatalf("Export error: %v\n", err) + + switch { + // DOT format: + case strings.HasSuffix(fn, ".dot"): + err = exportDOT(writer, gdb, cfg, from, to) + if err != nil { + utils.Fatalf("Export DOT error: %v\n", err) + } + // RLP format: + default: + // Write header and version + _, err = writer.Write(append(eventsFileHeader, eventsFileVersion...)) + if err != nil { + return err + } + err = exportRLP(writer, gdb, from, to) + if err != nil { + utils.Fatalf("Export RLP error: %v\n", err) + } } return nil } -// exportTo writer the active chain. -func exportTo(w io.Writer, gdb *gossip.Store, from, to idx.Epoch) (err error) { +// exportRLP writer the active chain. +func exportRLP(w io.Writer, gdb *gossip.Store, from, to idx.Epoch) (err error) { start, reported := time.Now(), time.Time{} var ( @@ -119,6 +133,24 @@ func exportTo(w io.Writer, gdb *gossip.Store, from, to idx.Epoch) (err error) { return } +// exportDOT writer the active chain. +func exportDOT(writer io.Writer, gdb *gossip.Store, cfg *config, from, to idx.Epoch) (err error) { + consensusCfg := integration.Configs{ + Opera: cfg.Opera, + Lachesis: cfg.Lachesis, + VectorClock: cfg.VectorClock, + } + + graph := dag.Graph(gdb, consensusCfg, from, to) + + _, err = writer.Write([]byte(graph.String())) + if err != nil { + return err + } + + return nil +} + func exportEvmKeys(ctx *cli.Context) error { if len(ctx.Args()) < 1 { utils.Fatalf("This command requires an argument.") diff --git a/go.mod b/go.mod index 1f97835a5..c21bea709 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( go.uber.org/atomic v1.5.1 // indirect golang.org/x/crypto v0.7.0 golang.org/x/sys v0.6.0 - golang.org/x/tools v0.6.0 // indirect + golang.org/x/tools v0.7.0 // indirect gopkg.in/urfave/cli.v1 v1.20.0 ) @@ -94,7 +94,7 @@ require ( github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/tklauser/go-sysconf v0.3.5 // indirect github.com/tklauser/numcpus v0.2.2 // indirect - golang.org/x/exp v0.0.0-20200513190911-00229845015e // indirect + golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect golang.org/x/net v0.8.0 // indirect golang.org/x/sync v0.1.0 // indirect diff --git a/go.sum b/go.sum index 58e6e50e5..d630269a6 100644 --- a/go.sum +++ b/go.sum @@ -308,8 +308,9 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -680,6 +681,7 @@ golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -695,8 +697,9 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20200513190911-00229845015e h1:rMqLP+9XLy+LdbCXHjJHAmTfXCr93W7oruWA6Hq1Alc= golang.org/x/exp v0.0.0-20200513190911-00229845015e/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -721,8 +724,10 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -766,6 +771,7 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= @@ -853,12 +859,14 @@ golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -870,6 +878,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= @@ -933,8 +942,10 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/utils/dag/dag.go b/utils/dag/dag.go new file mode 100644 index 000000000..0630322f2 --- /dev/null +++ b/utils/dag/dag.go @@ -0,0 +1,15 @@ +package dag + +import ( + "github.com/Fantom-foundation/lachesis-base/inter/idx" + + "github.com/Fantom-foundation/go-opera/gossip" + "github.com/Fantom-foundation/go-opera/integration" + "github.com/Fantom-foundation/go-opera/utils/dag/dot" +) + +func Graph(db *gossip.Store, cfg integration.Configs, from, to idx.Epoch) *dot.Graph { + g := readDagGraph(db, cfg, from, to) + + return g +} diff --git a/utils/dag/dot/LICENSE.md b/utils/dag/dot/LICENSE.md new file mode 100644 index 000000000..99daa6bda --- /dev/null +++ b/utils/dag/dot/LICENSE.md @@ -0,0 +1,7 @@ +Copyright (C) 2012 Travis Cline + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/utils/dag/dot/README.md b/utils/dag/dot/README.md new file mode 100644 index 000000000..bc40901cc --- /dev/null +++ b/utils/dag/dot/README.md @@ -0,0 +1,16 @@ +dot +=== + +dot language support for Go + + +See http://godoc.org/github.com/tmc/dot + +Todo: + +* parser +* regression suite + +The pydot library used as a reference + +License: MIT diff --git a/utils/dag/dot/dot.go b/utils/dag/dot/dot.go new file mode 100644 index 000000000..0c3ec2429 --- /dev/null +++ b/utils/dag/dot/dot.go @@ -0,0 +1,531 @@ +/* +Package dot implements an API to produce Graphviz dot language output. + +Basic Graph creation: + + g := dot.NewGraph("G") + g.SetType(dot.DIGRAPH) + ... + g.AddEdge(dot.NewNode("A"), dot.NewNode("B")) + ... + fmt.Sprint(g) +*/ +package dot + +import ( + "errors" + "fmt" + "regexp" + "sort" + "strings" +) + +var AttributeError = errors.New("Invalid Attribute") + +var graphAttributes = []string{"Damping", "K", "URL", "aspect", "bb", "bgcolor", + "center", "charset", "clusterrank", "colorscheme", "comment", "compound", + "concentrate", "defaultdist", "dim", "dimen", "diredgeconstraints", + "dpi", "epsilon", "esep", "fontcolor", "fontname", "fontnames", + "fontpath", "fontsize", "id", "label", "labeljust", "labelloc", + "landscape", "layers", "layersep", "layout", "levels", "levelsgap", + "lheight", "lp", "lwidth", "margin", "maxiter", "mclimit", "mindist", + "mode", "model", "mosek", "nodesep", "nojustify", "normalize", "nslimit", + "nslimit1", "ordering", "orientation", "outputorder", "overlap", + "overlap_scaling", "pack", "packmode", "pad", "page", "pagedir", + "quadtree", "quantum", "rankdir", "ranksep", "ratio", "remincross", + "repulsiveforce", "resolution", "root", "rotate", "searchsize", "sep", + "showboxes", "style", "size", "smoothing", "sortv", "splines", "start", + "stylesheet", "target", "truecolor", "viewport", "voro_margin", + "rank", "newrank"} + +var edgeAttributes = []string{"URL", "arrowhead", "arrowsize", "arrowtail", + "color", "colorscheme", "comment", "constraint", "decorate", "dir", + "edgeURL", "edgehref", "edgetarget", "edgetooltip", "fontcolor", + "fontname", "fontsize", "headURL", "headclip", "headhref", "headlabel", + "headport", "headtarget", "headtooltip", "href", "id", "label", + "labelURL", "labelangle", "labeldistance", "labelfloat", "labelfontcolor", + "labelfontname", "labelfontsize", "labelhref", "labeltarget", + "labeltooltip", "layer", "len", "lhead", "lp", "ltail", "minlen", + "nojustify", "penwidth", "pos", "samehead", "sametail", "showboxes", + "style", "tailURL", "tailclip", "tailhref", "taillabel", "tailport", + "tailtarget", "tailtooltip", "target", "tooltip", "weight", + // for subgraphs + "rank"} + +var nodeAttributes = []string{"URL", "color", "colorscheme", "comment", + "distortion", "fillcolor", "fixedsize", "fontcolor", "fontname", + "fontsize", "group", "height", "id", "image", "imagescale", "label", + "labelloc", "layer", "margin", "nojustify", "orientation", "penwidth", + "peripheries", "pin", "pos", "rects", "regular", "root", "samplepoints", + "shape", "shapefile", "showboxes", "sides", "skew", "sortv", "style", + "target", "tooltip", "vertices", "width", "z", + // The following are attributes dot2tex + "texlbl", "texmode"} + +var clusterAttributes = []string{"K", "URL", "bgcolor", "color", "colorscheme", + "fillcolor", "fontcolor", "fontname", "fontsize", "label", "labeljust", + "labelloc", "lheight", "lp", "lwidth", "nojustify", "pencolor", + "penwidth", "peripheries", "sortv", "style", "target", "tooltip"} + +var dotKeywords = []string{"graph", "subgraph", "digraph", "node", "edge", "strict"} + +type GraphType int + +const ( + DIGRAPH GraphType = iota + GRAPH + SUBGRAPH +) + +// Fields common to all graph object types +type common struct { + _type string + name string + attributes map[string]string + sequence int + parentGraph *Graph +} + +type GraphObject interface { + Type() string + Get(string) string + Set(string, string) error + GetParentGraph() *Graph + SetParentGraph(g *Graph) + Sequence() int +} + +type graphObjects []GraphObject + +func (gol graphObjects) Len() int { + return len(gol) +} + +func (gol graphObjects) Less(i, j int) bool { + return gol[i].Sequence() < gol[j].Sequence() +} + +func (gol graphObjects) Swap(i, j int) { + gol[i], gol[j] = gol[j], gol[i] +} + +type Graph struct { + common + nodeAttributes map[string]string + edgeAttributes map[string]string + sameRank [][]string + strict bool + graphType GraphType + supressDisconnected bool + simplify bool + currentChildSequence int + nodes map[string][]*Node + edges map[string][]*Edge + subgraphs map[string][]*SubGraph +} + +func NewGraph(name string) *Graph { + g := &Graph{ + common: common{ + _type: "graph", + name: name, + attributes: make(map[string]string, 0), + }, + nodeAttributes: make(map[string]string), + edgeAttributes: make(map[string]string), + sameRank: make([][]string, 0), + nodes: make(map[string][]*Node, 0), + edges: make(map[string][]*Edge, 0), + subgraphs: make(map[string][]*SubGraph, 0), + currentChildSequence: 1, + } + g.SetParentGraph(g) + return g +} + +type SubGraph struct { + Graph +} + +func NewSubgraph(name string) *SubGraph { + result := &SubGraph{ + Graph: *NewGraph(name), + } + result._type = "subgraph" + result.graphType = SUBGRAPH + return result +} + +func indexInSlice(slice []string, toFind string) int { + for i, v := range slice { + if v == toFind { + return i + } + } + return -1 +} + +var alreadyQuotedRegex = regexp.MustCompile("^\".+\"$") +var validIdentifierRegexWithPort = regexp.MustCompile("^[_a-zA-Z][a-zA-Z0-9_,:\"]*[a-zA-Z0-9_,\"]+$") +var validIdentifierRegex = regexp.MustCompile("^[_a-zA-Z][a-zA-Z0-9_,]*$") + +func needsQuotes(s string) bool { + if indexInSlice(dotKeywords, s) != -1 { + return false + } + if alreadyQuotedRegex.MatchString(s) { + return false + } + if validIdentifierRegexWithPort.MatchString(s) || validIdentifierRegex.MatchString(s) { + return false + } + + return true +} + +func QuoteIfNecessary(s string) (result string) { + if needsQuotes(s) { + s = strings.Replace(s, "\"", "\\\"", -1) + s = strings.Replace(s, "\n", "\\n", -1) + s = strings.Replace(s, "\r", "\\r", -1) + s = "\"" + s + "\"" + } + return s +} + +func validAttribute(attributeCollection []string, attributeName string) bool { + return indexInSlice(attributeCollection, attributeName) != -1 +} + +func validGraphAttribute(attributeName string) bool { + return validAttribute(graphAttributes, attributeName) +} + +func validNodeAttribute(attributeName string) bool { + return validAttribute(nodeAttributes, attributeName) +} + +func sortedKeys(sourceMap map[string]string) []string { + keys := make([]string, 0, len(sourceMap)) + for k, _ := range sourceMap { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +//////////////////////////////////////////////////////////////////////////////////////// + +func (gt GraphType) String() string { + if gt == DIGRAPH { + return "digraph" + } else if gt == GRAPH { + return "graph" + } else if gt == SUBGRAPH { + return "subgraph" + } + return "(invalid)" +} + +func (c *common) Type() string { + return c._type +} + +func (c *common) GetParentGraph() *Graph { + return c.parentGraph +} + +func (c *common) SetParentGraph(g *Graph) { + c.parentGraph = g +} + +func (c *common) Sequence() int { + return c.sequence +} + +func (c *common) Get(attributeName string) string { + return c.attributes[attributeName] +} + +func (c *common) Set(attributeName, attributeValue string) error { + c.attributes[attributeName] = attributeValue + return nil +} + +func setAttribute(validAttributes []string, attributes map[string]string, attributeName, attributeValue string) error { + if validAttribute(validAttributes, attributeName) { + attributes[attributeName] = attributeValue + return nil + } + return AttributeError +} + +func (g *Graph) Set(attributeName, attributeValue string) error { + return setAttribute(graphAttributes, g.common.attributes, attributeName, attributeValue) +} + +func (g *Graph) SetGlobalNodeAttr(attributeName, attributeValue string) error { + return setAttribute(nodeAttributes, g.nodeAttributes, attributeName, attributeValue) +} + +func (g *Graph) SetGlobalEdgeAttr(attributeName, attributeValue string) error { + return setAttribute(edgeAttributes, g.edgeAttributes, attributeName, attributeValue) +} + +func (n *Node) Set(attributeName, attributeValue string) error { + return setAttribute(nodeAttributes, n.common.attributes, attributeName, attributeValue) +} + +func (e *Edge) Set(attributeName, attributeValue string) error { + return setAttribute(edgeAttributes, e.common.attributes, attributeName, attributeValue) +} + +func (c *common) setSequence(sequence int) { + c.sequence = sequence +} + +// SameRank enforces alignment of the given nodes +func (g *Graph) SameRank(nodes []string) { + g.sameRank = append(g.sameRank, nodes) +} + +// Set the type of the graph, valid values are GRAPH or DIGRAPH +func (g *Graph) SetType(t GraphType) { + g.graphType = t + // @todo consider disallowing setting type to SUBGRAPH +} + +func (c common) Name() string { + return c.name +} + +func (g *Graph) GetRoot() (result *Graph) { + result = g + for parent := g.GetParentGraph(); parent != result; parent = parent.GetParentGraph() { + result = parent + } + return result +} + +func (g *Graph) getNextSequenceNumber() (next int) { + next = g.currentChildSequence + g.currentChildSequence += 1 + return +} +func (g *Graph) AddNode(n *Node) { + name := n.Name() + if _, ok := g.nodes[name]; !ok { + g.nodes[name] = make([]*Node, 0) + } + n.setSequence(g.getNextSequenceNumber()) + n.SetParentGraph(g.GetParentGraph()) + g.nodes[name] = append(g.nodes[name], n) +} + +func (g *Graph) AddEdge(e *Edge) { + name := e.Name() + if _, ok := g.edges[name]; !ok { + g.edges[name] = make([]*Edge, 0) + } + e.setSequence(g.getNextSequenceNumber()) + e.SetParentGraph(g.GetParentGraph()) + g.edges[name] = append(g.edges[name], e) +} + +func (g *Graph) AddSubgraph(sg *SubGraph) { + name := sg.Name() + if _, ok := g.subgraphs[name]; !ok { + g.subgraphs[name] = make([]*SubGraph, 0) + } + sg.setSequence(g.getNextSequenceNumber()) + g.subgraphs[name] = append(g.subgraphs[name], sg) +} + +func (g *Graph) GetSubgraphs() (result []*SubGraph) { + result = make([]*SubGraph, 0) + for _, sgs := range g.subgraphs { + for _, sg := range sgs { + result = append(result, sg) + } + } + return result +} + +func (g Graph) String() string { + var parts []string + if g.strict { + parts = append(parts, "strict ") + } + if g.name == "" { + parts = append(parts, "{\n") + } else { + parts = append(parts, fmt.Sprintf("%s %s {\n", g.graphType, QuoteIfNecessary(g.name))) + } + + if len(g.attributes) > 0 { + attrs := make([]string, 0, len(g.attributes)) + for _, key := range sortedKeys(g.attributes) { + attrs = append(attrs, " "+key+"="+QuoteIfNecessary(g.attributes[key])) + } + if len(attrs) > 0 { + parts = append(parts, "graph [\n") + parts = append(parts, strings.Join(attrs, ";\n")) + parts = append(parts, ";\n];\n") + } + } + + if len(g.nodeAttributes) > 0 { + attrs := make([]string, 0, len(g.nodeAttributes)) + for _, key := range sortedKeys(g.nodeAttributes) { + attrs = append(attrs, " "+key+"="+QuoteIfNecessary(g.nodeAttributes[key])) + } + if len(attrs) > 0 { + parts = append(parts, "node [\n") + parts = append(parts, strings.Join(attrs, ";\n")) + parts = append(parts, ";\n];\n") + } + } + + if len(g.edgeAttributes) > 0 { + attrs := make([]string, 0, len(g.edgeAttributes)) + for _, key := range sortedKeys(g.edgeAttributes) { + attrs = append(attrs, " "+key+"="+QuoteIfNecessary(g.edgeAttributes[key])) + } + if len(attrs) > 0 { + parts = append(parts, "edge [\n") + parts = append(parts, strings.Join(attrs, ";\n")) + parts = append(parts, ";\n];\n") + } + } + + objectList := make(graphObjects, 0) + + for _, nodes := range g.nodes { + for _, node := range nodes { + objectList = append(objectList, node) + } + } + for _, edges := range g.edges { + for _, edge := range edges { + objectList = append(objectList, edge) + } + } + for _, subgraphs := range g.subgraphs { + for _, subgraph := range subgraphs { + objectList = append(objectList, subgraph) + } + } + sort.Sort(objectList) + + for _, obj := range objectList { + //@todo type-based decision making re: supressDisconnected and simplify + //switch o := obj.(type) { + //case *Node: + //} + parts = append(parts, fmt.Sprintf("%s\n", obj)) + } + + for _, nodes := range g.sameRank { + parts = append(parts, fmt.Sprintf("{ rank=same %s }", strings.Join(nodes, " "))) + } + + parts = append(parts, "}\n") + return strings.Join(parts, "") +} + +type Node struct { + common +} + +func NewNode(name string) *Node { + return &Node{ + common{ + name: name, + attributes: make(map[string]string, 0), + }, + } +} + +func (n Node) String() string { + + name := QuoteIfNecessary(n.name) + + parts := make([]string, 0) + + attrs := make([]string, 0) + for _, key := range sortedKeys(n.attributes) { + value := n.attributes[key] + if key == "label" && len(value) > 4 && value[0] == '<' && value[len(value)-1] == '>' { + attrs = append(attrs, key+"="+value) + } else { + attrs = append(attrs, key+"="+QuoteIfNecessary(value)) + } + } + if len(attrs) > 0 { + parts = append(parts, strings.Join(attrs, ", ")) + } + + //@todo don't print if node is empty + if len(parts) > 0 { + name += " [" + strings.Join(parts, ", ") + "]" + } + + return name + ";" +} + +type Edge struct { + common + points [2]*Node +} + +func NewEdge(src, dst *Node) *Edge { + return &Edge{ + common{ + _type: "edge", + attributes: make(map[string]string, 0), + }, + [2]*Node{src, dst}, + } +} + +func (e Edge) Source() *Node { + return e.points[0] +} + +func (e Edge) Destination() *Node { + return e.points[1] +} + +func (e Edge) String() string { + src, dst := e.Source(), e.Destination() + parts := make([]string, 0) + + parts = append(parts, QuoteIfNecessary(src.Name())) + + parent := e.GetParentGraph() + if parent != nil && parent.GetRoot() != nil && parent.GetRoot().graphType == DIGRAPH { + parts = append(parts, "->") + } else { + parts = append(parts, "--") + } + parts = append(parts, QuoteIfNecessary(dst.Name())) + + attrs := make([]string, 0) + for _, key := range sortedKeys(e.attributes) { + attrs = append(attrs, key+"="+QuoteIfNecessary(e.attributes[key])) + } + if len(attrs) > 0 { + parts = append(parts, " [") + parts = append(parts, strings.Join(attrs, ", ")) + parts = append(parts, "]") + } + + return strings.Join(parts, " ") +} + +func init() { + sort.Strings(graphAttributes) + sort.Strings(nodeAttributes) + sort.Strings(edgeAttributes) + sort.Strings(clusterAttributes) +} diff --git a/utils/dag/dot/dot_test.go b/utils/dag/dot/dot_test.go new file mode 100644 index 000000000..431fd0db8 --- /dev/null +++ b/utils/dag/dot/dot_test.go @@ -0,0 +1,151 @@ +package dot_test + +import ( + "fmt" + "testing" + + "github.com/Fantom-foundation/go-opera/utils/dag/dot" +) + +func TestQuotingIfNecessary(t *testing.T) { + cases := map[string]string{ + "foo": "foo", + "\"foo\"": "\"foo\"", + "foo bar": "\"foo bar\"", + "Allen, C.": "\"Allen, C.\"", + } + + for input, expected := range cases { + if dot.QuoteIfNecessary(input) != expected { + t.Errorf("'%s' != '%s'", dot.QuoteIfNecessary(input), expected) + } + } +} + +func TestGraphPrinting(t *testing.T) { + g1 := dot.NewGraph("foo") + expected1 := "digraph foo {\n}\n" + g2 := dot.NewGraph("foo bar") + expected2 := "digraph \"foo bar\" {\n}\n" + + if fmt.Sprint(g1) != expected1 { + t.Errorf("'%s' != '%s'", fmt.Sprint(g1), expected1) + } + if fmt.Sprint(g2) != expected2 { + t.Errorf("'%s' != '%s'", fmt.Sprint(g2), expected2) + } +} + +func TestCreateSimpleGraphWithNode(t *testing.T) { + g := dot.NewGraph("Test") + + expected := "digraph Test {\n}\n" + if fmt.Sprint(g) != expected { + t.Errorf("'%s' != '%s'", fmt.Sprint(g), expected) + } + g.SetType(dot.GRAPH) + + expected = "graph Test {\n}\n" + if fmt.Sprint(g) != expected { + t.Errorf("'%s' != '%s'", fmt.Sprint(g), expected) + } + g.SetType(dot.DIGRAPH) + + node := dot.NewNode("legend") + node.Set("shape", "box") + g.AddNode(node) + node.Set("label", "value with spaces") + + node = dot.NewNode("html") + node.Set("shape", "plain") + node.Set("label", "<bold>") + g.AddNode(node) + + expected = "digraph Test {\nlegend [label=\"value with spaces\", shape=box];\nhtml [label=<bold>, shape=plain];\n}\n" + if fmt.Sprint(g) != expected { + t.Errorf("'%s' != '%s'", fmt.Sprint(g), expected) + } +} + +func TestCreateSimpleNode(t *testing.T) { + node := dot.NewNode("nodename") + node.Set("shape", "box") + node.Set("label", "mine") + + expected := "nodename [label=mine, shape=box];" + if fmt.Sprint(node) != expected { + t.Errorf("'%s' != '%s'", fmt.Sprint(node), expected) + } +} + +func TestGraphAttributeSetting(t *testing.T) { + g := dot.NewGraph("Test") + if g.Set("label", "foo") != nil { + t.Error("Error setting value on g", g) + } + g.Set("Damping", "x") + if g.Set("this_does_not_exist", "and_should_error") != dot.AttributeError { + t.Error("Did not get godot.AttributeError when setting invalid attribute on g", g) + } +} + +func TestSubGraphs(t *testing.T) { + g := dot.NewGraph("G") + s := dot.NewSubgraph("SG") + + subgraphs := make([]*dot.SubGraph, 0) + if subgraphs = g.GetSubgraphs(); len(subgraphs) != 0 { + t.Error("Non-empty subgraphs returned:", subgraphs) + } + g.AddSubgraph(s) + if g.GetSubgraphs()[0].Name() != s.Name() { + t.Error(g.GetSubgraphs()[0].Name(), " != ", s.Name()) + } + + expected := `digraph G { +subgraph SG { +} + +} +` + + if fmt.Sprint(g) != expected { + t.Errorf("'%s' != '%s'", g, expected) + } +} + +func TestEdgeAddition(t *testing.T) { + simple_graph := `digraph G { +graph [ + label="this is a graph"; +]; +a -> b +} +` + g := dot.NewGraph("G") + g.Set("label", "this is a graph") + a, b := dot.NewNode("a"), dot.NewNode("b") + e := dot.NewEdge(a, b) + g.AddEdge(e) + + if fmt.Sprint(g) != simple_graph { + t.Errorf("'%s' != '%s'", g, simple_graph) + } + +} + +func TestQuoting(t *testing.T) { + g := dot.NewGraph("G") + a, b := dot.NewNode("192.168.1.1"), dot.NewNode("192.168.1.2") + e := dot.NewEdge(a, b) + g.AddEdge(e) + + expected := `digraph G { +"192.168.1.1" -> "192.168.1.2" +} +` + if fmt.Sprint(g) != expected { + t.Errorf("'%s' != '%s'", g, expected) + } + +} diff --git a/utils/dag/dot/example_test.go b/utils/dag/dot/example_test.go new file mode 100644 index 000000000..3f2dd9f66 --- /dev/null +++ b/utils/dag/dot/example_test.go @@ -0,0 +1,34 @@ +package dot_test + +import ( + "fmt" + + "github.com/Fantom-foundation/go-opera/utils/dag/dot" +) + +func ExampleNewGraph() { + g := dot.NewGraph("G") + g.Set("label", "Example graph") + n1, n2 := dot.NewNode("Node1"), dot.NewNode("Node2") + + n1.Set("color", "sienna") + + g.AddNode(n1) + g.AddNode(n2) + + e := dot.NewEdge(n1, n2) + e.Set("dir", "both") + g.AddEdge(e) + + fmt.Println(g) + // Output: + // digraph G { + // graph [ + // label="Example graph"; + // ]; + // Node1 [color=sienna]; + // Node2; + // Node1 -> Node2 [ dir=both ] + // } + // +} diff --git a/utils/dag/graph_inmem.go b/utils/dag/graph_inmem.go new file mode 100644 index 000000000..84a03beae --- /dev/null +++ b/utils/dag/graph_inmem.go @@ -0,0 +1,241 @@ +package dag + +import ( + "fmt" + "math" + "strings" + + "github.com/Fantom-foundation/lachesis-base/abft" + "github.com/Fantom-foundation/lachesis-base/gossip/dagordering" + "github.com/Fantom-foundation/lachesis-base/hash" + "github.com/Fantom-foundation/lachesis-base/inter/dag" + "github.com/Fantom-foundation/lachesis-base/inter/idx" + "github.com/Fantom-foundation/lachesis-base/kvdb/memorydb" + "github.com/ethereum/go-ethereum/log" + + "github.com/Fantom-foundation/go-opera/gossip" + "github.com/Fantom-foundation/go-opera/integration" + "github.com/Fantom-foundation/go-opera/inter" + "github.com/Fantom-foundation/go-opera/inter/iblockproc" + "github.com/Fantom-foundation/go-opera/utils/adapters/vecmt2dagidx" + "github.com/Fantom-foundation/go-opera/utils/dag/dot" + "github.com/Fantom-foundation/go-opera/vecmt" +) + +// readDagGraph read gossip.Store into inmem dot.Graph +func readDagGraph(gdb *gossip.Store, cfg integration.Configs, from, to idx.Epoch) *dot.Graph { + // 0. Set gossip data: + + cdb := abft.NewMemStore() + defer cdb.Close() + // ApplyGenesis() + cdb.SetEpochState(&abft.EpochState{ + Epoch: from, + }) + cdb.SetLastDecidedState(&abft.LastDecidedState{ + LastDecidedFrame: abft.FirstFrame - 1, + }) + + dagIndexer := vecmt.NewIndex(panics("Vector clock"), cfg.VectorClock) + orderer := abft.NewOrderer( + cdb, + &integration.GossipStoreAdapter{gdb}, + vecmt2dagidx.Wrap(dagIndexer), + panics("Lachesis"), + cfg.Lachesis) + err := orderer.Bootstrap(abft.OrdererCallbacks{}) + if err != nil { + panic(err) + } + + // 1. Set dot.Graph data: + + g := dot.NewGraph("DOT") + g.Set("clusterrank", "local") + g.Set("compound", "true") + g.Set("newrank", "true") + g.Set("ranksep", "0.05") + g.SetGlobalEdgeAttr("constraint", "true") + + var ( + nodes = make(map[hash.Event]*dot.Node) + subGraphs = make(map[idx.ValidatorID]*dot.SubGraph) + ) + + // 2. Set event processor data: + + var ( + epoch idx.Epoch + prevBS *iblockproc.BlockState + processed map[hash.Event]dag.Event + ) + + readRestoredAbftStore := func() { + bs, _ := gdb.GetHistoryBlockEpochState(epoch) + + for f := idx.Frame(0); f <= cdb.GetLastDecidedFrame(); f++ { + rr := cdb.GetFrameRoots(f) + for _, r := range rr { + n := nodes[r.ID] + markAsRoot(n) + } + } + + if prevBS != nil { + + maxBlock := idx.Block(math.MaxUint64) + if bs != nil { + maxBlock = bs.LastBlock.Idx + } + + for b := prevBS.LastBlock.Idx + 1; b <= maxBlock; b++ { + block := gdb.GetBlock(b) + if block == nil { + break + } + n := nodes[block.Atropos] + if n == nil { + continue + } + markAsAtropos(n) + } + } + + prevBS = bs + } + + resetToNewEpoch := func() { + validators := gdb.GetHistoryEpochState(epoch).Validators + + for _, v := range validators.IDs() { + if _, ok := subGraphs[v]; ok { + continue + } + group := groupName(v) + sg := dot.NewSubgraph(fmt.Sprintf("cluster%d", len(subGraphs))) + sg.Set("label", group) + sg.Set("sortv", fmt.Sprintf("%d", v)) + sg.Set("style", "dotted") + + pseudoNode := dot.NewNode(group) + pseudoNode.Set("style", "invis") + pseudoNode.Set("width", "0") + sg.AddNode(pseudoNode) + + subGraphs[v] = sg + g.AddSubgraph(sg) + } + + processed = make(map[hash.Event]dag.Event, 1000) + err := orderer.Reset(epoch, validators) + if err != nil { + panic(err) + } + dagIndexer.Reset(validators, memorydb.New(), func(id hash.Event) dag.Event { + return gdb.GetEvent(id) + }) + } + + buffer := dagordering.New( + cfg.Opera.Protocol.DagProcessor.EventsBufferLimit, + dagordering.Callback{ + Process: func(e dag.Event) error { + processed[e.ID()] = e + err = dagIndexer.Add(e) + if err != nil { + panic(err) + } + dagIndexer.Flush() + orderer.Process(e) + + name := fmt.Sprintf("%s\n%d", e.ID().String(), e.Creator()) + n := dot.NewNode(name) + sg := subGraphs[e.Creator()] + sg.AddNode(n) + nodes[e.ID()] = n + + for _, h := range e.Parents() { + p := nodes[h] + ref := dot.NewEdge(n, p) + if processed[h].Creator() == e.Creator() { + sg.AddEdge(ref) + } else { + g.AddEdge(ref) + } + } + + return nil + }, + Released: func(e dag.Event, peer string, err error) { + if err != nil { + panic(err) + } + }, + Get: func(id hash.Event) dag.Event { + return processed[id] + }, + Exists: func(id hash.Event) bool { + _, ok := processed[id] + return ok + }, + }) + + // 3. Iterate over events: + + gdb.ForEachEvent(from, func(e *inter.EventPayload) bool { + // current epoch is finished, so process accumulated events + if epoch < e.Epoch() { + readRestoredAbftStore() + + epoch = e.Epoch() + // break after last epoch: + if to >= from && epoch > to { + return false + } + + resetToNewEpoch() + } + + buffer.PushEvent(e, "") + + return true + }) + epoch++ + readRestoredAbftStore() + + // 4. Result + + // NOTE: github.com/tmc/dot renders subgraphs not in the ordering that specified + // so we introduce pseudo nodes and edges to work around + groups := make([]string, 0, len(subGraphs)) + for v := range subGraphs { + groups = append(groups, groupName(v)) + } + g.SameRank([]string{ + "\"" + strings.Join(groups, `" -> "`) + "\" [style = invis, constraint = true];", + }) + + return g +} + +func panics(name string) func(error) { + return func(err error) { + log.Crit(fmt.Sprintf("%s error", name), "err", err) + } +} + +func markAsRoot(n *dot.Node) { + // n.setAttr("xlabel", "root") + n.Set("style", "filled") + n.Set("fillcolor", "#FFFF00") +} + +func markAsAtropos(n *dot.Node) { + // n.setAttr("xlabel", "atropos") + n.Set("style", "filled") + n.Set("fillcolor", "#FF0000") +} + +func groupName(v idx.ValidatorID) string { + return fmt.Sprintf("host-%d", v) +}