├── .gitignore ├── .goreleaser.yml ├── AUTHORS ├── LICENSE ├── README.md ├── client └── client.go ├── cmd ├── expectations │ └── main.go ├── generate-test │ └── generate-test.go ├── log-splicer │ └── main.go ├── netsim │ └── main.go ├── print-follow-graph │ └── print-follow-graph.go └── sim │ └── main.go ├── docs ├── caveats.md ├── commands.md ├── domain-specific-language.md ├── initial-design-doc.md └── tutorial.md ├── expectations ├── expectations.go └── expectations_test.go ├── generation └── generate-test.go ├── go.mod ├── go.sum ├── go.sum.license ├── internal ├── keys │ └── keys.go └── parser │ ├── parser.go │ └── parser_test.go ├── sim-shims ├── go-sim-shim.sh └── js-sim-shim.sh ├── sim ├── commands.go ├── dsl.go ├── instruction.go ├── puppet.go └── util.go └── splicer ├── lfo_to_multimsg_codec.go └── splicer.go /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 the netsim authors 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | .*.sw[a-z] 6 | *.diff 7 | *.txt 8 | *.json 9 | *.tar.gz 10 | fixtures-output 11 | care-package 12 | puppets/ 13 | cmd/expectations/expectations 14 | cmd/generate-test/generate-test 15 | cmd/log-splicer/log-splicer 16 | cmd/print-follow-graph/print-follow-graph 17 | cmd/sim/sim 18 | cmd/netsim/debugging-glyph/ 19 | cmd/netsim/netsim 20 | expectations/graphmap 21 | go-sim-shim.sh 22 | netsim-db2/ 23 | 24 | 25 | dist/ 26 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 the netsim authors 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | builds: 6 | - main: ./cmd/netsim 7 | binary: netsim 8 | goos: 9 | - linux 10 | # - windows # we use a .sh as an essential component; so windows will only work under WSL 11 | - darwin 12 | archives: 13 | - replacements: 14 | darwin: Darwin 15 | linux: Linux 16 | 386: i386 17 | amd64: x86_64 18 | wrap_in_directory: true 19 | files: 20 | - README.md 21 | - docs/tutorial.md 22 | - docs/commands.md 23 | - LICENSE 24 | # - ssb-fixtures/ 25 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 the netsim authors 2 | # SPDX-License-Identifier: LGPL-3.0-or-later 3 | 4 | Alexander Cobleigh 5 | Henry 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Network Simulator 8 | _a simulator for testing [secure scuttlebutt](https://ssb.nz) implementations against each other_ 9 | 10 | ## Goals 11 | The network simulator should: 12 | * be a tool to measure performance metrics before & after partial replication 13 | * be reusable by other scuttlebutts for verifying changes & debugging situations—without requiring any build step to run the tool 14 | * be flexible enough to add new types of peers (e.g. rust) 15 | * provide assurance + insurance that the bedrock of scuttlebutt works as intended 16 | 17 | ## Usage 18 | ```sh 19 | netsim generate 20 | netsim run --spec netsim-test.txt path-to-sbot1 path-to-sbot2 ... path-to-sbotn 21 | ``` 22 | 23 | The `netsim` utility has two commands: 24 | * `netsim generate` consumes output generated by 25 | [`ssb-fixtures`](https://github.com/ssb-ngi-pointer/ssb-fixtures) and outputs a _netsim-adapted_ 26 | ssb-fixtures folder, and an automatically generated netsim test file 27 | * `netsim run` runs the specified netsim test file using the specified sbot implementations 28 | 29 | _**Note**: when passing `--flags`_ 30 | 31 | _Always pass flags directly after a command, and **before** regular arguments. You can 32 | pass flags as either `-flag` or `--flag`._ 33 | 34 | ### Downloading 35 | To get started quickly, [download a netsim release](https://github.com/ssb-ngi-pointer/netsim/releases). 36 | 37 | ### SSB Fixtures 38 | When auto-generating a netsim test, netsim makes use of pre-generated [ssb-fixtures](https://github.com/ssb-ngi-pointer/ssb-fixtures) 39 | to: 40 | * generate source identities and their public keypair 41 | * determine the follow graph 42 | * the amount of peers in the simulation, and the total amount of messages 43 | 44 | The `netsim` utility, however, operates on output _adapted_ from a given `ssb-fixtures` dump. 45 | The adapted fixtures are generated by the `netsim generate` tool. If you only want to generate an 46 | adapted fixtures, and no test file, run: 47 | 48 | ```sh 49 | netsim generate --no-test-script 50 | ``` 51 | 52 | ### Learn more 53 | For more options: 54 | ```sh 55 | netsim generate -h 56 | netsim run -h 57 | ``` 58 | 59 | For more on authoring netsim commands: 60 | * [`commands.md`](./docs/commands.md) 61 | * [`ssb-netsim`](https://github.com/ssb-ngi-pointer/ssb-netsim), the nodejs helper library 62 | * [caveats & edgecases](./docs/caveats.md) learn about gotchas surrounding testing of specific ssb implementations 63 | 64 | There's also a [tutorial](./docs/tutorial.md) that demonstrates fixture generation. Finally, you can 65 | always peek at the [original design doc](./docs/initial-design-doc.md) if you are curious to see how 66 | things began. 67 | 68 | ## Example 69 | Say we want to test an sbot implementation in `~/code/ssb-server`, just to make sure the basics 70 | are still working. 71 | 72 | First, we write a netsim test called `basic-test.txt`: 73 | ```m68k 74 | # booting 75 | enter peer 76 | hops peer 1 77 | enter server 78 | hops server 1 79 | 80 | start peer ssb-server 81 | start server ssb-server 82 | post server 83 | post server 84 | follow peer server 85 | follow server peer 86 | connect peer server 87 | waituntil peer server@latest 88 | stop peer 89 | ``` 90 | 91 | Now, let's run it: 92 | ```sh 93 | netsim run --spec basic-test.txt ~/code/ssb-server 94 | ``` 95 | 96 | **Note:** the folder containing the sbot implementation, `ssb-server`, and the `start` 97 | command's last operand are the same. If you want to run more sbots in the same test, just add 98 | them onto the `netsim run` invocation, while making sure to match the folder name with 99 | `start`'s operand. 100 | 101 | ### Building 102 | If you want to build the code yourself: 103 | 104 | ```sh 105 | git clone git@github.com:ssb-ngi-pointer/netsim 106 | cd netsim/cmd/netsim 107 | go build 108 | ./netsim 109 | ``` 110 | 111 | For the unbundled netsim utilities, see the 112 | [`cmd/`](https://github.com/ssb-ngi-pointer/netsim/tree/main/cmd) folder. 113 | 114 | ## Simulation Shims 115 | Before you run netsim against an sbot implementation, make sure you have a `sim-shim.sh` script 116 | in the root of the implementation. Simulation shims encapsulate implementation-specific details & 117 | procedures such as: ingesting a [`log.offset`](https://github.com/flumedb/flumelog-offset) 118 | file, passing `hops` and `caps` settings to the underlying sbot, and other details. 119 | 120 | The `sim-shim.sh` script is passed, and should use, the following arguments and environment variables: 121 | 122 | ```sh 123 | DIR="$1" 124 | PORT="$2" 125 | # the following env variables are always set from netsim: 126 | # ${CAPS} the capability key / app key / shs key 127 | # ${HOPS} a integer determining the hops setting for this ssb node 128 | # if ssb-fixtures are provided, the following variables are also set: 129 | # ${LOG_OFFSET} the location of the log.offset file to be used 130 | # ${SECRET} the location of the secret file which should be copied to the new ssb-dir 131 | ``` 132 | 133 | For go and nodejs examples of sim-shims, see [`sim-shims/`](./sim-shims). 134 | 135 | **Note:** the file must be named `sim-shim.sh` for the netsim to work. 136 | 137 | ## Required muxrpc calls 138 | In order to test different implementations against each other, netsim makes heavy use of 139 | Secure Scuttlebutt's [`muxrpc`](https://github.com/ssb-js/muxrpc) calls. For a brief primer, [see 140 | the protocol guide](https://ssbc.github.io/scuttlebutt-protocol-guide/#rpc-protocol). 141 | 142 | Currently, the following calls are required to be implemented before an ssb implementation is 143 | testable in netsim: 144 | 145 | **Essential** 146 | * `createLogStream` 147 | * `createHistoryStream` 148 | * `whoami` 149 | * `publish` 150 | * `conn.connect` 151 | * `conn.disconnect` 152 | 153 | **Extras** 154 | * `friends.isFollowing` used by `isfollowing` / `isnotfollowing` 155 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 the netsim authors 2 | // 3 | // SPDX-License-Identifier: LGPL-3.0-or-later 4 | 5 | package client 6 | 7 | import ( 8 | "encoding/base64" 9 | "fmt" 10 | "net" 11 | 12 | "github.com/ssb-ngi-pointer/netsim/internal/keys" 13 | "go.cryptoscope.co/muxrpc/v2" 14 | "go.cryptoscope.co/netwrap" 15 | "go.cryptoscope.co/secretstream" 16 | ) 17 | 18 | func NewTCP(port int, capsKey, secretPath string) (muxrpc.Endpoint, error) { 19 | kp, err := keys.LoadKeyPair(secretPath) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | // default app key for the secret-handshake connection 25 | appkey, err := base64.StdEncoding.DecodeString(capsKey) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | // create a shs client to authenticate and encrypt the connection 31 | clientSHS, err := secretstream.NewClient(kp.Pair, appkey) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | tcpAddr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("localhost:%d", port)) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | // returns a new connection that went through shs and does boxstream 42 | authedConn, err := netwrap.Dial(tcpAddr, clientSHS.ConnWrapper(kp.Feed.PubKey())) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | var muxMock = new(muxrpc.FakeHandler) 48 | muxClient := muxrpc.Handle(muxrpc.NewPacker(authedConn), muxMock) 49 | 50 | // TODO: how to waitgroups 51 | go func() { 52 | srv := muxClient.(muxrpc.Server) 53 | err = srv.Serve() 54 | if err != nil { 55 | fmt.Printf("mux client(%d) error: %v\n", port, err) 56 | } 57 | }() 58 | 59 | return muxClient, nil 60 | } 61 | -------------------------------------------------------------------------------- /cmd/expectations/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 the netsim authors 2 | // 3 | // SPDX-License-Identifier: LGPL-3.0-or-later 4 | 5 | package main 6 | 7 | import ( 8 | "encoding/json" 9 | "flag" 10 | "fmt" 11 | "github.com/ssb-ngi-pointer/netsim/expectations" 12 | "log" 13 | "os" 14 | ) 15 | 16 | func check(err error) { 17 | if err != nil { 18 | log.Fatalln(err) 19 | } 20 | } 21 | 22 | func main() { 23 | var args expectations.Args 24 | flag.IntVar(&args.MaxHops, "hops", 2, "the default global hops setting") 25 | flag.BoolVar(&args.ReplicateBlocked, "replicate-blocked", false, "if flag is present, blocked peers will be replicated") 26 | flag.StringVar(&args.Outpath, "out", "./expectations.json", "the filename and path where the expectations will be dumped") 27 | flag.Parse() 28 | 29 | if len(flag.Args()) == 0 { 30 | fmt.Println("Usage:\n expectations ") 31 | os.Exit(1) 32 | } 33 | 34 | graphpath := expectations.PathAndFile(flag.Args()[0], "follow-graph.json") 35 | outputMap, err := expectations.ProduceExpectations(args, graphpath) 36 | check(err) 37 | 38 | // persist to disk 39 | b, err := json.MarshalIndent(outputMap, "", " ") 40 | check(err) 41 | err = os.WriteFile(expectations.PathAndFile(args.Outpath, "expectations.json"), b, 0666) 42 | check(err) 43 | } 44 | -------------------------------------------------------------------------------- /cmd/generate-test/generate-test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 the netsim authors 2 | // 3 | // SPDX-License-Identifier: LGPL-3.0-or-later 4 | 5 | // Generates a full netsim test, given a log-splicer processed ssb-fixtures folder and a replication expectations file 6 | // expectations.json. 7 | package main 8 | 9 | import ( 10 | "flag" 11 | "fmt" 12 | "github.com/ssb-ngi-pointer/netsim/expectations" 13 | "github.com/ssb-ngi-pointer/netsim/generation" 14 | "os" 15 | "path" 16 | ) 17 | 18 | func main() { 19 | var args generation.Args 20 | var expectationsArgs expectations.Args 21 | flag.StringVar(&args.FixturesRoot, "fixtures", "./fixtures-output", "root folder containing spliced out ssb-fixtures") 22 | flag.StringVar(&args.SSBServer, "sbot", "ssb-server", "the ssb server to start puppets with") 23 | flag.IntVar(&args.MaxHops, "hops", 2, "the max hops count to use") 24 | flag.BoolVar(&expectationsArgs.ReplicateBlocked, "replicate-blocked", false, "if flag is present, blocked peers will be replicated") 25 | flag.IntVar(&args.FocusedCount, "focused", 2, "number of puppets to use for focus group (i.e. # of puppets that verify they are replicating others)") 26 | flag.Parse() 27 | 28 | if len(os.Args) == 1 { 29 | fmt.Println("Usage: generate-test ") 30 | flag.PrintDefaults() 31 | os.Exit(1) 32 | } 33 | 34 | expectationsArgs.MaxHops = args.MaxHops 35 | expectations, err := expectations.ProduceExpectations(expectationsArgs, path.Join(args.FixturesRoot, "follow-graph.json")) 36 | 37 | if err != nil { 38 | fmt.Fprintf(os.Stderr, "%s: %s\n", "expectations", err) 39 | os.Exit(1) 40 | } 41 | 42 | generation.GenerateTest(args, expectations, os.Stderr) 43 | } 44 | -------------------------------------------------------------------------------- /cmd/log-splicer/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 the netsim authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | // Tasks the log splicer takes care of during its runtime: 6 | // * Creates folder super structure, encapsulating each identity in its own folder 7 | // * Maps identities to folder names they live in 8 | // * Copies secrets from the fixtures folder to each identity folder 9 | // * Splics out messages from the one ssb-fixtures log.offset to the many log.offset in said folder structure 10 | // * Persists an identity mapping of ID to {folderName, latest sequence number} to secret-ids.json 11 | 12 | // The log splicer command takes an ssb-fixtures generated folder as input, and a destination folder as output. The 13 | // destination folder will be populated with identity folders, one folder per identity found in the generated fixtures. 14 | // 15 | // Each identity folder contains a log.offset, with the posts created by that identity, and the identity's secret file. The 16 | // identity folders are named after the filenames of the secrets found in the ssb-fixtures folder, which preserves the 17 | // pareto distribution of authors (secrets in the lower number ranges have issued more posts). 18 | // 19 | // Finally, a mapping from ssb identities @[...].ed25199 to the identity folders is dumped as json to the root of the 20 | // destination folder. The mapping, in addition to naming the secret folder, also contains an integer count tracking the latest 21 | // sequence number posted by that identity. 22 | package main 23 | 24 | import ( 25 | "flag" 26 | "fmt" 27 | "github.com/ssb-ngi-pointer/netsim/splicer" 28 | "os" 29 | ) 30 | 31 | /* 32 | * Given a ssb-fixtures directory, and its monolithic flume log legacy.offset (mfl) 33 | * 1. read all the secrets to figure out which authors exist 34 | * 2. for each discovered author create a key in a map[string]margaret.Log 35 | * 3. go through each message in the mfl and filter out the messages into the corresponding log of the map 36 | * 4. finally, create folders for each author, using the author's pubkey as directory name, and dump an lfo 37 | * version of their log.offset representation. inside each folder, dump the correct secret as well 38 | */ 39 | 40 | func main() { 41 | args := splicer.Args{} 42 | flag.BoolVar(&args.Verbose, "v", false, "verbose: talks a bit more than than the tool otherwise is inclined to do") 43 | flag.BoolVar(&args.DryRun, "dry", false, "only output what it would do") 44 | flag.BoolVar(&args.Prune, "prune", false, "removes existing output logs before writing to them (if -prune omitted, the splicer will instead exit with an error)") 45 | 46 | flag.Parse() 47 | logpaths := flag.Args() 48 | var err error 49 | if len(logpaths) != 2 { 50 | cmdName := os.Args[0] 51 | fmt.Printf("Usage: %s \nOptions:\n", cmdName) 52 | flag.PrintDefaults() 53 | os.Exit(1) 54 | } 55 | if err != nil { 56 | fmt.Fprintf(os.Stderr, "%s: %s\n", getToolName(), err) 57 | os.Exit(1) 58 | } 59 | args.Indir, args.Outdir = logpaths[0], logpaths[1] 60 | 61 | err = splicer.SpliceLogs(args) 62 | if err != nil { 63 | fmt.Fprintf(os.Stderr, "%s: %s\n", getToolName(), err) 64 | os.Exit(1) 65 | } 66 | } 67 | 68 | func getToolName() string { 69 | return os.Args[0] 70 | } 71 | -------------------------------------------------------------------------------- /cmd/netsim/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 the netsim authors 2 | // 3 | // SPDX-License-Identifier: LGPL-3.0-or-later 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "github.com/ssb-ngi-pointer/netsim/expectations" 11 | "github.com/ssb-ngi-pointer/netsim/generation" 12 | "github.com/ssb-ngi-pointer/netsim/sim" 13 | "github.com/ssb-ngi-pointer/netsim/splicer" 14 | "os" 15 | "path" 16 | "strings" 17 | ) 18 | 19 | func usageExit() { 20 | fmt.Println("Usage: netsim [generate, run] ") 21 | os.Exit(1) 22 | } 23 | 24 | var version = "devbuild" 25 | 26 | func main() { 27 | if len(os.Args) < 2 { 28 | usageExit() 29 | } 30 | // get the command name and shift the arguments around for flag parsing 31 | cmd := prepareCommand() 32 | 33 | // define flags common across all commands 34 | var fixturesDir string 35 | var testfile string 36 | var hops int 37 | var versionFlag bool 38 | flag.StringVar(&testfile, "spec", "netsim-test.txt", "path to netsim test file") 39 | flag.IntVar(&hops, "hops", 2, "hops controls the distance from a peer that information should still be retrieved") 40 | flag.BoolVar(&versionFlag, "version", false, "output the version of netsim") 41 | 42 | // handle each command, optionally defining command-specific flags, and finally invoking the command 43 | switch cmd { 44 | case "generate": 45 | var replicateBlocked bool 46 | var outpath string 47 | var ssbServer string 48 | var focusedPuppets int 49 | var onlySplice bool 50 | var generationSeed int64 51 | flag.BoolVar(&onlySplice, "no-test-script", false, "only converts the input fixtures to netsim-style fixtures") 52 | flag.BoolVar(&replicateBlocked, "replicate-blocked", false, "if flag is present, blocked peers will be replicated") 53 | flag.StringVar(&outpath, "out", "./", "the output path of the generated netsim test & its auxiliary files") 54 | flag.StringVar(&ssbServer, "sbot", "ssb-server", "the ssb server to start puppets with") 55 | flag.IntVar(&focusedPuppets, "focused", 2, "number of puppets that verify they are fully replicating their hops") 56 | flag.Int64Var(&generationSeed, "seed", 0, "seed used by test generation") 57 | flag.Parse() 58 | 59 | checkVersionFlag(versionFlag) 60 | 61 | if len(flag.Args()) == 0 { 62 | printHelp("generate", 63 | "path-to-ssb-fixtures-output", 64 | "Generate a netsim test from a ssb-fixtures folder") 65 | } 66 | fixturesDir = flag.Args()[0] 67 | 68 | // splice out the logs into a separate folder 69 | fixturesOutput := path.Join(outpath, "fixtures-output") 70 | spliceLogs(fixturesDir, fixturesOutput) 71 | if onlySplice { 72 | os.Exit(0) 73 | } 74 | // use the spliced logs to generate expectations 75 | expectations := generateExpectations(fixturesOutput, hops, replicateBlocked) 76 | // use the generated expectations & generate the test 77 | generatedTest := generateTest(fixturesOutput, ssbServer, focusedPuppets, hops, generationSeed, expectations) 78 | // echo 79 | fmt.Println(generatedTest) 80 | // save test file to disk 81 | err := os.WriteFile(path.Join(outpath, testfile), []byte(generatedTest), 0666) 82 | if err != nil { 83 | errOut("netsim generate", fmt.Errorf("failed to write test to disk (%w)", err)) 84 | } 85 | case "run": 86 | var simArgs sim.Args 87 | flag.StringVar(&simArgs.Caps, "caps", sim.DefaultShsCaps, "the secret handshake capability key") 88 | flag.StringVar(&fixturesDir, "fixtures", "", "optional: path to the output of a ssb-fixtures run, if using") 89 | flag.StringVar(&simArgs.Outdir, "out", "./puppets", "the output directory containing instantiated netsim peers") 90 | flag.IntVar(&simArgs.BasePort, "port", 18888, "start of port range used for each running sbot") 91 | flag.BoolVar(&simArgs.Verbose, "v", false, "increase logging verbosity") 92 | flag.Parse() 93 | 94 | checkVersionFlag(versionFlag) 95 | 96 | simArgs.Hops = hops 97 | simArgs.Testfile = testfile 98 | simArgs.FixturesDir = fixturesDir 99 | 100 | if len(flag.Args()) == 0 { 101 | printHelp("run", 102 | "path-to-sbot1 path-to-sbot2.. path-to-sbotn", 103 | "Run a simulation with the passed-in sbots and a netsim test") 104 | } 105 | sim.Run(simArgs, flag.Args()) 106 | default: 107 | usageExit() 108 | } 109 | } 110 | 111 | func spliceLogs(fixturesPath, dst string) { 112 | var args splicer.Args 113 | args.Prune = true 114 | args.Indir, args.Outdir = fixturesPath, dst 115 | err := splicer.SpliceLogs(args) 116 | errOut("splicer", err) 117 | } 118 | 119 | func generateExpectations(fixturesRoot string, hops int, replicateBlocked bool) map[string][]string { 120 | var args expectations.Args 121 | args.MaxHops = hops 122 | args.ReplicateBlocked = replicateBlocked 123 | outputMap, err := expectations.ProduceExpectations(args, path.Join(fixturesRoot, "follow-graph.json")) 124 | errOut("expectations", err) 125 | return outputMap 126 | } 127 | 128 | func printHelp(cmd, usage, description string) { 129 | fmt.Printf("netsim %s: %s\n", cmd, usage) 130 | fmt.Println(description, "\n") 131 | fmt.Println("Options:") 132 | flag.PrintDefaults() 133 | os.Exit(1) 134 | } 135 | 136 | func generateTest(fixturesRoot, sbot string, focused, hops int, seed int64, expectations map[string][]string) string { 137 | var generationArgs generation.Args 138 | generationArgs.FixturesRoot = fixturesRoot 139 | generationArgs.SSBServer = sbot 140 | generationArgs.MaxHops = hops 141 | generationArgs.Seed = seed 142 | generationArgs.FocusedCount = focused 143 | 144 | s := new(strings.Builder) 145 | generation.GenerateTest(generationArgs, expectations, s) 146 | return s.String() 147 | } 148 | 149 | func errOut(tool string, err error) { 150 | if err != nil { 151 | fmt.Fprintf(os.Stderr, "%s: %s\n", tool, err) 152 | os.Exit(1) 153 | } 154 | } 155 | 156 | func prepareCommand() string { 157 | cmd := os.Args[1] 158 | // splice out the command argument from os.Args 159 | var args []string 160 | args = append(args, os.Args[0]) 161 | args = append(args, os.Args[2:]...) 162 | os.Args = args 163 | return cmd 164 | } 165 | 166 | func checkVersionFlag(showVersion bool) { 167 | if showVersion { 168 | fmt.Println(version) 169 | os.Exit(0) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /cmd/print-follow-graph/print-follow-graph.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 the netsim authors 2 | // 3 | // SPDX-License-Identifier: LGPL-3.0-or-later 4 | 5 | // Prints out a follow graph, as seen from the point of view of the focused peers. 6 | // This command is literally a pared down version of generate-test.go 7 | package main 8 | 9 | import ( 10 | "flag" 11 | "fmt" 12 | "github.com/ssb-ngi-pointer/netsim/generation" 13 | "log" 14 | "math/rand" 15 | "os" 16 | "path" 17 | "sort" 18 | ) 19 | 20 | func check(err error) { 21 | if err != nil { 22 | log.Fatalln(err) 23 | } 24 | } 25 | 26 | func main() { 27 | var args generation.Args 28 | flag.IntVar(&args.MaxHops, "hops", 2, "the max hops count to use") 29 | flag.IntVar(&args.FocusedCount, "focused", 2, "number of puppets to use for focus group (i.e. # of puppets that verify they are replicating others)") 30 | flag.Parse() 31 | if len(flag.Args()) == 0 { 32 | fmt.Printf("print-follow-graph path-to-spliced-fixtures\n") 33 | fmt.Println("prints the follow graph described by a spliced out fixtures folder (netsim generate output), and its replication expectations") 34 | fmt.Println("Options:") 35 | flag.PrintDefaults() 36 | os.Exit(1) 37 | } 38 | args.FixturesRoot = flag.Args()[0] 39 | 40 | var err error 41 | g := generation.Generator{Args: args, Output: os.Stdout} 42 | // map of id -> [list of followed ids] 43 | var followMap map[string][]string 44 | followMap, _, err = generation.GetFollowMap(path.Join(args.FixturesRoot, "follow-graph.json")) 45 | check(err) 46 | 47 | g.IDsToNames, err = generation.GetIdentities(args.FixturesRoot) 48 | check(err) 49 | 50 | puppetNames := make([]string, 0, len(g.IDsToNames)) 51 | g.NamesToIDs = make(map[string]string) 52 | // map puppet names to their ids with g.NamesToIDs 53 | for id, secretFolder := range g.IDsToNames { 54 | g.NamesToIDs[secretFolder] = id 55 | puppetNames = append(puppetNames, g.IDsToNames[id]) 56 | } 57 | sort.Strings(puppetNames) 58 | 59 | // g.FocusGroup is the cohort of peers we care about; the ones who will be issuing `has` stmts, the ones whose data we 60 | // will inspect 61 | g.FocusGroup = make([]string, args.FocusedCount) 62 | for i := 0; i < args.FocusedCount; i++ { 63 | g.FocusGroup[i] = fmt.Sprintf("puppet-%05d", i) 64 | } 65 | // deterministically shuffle the focus group 66 | // TODO: accept --seed flag to change shuffling 67 | rand.Shuffle(len(g.FocusGroup), func(i, j int) { 68 | g.FocusGroup[i], g.FocusGroup[j] = g.FocusGroup[j], g.FocusGroup[i] 69 | }) 70 | 71 | /* given our starting set of puppets, called focus, and hops = 3, we will want to generate 72 | the following connection graph: 73 | focus -> hops 1 -> hops 2 -> hops 3 74 | 75 | ======================== 76 | start start start start 77 | v hops 3 > connect > hops 2 v 78 | v hops 2 > connect > hops 1 v 79 | v hops 1 > connect > focus v 80 | done done done done done 81 | ======================== 82 | */ 83 | focusIds := g.GetIDs(g.FocusGroup) 84 | var hopsPairs []generation.Pair 85 | for _, id := range focusIds { 86 | fmt.Println(0, g.IDsToNames[id]) 87 | graph := generation.Graph{FollowMap: followMap, Gen: g, Seen: make(map[string]bool)} 88 | hopsPairs = append(hopsPairs, graph.RecurseFollows(id, args.MaxHops, true)...) 89 | fmt.Println("") 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /cmd/sim/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 the netsim authors 2 | // 3 | // SPDX-License-Identifier: LGPL-3.0-or-later 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "github.com/ssb-ngi-pointer/netsim/sim" 11 | ) 12 | 13 | func main() { 14 | var args sim.Args 15 | flag.StringVar(&args.Caps, "caps", sim.DefaultShsCaps, "the secret handshake capability key") 16 | flag.IntVar(&args.Hops, "hops", 2, "the hops setting controls the distance from a peer that information should still be retrieved") 17 | flag.StringVar(&args.FixturesDir, "fixtures", "", "optional: path to the output of a ssb-fixtures run, if using") 18 | flag.StringVar(&args.Testfile, "spec", "./test.txt", "test file containing network simulator test instructions") 19 | flag.StringVar(&args.Outdir, "out", "./puppets", "the output directory containing instantiated netsim peers") 20 | flag.IntVar(&args.BasePort, "port", 18888, "start of port range used for each running sbot") 21 | flag.BoolVar(&args.Verbose, "v", false, "increase logging verbosity") 22 | flag.Parse() 23 | 24 | if len(flag.Args()) == 0 { 25 | PrintUsage() 26 | os.Exit(1) 27 | return 28 | } 29 | 30 | sim.Run(args, flag.Args()) 31 | } 32 | 33 | func PrintUsage() { 34 | fmt.Println("netsim: path-to-sbot1 path-to-sbot2.. path-to-sbotn") 35 | fmt.Println("Options:") 36 | flag.PrintDefaults() 37 | } 38 | -------------------------------------------------------------------------------- /docs/caveats.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Caveats 8 | Unexpected situations you may run into when testing across scuttlebutt implementations, and 9 | some suggested ways forward for getting around them. 10 | 11 | ## `go-ssb` 12 | ### Hops in Go equals hops in Nodejs + 1 13 | As of writing, [go-ssb](https://github.com/cryptoscope/ssb) currently has a different 14 | interpretation of SSB's hops concept, or how many layers from yourself you want to 15 | download messages. 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
InterpretationHops (Go)Hops (nodejs)
Only yourself 0
Include direct follows01
Include transitive follows, one step away12
Include transitive follows, two steps away23
43 | 44 | **Mitigation:** 45 | 46 | Set go-ssb's hops parameter to be one less than is defined on the `${HOPS}` env var ingested by 47 | `sim-shim.sh`: 48 | 49 | ```diff 50 | go-sbot \ 51 | -lis :"$PORT" \ 52 | -wslis "" \ 53 | -debuglis ":$(($PORT+1))" \ 54 | -repo "$DIR" \ 55 | -shscap "${CAPS}" \ 56 | + -hops "$(( ${HOPS} - 1 ))" 57 | ``` 58 | 59 | **Note**: This mitigation is already implemented in the [default go-ssb sim-shim](https://github.com/ssb-ngi-pointer/netsim/blob/main/sim-shims/go-sim-shim.sh#L41-L47). 60 | 61 | ### Following in go-ssb takes three seconds to take effect (wrt connections) 62 | Given a netsim puppet `peer` running [go-ssb](https://github.com/cryptoscope/ssb) and the following netsim snippet: 63 | 64 | ``` 65 | follow puppet alice 66 | connect puppet alice 67 | ``` 68 | 69 | The `follow` statement will succeed as expected, but the `connect` statement will likely fail. 70 | The reason is that for technical tradeoff reasons it takes, at writing, 3 seconds for a follow 71 | action to propagate through to go-ssb's hops connection allowlist, [source 72 | code](https://github.com/cryptoscope/ssb/blob/80b8875e81408101f83c24eb83ec620037e68f77/sbot/replicate.go#L73). 73 | 74 | **Mitigation:** 75 | 76 | ``` 77 | follow puppet alice 78 | wait 4000 79 | connect puppet alice 80 | ``` 81 | 82 | ### `nope - access denied` when connecting to peer 83 | Given two puppets `alice` and `gopher`, where `alice` does not follow `gopher`, the latter is running [go-ssb](https://github.com/cryptoscope/ssb), and the following statement: 84 | 85 | ``` 86 | connect alice gopher 87 | # or, equivalent 88 | connect gopher alice 89 | ``` 90 | 91 | Depending on how you are starting your go-sbot, you might get a failure and an error log (when 92 | running in `-v`erbose mode): 93 | ``` 94 | node - access denied 95 | ``` 96 | 97 | The reason is that the go server is strict with who it allows to establish connections with it. 98 | If the peer running the go server does not follow the connecting party, then the connection 99 | will be denied. 100 | 101 | **Mitigations:** 102 | 103 | 1. Make sure `alice` follows `gopher`, `gopher` follows `alice` (and observe the 3 second caveat mentioned elsewhere in this document) 104 | 2. Run go-ssb in so-called `promiscuous` mode by appending a `-promisc` flag when starting it: 105 | 106 | ```diff 107 | exec go-sbot \ 108 | -lis :"$PORT" \ 109 | -wslis "" \ 110 | -debuglis ":$(($PORT+1))" \ 111 | -repo "$DIR" \ 112 | -shscap "${CAPS}" \ 113 | -hops "$(( ${HOPS} - 1 ))" 114 | + -promisc 115 | ``` 116 | 117 | -------------------------------------------------------------------------------- /docs/commands.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Network Simulator Test Commands 8 | As described in the [initial syntax proposal](/docs/domain-specific-language.md), the network 9 | simulator works by executing a test specification file. The current set of implemented commands 10 | can be read below. 11 | 12 | ### Command Parameter Legend 13 | The listing below provides brief explanations of various commands and their 14 | parameters provided by the network simulator and used when writing test files. 15 | 16 | * `` is non-spaced name used to refer to the same test puppet (i.e. an ssb identity), 17 | * `` is a sequence number as derived from the log of a particular peer e.g. 1, 2, 5, 1337 18 | * the keyword `@latest` is implemented as a shorthand for referring to the latest 19 | sequence number according to the identity whose log it is. 20 | * `` is defined as the last folder name of the path passed to the 21 | network simulator on startup. Since many implementations may be passed and used during a 22 | single simulation, they are provided as flagless arguments **after** any command-line flags. 23 | Example: `netsim -out ~/netsim-tests ~/code/ssb-server-19 ~/code/ssb-server-new ~/code/go-ssb-sbot` 24 | * the -out flag defines the output directory for logs and generated identities (`~/netsim-tests` in this case) 25 | * `~/code/ssb-server-19` would be referenced as `ssb-server-19` in the test specification 26 | as e.g. `start alice ssb-server-19` 27 | 28 | ## Implemented Commands 29 | ``` 30 | enter // should be called before any other command dealing with the named peer 31 | hops // should be called before starting a peer to have any effect 32 | caps // should be called before starting a peer to have any effect 33 | skipoffset // should be called before starting a peer to have any effect (omits copying over log.offset when loading identity from fixtures) 34 | alloffsets // should be called before starting a peer to have any effect (preloads the non-spliced input ssb-fixtures => puppet acts like a pub) 35 | load @.ed25519 // loads an id & its associated secret + log.offset from fixtures 36 | start // spin up name as ssb peer using the specifed sbot implementation 37 | reset // resets a peer's execution folder -> they will have forgotten any messages they synced from others 38 | stop // stop a currently running peer 39 | log 40 | wait // pause script execution 41 | waituntil @ // pause script execution until name1 has name2 at seqno in local db 42 | timerstart