├── .editorconfig ├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── _docs └── architecture_overview.md ├── cmd ├── gordian-echo │ ├── echoapp.go │ └── main.go ├── gordian-stress │ ├── integration_test.go │ ├── internal │ │ └── gstress │ │ │ ├── bootstrap_test.go │ │ │ ├── bootstrapclient.go │ │ │ ├── bootstraphost.go │ │ │ ├── bstate.go │ │ │ ├── fixture_test.go │ │ │ ├── seed.go │ │ │ └── seedrpc_test.go │ └── main.go └── internal │ └── gcmd │ ├── echoapp.go │ └── passphrase.go ├── demo-echo.bash ├── demo-stress.bash ├── gassert ├── cmd │ └── generate-nodebug │ │ ├── main.go │ │ └── main_test.go ├── doc.go ├── env_debug.go ├── env_debug_test.go ├── env_nodebug.go └── gasserttest │ ├── defaultenv_debug.go │ ├── defaultenv_nodebug.go │ └── doc.go ├── gcrypto ├── commonmessagesignatureproof.go ├── doc.go ├── ed25519.go ├── ed25519_test.go ├── errors.go ├── gblsminsig │ ├── bls.go │ ├── bls_test.go │ ├── doc.go │ ├── gblsminsigtest │ │ └── pubkey.go │ ├── internal │ │ ├── integration │ │ │ └── integration_test.go │ │ ├── memstorecompliance │ │ │ └── store_test.go │ │ └── sigtree │ │ │ ├── tree.go │ │ │ └── tree_test.go │ ├── signatureproof.go │ ├── signatureproof_test.go │ ├── signatureproofscheme.go │ ├── signatureproofscheme_internal_test.go │ └── signatureproofscheme_test.go ├── gcryptotest │ ├── commonmessagesignatureproof.go │ ├── ed25519.go │ ├── ed25519_keys.go │ ├── ed25519_test.go │ ├── generate.go │ ├── main.go │ └── signatureproofcompliance.go ├── pubkey.go ├── registry.go ├── registry_test.go ├── signatureproofmergeresult.go ├── signatureproofmergeresult_test.go ├── signer.go ├── simplecommonmessagesignatureproof.go └── simplecommonmessagesignatureproof_test.go ├── gdriver ├── doc.go └── gtxbuf │ ├── errors.go │ ├── txbuffer.go │ ├── txbuffer_test.go │ └── workingstate.go ├── gerasure ├── coding.go ├── doc.go ├── gerasuretest │ ├── compliance.go │ └── doc.go └── gereedsolomon │ ├── compliance_test.go │ ├── doc.go │ ├── encoder.go │ └── reconstructor.go ├── gexchange ├── doc.go ├── feedback.go └── feedback_string.go ├── gnetdag ├── doc.go ├── fixedtree.go └── fixedtree_test.go ├── go.mod ├── go.sum ├── gwatchdog ├── doc.go ├── error.go ├── monitor.go ├── watchdog.go └── watchdog_test.go ├── internal ├── gchan │ ├── send.go │ └── send_test.go ├── glog │ ├── hr.go │ └── valuer.go └── gtest │ ├── channels.go │ ├── channels_test.go │ ├── log.go │ └── time.go ├── tm ├── internal │ └── tmtimeout │ │ ├── errors.go │ │ └── manager.go ├── tmcodec │ ├── codec.go │ ├── doc.go │ ├── tmcodectest │ │ └── codeccompliance.go │ └── tmjson │ │ ├── codec.go │ │ ├── codec_test.go │ │ ├── doc.go │ │ └── json.go ├── tmconsensus │ ├── consensusstrategy.go │ ├── doc.go │ ├── errors.go │ ├── feedbackmapper.go │ ├── genesis.go │ ├── handleproposedheaderresult_string.go │ ├── handler.go │ ├── handlevoteproofsresult_string.go │ ├── hashscheme.go │ ├── header.go │ ├── math.go │ ├── math_test.go │ ├── precommit.go │ ├── prevote.go │ ├── roundview.go │ ├── signaturescheme.go │ ├── signer.go │ ├── sparsesignaturecollection.go │ ├── tmconsensustest │ │ ├── annotations.go │ │ ├── channelconsensushandler.go │ │ ├── consensusstrategy.go │ │ ├── doc.go │ │ ├── ed25519fixture.go │ │ ├── ed25519validators.go │ │ ├── ed25519validators_test.go │ │ ├── fixture.go │ │ ├── hashschemecompliance.go │ │ ├── nopconsensusstrategy.go │ │ ├── privval.go │ │ ├── simplehashscheme.go │ │ ├── simplehashscheme_test.go │ │ ├── simplesignaturescheme.go │ │ └── simplesignaturescheme_test.go │ ├── validator.go │ ├── validator_test.go │ ├── votesummary.go │ ├── votesummary_test.go │ └── votetarget.go ├── tmdebug │ ├── consensushandler.go │ └── doc.go ├── tmdriver │ ├── doc.go │ └── requests.go ├── tmengine │ ├── doc.go │ ├── engine.go │ ├── engine_test.go │ ├── internal │ │ ├── tmeil │ │ │ ├── doc.go │ │ │ └── statemachine.go │ │ ├── tmemetrics │ │ │ ├── doc.go │ │ │ └── metrics.go │ │ ├── tmmirror │ │ │ ├── backfillcommitstatus.go │ │ │ ├── backfillcommitstatus_string.go │ │ │ ├── internal │ │ │ │ └── tmi │ │ │ │ │ ├── doc.go │ │ │ │ │ ├── fixture_test.go │ │ │ │ │ ├── gossipviewmanager.go │ │ │ │ │ ├── kernel.go │ │ │ │ │ ├── kernel_debug.go │ │ │ │ │ ├── kernel_nodebug.go │ │ │ │ │ ├── kernel_test.go │ │ │ │ │ ├── kstate.go │ │ │ │ │ ├── lag.go │ │ │ │ │ ├── networkheightround.go │ │ │ │ │ ├── phcheck.go │ │ │ │ │ ├── phcheckstatus_string.go │ │ │ │ │ ├── snapshot.go │ │ │ │ │ ├── statemachineviewmanager.go │ │ │ │ │ ├── view.go │ │ │ │ │ ├── viewid_string.go │ │ │ │ │ ├── viewlookup.go │ │ │ │ │ ├── viewlookupstatus_string.go │ │ │ │ │ ├── votedistribution.go │ │ │ │ │ └── votes.go │ │ │ ├── mirror.go │ │ │ ├── mirror_test.go │ │ │ └── tmmirrortest │ │ │ │ ├── fixture.go │ │ │ │ └── fixture_test.go │ │ └── tmstate │ │ │ ├── internal │ │ │ └── tsi │ │ │ │ ├── commitprooffinalizer.go │ │ │ │ ├── consensusmanager.go │ │ │ │ ├── roundlifecycle.go │ │ │ │ ├── roundlifecycle_debug.go │ │ │ │ ├── roundlifecycle_nodebug.go │ │ │ │ ├── step.go │ │ │ │ ├── step_string.go │ │ │ │ └── step_test.go │ │ │ ├── roundtimer.go │ │ │ ├── roundtimer_test.go │ │ │ ├── statemachine.go │ │ │ ├── statemachine_test.go │ │ │ └── tmstatetest │ │ │ ├── fixture.go │ │ │ ├── proposedheaders.go │ │ │ ├── roundtimer.go │ │ │ └── roundtimer_test.go │ ├── metrics.go │ ├── mirror.go │ ├── opts.go │ ├── timeoutstrategy.go │ ├── tmelink │ │ ├── blockdataarrival.go │ │ ├── doc.go │ │ ├── lagstate.go │ │ ├── lagstatus_string.go │ │ ├── networkviewupdate.go │ │ ├── phfetcher.go │ │ ├── replayedheader.go │ │ └── tmelinktest │ │ │ ├── doc.go │ │ │ └── phfetcher.go │ └── tmenginetest │ │ ├── doc.go │ │ ├── fixture.go │ │ ├── timeoutmanager.go │ │ └── timeoutmanager_test.go ├── tmgossip │ ├── chattystrategy.go │ ├── doc.go │ ├── strategy.go │ └── tmgossiptest │ │ ├── doc.go │ │ ├── nopstrategy.go │ │ └── passthroughstrategy.go ├── tmintegration │ ├── consensusfixture.go │ ├── daisychain.go │ ├── daisychain_inmem_test.go │ ├── doc.go │ ├── factory.go │ ├── identityapp.go │ ├── inmem.go │ ├── integration.go │ ├── libp2p.go │ ├── libp2p_inmem_test.go │ └── valshuffleapp.go ├── tmp2p │ ├── doc.go │ ├── network.go │ ├── tmlibp2p │ │ ├── connection.go │ │ ├── doc.go │ │ ├── host.go │ │ └── tmlibp2ptest │ │ │ ├── doc.go │ │ │ ├── network.go │ │ │ └── network_test.go │ └── tmp2ptest │ │ ├── channelbroadcaster.go │ │ ├── daisychainnetwork.go │ │ ├── daisychainnetwork_test.go │ │ ├── doc.go │ │ └── networkcompliance.go └── tmstore │ ├── actionstore.go │ ├── committedheaderstore.go │ ├── doc.go │ ├── errors.go │ ├── finalizationstore.go │ ├── mirrorstore.go │ ├── roundstore.go │ ├── statemachinestore.go │ ├── tmmemstore │ ├── actionstore.go │ ├── actionstore_test.go │ ├── committedheaderstore.go │ ├── committedheaderstore_test.go │ ├── doc.go │ ├── finalizationstore.go │ ├── finalizationstore_test.go │ ├── memmultistore_test.go │ ├── mirrorstore.go │ ├── mirrorstore_test.go │ ├── roundstore.go │ ├── roundstore_test.go │ ├── statemachinestore.go │ ├── statemachinestore_test.go │ ├── validatorstore.go │ └── validatorstore_test.go │ ├── tmstoretest │ ├── actionstorecompliance.go │ ├── committedheaderstorecompliance.go │ ├── doc.go │ ├── finalizationstorecompliance.go │ ├── fixturefactory.go │ ├── mirrorstorecompliance.go │ ├── multistorecompliance.go │ ├── roundstorecompliance.go │ ├── statemachinestorecompliance.go │ └── validatorstorecompliance.go │ └── validatorstore.go └── tools.go /.editorconfig: -------------------------------------------------------------------------------- 1 | # See https://editorconfig.org/. 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{sh,bash}] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Go Build and Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | tags: 7 | - '**' 8 | branches: 9 | - '**' 10 | 11 | env: 12 | GORDIAN_TEST_TIME_FACTOR: 4 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version-file: go.mod # Use whatever version is in the header of go.mod. 24 | 25 | - name: Build 26 | run: go build ./... 27 | 28 | - name: Test 29 | run: go test -race ./... 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # _____ _______ ____ _____ _ 2 | # / ____| |__ __| / __ \ | __ \ | | 3 | # | (___ | | | | | | | |__) | | | 4 | # \___ \ | | | | | | | ___/ | | 5 | # ____) | | | | |__| | | | |_| 6 | # |_____/ |_| \____/ |_| (_) 7 | 8 | # Before updating this repository's .gitignore, 9 | # do your changes belong in the global gitignore? 10 | # https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files#configuring-ignored-files-for-all-repositories-on-your-computer 11 | 12 | demo/ 13 | **/bin 14 | **/.DS_Store 15 | 16 | # If a test binary gets compiled, never check it in. 17 | *.test 18 | -------------------------------------------------------------------------------- /cmd/gordian-stress/internal/gstress/bstate.go: -------------------------------------------------------------------------------- 1 | package gstress 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | "sync" 7 | 8 | "github.com/gordian-engine/gordian/tm/tmconsensus" 9 | ) 10 | 11 | type bState struct { 12 | startOnce sync.Once 13 | started chan struct{} 14 | 15 | mu sync.Mutex 16 | 17 | app string 18 | chainID string 19 | validators []tmconsensus.Validator 20 | } 21 | 22 | func (s *bState) ChainID() string { 23 | s.mu.Lock() 24 | defer s.mu.Unlock() 25 | 26 | return s.chainID 27 | } 28 | 29 | func (s *bState) SetChainID(newID string) { 30 | s.mu.Lock() 31 | defer s.mu.Unlock() 32 | 33 | s.chainID = newID 34 | } 35 | 36 | func (s *bState) App() string { 37 | s.mu.Lock() 38 | defer s.mu.Unlock() 39 | 40 | return s.app 41 | } 42 | 43 | func (s *bState) SetApp(a string) { 44 | s.mu.Lock() 45 | defer s.mu.Unlock() 46 | 47 | s.app = a 48 | } 49 | 50 | func (s *bState) AddValidator(v tmconsensus.Validator) { 51 | s.mu.Lock() 52 | defer s.mu.Unlock() 53 | 54 | if slices.ContainsFunc(s.validators, func(val tmconsensus.Validator) bool { 55 | return v.PubKey.Equal(val.PubKey) 56 | }) { 57 | panic(fmt.Errorf("validator with pub key %q already registered", v.PubKey.PubKeyBytes())) 58 | } 59 | 60 | s.validators = append(s.validators, v) 61 | } 62 | 63 | func (s *bState) Validators() []tmconsensus.Validator { 64 | s.mu.Lock() 65 | defer s.mu.Unlock() 66 | 67 | return slices.Clone(s.validators) 68 | } 69 | 70 | func (s *bState) Start() { 71 | s.startOnce.Do(func() { 72 | close(s.started) 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /cmd/internal/gcmd/passphrase.go: -------------------------------------------------------------------------------- 1 | package gcmd 2 | 3 | import ( 4 | "crypto/ed25519" 5 | 6 | "github.com/gordian-engine/gordian/gcrypto" 7 | libp2pcrypto "github.com/libp2p/go-libp2p/core/crypto" 8 | "golang.org/x/crypto/blake2b" 9 | ) 10 | 11 | func SignerFromInsecurePassphrase(prefix, insecurePassphrase string) (gcrypto.Ed25519Signer, error) { 12 | bh, err := blake2b.New(ed25519.SeedSize, nil) 13 | if err != nil { 14 | return gcrypto.Ed25519Signer{}, err 15 | } 16 | bh.Write([]byte(prefix + insecurePassphrase)) 17 | seed := bh.Sum(nil) 18 | 19 | privKey := ed25519.NewKeyFromSeed(seed) 20 | 21 | return gcrypto.NewEd25519Signer(privKey), nil 22 | } 23 | 24 | func Libp2pKeyFromInsecurePassphrase(prefix, insecurePassphrase string) (libp2pcrypto.PrivKey, error) { 25 | bh, err := blake2b.New(ed25519.SeedSize, nil) 26 | if err != nil { 27 | return nil, err 28 | } 29 | bh.Write([]byte("gordian-echo:network|")) 30 | bh.Write([]byte(prefix + insecurePassphrase)) 31 | seed := bh.Sum(nil) 32 | 33 | privKey := ed25519.NewKeyFromSeed(seed) 34 | 35 | priv, _, err := libp2pcrypto.KeyPairFromStdKey(&privKey) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return priv, nil 41 | } 42 | -------------------------------------------------------------------------------- /demo-stress.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | stress () { 5 | _start () { 6 | local network_size 7 | readonly network_size="${1:?network size required}" 8 | 9 | local running_vals 10 | readonly running_vals="${2:-$1}" 11 | 12 | >&2 echo 'Compiling...' 13 | go build -o ./bin/gstress ./cmd/gordian-stress 14 | 15 | local group_id 16 | readonly group_id="$(./bin/gstress petname).$(date +%s)" 17 | local socket_path 18 | readonly socket_path="/tmp/gstress.${group_id}.sock" 19 | 20 | local log_dir 21 | readonly log_dir="./bin/gstress-run-${group_id}" 22 | mkdir "$log_dir" 23 | 24 | ./bin/gstress seed "$socket_path" > "${log_dir}/seed.log" 2>&1 & 25 | ./bin/gstress wait-for-seed "$socket_path" 26 | 27 | >&2 echo 'Starting validators...' 28 | local i 29 | for (( i=0; i < "$running_vals"; i++)); do 30 | ./bin/gstress register-validator "$socket_path" "val-${i}" 1 31 | ./bin/gstress validator "$socket_path" "val-${i}" > "${log_dir}/val-${i}.log" 2>&1 & 32 | done 33 | 34 | ./bin/gstress start "$socket_path" 35 | 36 | >&2 printf 'Network started; logs available at: %s ' "${log_dir}/seed.log" 37 | for (( i=0; i < "$running_vals"; i++)); do 38 | >&2 printf "${log_dir}/val-${i}.log " 39 | done 40 | >&2 echo 41 | >&2 echo "Watch logs with: tail -f ${log_dir}/val-*.log" 42 | >&2 echo 43 | >&2 echo "Stop the network with: ./bin/gstress halt ${socket_path}" 44 | } 45 | 46 | commands () { 47 | >&2 cat <<-EOF 48 | stress commands: 49 | start NETWORK_SIZE [RUNNING_VALS] -> start the stress network 50 | startr NETWORK_SIZE [RUNNING_VALS] -> start the stress network, with the data race detector enabled 51 | EOF 52 | } 53 | 54 | local cmd="${1:-}" 55 | shift || true # If no args, shift would cause the script to fail due to set -e at the top. 56 | 57 | if [ "$cmd" == startr ]; then 58 | # Data race detection enabled -- just set go environment variables, 59 | # which will propagate to go build. 60 | cmd=start 61 | export GOFLAGS=-race GODEBUG=halt_on_error=1 62 | >&2 echo 'Data race detection enabled' 63 | fi 64 | 65 | case "$cmd" in 66 | start) 67 | if [ $# -ne 1 ] && [ $# -ne 2 ]; then 68 | >&2 echo "USAGE: $0 NETWORK_SIZE [RUNNING_VALS] 69 | 70 | The NETWORK_SIZE argument determines the number of validators in the network. 71 | 72 | RUNNING_VALS defaults to NETWORK_SIZE; 73 | otherwise, if provided, it determines the nuber of validators to run locally. 74 | RUNNING_VALS may be set less than or greater than NETWORK_SIZE." 75 | exit 1 76 | fi 77 | _start "$@" 78 | ;; 79 | *) 80 | commands 81 | ;; 82 | esac 83 | } 84 | 85 | stress "$@" 86 | -------------------------------------------------------------------------------- /gassert/doc.go: -------------------------------------------------------------------------------- 1 | // Package gassert (Gordian assert) provides functionality around assertions at runtime. 2 | // 3 | // It is assumed to be prohibitively expensive to validate every invariant 4 | // at every function entrypoint in production. 5 | // But, if unexpected behavior is observed, enabling invariant checks 6 | // may immediately reveal the problem. 7 | // 8 | // To enable invariant checks is a two-step process. 9 | // First, the assertion functionality is not compiled into Gordian code by default. 10 | // To enable assertions, you must build with the "debug" build tag, 11 | // i.e. use "go build -tags debug" or "go run -tags debug". 12 | // Second, you must enable some set of assertions, 13 | // by producing an [Env] via the [EnvironmentFromString] or [ParseEnvironment] functions 14 | // (which are only available in debug builds). 15 | // A fully developed chain using Gordian should support the debug build tag 16 | // and allow setting rules from the command line. 17 | // But if not, you probably have to build from source anyway, 18 | // so you can just manually set the rules when the [Env] is created. 19 | // 20 | // Rule behavior is as follows: 21 | // - Individual components call [*Environment.Enabled] to determine whether to make the assertion. 22 | // They provide a dot-separated string indicating the path of the assertion they may make. 23 | // - The assertion environment checks the path against the registered rules. 24 | // - No rules are enabled by default. 25 | // - A top level rule of "*" (wildcard) enables all assertions. 26 | // - The "*" wildcard may only occur as the last segment of a dot-separated rule, 27 | // so "foo.bar.*" is valid but "foo.*.bar" is not. 28 | // - A rule with a leading "!" excludes certain exact matches from a wildcard rule, 29 | // so "foo.bar.*,!foo.bar.baz" would match "foo.bar.quux" but not "foo.bar.baz". 30 | // - Exact match rules are also accepted, so "foo.bar.baz" would match that exact rule 31 | // but not "foo.bar.baz_quux" 32 | // - The rules expect plain ASCII words between dots, with no special symbols 33 | // other than dash or underscore. 34 | // This is not currently enforced but may be in the future. 35 | // - The [EnvironmentFromString] function expects a comma-separated list of rules. 36 | // - The [ParseEnvironmentFromString] function operates on an [io.Reader], 37 | // and it ignores blank lines and any comment lines whose first character is "#". 38 | package gassert 39 | -------------------------------------------------------------------------------- /gassert/env_nodebug.go: -------------------------------------------------------------------------------- 1 | //go:build !debug 2 | 3 | package gassert 4 | 5 | // Env is the environment, a pseudo-global state indicating 6 | // which assertions or debugers are enabled. 7 | // 8 | // Core engine types that support assertions 9 | // should always accept a gassert.Env. 10 | // In non-debug builds, Env is an empty struct so as to not consume any memory. 11 | // In debug builds, Env is a type alias to *Environment 12 | // (a type which is only compiled into debug builds). 13 | // 14 | // The non-debug Env deliberately does not have any defined methods. 15 | // User code depending on the assertion environment should also be guarded 16 | // behind the build tag "debug". 17 | type Env struct{} 18 | -------------------------------------------------------------------------------- /gassert/gasserttest/defaultenv_debug.go: -------------------------------------------------------------------------------- 1 | //go:build debug 2 | 3 | package gasserttest 4 | 5 | import "github.com/gordian-engine/gordian/gassert" 6 | 7 | // DefaultEnv returns an assertion environment that enables all assertion checks. 8 | func DefaultEnv() gassert.Env { 9 | env, err := gassert.EnvironmentFromString("*") 10 | if err != nil { 11 | panic(err) 12 | } 13 | env.UseCaching() 14 | return env 15 | } 16 | 17 | // NopEnv returns an assertion environment that disables all assertion checks. 18 | // This should generally not be used, but maybe it is helpful in already expensive tests. 19 | func NopEnv() gassert.Env { 20 | env, err := gassert.EnvironmentFromString("") 21 | if err != nil { 22 | panic(err) 23 | } 24 | env.UseCaching() 25 | return env 26 | } 27 | -------------------------------------------------------------------------------- /gassert/gasserttest/defaultenv_nodebug.go: -------------------------------------------------------------------------------- 1 | //go:build !debug 2 | 3 | package gasserttest 4 | 5 | import "github.com/gordian-engine/gordian/gassert" 6 | 7 | // DefaultEnv returns the no-op Env, in non-debug builds. 8 | func DefaultEnv() gassert.Env { 9 | return gassert.Env{} 10 | } 11 | 12 | // NopEnv returns the no-op Env. 13 | // This should generally not be used, but maybe it is helpful in already expensive tests, 14 | // when debug builds are enabled. 15 | func NopEnv() gassert.Env { 16 | return gassert.Env{} 17 | } 18 | -------------------------------------------------------------------------------- /gassert/gasserttest/doc.go: -------------------------------------------------------------------------------- 1 | // Package gasserttest contains helpers for tests involving gassert. 2 | package gasserttest 3 | -------------------------------------------------------------------------------- /gcrypto/doc.go: -------------------------------------------------------------------------------- 1 | // Package gcrypto contains cryptographic primitives for the Gordian engine. 2 | package gcrypto 3 | -------------------------------------------------------------------------------- /gcrypto/ed25519.go: -------------------------------------------------------------------------------- 1 | package gcrypto 2 | 3 | import ( 4 | "context" 5 | "crypto" 6 | "crypto/ed25519" 7 | ) 8 | 9 | const ed25519TypeName = "ed25519" 10 | 11 | // RegisterEd25519 registers ed25519 with the given Registry. 12 | // There is no global registry; it is the caller's responsibility 13 | // to register as needed. 14 | func RegisterEd25519(reg *Registry) { 15 | reg.Register(ed25519TypeName, Ed25519PubKey{}, NewEd25519PubKey) 16 | } 17 | 18 | type Ed25519PubKey ed25519.PublicKey 19 | 20 | func NewEd25519PubKey(b []byte) (PubKey, error) { 21 | return Ed25519PubKey(b), nil 22 | } 23 | 24 | func (e Ed25519PubKey) PubKeyBytes() []byte { 25 | return []byte(e) 26 | } 27 | 28 | func (e Ed25519PubKey) Verify(msg, sig []byte) bool { 29 | return ed25519.Verify(ed25519.PublicKey(e), msg, sig) 30 | } 31 | 32 | func (e Ed25519PubKey) Equal(other PubKey) bool { 33 | o, ok := other.(Ed25519PubKey) 34 | if !ok { 35 | return false 36 | } 37 | 38 | return ed25519.PublicKey(e).Equal(ed25519.PublicKey(o)) 39 | } 40 | 41 | func (e Ed25519PubKey) TypeName() string { 42 | return ed25519TypeName 43 | } 44 | 45 | type Ed25519Signer struct { 46 | priv ed25519.PrivateKey 47 | pub Ed25519PubKey 48 | } 49 | 50 | func NewEd25519Signer(priv ed25519.PrivateKey) Ed25519Signer { 51 | return Ed25519Signer{ 52 | priv: priv, 53 | pub: Ed25519PubKey(priv.Public().(ed25519.PublicKey)), 54 | } 55 | } 56 | 57 | func (s Ed25519Signer) PubKey() PubKey { 58 | return s.pub 59 | } 60 | 61 | func (s Ed25519Signer) Sign(_ context.Context, input []byte) ([]byte, error) { 62 | return s.priv.Sign(nil, input, crypto.Hash(0)) 63 | } 64 | -------------------------------------------------------------------------------- /gcrypto/ed25519_test.go: -------------------------------------------------------------------------------- 1 | package gcrypto_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/gordian-engine/gordian/gcrypto" 8 | "github.com/gordian-engine/gordian/gcrypto/gcryptotest" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestEd25519(t *testing.T) { 13 | t.Parallel() 14 | 15 | // TODO: this structure could probably be converted 16 | // into a pubkey compliance test. 17 | 18 | var reg gcrypto.Registry 19 | gcrypto.RegisterEd25519(®) 20 | 21 | signers := gcryptotest.DeterministicEd25519Signers(2) 22 | 23 | s1, s2 := signers[0], signers[1] 24 | 25 | dec1, err := reg.Decode(s1.PubKey().TypeName(), s1.PubKey().PubKeyBytes()) 26 | require.NoError(t, err) 27 | 28 | require.True(t, s1.PubKey().Equal(dec1)) 29 | require.False(t, s2.PubKey().Equal(dec1)) 30 | 31 | msg := []byte("hello") 32 | sig, err := s1.Sign(context.Background(), msg) 33 | require.NoError(t, err) 34 | 35 | require.True(t, s1.PubKey().Verify(msg, sig)) 36 | require.False(t, s2.PubKey().Verify(msg, sig)) 37 | } 38 | -------------------------------------------------------------------------------- /gcrypto/errors.go: -------------------------------------------------------------------------------- 1 | package gcrypto 2 | 3 | import "errors" 4 | 5 | var ErrInvalidSignature = errors.New("signature could not be verified") 6 | 7 | var ErrUnknownKey = errors.New("unknown key") 8 | -------------------------------------------------------------------------------- /gcrypto/gblsminsig/bls_test.go: -------------------------------------------------------------------------------- 1 | package gblsminsig_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/gordian-engine/gordian/gcrypto/gblsminsig" 8 | "github.com/stretchr/testify/require" 9 | blst "github.com/supranational/blst/bindings/go" 10 | ) 11 | 12 | func TestSignAndVerify_single(t *testing.T) { 13 | t.Parallel() 14 | 15 | ikm := make([]byte, 32) 16 | for i := range ikm { 17 | ikm[i] = byte(i) 18 | } 19 | 20 | s, err := gblsminsig.NewSigner(ikm) 21 | require.NoError(t, err) 22 | 23 | msg := []byte("hello world") 24 | 25 | sig, err := s.Sign(context.Background(), msg) 26 | require.NoError(t, err) 27 | 28 | require.True(t, s.PubKey().Verify(msg, sig)) 29 | 30 | // Modifying the message fails verification. 31 | msg[0]++ 32 | require.False(t, s.PubKey().Verify(msg, sig)) 33 | msg[0]-- 34 | 35 | // Modifying the signature fails verification too. 36 | sig[0]++ 37 | require.False(t, s.PubKey().Verify(msg, sig)) 38 | } 39 | 40 | func TestSignAndVerify_multiple(t *testing.T) { 41 | t.Parallel() 42 | 43 | ikm1 := make([]byte, 32) 44 | ikm2 := make([]byte, 32) 45 | for i := range ikm1 { 46 | ikm1[i] = byte(i) 47 | ikm2[i] = byte(i) + 32 48 | } 49 | 50 | s1, err := gblsminsig.NewSigner(ikm1) 51 | require.NoError(t, err) 52 | s2, err := gblsminsig.NewSigner(ikm2) 53 | require.NoError(t, err) 54 | 55 | msg := []byte("hello world") 56 | 57 | sig1, err := s1.Sign(context.Background(), msg) 58 | require.NoError(t, err) 59 | 60 | sig2, err := s2.Sign(context.Background(), msg) 61 | require.NoError(t, err) 62 | 63 | sigp11 := new(blst.P1Affine).Uncompress(sig1) 64 | require.NotNil(t, sigp11) 65 | sigp12 := new(blst.P1Affine).Uncompress(sig2) 66 | require.NotNil(t, sigp12) 67 | 68 | // Aggregate the signatures into a single affine point. 69 | sigAgg := new(blst.P1Aggregate) 70 | require.True(t, sigAgg.AggregateCompressed([][]byte{sig1, sig2}, true)) 71 | finalSig := sigAgg.ToAffine().Compress() 72 | 73 | // Aggregate the keys too. 74 | keyAgg := new(blst.P2Aggregate) 75 | require.True(t, keyAgg.AggregateCompressed([][]byte{ 76 | s1.PubKey().PubKeyBytes(), 77 | s2.PubKey().PubKeyBytes(), 78 | }, true)) 79 | 80 | finalKeyAffine := keyAgg.ToAffine() 81 | finalKey := gblsminsig.PubKey(*finalKeyAffine) 82 | 83 | require.True(t, finalKey.Verify(msg, finalSig)) 84 | 85 | // Changing the message fails verification. 86 | msg[0]++ 87 | require.False(t, finalKey.Verify(msg, finalSig)) 88 | msg[0]-- 89 | 90 | // Modifying the signature fails verification too. 91 | finalSig[0]++ 92 | require.False(t, finalKey.Verify(msg, finalSig)) 93 | } 94 | -------------------------------------------------------------------------------- /gcrypto/gblsminsig/doc.go: -------------------------------------------------------------------------------- 1 | // Package gblsminsig wraps [github.com/supranational/blst/bindings/go] 2 | // to provide a [gcrypto.PubKey] implementation backed by BLS keys, 3 | // where the BLS keys have minimized signatures. 4 | // 5 | // We are not currently providing an alternate implementation with minimized keys, 6 | // as signatures are expected to be transmitted and stored much more frequently than keys. 7 | // 8 | // The blst dependency requires CGo, 9 | // so therefore this package also requires CGo. 10 | // 11 | // Two key references for correctly understanding and using BLS keys are 12 | // [RFC9380] (Hashing to Elliptic Curves) 13 | // and the IETF draft for [BLS Signatures]. 14 | // 15 | // See the [SignatureProofScheme] docs for a detailed explanation 16 | // of how this package aggregates keys and signatures. 17 | // 18 | // [RFC9380]: https://www.rfc-editor.org/rfc/rfc9380.html 19 | // [BLS Signatures]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature-05 20 | package gblsminsig 21 | -------------------------------------------------------------------------------- /gcrypto/gblsminsig/gblsminsigtest/pubkey.go: -------------------------------------------------------------------------------- 1 | package gblsminsigtest 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/gordian-engine/gordian/gcrypto/gblsminsig" 9 | ) 10 | 11 | var muSigners sync.RWMutex 12 | var generatedSigners []gblsminsig.Signer 13 | 14 | func DeterministicSigners(n int) []gblsminsig.Signer { 15 | res := optimisticLoadSigners(n) 16 | 17 | if len(res) >= n { 18 | return res 19 | } 20 | 21 | // We weren't able to load all the signers from the read lock, so take the write lock. 22 | muSigners.Lock() 23 | defer muSigners.Unlock() 24 | 25 | // Check the length again, because it is possible that 26 | // another writer filled the generated slice before we acquired the lock. 27 | if len(generatedSigners) < n { 28 | sizedSigners := make([]gblsminsig.Signer, 0, n) 29 | generatedSigners = append(sizedSigners, generatedSigners...) 30 | generatedSigners = generatedSigners[:n] 31 | 32 | var wg sync.WaitGroup 33 | for i := len(res); i < n; i++ { 34 | wg.Add(1) 35 | go generateOneSigner(&wg, &generatedSigners[i], i) 36 | } 37 | 38 | wg.Wait() 39 | } 40 | 41 | for i := len(res); i < n; i++ { 42 | res = append(res, generatedSigners[i]) 43 | } 44 | 45 | return res 46 | } 47 | 48 | func optimisticLoadSigners(n int) []gblsminsig.Signer { 49 | res := make([]gblsminsig.Signer, 0, n) 50 | 51 | muSigners.RLock() 52 | defer muSigners.RUnlock() 53 | 54 | for i, s := range generatedSigners { 55 | if i >= n { 56 | break 57 | } 58 | 59 | res = append(res, s) 60 | } 61 | 62 | return res 63 | } 64 | 65 | func generateOneSigner(wg *sync.WaitGroup, dst *gblsminsig.Signer, i int) { 66 | defer wg.Done() 67 | 68 | ikm := [32]byte{} 69 | binary.BigEndian.PutUint64(ikm[24:32], uint64(i)) 70 | 71 | s, err := gblsminsig.NewSigner(ikm[:]) 72 | if err != nil { 73 | panic(fmt.Errorf("failed to make signer: %w", err)) 74 | } 75 | 76 | *dst = s 77 | } 78 | 79 | func DeterministicPubKeys(n int) []gblsminsig.PubKey { 80 | out := make([]gblsminsig.PubKey, n) 81 | for i, s := range DeterministicSigners(n) { 82 | out[i] = s.PubKey().(gblsminsig.PubKey) 83 | } 84 | return out 85 | } 86 | -------------------------------------------------------------------------------- /gcrypto/gblsminsig/internal/integration/integration_test.go: -------------------------------------------------------------------------------- 1 | package integration_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/gordian-engine/gordian/gcrypto" 8 | "github.com/gordian-engine/gordian/gcrypto/gblsminsig" 9 | "github.com/gordian-engine/gordian/gcrypto/gblsminsig/gblsminsigtest" 10 | "github.com/gordian-engine/gordian/tm/tmconsensus" 11 | "github.com/gordian-engine/gordian/tm/tmconsensus/tmconsensustest" 12 | "github.com/gordian-engine/gordian/tm/tmintegration" 13 | ) 14 | 15 | type blsFactory struct { 16 | tmintegration.Libp2pFactory 17 | tmintegration.InmemStoreFactory 18 | tmintegration.InmemSchemeFactory 19 | } 20 | 21 | func newBLSFactory(e *tmintegration.Env) blsFactory { 22 | return blsFactory{ 23 | Libp2pFactory: tmintegration.NewLibp2pFactory(e), 24 | 25 | // Zero-value structs are fine for the two in-mem factories. 26 | } 27 | } 28 | 29 | func (f blsFactory) CommonMessageSignatureProofScheme(_ context.Context, idx int) ( 30 | gcrypto.CommonMessageSignatureProofScheme, error, 31 | ) { 32 | return gblsminsig.SignatureProofScheme{}, nil 33 | } 34 | 35 | func (f blsFactory) NewConsensusFixture(nVals int) *tmconsensustest.Fixture { 36 | var reg gcrypto.Registry 37 | gblsminsig.Register(®) 38 | 39 | privVals := make(tmconsensustest.PrivVals, nVals) 40 | signers := gblsminsigtest.DeterministicSigners(nVals) 41 | 42 | for i := range privVals { 43 | privVals[i] = tmconsensustest.PrivVal{ 44 | Val: tmconsensus.Validator{ 45 | PubKey: signers[i].PubKey().(gblsminsig.PubKey), 46 | 47 | // Order by power descending, 48 | // with the power difference being negligible, 49 | // so that the validator order matches the default deterministic key order. 50 | // (Without this power adjustment, the validators would be ordered 51 | // by public key or by ID, which is unlikely to match their order 52 | // as defined in fixtures or other uses of determinsitic validators. 53 | Power: uint64(100_000 - i), 54 | }, 55 | Signer: signers[i], 56 | } 57 | } 58 | 59 | return &tmconsensustest.Fixture{ 60 | PrivVals: privVals, 61 | 62 | SignatureScheme: tmconsensustest.SimpleSignatureScheme{}, 63 | HashScheme: tmconsensustest.SimpleHashScheme{}, 64 | CommonMessageSignatureProofScheme: gblsminsig.SignatureProofScheme{}, 65 | 66 | Registry: reg, 67 | 68 | // The fixture also has prevCommitProof and prevAppStateHash fields, 69 | // which are unexported so we can't access them from this package. 70 | // Tests are passing currently, but the inability to set those fields 71 | // seems likely to cause an issue at some point. 72 | } 73 | } 74 | 75 | func TestGblsminsig(t *testing.T) { 76 | tmintegration.RunIntegrationTest(t, func(e *tmintegration.Env) tmintegration.Factory { 77 | return newBLSFactory(e) 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /gcrypto/gblsminsig/signatureproofscheme_internal_test.go: -------------------------------------------------------------------------------- 1 | package gblsminsig 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "testing" 7 | 8 | "github.com/bits-and-blooms/bitset" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestMainCombinationRoundTrip(t *testing.T) { 13 | tcs := []struct { 14 | n int 15 | setBits func(bs *bitset.BitSet) 16 | }{ 17 | { 18 | n: 100, 19 | setBits: func(bs *bitset.BitSet) { 20 | for i := range 80 { 21 | bs.Set(uint(i)) 22 | } 23 | }, 24 | }, 25 | { 26 | n: 100, 27 | setBits: func(bs *bitset.BitSet) { 28 | for i := range 100 { 29 | if i%5 == 0 { 30 | continue 31 | } 32 | bs.Set(uint(i)) 33 | } 34 | }, 35 | }, 36 | { 37 | n: 500, 38 | setBits: func(bs *bitset.BitSet) { 39 | bs.FlipRange(0, 500) 40 | bs.Clear(100) 41 | bs.Clear(200) 42 | bs.Clear(300) 43 | bs.Clear(400) 44 | }, 45 | }, 46 | } 47 | 48 | for _, tc := range tcs { 49 | n := tc.n 50 | 51 | var bs bitset.BitSet 52 | tc.setBits(&bs) 53 | k := bs.Count() 54 | 55 | name := fmt.Sprintf("n=%d, k=%d, bs=%x", n, k, bs.Words()) 56 | t.Run(name, func(t *testing.T) { 57 | t.Parallel() 58 | 59 | var combIndex big.Int 60 | calculateCombinationIndex(n, &bs, &combIndex) 61 | t.Logf("combination index: %s", combIndex.String()) 62 | 63 | var got bitset.BitSet 64 | decodeCombinationIndex(n, int(k), &combIndex, &got) 65 | 66 | // Equal has some ostensibly odd semantics, so dump the string if equality fails. 67 | require.Truef(t, got.Equal(&bs), "got bitset %s, differed from original bitset %s", got.String(), bs.String()) 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /gcrypto/gcryptotest/commonmessagesignatureproof.go: -------------------------------------------------------------------------------- 1 | package gcryptotest 2 | 3 | import ( 4 | "bytes" 5 | "slices" 6 | 7 | "github.com/gordian-engine/gordian/gcrypto" 8 | ) 9 | 10 | // CloneFinalized returns a deep copy of in, 11 | // which is very useful for tests that need to assert that modifications 12 | // to the proof cause validation to fail. 13 | // 14 | // There has not been a use case for this in production code, 15 | // hence why this function lives in gcryptotest. 16 | func CloneFinalizedCommonMessageSignatureProof( 17 | in gcrypto.FinalizedCommonMessageSignatureProof, 18 | ) gcrypto.FinalizedCommonMessageSignatureProof { 19 | out := gcrypto.FinalizedCommonMessageSignatureProof{ 20 | Keys: slices.Clone(in.Keys), 21 | PubKeyHash: in.PubKeyHash, 22 | 23 | MainMessage: bytes.Clone(in.MainMessage), 24 | } 25 | 26 | out.MainMessage = slices.Clone(out.MainMessage) 27 | out.MainSignatures = make([]gcrypto.SparseSignature, len(in.MainSignatures)) 28 | 29 | for i, ss := range in.MainSignatures { 30 | out.MainSignatures[i] = gcrypto.SparseSignature{ 31 | KeyID: bytes.Clone(ss.KeyID), 32 | Sig: bytes.Clone(ss.Sig), 33 | } 34 | } 35 | 36 | out.Rest = make(map[string][]gcrypto.SparseSignature) 37 | 38 | for k, sss := range in.Rest { 39 | outSigs := make([]gcrypto.SparseSignature, len(sss)) 40 | for i, ss := range sss { 41 | outSigs[i] = gcrypto.SparseSignature{ 42 | KeyID: bytes.Clone(ss.KeyID), 43 | Sig: bytes.Clone(ss.Sig), 44 | } 45 | } 46 | 47 | out.Rest[k] = outSigs 48 | } 49 | 50 | return out 51 | } 52 | -------------------------------------------------------------------------------- /gcrypto/gcryptotest/ed25519.go: -------------------------------------------------------------------------------- 1 | package gcryptotest 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ed25519" 6 | "fmt" 7 | "sync" 8 | 9 | "github.com/gordian-engine/gordian/gcrypto" 10 | ) 11 | 12 | // DeterministicEd25519Signers returns a deterministic slice of ed25519 signer values. 13 | // 14 | // There are two advantages to using deterministic keys. 15 | // First, subsequent runs of the same test will use the same keys, 16 | // so logs involving keys or IDs will not change across runs, 17 | // simplifying the debugging process. 18 | // Second, the generated keys are cached, 19 | // so there is effectively zero CPU time cost for additional tests 20 | // calling this function, beyond the first call. 21 | func DeterministicEd25519Signers(n int) []gcrypto.Ed25519Signer { 22 | // See if we can get all the signers under a read lock. 23 | res, got := optimisticLoadEd25519Signers(n) 24 | if got >= len(res) { 25 | return res 26 | } 27 | 28 | // Otherwise we don't have enough. 29 | // Take a write lock and copy in what we are missing. 30 | muEd.Lock() 31 | defer muEd.Unlock() 32 | 33 | // Now that we have the lock, check once more if we have enough private keys. 34 | // It's possible another goroutine grabbed the write lock before we did. 35 | haveGenerated := len(generatedEd25519) 36 | if haveGenerated < len(res) { 37 | newPrivs := make([]ed25519.PrivateKey, len(res)-haveGenerated) 38 | generatedEd25519 = append(generatedEd25519, newPrivs...) 39 | 40 | // Now that the generated private key slice has grown, 41 | // start a goroutine for each private key 42 | // to spread generation work across available CPUs. 43 | // It's fine if we start many goroutines and they content 44 | var wg sync.WaitGroup 45 | 46 | for i := haveGenerated; i < len(generatedEd25519); i++ { 47 | wg.Add(1) 48 | go generateEd25519PrivKey(i, &wg) 49 | } 50 | 51 | wg.Wait() 52 | } 53 | 54 | for i := got; i < len(res); i++ { 55 | privKeyBytes := bytes.Clone([]byte(generatedEd25519[i])) 56 | res[i] = gcrypto.NewEd25519Signer(privKeyBytes) 57 | } 58 | 59 | return res 60 | } 61 | 62 | func optimisticLoadEd25519Signers(n int) ([]gcrypto.Ed25519Signer, int) { 63 | res := make([]gcrypto.Ed25519Signer, n) 64 | 65 | // With the read lock, copy as many private keys as we have. 66 | muEd.RLock() 67 | defer muEd.RUnlock() 68 | 69 | got := 0 70 | for i := range res { 71 | if i >= len(generatedEd25519) { 72 | break 73 | } 74 | 75 | privKeyBytes := bytes.Clone([]byte(generatedEd25519[i])) 76 | res[i] = gcrypto.NewEd25519Signer(privKeyBytes) 77 | got++ 78 | } 79 | 80 | return res, got 81 | } 82 | 83 | func generateEd25519PrivKey(idx int, wg *sync.WaitGroup) { 84 | defer wg.Done() 85 | 86 | seed := fmt.Sprintf("%032d", idx) // Seed must be 32 bytes long. 87 | generatedEd25519[idx] = ed25519.NewKeyFromSeed([]byte(seed)) 88 | } 89 | 90 | var muEd sync.RWMutex 91 | -------------------------------------------------------------------------------- /gcrypto/gcryptotest/ed25519_keys.go: -------------------------------------------------------------------------------- 1 | // Code generated by go generate; DO NOT EDIT. 2 | package gcryptotest 3 | 4 | import ( 5 | "crypto/ed25519" 6 | ) 7 | 8 | var generatedEd25519 = []ed25519.PrivateKey{ 9 | ed25519.PrivateKey("00000000000000000000000000000000\x1b\xa4\a[w\xc9\xe3\xfb>\xcd\xe1\\\xda\xf5\"\x1f<\x107>b?{\x0e\x1e\xf7cf\xb0\xafq7"), 10 | ed25519.PrivateKey("00000000000000000000000000000001\x1d:ů\x95\xf02C\x86\xf4h\f\xa5\xff\xfa\xc8\x16\x93K\xd2W\x82Ce\x18͢\xa2w\x94}^\xe4DL\x03"), 16 | ed25519.PrivateKey("00000000000000000000000000000007\x10\x1b\xb5\xc2p[\xb0\xd4jҥ\xea\\\x05b\v\xbbc{\x88\xdfN\vK\xd3W\xc3\x00`L\x10\x8f"), 17 | ed25519.PrivateKey("00000000000000000000000000000008r\xe0\xeek\xba\xcc6\xe6\x17D\xbb\x82JQ\xe56і\xcdAE\x89\x19\xb1\xf0:+\xa5?wρ"), 18 | ed25519.PrivateKey("00000000000000000000000000000009\xea\xef\x97\tЫF\x81\tD\xbdA\xa3FP\xb4\xa3c\x97\xac?W}K\xb9\xeb\x8f\rH\x9c\xbee"), 19 | ed25519.PrivateKey("00000000000000000000000000000010\x9a\xa2~7L\x9b\xe9^\x9a\xfbo\xd1\x1cА\xcf\xd2PmP\xf5(r\x90to*3\x95N3\x9d"), 20 | ed25519.PrivateKey("00000000000000000000000000000011D\r -\aKCgvYg\xaa\xad\x1cq\xcb\xf6\x93\x11\x82\xa5\x82O\x85\xea\xbeD\xd8I\xa5\x19N"), 21 | ed25519.PrivateKey("00000000000000000000000000000012\x98ӵ$\x96\x18\x9a\xe3]w\x87$\xd8\xd8\x10\x9c7ģ\x1e\xffw\xa6Of\xb6\xd0\xe2\xc8vZ\xe1"), 22 | ed25519.PrivateKey("00000000000000000000000000000013@\x0f^\xf2\x80\x87m\x9a\xec=\aI\xb4\xa0\xe4t罢\xbep\x17\x00\xd1\x05K\xe1\u0382Y\xa2\xf8"), 23 | ed25519.PrivateKey("00000000000000000000000000000014V\xe3\xfe\x12\xfd\xcc\xe9?\xa4\xcf\x13\xf01EN\xce_\x00\xd8s\xd2\x02w㍥\xf4\xc5u\x857\xdc"), 24 | ed25519.PrivateKey("00000000000000000000000000000015w\xae7w\x86\xe3+\xb5\xfbim\xdbS\xa5\xe0w)Q\xee\xae\f\x18\x95t\x18U\x16\xaaP\xc8\xfb,"), 25 | } 26 | -------------------------------------------------------------------------------- /gcrypto/gcryptotest/ed25519_test.go: -------------------------------------------------------------------------------- 1 | package gcryptotest_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/gordian-engine/gordian/gcrypto/gcryptotest" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestDeterministicEd25519Signers(t *testing.T) { 12 | t.Parallel() 13 | 14 | ctx := context.Background() 15 | 16 | t.Run("key determinism", func(t *testing.T) { 17 | s4 := gcryptotest.DeterministicEd25519Signers(4) 18 | 19 | t.Run("same keys when same size", func(t *testing.T) { 20 | again := gcryptotest.DeterministicEd25519Signers(4) 21 | 22 | for i := range s4 { 23 | pub1 := s4[i].PubKey() 24 | pub2 := again[i].PubKey() 25 | 26 | require.Truef( 27 | t, 28 | pub1.Equal(pub2), 29 | "got different public keys at index %d across invocations: %X -> %X", 30 | i, pub1.PubKeyBytes(), pub2.PubKeyBytes(), 31 | ) 32 | 33 | sig1, err := s4[i].Sign(ctx, []byte("test")) 34 | require.NoError(t, err) 35 | sig2, err := again[i].Sign(ctx, []byte("test")) 36 | require.NoError(t, err) 37 | require.Equal(t, sig1, sig2) 38 | } 39 | }) 40 | 41 | t.Run("same keys when shrinking", func(t *testing.T) { 42 | s2 := gcryptotest.DeterministicEd25519Signers(2) 43 | 44 | for i := range s2 { 45 | pub1 := s4[i].PubKey() 46 | pub2 := s2[i].PubKey() 47 | 48 | require.Truef( 49 | t, 50 | pub1.Equal(pub2), 51 | "got different public keys at index %d across invocations: %X -> %X", 52 | i, pub1.PubKeyBytes(), pub2.PubKeyBytes(), 53 | ) 54 | 55 | sig1, err := s4[i].Sign(ctx, []byte("test")) 56 | require.NoError(t, err) 57 | sig2, err := s2[i].Sign(ctx, []byte("test")) 58 | require.NoError(t, err) 59 | require.Equal(t, sig1, sig2) 60 | } 61 | }) 62 | 63 | t.Run("same keys when growing", func(t *testing.T) { 64 | s6 := gcryptotest.DeterministicEd25519Signers(6) 65 | 66 | for i := range s4 { 67 | pub1 := s4[i].PubKey() 68 | pub2 := s6[i].PubKey() 69 | 70 | require.Truef( 71 | t, 72 | pub1.Equal(pub2), 73 | "got different public keys at index %d across invocations: %X -> %X", 74 | i, pub1.PubKeyBytes(), pub2.PubKeyBytes(), 75 | ) 76 | 77 | sig1, err := s4[i].Sign(ctx, []byte("test")) 78 | require.NoError(t, err) 79 | sig2, err := s6[i].Sign(ctx, []byte("test")) 80 | require.NoError(t, err) 81 | require.Equal(t, sig1, sig2) 82 | } 83 | }) 84 | }) 85 | 86 | t.Run("underlying byte slices are independent", func(t *testing.T) { 87 | a := gcryptotest.DeterministicEd25519Signers(1) 88 | b := gcryptotest.DeterministicEd25519Signers(1) 89 | 90 | pub1 := a[0].PubKey().PubKeyBytes() 91 | pub2 := b[0].PubKey().PubKeyBytes() 92 | pub2[0]++ 93 | 94 | require.NotEqual( 95 | t, 96 | pub1, pub2, 97 | "expected public key bytes to differ after modifying one but they were the same", 98 | ) 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /gcrypto/gcryptotest/generate.go: -------------------------------------------------------------------------------- 1 | //go:generate go run main.go 2 | package gcryptotest 3 | 4 | import ( 5 | "crypto/ed25519" 6 | "fmt" 7 | "io" 8 | "text/template" 9 | ) 10 | 11 | var packageTemplate = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT. 12 | package gcryptotest 13 | 14 | import ( 15 | "crypto/ed25519" 16 | ) 17 | 18 | var generatedEd25519 = []ed25519.PrivateKey{ 19 | {{- range .Keys }} 20 | ed25519.PrivateKey({{.}}), 21 | {{- end }} 22 | } 23 | `)) 24 | 25 | func GenerateKeys(w io.Writer) error { 26 | keys := make([]string, 16) 27 | for i := range keys { 28 | seed := fmt.Sprintf("%032d", i) // Seed must be 32 bytes long. 29 | pk := ed25519.NewKeyFromSeed([]byte(seed)) 30 | keys[i] = fmt.Sprintf("%q", pk) 31 | } 32 | 33 | return packageTemplate.Execute(w, struct { 34 | Keys []string 35 | }{ 36 | Keys: keys, 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /gcrypto/gcryptotest/main.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | package main 5 | 6 | import ( 7 | "log" 8 | "os" 9 | 10 | "github.com/gordian-engine/gordian/gcrypto/gcryptotest" 11 | ) 12 | 13 | func main() { 14 | f, err := os.Create("ed25519_keys.go") 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | 19 | if err := gcryptotest.GenerateKeys(f); err != nil { 20 | log.Fatal(err) 21 | } 22 | if err := f.Close(); err != nil { 23 | log.Fatal(err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /gcrypto/pubkey.go: -------------------------------------------------------------------------------- 1 | package gcrypto 2 | 3 | // PubKey is the interface for an instance of a public key. 4 | type PubKey interface { 5 | // The raw bytes constituting the public key. 6 | // Implementers are free to return the same underlying slice on every call, 7 | // and callers must not modify the slice; 8 | // callers may also assume the slice will never be modified. 9 | PubKeyBytes() []byte 10 | 11 | // Equal reports whether the other key is of the same type 12 | // and has the same public key bytes. 13 | Equal(other PubKey) bool 14 | 15 | // Verify reports whether the signature is authentic, 16 | // for the given message against this public key. 17 | Verify(msg, sig []byte) bool 18 | 19 | // The internal name of this key's type. 20 | // This must be a valid ASCII string of length 8 bytes or fewer, 21 | // and it must be an identical string for every instance of this type. 22 | TypeName() string 23 | } 24 | -------------------------------------------------------------------------------- /gcrypto/registry.go: -------------------------------------------------------------------------------- 1 | package gcrypto 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "reflect" 7 | ) 8 | 9 | // Prefixes are encoded as a fixed width. 10 | const prefixSize = 8 11 | 12 | // Registry is a runtime-defined registry to manage encoding and decoding 13 | // a predetermined set of public key types. 14 | type Registry struct { 15 | byType map[reflect.Type]string 16 | 17 | // For unmarshalling 18 | byPrefix map[string]NewPubKeyFunc 19 | } 20 | 21 | type NewPubKeyFunc func([]byte) (PubKey, error) 22 | 23 | func (r *Registry) Register(name string, inst PubKey, newFn NewPubKeyFunc) { 24 | // TODO: validation on name. 25 | // TODO: fail if previously registered. 26 | 27 | if len(name) > prefixSize { 28 | panic(fmt.Errorf( 29 | "BUG: gcrypto.Registry entries must be <= 8 bytes (got %q, %d bytes)", 30 | name, len(name), 31 | )) 32 | } 33 | 34 | if r.byPrefix == nil { 35 | r.byPrefix = map[string]NewPubKeyFunc{} 36 | } 37 | r.byPrefix[name] = newFn 38 | 39 | if r.byType == nil { 40 | r.byType = map[reflect.Type]string{} 41 | } 42 | r.byType[reflect.TypeOf(inst)] = name 43 | } 44 | 45 | func (r *Registry) Marshal(pubKey PubKey) []byte { 46 | var nameHeader [prefixSize]byte 47 | 48 | typ := reflect.TypeOf(pubKey) 49 | prefix, ok := r.byType[typ] 50 | if !ok { 51 | panic(fmt.Errorf( 52 | "BUG: attempted to Marshal a public key that was never registered (reflect type: %s, type name: %s)", 53 | typ, pubKey.TypeName(), 54 | )) 55 | } 56 | 57 | copy(nameHeader[:], prefix) 58 | 59 | return append(nameHeader[:], pubKey.PubKeyBytes()...) 60 | } 61 | 62 | // Unmarshal returns a new public key based on b, 63 | // which should be the result of a previous call to [*Registry.Marshal]. 64 | // 65 | // Callers should assume that the newly returned PubKey 66 | // will retain a reference to b; 67 | // therefore the slice must not be modified after calling Unmarshal. 68 | func (r *Registry) Unmarshal(b []byte) (PubKey, error) { 69 | // TODO: more validation against b 70 | prefix := bytes.TrimRight(b[:prefixSize], "\x00") 71 | 72 | fn := r.byPrefix[string(prefix)] 73 | if fn == nil { 74 | return nil, fmt.Errorf("no registered public key type for prefix %q", prefix) 75 | } 76 | 77 | return fn(b[prefixSize:]) 78 | } 79 | 80 | // Decode returns a new PubKey from the given type and public key bytes. 81 | // It returns an error if the typeName was not previously registered, 82 | // or if the registered [NewPubKeyFunc] itself returns an error. 83 | // 84 | // Callers must assume that the returned public key retains a reference to b, 85 | // and therefore b must not be modified after calling Decode. 86 | func (r *Registry) Decode(typeName string, b []byte) (PubKey, error) { 87 | fn := r.byPrefix[typeName] 88 | if fn == nil { 89 | return nil, fmt.Errorf("no registered public key type for name %q", typeName) 90 | } 91 | 92 | return fn(b) 93 | } 94 | -------------------------------------------------------------------------------- /gcrypto/registry_test.go: -------------------------------------------------------------------------------- 1 | package gcrypto_test 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "testing" 6 | 7 | "github.com/gordian-engine/gordian/gcrypto" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestRegistry_RoundTrip(t *testing.T) { 12 | pubKey, _, err := ed25519.GenerateKey(nil) 13 | require.NoError(t, err) 14 | 15 | origKey := gcrypto.Ed25519PubKey(pubKey) 16 | 17 | reg := new(gcrypto.Registry) 18 | reg.Register("ed25519", gcrypto.Ed25519PubKey{}, gcrypto.NewEd25519PubKey) 19 | 20 | b := reg.Marshal(origKey) 21 | require.NoError(t, err) 22 | 23 | newKey, err := reg.Unmarshal(b) 24 | require.NoError(t, err) 25 | 26 | require.Equal(t, origKey.PubKeyBytes(), newKey.PubKeyBytes()) 27 | } 28 | 29 | func TestRegistry_Unmarshal_UnknownType(t *testing.T) { 30 | reg := new(gcrypto.Registry) 31 | reg.Register("ed25519", gcrypto.Ed25519PubKey{}, gcrypto.NewEd25519PubKey) 32 | 33 | _, err := reg.Unmarshal([]byte("abcd\x00\x00\x00\x00111222333")) 34 | require.ErrorContains(t, err, "no registered public key type for prefix \"abcd\"") 35 | } 36 | -------------------------------------------------------------------------------- /gcrypto/signatureproofmergeresult.go: -------------------------------------------------------------------------------- 1 | package gcrypto 2 | 3 | // SignatureProofMergeResult includes three important details that determine 4 | // whether meaningful new information was learned from two signature proofs, 5 | // and whether the message for the "other" proof should be propagated further 6 | // or if the current/merged proof should be sent in its place. 7 | // 8 | // If AllValidSignatures was false, then the other message should not be propagated. 9 | // 10 | // IncreasedSignatures indicates whether the other proof had any signatures 11 | // missing from the current proof. 12 | // This does not indicate whether the current proof had any signatures 13 | // missing from other. 14 | type SignatureProofMergeResult struct { 15 | // Whether every signature in the "other" proof was valid. 16 | AllValidSignatures bool 17 | 18 | // Whether merging resulted in signatures we did not yet have. 19 | IncreasedSignatures bool 20 | 21 | // Was the "other" proof a strict superset of the current proof? 22 | WasStrictSuperset bool 23 | } 24 | 25 | // Combine returns a new SignatureProofMergeResult, the result of combining r and other. 26 | // This is helpful for methods that combine multiple proofs, such as a prevote merge 27 | // that must handle both active and nil prevotes. 28 | func (r SignatureProofMergeResult) Combine(other SignatureProofMergeResult) SignatureProofMergeResult { 29 | return SignatureProofMergeResult{ 30 | AllValidSignatures: r.AllValidSignatures && other.AllValidSignatures, 31 | 32 | IncreasedSignatures: r.IncreasedSignatures || other.IncreasedSignatures, 33 | 34 | WasStrictSuperset: r.WasStrictSuperset && other.WasStrictSuperset, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /gcrypto/signatureproofmergeresult_test.go: -------------------------------------------------------------------------------- 1 | package gcrypto_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gordian-engine/gordian/gcrypto" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestSignatureProofMergeResult_Combine(t *testing.T) { 11 | for _, tc := range []struct { 12 | res1, res2, want gcrypto.SignatureProofMergeResult 13 | }{ 14 | { 15 | // All true fields. 16 | res1: gcrypto.SignatureProofMergeResult{ 17 | AllValidSignatures: true, 18 | IncreasedSignatures: true, 19 | WasStrictSuperset: true, 20 | }, 21 | res2: gcrypto.SignatureProofMergeResult{ 22 | AllValidSignatures: true, 23 | IncreasedSignatures: true, 24 | WasStrictSuperset: true, 25 | }, 26 | 27 | want: gcrypto.SignatureProofMergeResult{ 28 | AllValidSignatures: true, 29 | IncreasedSignatures: true, 30 | WasStrictSuperset: true, 31 | }, 32 | }, 33 | 34 | { 35 | // All false fields. 36 | res1: gcrypto.SignatureProofMergeResult{ 37 | AllValidSignatures: false, 38 | IncreasedSignatures: false, 39 | WasStrictSuperset: false, 40 | }, 41 | res2: gcrypto.SignatureProofMergeResult{ 42 | AllValidSignatures: false, 43 | IncreasedSignatures: false, 44 | WasStrictSuperset: false, 45 | }, 46 | 47 | want: gcrypto.SignatureProofMergeResult{ 48 | AllValidSignatures: false, 49 | IncreasedSignatures: false, 50 | WasStrictSuperset: false, 51 | }, 52 | }, 53 | 54 | { 55 | // One all false, one all true. 56 | res1: gcrypto.SignatureProofMergeResult{ 57 | AllValidSignatures: false, 58 | IncreasedSignatures: false, 59 | WasStrictSuperset: false, 60 | }, 61 | res2: gcrypto.SignatureProofMergeResult{ 62 | AllValidSignatures: true, 63 | IncreasedSignatures: true, 64 | WasStrictSuperset: true, 65 | }, 66 | 67 | want: gcrypto.SignatureProofMergeResult{ 68 | AllValidSignatures: false, 69 | IncreasedSignatures: true, 70 | WasStrictSuperset: false, 71 | }, 72 | }, 73 | } { 74 | out := tc.res1.Combine(tc.res2) 75 | require.Equalf(t, tc.want, out, "(%#v).Combine(%#v)", tc.res1, tc.res2) 76 | 77 | // Results should be commutative. 78 | out = tc.res2.Combine(tc.res1) 79 | require.Equalf(t, tc.want, out, "(%#v).Combine(%#v)", tc.res2, tc.res1) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /gcrypto/signer.go: -------------------------------------------------------------------------------- 1 | package gcrypto 2 | 3 | import "context" 4 | 5 | // Signer produces cryptographic signatures against an input. 6 | type Signer interface { 7 | // PubKey returns the gcrypto public key. 8 | PubKey() PubKey 9 | 10 | // Sign returns the signature for a given input. 11 | // It accepts a context in case the signing happens remotely. 12 | Sign(ctx context.Context, input []byte) (signature []byte, err error) 13 | } 14 | -------------------------------------------------------------------------------- /gcrypto/simplecommonmessagesignatureproof_test.go: -------------------------------------------------------------------------------- 1 | package gcrypto_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gordian-engine/gordian/gcrypto" 7 | "github.com/gordian-engine/gordian/gcrypto/gcryptotest" 8 | ) 9 | 10 | func TestSimpleCommonMessageSignatureProof(t *testing.T) { 11 | gcryptotest.TestCommonMessageSignatureProofCompliance_Ed25519( 12 | t, 13 | gcrypto.SimpleCommonMessageSignatureProofScheme{}, 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /gdriver/doc.go: -------------------------------------------------------------------------------- 1 | // Package gdriver and its subpackages provide useful types for writing Gordian drivers. 2 | // 3 | // In this context, "driver" is the low level interface between the Gordian consensus engine 4 | // and a higher level framework such as the Cosmos SDK or even a direct, user-written application. 5 | package gdriver 6 | -------------------------------------------------------------------------------- /gdriver/gtxbuf/errors.go: -------------------------------------------------------------------------------- 1 | package gtxbuf 2 | 3 | import "fmt" 4 | 5 | // TxInvalidError indicates that a transaction failed to apply against the state. 6 | // For correct [Buffer] behavior, the addTxFunc argument passed to [New] 7 | // must wrap the returned error in TxInvalidError, 8 | // to indicate that a particular transaction could not be applied to the state. 9 | // Any other error type is effectively fatal to the Buffer. 10 | type TxInvalidError struct { 11 | Err error 12 | } 13 | 14 | func (e TxInvalidError) Error() string { 15 | return fmt.Sprintf("transaction invalid: %v", e.Err) 16 | } 17 | 18 | func (e TxInvalidError) Unwrap() error { 19 | return e.Err 20 | } 21 | -------------------------------------------------------------------------------- /gdriver/gtxbuf/workingstate.go: -------------------------------------------------------------------------------- 1 | package gtxbuf 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "slices" 7 | ) 8 | 9 | type workingState[S, T any] struct { 10 | BaseState S 11 | 12 | curState S 13 | isUpdated bool 14 | 15 | Txs []T 16 | 17 | addTx func(context.Context, S, T) (S, error) 18 | txDeleter func(ctx context.Context, reject []T) func(T) bool 19 | } 20 | 21 | func (w *workingState[S, T]) CheckAddTx( 22 | ctx context.Context, 23 | tx T, 24 | ) error { 25 | var cur S 26 | if w.isUpdated { 27 | cur = w.curState 28 | } else { 29 | cur = w.BaseState 30 | } 31 | 32 | newState, err := w.addTx(ctx, cur, tx) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | // On success, update the current state. 38 | w.curState = newState 39 | w.isUpdated = true 40 | w.Txs = append(w.Txs, tx) 41 | 42 | return nil 43 | } 44 | 45 | func (w *workingState[S, T]) Buffered(dst []T) []T { 46 | dst = append(dst, w.Txs...) 47 | return dst 48 | } 49 | 50 | func (w *workingState[S, T]) Rebase( 51 | ctx context.Context, newBase S, applied []T, 52 | ) rebaseResponse[T] { 53 | w.BaseState = newBase 54 | w.curState = newBase 55 | w.isUpdated = false 56 | 57 | if len(w.Txs) == 0 { 58 | // No further work required. 59 | return rebaseResponse[T]{} 60 | } 61 | 62 | if len(applied) > 0 { 63 | // Only call back into user code if there may be something to delete. 64 | w.Txs = slices.DeleteFunc(w.Txs, w.txDeleter(ctx, applied)) 65 | } 66 | 67 | var invalidated []T 68 | 69 | for _, tx := range w.Txs { 70 | newState, err := w.addTx(ctx, w.curState, tx) 71 | if err != nil { 72 | if errors.As(err, new(TxInvalidError)) { 73 | // Simple invalid transaction. 74 | // Add it to the invalidated collection. 75 | invalidated = append(invalidated, tx) 76 | continue 77 | } 78 | 79 | // Otherwise, it wasn't a simple transaction error, 80 | // so we have to fail now. 81 | return rebaseResponse[T]{Err: err} 82 | } 83 | 84 | // We have new state from successfully applying this transaction. 85 | w.curState = newState 86 | w.isUpdated = true 87 | } 88 | 89 | // All transactions were applied or invalidated. 90 | // Prune the invalidated transactions, if any exist. 91 | if len(invalidated) > 0 { 92 | w.Txs = slices.DeleteFunc(w.Txs, w.txDeleter(ctx, invalidated)) 93 | } 94 | 95 | return rebaseResponse[T]{Invalidated: invalidated} 96 | } 97 | -------------------------------------------------------------------------------- /gerasure/coding.go: -------------------------------------------------------------------------------- 1 | package gerasure 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | // Encoder encodes data into a set of erasure-corrected shards of bytes. 9 | // The precise set of byte slices returned, 10 | // and which of them are required to reconstitute the original data, 11 | // are determined by the implementation. 12 | // 13 | // Settings for the encoder are typically also applied to the [Reconstructor], 14 | // but the source of those settings is out of scope of these interfaces. 15 | type Encoder interface { 16 | Encode(ctx context.Context, data []byte) ([][]byte, error) 17 | } 18 | 19 | // Reconstructor manages reconstructing the original data 20 | // from a series of erasure-coded shards, 21 | // typically produced from an instance of [Encoder]. 22 | // 23 | // This interface currently is focused on reconstructing only the data shards. 24 | // In the future, we may expand the interface with a new method 25 | // to repopulate any missing parity shards, if that proves useful. 26 | type Reconstructor interface { 27 | // Reconstruct attempts to use the shard to reconstruct the original data, 28 | // and the returned error value indicates: 29 | // - whether the shard was invalid, by an implementation-specific error 30 | // - whether the shard was valid but insufficient to reconstruct the original data, 31 | // by returning [ErrIncompleteSet] 32 | // - or whether the shard was accepted and the data is able to be reconstructed 33 | // with a call to Data, by returning nil. 34 | // 35 | // Some erasure coding schemes (such as Reed-Solomon) have an explicit index for each shard; 36 | // "rateless" erasure codes may ignore the index or may require that each call 37 | // uses an incremented index. 38 | // 39 | // Callers should preferably track which indices are passed in, 40 | // and ensure each index is only passed to ReconstructData once. 41 | ReconstructData(ctx context.Context, idx int, shard []byte) error 42 | 43 | // Data appends the reconstructed data to dst, 44 | // returning the modified dst slice if dst has sufficient capacity, 45 | // otherwise returning a newly allocated slice. 46 | // 47 | // The dataSize parameter is required because the reconstructor cannot 48 | // determine the size from shards alone, 49 | // as the final data shard may be padded with zeros. 50 | // 51 | // We are assuming for now that all erasure-coded data encountered will fit in memory; 52 | // if that assumption changes, we may add a new method to this interface 53 | // or add a separate interface altogether. 54 | // 55 | // Any error in producing the data may be returned directly, 56 | // with no wrapping by any gerasure errors. 57 | Data(dst []byte, dataSize int) ([]byte, error) 58 | } 59 | 60 | // ErrIncompleteSet is returned by [Reconstructor.RestructData] when a shard was accepted 61 | // but was not sufficient to fully restore the original data. 62 | var ErrIncompleteSet = errors.New("insufficient shard received to reconstruct data") 63 | -------------------------------------------------------------------------------- /gerasure/doc.go: -------------------------------------------------------------------------------- 1 | // Package gerasure contains generalized interfaces for erasure coding and decoding. 2 | // 3 | // Subpackages of gerasure contain implementations of those interfaces. 4 | package gerasure 5 | -------------------------------------------------------------------------------- /gerasure/gerasuretest/doc.go: -------------------------------------------------------------------------------- 1 | // Package gerasuretest contains compliance tests for erasure coding 2 | // encoders and reconstructors. 3 | package gerasuretest 4 | -------------------------------------------------------------------------------- /gerasure/gereedsolomon/compliance_test.go: -------------------------------------------------------------------------------- 1 | package gereedsolomon_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gordian-engine/gordian/gerasure" 7 | "github.com/gordian-engine/gordian/gerasure/gerasuretest" 8 | "github.com/gordian-engine/gordian/gerasure/gereedsolomon" 9 | "github.com/klauspost/reedsolomon" 10 | ) 11 | 12 | func TestReconstructionCompliance(t *testing.T) { 13 | gerasuretest.TestFixedRateErasureReconstructionCompliance( 14 | t, 15 | func(origData []byte, nData, nParity int) (gerasure.Encoder, gerasure.Reconstructor) { 16 | rs, err := reedsolomon.New(nData, nParity) 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | // We don't know the shard size until we encode. 22 | // (Or at least I don't see how to get that from the reedsolomon package.) 23 | allShards, err := rs.Split(origData) 24 | if err != nil { 25 | panic(err) 26 | } 27 | shardSize := len(allShards[0]) 28 | 29 | enc := gereedsolomon.NewEncoder(rs) 30 | 31 | // Separate reedsolomon encoder for the reconstructor. 32 | rrs, err := reedsolomon.New(nData, nParity) 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | r := gereedsolomon.NewReconstructor(rrs, shardSize) 38 | 39 | return enc, r 40 | }, 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /gerasure/gereedsolomon/doc.go: -------------------------------------------------------------------------------- 1 | // Package gereedsolomon provides Reed-Solomon implementations 2 | // of the gerasure interfaces, 3 | // by wrapping the [github.com/klauspost/reedsolomon] package. 4 | package gereedsolomon 5 | -------------------------------------------------------------------------------- /gerasure/gereedsolomon/encoder.go: -------------------------------------------------------------------------------- 1 | package gereedsolomon 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/klauspost/reedsolomon" 8 | ) 9 | 10 | // Encoder is a wrapper around [reedsolomon.Encoder] 11 | // satisfying the [gerasure.Encoder] interface. 12 | type Encoder struct { 13 | rs reedsolomon.Encoder 14 | } 15 | 16 | // NewEncoder returns a new Encoder. 17 | // The options within the given reedsolomon.Encoder determine the number of shards. 18 | func NewEncoder(rs reedsolomon.Encoder) Encoder { 19 | return Encoder{rs: rs} 20 | } 21 | 22 | // Encode satisfies [gerasure.Encoder]. 23 | // Callers should assume that the Encoder takes ownership of the given data slice. 24 | func (e Encoder) Encode(_ context.Context, data []byte) ([][]byte, error) { 25 | // From the original data, produce new subslices for the data shards and parity shards. 26 | allShards, err := e.rs.Split(data) 27 | if err != nil { 28 | return nil, fmt.Errorf("failed to split input data: %w", err) 29 | } 30 | 31 | // But just splitting doesn't populate the parity shards, 32 | // so we have to call encode in order to calculate and populate those shards. 33 | if err := e.rs.Encode(allShards); err != nil { 34 | return nil, fmt.Errorf("failed to encode parity: %w", err) 35 | } 36 | 37 | return allShards, nil 38 | } 39 | -------------------------------------------------------------------------------- /gexchange/doc.go: -------------------------------------------------------------------------------- 1 | // Package gexchange contains generalized types for the interactions between 2 | // a p2p layer and a consensus engine. 3 | package gexchange 4 | -------------------------------------------------------------------------------- /gexchange/feedback.go: -------------------------------------------------------------------------------- 1 | package gexchange 2 | 3 | // Feedback is an indicator sent back to the p2p layer 4 | // to give feedback about a particular message. 5 | // 6 | // Depending on the p2p implementation, 7 | // feedback may increase a peer's score, giving preference to that peer, 8 | // or it may reduce a peer's score, eventually rejecting 9 | // all future messages from that peer. 10 | type Feedback uint8 11 | 12 | // Valid feedback values. 13 | // 14 | //go:generate go run golang.org/x/tools/cmd/stringer -type Feedback -trimprefix=Feedback 15 | const ( 16 | // FeedbackUnspecified is the zero value for Feedback. 17 | // Returning FeedbackUnspecified is a bug. 18 | FeedbackUnspecified Feedback = iota 19 | 20 | // FeedbackAccepted indicates that the input was valid 21 | // and that the message should continue to propagate. 22 | FeedbackAccepted 23 | 24 | // FeedbackRejected indicates that the input was invalid, 25 | // the message should not be propagated, 26 | // and the sender should be penalized. 27 | FeedbackRejected 28 | 29 | // FeedbackIgnored indicates that the input was invalid; 30 | // although we are not going to propagate this message, 31 | // in contrast to FeedbackRejected, we will not penalize the sender. 32 | FeedbackIgnored 33 | 34 | // FeedbackRejectAndDisconnect indicates that the input was invalid 35 | // and appeared to be malicious. 36 | // The message will not be propagated, 37 | // and no future messages will be sent to that peer. 38 | FeedbackRejectAndDisconnect 39 | ) 40 | -------------------------------------------------------------------------------- /gexchange/feedback_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type Feedback -trimprefix=Feedback"; DO NOT EDIT. 2 | 3 | package gexchange 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[FeedbackUnspecified-0] 12 | _ = x[FeedbackAccepted-1] 13 | _ = x[FeedbackRejected-2] 14 | _ = x[FeedbackIgnored-3] 15 | _ = x[FeedbackRejectAndDisconnect-4] 16 | } 17 | 18 | const _Feedback_name = "UnspecifiedAcceptedRejectedIgnoredRejectAndDisconnect" 19 | 20 | var _Feedback_index = [...]uint8{0, 11, 19, 27, 34, 53} 21 | 22 | func (i Feedback) String() string { 23 | if i >= Feedback(len(_Feedback_index)-1) { 24 | return "Feedback(" + strconv.FormatInt(int64(i), 10) + ")" 25 | } 26 | return _Feedback_name[_Feedback_index[i]:_Feedback_index[i+1]] 27 | } 28 | -------------------------------------------------------------------------------- /gnetdag/doc.go: -------------------------------------------------------------------------------- 1 | // Package gnetdag (Gordian NETwork Directed Acyclic Graph) 2 | // contains types for determining directional flow of network traffic. 3 | // 4 | // Types in this package are focused on int values, 5 | // so that they remain decoupled from any concrete implementations 6 | // of validators, network addresses, and so on. 7 | // Callers may simply use the int values as indices into slices 8 | // of the actual type needing the directed graph. 9 | // 10 | // This package currently contains the [FixedTree] type, 11 | // which effectively maps indices in a slice such that 12 | // every non-root node contains a fixed number of children. 13 | // This package will be expanded with more types as deemed necessary. 14 | package gnetdag 15 | -------------------------------------------------------------------------------- /gnetdag/fixedtree_test.go: -------------------------------------------------------------------------------- 1 | package gnetdag_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gordian-engine/gordian/gnetdag" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | // Most of these tests use a branch factor of 3, 11 | // resulting in layers like: 12 | // 0 (L0) 13 | // 1 2 3 (L1) 14 | // 4 5 6 7 8 9 10 11 12 (L2) 15 | // 13 14 15 16... (L3) 16 | 17 | func TestFixedTree_Layer(t *testing.T) { 18 | t.Parallel() 19 | 20 | tree := gnetdag.FixedTree{BranchFactor: 3} 21 | require.Equal(t, 0, tree.Layer(0)) 22 | require.Equal(t, 1, tree.Layer(1)) 23 | require.Equal(t, 2, tree.Layer(4)) 24 | 25 | tree.BranchFactor = 5 26 | require.Equal(t, 0, tree.Layer(0)) 27 | require.Equal(t, 1, tree.Layer(4)) 28 | } 29 | 30 | func TestFixedTree_Parent(t *testing.T) { 31 | t.Parallel() 32 | 33 | tree := gnetdag.FixedTree{BranchFactor: 3} 34 | require.Equal(t, -1, tree.Parent(0)) 35 | 36 | require.Equal(t, 0, tree.Parent(1)) 37 | require.Equal(t, 0, tree.Parent(2)) 38 | require.Equal(t, 0, tree.Parent(3)) 39 | 40 | require.Equal(t, 1, tree.Parent(4)) 41 | require.Equal(t, 1, tree.Parent(5)) 42 | require.Equal(t, 1, tree.Parent(6)) 43 | require.Equal(t, 2, tree.Parent(7)) 44 | require.Equal(t, 2, tree.Parent(8)) 45 | require.Equal(t, 2, tree.Parent(9)) 46 | require.Equal(t, 3, tree.Parent(10)) 47 | require.Equal(t, 3, tree.Parent(11)) 48 | require.Equal(t, 3, tree.Parent(12)) 49 | 50 | require.Equal(t, 4, tree.Parent(13)) 51 | } 52 | 53 | func TestFixedTree_FirstChild(t *testing.T) { 54 | t.Parallel() 55 | 56 | tree := gnetdag.FixedTree{BranchFactor: 3} 57 | 58 | require.Equal(t, 1, tree.FirstChild(0)) 59 | 60 | require.Equal(t, 4, tree.FirstChild(1)) 61 | require.Equal(t, 7, tree.FirstChild(2)) 62 | require.Equal(t, 10, tree.FirstChild(3)) 63 | 64 | require.Equal(t, 13, tree.FirstChild(4)) 65 | } 66 | -------------------------------------------------------------------------------- /gwatchdog/doc.go: -------------------------------------------------------------------------------- 1 | // Package gwatchdog provides a Watchdog type that periodically communicates 2 | // with subsystems that have opted in to the watchdog. 3 | // Each subsystem that opts in provides an interval and jitter indicating 4 | // how frequently the watchdog will poll the subsystem, 5 | // and a timeout indicating the tolerable duration for the subystem's response. 6 | // If the subsystem does not repsond within the tolerable duration, 7 | // the watchdog invokes a termination by canceling the root context. 8 | package gwatchdog 9 | -------------------------------------------------------------------------------- /gwatchdog/error.go: -------------------------------------------------------------------------------- 1 | package gwatchdog 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | // IsTermination reports whether the context was cancelled by the watchdog. 9 | func IsTermination(ctx context.Context) bool { 10 | e := context.Cause(ctx) 11 | if e == nil { 12 | return false 13 | } 14 | 15 | var ftr FailureToRespondError 16 | if errors.As(e, &ftr) { 17 | return true 18 | } 19 | 20 | var ft ForcedTerminationError 21 | return errors.As(e, &ft) 22 | } 23 | 24 | // FailureToRespondError indicates a particular subsystem failed to respond 25 | // to its watchdog monitor within the configured response expectation duration. 26 | type FailureToRespondError struct { 27 | SubsystemName string 28 | } 29 | 30 | func (e FailureToRespondError) Error() string { 31 | return e.SubsystemName + " failed to respond to watchdog monitoring within expected duration" 32 | } 33 | 34 | // ForcedTerminationError indicates that [*Watchdog.Terminate] was called. 35 | type ForcedTerminationError struct { 36 | Reason string 37 | } 38 | 39 | func (e ForcedTerminationError) Error() string { 40 | return "Watchdog forced termination: " + e.Reason 41 | } 42 | -------------------------------------------------------------------------------- /internal/glog/hr.go: -------------------------------------------------------------------------------- 1 | package glog 2 | 3 | import "log/slog" 4 | 5 | // HR returns a copy of log that includes fields for the given height and round. 6 | // 7 | // This is a convenient shorthand in many log calls where 8 | // the height and round are pertinent details. 9 | func HR(log *slog.Logger, height uint64, round uint32) *slog.Logger { 10 | return log.With("height", height, "round", round) 11 | } 12 | 13 | // HR returns a copy of log that includes fields for the given height, round, and error. 14 | // 15 | // This is a convenient shorthand in many log calls where 16 | // the height, round, and error are pertinent details. 17 | func HRE(log *slog.Logger, height uint64, round uint32, e error) *slog.Logger { 18 | return log.With("height", height, "round", round, "err", e) 19 | } 20 | -------------------------------------------------------------------------------- /internal/glog/valuer.go: -------------------------------------------------------------------------------- 1 | package glog 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | ) 7 | 8 | // Hex wraps a byte slice to ensure it serializes as a hex-encoded string. 9 | // Without this, it gets rendered as a Unicode string with embedded escape codes. 10 | type Hex []byte 11 | 12 | func (v Hex) LogValue() slog.Value { 13 | return slog.StringValue(fmt.Sprintf("%x", v)) 14 | } 15 | -------------------------------------------------------------------------------- /internal/gtest/log.go: -------------------------------------------------------------------------------- 1 | package gtest 2 | 3 | import ( 4 | "log/slog" 5 | "testing" 6 | 7 | "github.com/neilotoole/slogt" 8 | ) 9 | 10 | // NewLogger returns a *slog.Logger associated with the test t. 11 | func NewLogger(t testing.TB) *slog.Logger { 12 | // The slogt package has been stable and effective 13 | // for adapting slog to testing.T.Log calls. 14 | // Prefer to abstract slogt behind a gtest interface 15 | // to reduce a direct dependency from tests to an external module. 16 | return slogt.New(t, slogt.Text()) 17 | } 18 | -------------------------------------------------------------------------------- /internal/gtest/time.go: -------------------------------------------------------------------------------- 1 | package gtest 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | // TimeFactor is a multiplier that can be controlled by the 11 | // GORDIAN_TEST_TIME_FACTOR environment variable 12 | // to increase test-related timeouts. 13 | // 14 | // While a flat 100ms timer usually suffices on a workstation, 15 | // that duration may not suffice on a contended CI machine. 16 | // Rather than requiring tests to be changed to use a longer timeout, 17 | // the operator can set e.g. GORDIAN_TEST_TIME_FACTOR=3 18 | // to triple how long the timeouts are. 19 | // 20 | // The variable is exported in case programmatic control 21 | // outside of environment variables is needed. 22 | var TimeFactor ScaledDuration = 1 23 | 24 | func init() { 25 | f := os.Getenv("GORDIAN_TEST_TIME_FACTOR") 26 | if f == "" { 27 | return 28 | } 29 | 30 | n, err := strconv.Atoi(f) 31 | if err != nil { 32 | panic(fmt.Errorf( 33 | "failed to parse GORDIAN_TEST_TIME_FACTOR (%q) into an integer: %w", 34 | f, err, 35 | )) 36 | } 37 | 38 | if n <= 0 { 39 | panic(fmt.Errorf("GORDIAN_TEST_TIME_FACTOR must be positive; got %d", n)) 40 | } 41 | 42 | TimeFactor = ScaledDuration(n) 43 | } 44 | 45 | type ScaledDuration time.Duration 46 | 47 | // ScaleMs returns ms in milliseconds, multiplied by [TimeFactor] 48 | // so that test timeouts can be easily adjusted for machines running under load. 49 | // 50 | // This type is used in [SendOrTimeout] and [ReceiveOrTimeout] 51 | // to ensure callers do not use literal timeout values, 52 | // which would cause flaky tests on slower machines. 53 | func ScaleMs(ms int64) ScaledDuration { 54 | return TimeFactor * ScaledDuration(ms) * ScaledDuration(time.Millisecond) 55 | } 56 | 57 | // Sleep calls [time.Sleep] with the given scaled duration, 58 | // to avoid a consumer of gtest needing to convert between a ScaledDuration and a time.Duration. 59 | func Sleep(dur ScaledDuration) { 60 | time.Sleep(time.Duration(dur)) 61 | } 62 | -------------------------------------------------------------------------------- /tm/internal/tmtimeout/errors.go: -------------------------------------------------------------------------------- 1 | package tmtimeout 2 | 3 | import "errors" 4 | 5 | // These errors are sentinel errors used to indicate elapsed timeouts. 6 | // See [tmengine.Timer] for more details. 7 | // They are declared in this package because 8 | // some of the internal tmstate types depend on them. 9 | var ( 10 | ErrProposalTimedOut = errors.New("proposal timed out") 11 | ErrCommitWaitTimedOut = errors.New("commit wait timed out") 12 | 13 | ErrPrevoteDelayTimedOut = errors.New("prevote delay timed out") 14 | ErrPrecommitDelayTimedOut = errors.New("precommit delay timed out") 15 | ) 16 | -------------------------------------------------------------------------------- /tm/internal/tmtimeout/manager.go: -------------------------------------------------------------------------------- 1 | package tmtimeout 2 | 3 | import "context" 4 | 5 | // Manager is a copy of [tmengine.TimeoutManager]. 6 | // The declaration in [tmengine] is canonical so that readers don't have to follow 7 | // a type alias to see the underlying declaration. 8 | type Manager interface { 9 | WithProposalTimeout(ctx context.Context, height uint64, round uint32) (context.Context, context.CancelFunc) 10 | WithPrevoteDelayTimeout(ctx context.Context, height uint64, round uint32) (context.Context, context.CancelFunc) 11 | WithPrecommitDelayTimeout(ctx context.Context, height uint64, round uint32) (context.Context, context.CancelFunc) 12 | WithCommitWaitTimeout(ctx context.Context, height uint64, round uint32) (context.Context, context.CancelFunc) 13 | } 14 | -------------------------------------------------------------------------------- /tm/tmcodec/codec.go: -------------------------------------------------------------------------------- 1 | package tmcodec 2 | 3 | import ( 4 | "github.com/gordian-engine/gordian/tm/tmconsensus" 5 | ) 6 | 7 | type Marshaler interface { 8 | MarshalConsensusMessage(ConsensusMessage) ([]byte, error) 9 | 10 | MarshalHeader(tmconsensus.Header) ([]byte, error) 11 | MarshalProposedHeader(tmconsensus.ProposedHeader) ([]byte, error) 12 | MarshalCommittedHeader(tmconsensus.CommittedHeader) ([]byte, error) 13 | 14 | MarshalPrevoteProof(tmconsensus.PrevoteSparseProof) ([]byte, error) 15 | MarshalPrecommitProof(tmconsensus.PrecommitSparseProof) ([]byte, error) 16 | } 17 | 18 | type Unmarshaler interface { 19 | UnmarshalConsensusMessage([]byte, *ConsensusMessage) error 20 | 21 | UnmarshalHeader([]byte, *tmconsensus.Header) error 22 | UnmarshalProposedHeader([]byte, *tmconsensus.ProposedHeader) error 23 | UnmarshalCommittedHeader([]byte, *tmconsensus.CommittedHeader) error 24 | 25 | UnmarshalPrevoteProof([]byte, *tmconsensus.PrevoteSparseProof) error 26 | UnmarshalPrecommitProof([]byte, *tmconsensus.PrecommitSparseProof) error 27 | } 28 | 29 | // MarshalCodec marshals and unmarshals tmconsensus values, producing byte slices. 30 | // In the future we may have a plain Codec type that operates against an io.Writer. 31 | type MarshalCodec interface { 32 | Marshaler 33 | Unmarshaler 34 | } 35 | 36 | // ConsensusMessage is a wrapper around the three types of consensus values sent during rounds. 37 | // Exactly one of the fields must be set. 38 | // If zero or multiple fields are set, behavior is undefined. 39 | type ConsensusMessage struct { 40 | ProposedHeader *tmconsensus.ProposedHeader 41 | 42 | PrevoteProof *tmconsensus.PrevoteSparseProof 43 | PrecommitProof *tmconsensus.PrecommitSparseProof 44 | } 45 | -------------------------------------------------------------------------------- /tm/tmcodec/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmcodec contains types for marshalling and unmarshalling consensus messages, 2 | // without being coupled to any particular serialization scheme. 3 | package tmcodec 4 | -------------------------------------------------------------------------------- /tm/tmcodec/tmjson/codec_test.go: -------------------------------------------------------------------------------- 1 | package tmjson_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gordian-engine/gordian/gcrypto" 7 | "github.com/gordian-engine/gordian/tm/tmcodec" 8 | "github.com/gordian-engine/gordian/tm/tmcodec/tmcodectest" 9 | "github.com/gordian-engine/gordian/tm/tmcodec/tmjson" 10 | ) 11 | 12 | func TestMarshalCodec(t *testing.T) { 13 | tmcodectest.TestMarshalCodecCompliance(t, func() tmcodec.MarshalCodec { 14 | reg := new(gcrypto.Registry) 15 | gcrypto.RegisterEd25519(reg) 16 | return tmjson.MarshalCodec{ 17 | CryptoRegistry: reg, 18 | } 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /tm/tmcodec/tmjson/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmjson contains types satisfying the [tmcodec] interfaces 2 | // that serializing to and deserialize from JSON. 3 | // 4 | // These types are simple to work with, simple to maintain, and easy to read. 5 | // You can certainly get better performance with other serialization methods. 6 | package tmjson 7 | -------------------------------------------------------------------------------- /tm/tmconsensus/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmconsensus contains low-level consensus primitives. 2 | package tmconsensus 3 | -------------------------------------------------------------------------------- /tm/tmconsensus/genesis.go: -------------------------------------------------------------------------------- 1 | package tmconsensus 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // Genesis is the value used to initialize a consensus store. 9 | // 10 | // In normal use this is derived from InitChain, 11 | // but in tests is is constructed by hand, 12 | // usually through a [tmconsensustest.StandardFixture]. 13 | type Genesis struct { 14 | ChainID string 15 | 16 | // The height of the first block to be proposed. 17 | InitialHeight uint64 18 | 19 | // This determines PrevAppStateHash for the first proposed block. 20 | CurrentAppStateHash []byte 21 | 22 | // The set of validators to propose and vote on the first block. 23 | ValidatorSet ValidatorSet 24 | } 25 | 26 | // Header returns the genesis Header corresponding to g. 27 | // It will have only its Height, NextValidators, and Hash set. 28 | // If there is an error retrieving the hash, that error is returned. 29 | func (g Genesis) Header(hs HashScheme) (Header, error) { 30 | h := Header{ 31 | // Genesis initial height is the height of the first block to propose, 32 | // so the stored block must be one less. 33 | Height: g.InitialHeight - 1, 34 | 35 | NextValidatorSet: g.ValidatorSet, 36 | } 37 | 38 | bh, err := hs.Block(h) 39 | if err != nil { 40 | return h, fmt.Errorf("failed to calculate genesis block hash: %w", err) 41 | } 42 | 43 | h.Hash = bh 44 | return h, nil 45 | } 46 | 47 | // ExternalGenesis is a view of the externally defined genesis data, 48 | // sent to the app as part of [tmdriver.InitChainRequest]. 49 | type ExternalGenesis struct { 50 | ChainID string 51 | 52 | // Height to use for the first proposed block. 53 | InitialHeight uint64 54 | 55 | // Initial application state as specified by the external genesis description. 56 | // Format is determined by the application; it is opaque to the consensus engine. 57 | // 58 | // This is a Reader, not a byte slice, so that the consensus engine 59 | // isn't forced to load the entire state into memory. 60 | InitialAppState io.Reader 61 | 62 | // Validators according to the consensus engine's view. 63 | // Can be overridden in the [tmdriver.InitChainResponse]. 64 | GenesisValidatorSet ValidatorSet 65 | } 66 | -------------------------------------------------------------------------------- /tm/tmconsensus/handleproposedheaderresult_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type HandleProposedHeaderResult -trimprefix=HandleProposedHeader ."; DO NOT EDIT. 2 | 3 | package tmconsensus 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[HandleProposedHeaderAccepted-1] 12 | _ = x[HandleProposedHeaderAlreadyStored-2] 13 | _ = x[HandleProposedHeaderSignerUnrecognized-3] 14 | _ = x[HandleProposedHeaderBadBlockHash-4] 15 | _ = x[HandleProposedHeaderBadSignature-5] 16 | _ = x[HandleProposedHeaderMissingProposerPubKey-6] 17 | _ = x[HandleProposedHeaderBadPrevCommitProofPubKeyHash-7] 18 | _ = x[HandleProposedHeaderBadPrevCommitProofSignature-8] 19 | _ = x[HandleProposedHeaderBadPrevCommitProofDoubleSigned-9] 20 | _ = x[HandleProposedHeaderBadPrevCommitVoteCount-10] 21 | _ = x[HandleProposedHeaderRoundTooOld-11] 22 | _ = x[HandleProposedHeaderRoundTooFarInFuture-12] 23 | _ = x[HandleProposedHeaderInternalError-13] 24 | } 25 | 26 | const _HandleProposedHeaderResult_name = "AcceptedAlreadyStoredSignerUnrecognizedBadBlockHashBadSignatureMissingProposerPubKeyBadPrevCommitProofPubKeyHashBadPrevCommitProofSignatureBadPrevCommitProofDoubleSignedBadPrevCommitVoteCountRoundTooOldRoundTooFarInFutureInternalError" 27 | 28 | var _HandleProposedHeaderResult_index = [...]uint8{0, 8, 21, 39, 51, 63, 84, 112, 139, 169, 191, 202, 221, 234} 29 | 30 | func (i HandleProposedHeaderResult) String() string { 31 | i -= 1 32 | if i >= HandleProposedHeaderResult(len(_HandleProposedHeaderResult_index)-1) { 33 | return "HandleProposedHeaderResult(" + strconv.FormatInt(int64(i+1), 10) + ")" 34 | } 35 | return _HandleProposedHeaderResult_name[_HandleProposedHeaderResult_index[i]:_HandleProposedHeaderResult_index[i+1]] 36 | } 37 | -------------------------------------------------------------------------------- /tm/tmconsensus/handlevoteproofsresult_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type HandleVoteProofsResult -trimprefix=HandleVoteProofs ."; DO NOT EDIT. 2 | 3 | package tmconsensus 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[HandleVoteProofsAccepted-1] 12 | _ = x[HandleVoteProofsNoNewSignatures-2] 13 | _ = x[HandleVoteProofsEmpty-3] 14 | _ = x[HandleVoteProofsBadPubKeyHash-4] 15 | _ = x[HandleVoteProofsRoundTooOld-5] 16 | _ = x[HandleVoteProofsTooFarInFuture-6] 17 | _ = x[HandleVoteProofsInternalError-7] 18 | } 19 | 20 | const _HandleVoteProofsResult_name = "AcceptedNoNewSignaturesEmptyBadPubKeyHashRoundTooOldTooFarInFutureInternalError" 21 | 22 | var _HandleVoteProofsResult_index = [...]uint8{0, 8, 23, 28, 41, 52, 66, 79} 23 | 24 | func (i HandleVoteProofsResult) String() string { 25 | i -= 1 26 | if i >= HandleVoteProofsResult(len(_HandleVoteProofsResult_index)-1) { 27 | return "HandleVoteProofsResult(" + strconv.FormatInt(int64(i+1), 10) + ")" 28 | } 29 | return _HandleVoteProofsResult_name[_HandleVoteProofsResult_index[i]:_HandleVoteProofsResult_index[i+1]] 30 | } 31 | -------------------------------------------------------------------------------- /tm/tmconsensus/hashscheme.go: -------------------------------------------------------------------------------- 1 | package tmconsensus 2 | 3 | import "github.com/gordian-engine/gordian/gcrypto" 4 | 5 | // HashScheme defines ways to determine various hashes in a consensus engine. 6 | type HashScheme interface { 7 | // Block calculates and returns the block hash given a header, 8 | // without consulting or modifying existing Hash field on the header. 9 | Block(Header) ([]byte, error) 10 | 11 | // PubKeys calculates and returns the hash of the ordered set of public keys. 12 | PubKeys([]gcrypto.PubKey) ([]byte, error) 13 | 14 | // VotePowers calculates and returns the hash of the ordered set of voting power, 15 | // mapped 1:1 with the ordered set of public keys. 16 | VotePowers([]uint64) ([]byte, error) 17 | } 18 | -------------------------------------------------------------------------------- /tm/tmconsensus/math.go: -------------------------------------------------------------------------------- 1 | package tmconsensus 2 | 3 | import "errors" 4 | 5 | // ByzantineMajority returns the minimum value to exceed 2/3 of n. 6 | // Use should always involve >= comparison, not >. 7 | // For example, 2/3 of 12 is 8, so ByzantineMajority(12) = 9. 8 | // Similarly, 2/3 of 10 is 6+2/3, so ByzantineMajority(10) = 7. 9 | // 10 | // ByzantineMajority(0) panics. 11 | func ByzantineMajority(n uint64) uint64 { 12 | if n == 0 { 13 | panic(errors.New("ByzantineMajority: n must be positive")) 14 | } 15 | 16 | quo, rem := n/3, n%3 17 | if rem < 2 { 18 | return 2*quo + 1 19 | } 20 | return 2*quo + 2 21 | } 22 | 23 | // ByzantineMinority returns the minimum value to reach 1/3 of n. 24 | // Use should always involve >= comparison, not >. 25 | // 26 | // For votes that are simply in favor or against a binary decision, 27 | // reaching 1/3 means it is possible to reach a majority decision; 28 | // unless both the for and against votes have reached the minority, 29 | // in which case it is impossible for one vote to reach the majority. 30 | // 31 | // ByzantineMinority(0) panics. 32 | func ByzantineMinority(n uint64) uint64 { 33 | if n == 0 { 34 | panic(errors.New("ByzantineMinority: n must be positive")) 35 | } 36 | 37 | quo, rem := n/3, n%3 38 | if rem == 0 { 39 | return quo 40 | } 41 | 42 | return quo + 1 43 | } 44 | -------------------------------------------------------------------------------- /tm/tmconsensus/math_test.go: -------------------------------------------------------------------------------- 1 | package tmconsensus_test 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/gordian-engine/gordian/tm/tmconsensus" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestByzantineMajority(t *testing.T) { 12 | for _, tc := range []struct { 13 | n uint64 14 | want uint64 15 | }{ 16 | // Easily verified values. 17 | {n: 12, want: 9}, 18 | {n: 10, want: 7}, 19 | {n: 100, want: 67}, 20 | 21 | // Three numbers in sequence to ensure modulo-3 cases. 22 | {n: 3, want: 3}, 23 | {n: 4, want: 3}, 24 | {n: 5, want: 4}, 25 | 26 | // Max uint64 to ensure no overflow condition. 27 | {n: math.MaxUint64, want: ((math.MaxUint64 / 3) * 2) + 1}, 28 | } { 29 | require.Equal(t, tc.want, tmconsensus.ByzantineMajority(tc.n)) 30 | } 31 | 32 | require.Panics(t, func() { 33 | _ = tmconsensus.ByzantineMajority(0) 34 | }) 35 | } 36 | 37 | func TestByzantineMinority(t *testing.T) { 38 | for _, tc := range []struct { 39 | n uint64 40 | want uint64 41 | }{ 42 | // Easy values, complementary to the ByzantineMajority tests. 43 | {n: 12, want: 4}, 44 | {n: 10, want: 4}, 45 | {n: 100, want: 34}, 46 | 47 | // Three numbers in sequence to ensure modulo-3 cases. 48 | {n: 3, want: 1}, 49 | {n: 4, want: 2}, 50 | {n: 5, want: 2}, 51 | 52 | // Max uint64 to ensure no overflow condition. 53 | {n: math.MaxUint64, want: (math.MaxUint64 / 3)}, 54 | } { 55 | require.Equal(t, tc.want, tmconsensus.ByzantineMinority(tc.n)) 56 | } 57 | 58 | require.Panics(t, func() { 59 | _ = tmconsensus.ByzantineMinority(0) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /tm/tmconsensus/signaturescheme.go: -------------------------------------------------------------------------------- 1 | package tmconsensus 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "sync" 7 | ) 8 | 9 | // SignatureScheme determines the content to be signed, for consensus messages. 10 | // 11 | // Rather than returning a slice of bytes, its methods write to an io.Writer. 12 | // This enables the caller to potentially reduce allocations, 13 | // for example by reusing a bytes.Buffer: 14 | // 15 | // var scheme SignatureScheme = getSignatureScheme() 16 | // var buf bytes.Buffer 17 | // for i, pv := range prevotes { 18 | // buf.Reset() 19 | // _, err := scheme.WritePrevoteSigningContent(&buf, pv) 20 | // if err != nil { 21 | // panic(err) 22 | // } 23 | // checkSignature(buf.Bytes(), pv.Signature, publicKeys[i]) 24 | // } 25 | type SignatureScheme interface { 26 | WriteProposalSigningContent(w io.Writer, h Header, round uint32, pbAnnotations Annotations) (int, error) 27 | 28 | WritePrevoteSigningContent(io.Writer, VoteTarget) (int, error) 29 | 30 | WritePrecommitSigningContent(io.Writer, VoteTarget) (int, error) 31 | } 32 | 33 | var sigBufPool = sync.Pool{ 34 | New: func() any { 35 | return new(bytes.Buffer) 36 | }, 37 | } 38 | 39 | // ProposalSignBytes returns a new byte slice containing 40 | // the proposal sign bytes for h, as defined by s. 41 | // 42 | // Use this function for one-off calls, but prefer to maintain 43 | // a local bytes.Buffer in loops involving signatures. 44 | func ProposalSignBytes(h Header, round uint32, pbAnnotations Annotations, s SignatureScheme) ([]byte, error) { 45 | buf := sigBufPool.Get().(*bytes.Buffer) 46 | defer sigBufPool.Put(buf) 47 | 48 | buf.Reset() 49 | _, err := s.WriteProposalSigningContent(buf, h, round, pbAnnotations) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return bytes.Clone(buf.Bytes()), nil 55 | } 56 | 57 | // PrevoteSignBytes returns a new byte slice containing 58 | // the prevote sign bytes for v, as defined by s. 59 | // 60 | // Use this function for one-off calls, but prefer to maintain 61 | // a local bytes.Buffer in loops involving signatures. 62 | func PrevoteSignBytes(vt VoteTarget, s SignatureScheme) ([]byte, error) { 63 | buf := sigBufPool.Get().(*bytes.Buffer) 64 | defer sigBufPool.Put(buf) 65 | 66 | buf.Reset() 67 | _, err := s.WritePrevoteSigningContent(buf, vt) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return bytes.Clone(buf.Bytes()), nil 73 | } 74 | 75 | // PrecommitSignBytes returns a new byte slice containing 76 | // the precommit sign bytes for v, as defined by s. 77 | // 78 | // Use this function for one-off calls, but prefer to maintain 79 | // a local bytes.Buffer in loops involving signatures. 80 | func PrecommitSignBytes(vt VoteTarget, s SignatureScheme) ([]byte, error) { 81 | buf := sigBufPool.Get().(*bytes.Buffer) 82 | defer sigBufPool.Put(buf) 83 | 84 | buf.Reset() 85 | _, err := s.WritePrecommitSigningContent(buf, vt) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | return bytes.Clone(buf.Bytes()), nil 91 | } 92 | -------------------------------------------------------------------------------- /tm/tmconsensus/tmconsensustest/annotations.go: -------------------------------------------------------------------------------- 1 | package tmconsensustest 2 | 3 | import "github.com/gordian-engine/gordian/tm/tmconsensus" 4 | 5 | // AnnotationTestCase is a name and an Annotations value. 6 | // See [AnnotationCombinations] for more details. 7 | type AnnotationTestCase struct { 8 | Name string 9 | Annotations tmconsensus.Annotations 10 | } 11 | 12 | // AnnotationCombinations returns a slice of AnnotationTestCase with every combination of 13 | // nil, 0-length slice, and populated User and Driver fields set on the Annotations. 14 | // 15 | // These are useful in tests involving annotations, 16 | // to avoid repetition and to be sure that all cases are covered. 17 | func AnnotationCombinations() []AnnotationTestCase { 18 | return []AnnotationTestCase{ 19 | { 20 | Name: "empty annotations", 21 | }, 22 | { 23 | Name: "empty but non-nil user annotations", 24 | Annotations: tmconsensus.Annotations{ 25 | User: []byte{}, 26 | }, 27 | }, 28 | { 29 | Name: "empty but non-nil driver annotations", 30 | Annotations: tmconsensus.Annotations{ 31 | Driver: []byte{}, 32 | }, 33 | }, 34 | { 35 | Name: "empty but non-nil user and driver annotations", 36 | Annotations: tmconsensus.Annotations{ 37 | User: []byte{}, 38 | Driver: []byte{}, 39 | }, 40 | }, 41 | { 42 | Name: "populated user annotations with nil driver annotations", 43 | Annotations: tmconsensus.Annotations{ 44 | User: []byte("user"), 45 | }, 46 | }, 47 | { 48 | Name: "populated user annotations with empty driver annotations", 49 | Annotations: tmconsensus.Annotations{ 50 | User: []byte("user"), 51 | Driver: []byte{}, 52 | }, 53 | }, 54 | { 55 | Name: "populated driver annotations with nil user annotations", 56 | Annotations: tmconsensus.Annotations{ 57 | Driver: []byte("driver"), 58 | }, 59 | }, 60 | { 61 | Name: "populated driver annotations with empty user annotations", 62 | Annotations: tmconsensus.Annotations{ 63 | User: []byte{}, 64 | Driver: []byte("driver"), 65 | }, 66 | }, 67 | { 68 | Name: "fully populated", 69 | Annotations: tmconsensus.Annotations{ 70 | User: []byte("user"), 71 | Driver: []byte("driver"), 72 | }, 73 | }, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tm/tmconsensus/tmconsensustest/channelconsensushandler.go: -------------------------------------------------------------------------------- 1 | package tmconsensustest 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/gordian-engine/gordian/gexchange" 8 | "github.com/gordian-engine/gordian/tm/tmconsensus" 9 | ) 10 | 11 | // ChannelConsensusHandler is a [tmconsensus.ConsensusHandler] 12 | // that emits messages to a set of channels. 13 | // 14 | // This is useful in tests where you have a "client-only" connection 15 | // and you want to observe messages sent to the network, 16 | // without interfering with any individual engine. 17 | type ChannelConsensusHandler struct { 18 | incomingProposals chan tmconsensus.ProposedHeader 19 | 20 | incomingPrevoteProofs chan tmconsensus.PrevoteSparseProof 21 | incomingPrecommitProofs chan tmconsensus.PrecommitSparseProof 22 | 23 | closeOnce sync.Once 24 | } 25 | 26 | // NewChannelConsensusHandler returns a ChannelConsensusHandler 27 | // whose channels are all sized according to bufSize. 28 | func NewChannelConsensusHandler(bufSize int) *ChannelConsensusHandler { 29 | return &ChannelConsensusHandler{ 30 | incomingProposals: make(chan tmconsensus.ProposedHeader, bufSize), 31 | 32 | incomingPrevoteProofs: make(chan tmconsensus.PrevoteSparseProof, bufSize), 33 | incomingPrecommitProofs: make(chan tmconsensus.PrecommitSparseProof, bufSize), 34 | } 35 | } 36 | 37 | // HandleProposedProposedHeader implements [tmconsensus.ConsensusHandler]. 38 | func (h *ChannelConsensusHandler) HandleProposedHeader(ctx context.Context, ph tmconsensus.ProposedHeader) gexchange.Feedback { 39 | select { 40 | case h.incomingProposals <- ph: 41 | return gexchange.FeedbackAccepted 42 | case <-ctx.Done(): 43 | return gexchange.FeedbackIgnored 44 | } 45 | } 46 | 47 | func (h *ChannelConsensusHandler) HandlePrevoteProofs(ctx context.Context, p tmconsensus.PrevoteSparseProof) gexchange.Feedback { 48 | select { 49 | case h.incomingPrevoteProofs <- p: 50 | return gexchange.FeedbackAccepted 51 | case <-ctx.Done(): 52 | return gexchange.FeedbackIgnored 53 | } 54 | } 55 | 56 | func (h *ChannelConsensusHandler) HandlePrecommitProofs(ctx context.Context, p tmconsensus.PrecommitSparseProof) gexchange.Feedback { 57 | select { 58 | case h.incomingPrecommitProofs <- p: 59 | return gexchange.FeedbackAccepted 60 | case <-ctx.Done(): 61 | return gexchange.FeedbackIgnored 62 | } 63 | } 64 | 65 | // IncomingProposals returns a channel of the values that were passed to HandleProposedHeader. 66 | func (h *ChannelConsensusHandler) IncomingProposals() <-chan tmconsensus.ProposedHeader { 67 | return h.incomingProposals 68 | } 69 | 70 | // IncomingPrecommitProofs returns a channel of the values that were passed to HandlePrecommitProof. 71 | func (h *ChannelConsensusHandler) IncomingPrecommitProofs() <-chan tmconsensus.PrecommitSparseProof { 72 | return h.incomingPrecommitProofs 73 | } 74 | 75 | // IncomingPrevoteProofs returns a channel of the values that were passed to HandlePrevoteProof. 76 | func (h *ChannelConsensusHandler) IncomingPrevoteProofs() <-chan tmconsensus.PrevoteSparseProof { 77 | return h.incomingPrevoteProofs 78 | } 79 | 80 | // Close closes h. 81 | // It is safe to call Close multiple times. 82 | func (h *ChannelConsensusHandler) Close() { 83 | h.closeOnce.Do(func() { 84 | close(h.incomingProposals) 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /tm/tmconsensus/tmconsensustest/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmconsensustest contains utilities helpful for interacting with 2 | // [tmconsensus] types in tests. 3 | package tmconsensustest 4 | -------------------------------------------------------------------------------- /tm/tmconsensus/tmconsensustest/ed25519fixture.go: -------------------------------------------------------------------------------- 1 | package tmconsensustest 2 | 3 | import ( 4 | "github.com/gordian-engine/gordian/gcrypto" 5 | ) 6 | 7 | // NewEd25519Fixture returns an initialized Fixture 8 | // with the given number of determinstic ed25519 validators, 9 | // a [SimpleSignatureScheme], and a [SimpleHashScheme]. 10 | // 11 | // See the Fixture docs for other fields that 12 | // have default values but which may be overridden before use. 13 | func NewEd25519Fixture(numVals int) *Fixture { 14 | privVals := DeterministicValidatorsEd25519(numVals) 15 | 16 | var reg gcrypto.Registry 17 | gcrypto.RegisterEd25519(®) 18 | 19 | fx := NewBareFixture() 20 | fx.Registry = reg 21 | fx.PrivVals = privVals 22 | 23 | return fx 24 | } 25 | -------------------------------------------------------------------------------- /tm/tmconsensus/tmconsensustest/ed25519validators.go: -------------------------------------------------------------------------------- 1 | package tmconsensustest 2 | 3 | import ( 4 | "github.com/gordian-engine/gordian/gcrypto" 5 | "github.com/gordian-engine/gordian/gcrypto/gcryptotest" 6 | "github.com/gordian-engine/gordian/tm/tmconsensus" 7 | ) 8 | 9 | // DeterministicValidatorsEd25519 returns a deterministic set 10 | // of validators with ed25519 keys. 11 | // 12 | // Each validator will have its VotingPower set to 1. 13 | // 14 | // There are two advantages to using deterministic keys. 15 | // First, subsequent runs of the same test will use the same keys, 16 | // so logs involving keys or IDs will not change across runs, 17 | // simplifying the debugging process. 18 | // Second, the generated keys are cached, 19 | // so there is effectively zero CPU time cost for additional tests 20 | // calling this function, beyond the first call. 21 | func DeterministicValidatorsEd25519(n int) PrivVals { 22 | res := make(PrivVals, n) 23 | signers := gcryptotest.DeterministicEd25519Signers(n) 24 | 25 | for i := range res { 26 | res[i] = PrivVal{ 27 | Val: tmconsensus.Validator{ 28 | PubKey: signers[i].PubKey().(gcrypto.Ed25519PubKey), 29 | 30 | // Order by power descending, 31 | // with the power difference being negligible, 32 | // so that the validator order matches the default deterministic key order. 33 | // (Without this power adjustment, the validators would be ordered 34 | // by public key or by ID, which is unlikely to match their order 35 | // as defined in fixtures or other uses of determinsitic validators. 36 | Power: uint64(100_000 - i), 37 | }, 38 | Signer: signers[i], 39 | } 40 | } 41 | 42 | return res 43 | } 44 | -------------------------------------------------------------------------------- /tm/tmconsensus/tmconsensustest/nopconsensusstrategy.go: -------------------------------------------------------------------------------- 1 | package tmconsensustest 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gordian-engine/gordian/tm/tmconsensus" 7 | ) 8 | 9 | // NopConsensusStrategy is a [tmconsensus.ConsensusStrategy] that always prevotes and precommits nil. 10 | // 11 | // This should only be used as a placeholder in tests that require the presence of, 12 | // but do not interact with, a consensus strategy. 13 | type NopConsensusStrategy struct{} 14 | 15 | func (NopConsensusStrategy) EnterRound(context.Context, tmconsensus.RoundView, chan<- tmconsensus.Proposal) error { 16 | return nil 17 | } 18 | 19 | func (NopConsensusStrategy) ConsiderProposedBlocks( 20 | ctx context.Context, 21 | phs []tmconsensus.ProposedHeader, 22 | reason tmconsensus.ConsiderProposedBlocksReason, 23 | ) (string, error) { 24 | return "", nil 25 | } 26 | 27 | func (NopConsensusStrategy) ChooseProposedBlock(ctx context.Context, phs []tmconsensus.ProposedHeader) (string, error) { 28 | return "", nil 29 | } 30 | 31 | func (NopConsensusStrategy) DecidePrecommit(ctx context.Context, vs tmconsensus.VoteSummary) (string, error) { 32 | return "", nil 33 | } 34 | -------------------------------------------------------------------------------- /tm/tmconsensus/tmconsensustest/privval.go: -------------------------------------------------------------------------------- 1 | package tmconsensustest 2 | 3 | import ( 4 | "github.com/gordian-engine/gordian/gcrypto" 5 | "github.com/gordian-engine/gordian/tm/tmconsensus" 6 | ) 7 | 8 | // PrivVal is the "private" view of the validators for use in the [Fixture] type, 9 | // so that tests have access to the Signers backing the validators too. 10 | type PrivVal struct { 11 | // The plain consensus validator. 12 | Val tmconsensus.Validator 13 | 14 | Signer gcrypto.Signer 15 | } 16 | 17 | type PrivVals []PrivVal 18 | 19 | func (vs PrivVals) Vals() []tmconsensus.Validator { 20 | out := make([]tmconsensus.Validator, len(vs)) 21 | for i, v := range vs { 22 | out[i] = v.Val 23 | } 24 | return out 25 | } 26 | 27 | func (vs PrivVals) PubKeys() []gcrypto.PubKey { 28 | out := make([]gcrypto.PubKey, len(vs)) 29 | for i, v := range vs { 30 | out[i] = v.Signer.PubKey() 31 | } 32 | return out 33 | } 34 | -------------------------------------------------------------------------------- /tm/tmconsensus/tmconsensustest/simplehashscheme_test.go: -------------------------------------------------------------------------------- 1 | package tmconsensustest_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gordian-engine/gordian/gcrypto" 7 | "github.com/gordian-engine/gordian/tm/tmconsensus/tmconsensustest" 8 | ) 9 | 10 | func TestSimpleHashScheme(t *testing.T) { 11 | tmconsensustest.TestHashSchemeCompliance(t, tmconsensustest.SimpleHashScheme{}, gcrypto.SimpleCommonMessageSignatureProofScheme{}, nil) 12 | } 13 | -------------------------------------------------------------------------------- /tm/tmconsensus/tmconsensustest/simplesignaturescheme.go: -------------------------------------------------------------------------------- 1 | package tmconsensustest 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/gordian-engine/gordian/tm/tmconsensus" 8 | ) 9 | 10 | // SimpleSignatureScheme is a very basic signature scheme used for tests. 11 | // Its produced signing content is intended to be human-readable 12 | // and delimited across multiple lines, 13 | // so that if unexpected content is being signed, 14 | // it ought to be straightforward to determine what incorrect content was used. 15 | // 16 | // If this scheme were used in production, 17 | // it could be used for replay attacks on other chains 18 | // that reuse the same validator private keys; 19 | // at a minimum, the chain ID would need to be included. 20 | type SimpleSignatureScheme struct{} 21 | 22 | var _ tmconsensus.SignatureScheme = SimpleSignatureScheme{} 23 | 24 | func (s SimpleSignatureScheme) WriteProposalSigningContent( 25 | w io.Writer, h tmconsensus.Header, round uint32, pbAnnotations tmconsensus.Annotations, 26 | ) (int, error) { 27 | n, err := fmt.Fprintf(w, `PROPOSAL: 28 | Height=%d 29 | Round=%d 30 | PrevBlockHash=%x 31 | PrevAppStateHash=%x 32 | DataID=%x 33 | `, h.Height, round, h.PrevBlockHash, h.PrevAppStateHash, h.DataID) 34 | if err != nil { 35 | return n, err 36 | } 37 | 38 | if pbAnnotations.User != nil { 39 | m, err := fmt.Fprintf(w, "UserAnnotation=%x\n", pbAnnotations.User) 40 | n += m 41 | if err != nil { 42 | return n, err 43 | } 44 | } 45 | 46 | if pbAnnotations.Driver != nil { 47 | m, err := fmt.Fprintf(w, "DriverAnnotation=%x\n", pbAnnotations.Driver) 48 | n += m 49 | if err != nil { 50 | return n, err 51 | } 52 | } 53 | 54 | return n, nil 55 | } 56 | 57 | func (s SimpleSignatureScheme) WritePrevoteSigningContent(w io.Writer, vt tmconsensus.VoteTarget) (int, error) { 58 | if vt.BlockHash == "" { 59 | return fmt.Fprintf(w, `NIL PREVOTE: 60 | Height=%d 61 | Round=%d 62 | `, vt.Height, vt.Round) 63 | } 64 | 65 | return fmt.Fprintf(w, `PREVOTE: 66 | Height=%d 67 | Round=%d 68 | BlockHash=%x 69 | `, vt.Height, vt.Round, vt.BlockHash) 70 | } 71 | 72 | func (s SimpleSignatureScheme) WritePrecommitSigningContent(w io.Writer, vt tmconsensus.VoteTarget) (int, error) { 73 | if vt.BlockHash == "" { 74 | return fmt.Fprintf(w, `NIL PRECOMMIT: 75 | Height=%d 76 | Round=%d 77 | `, vt.Height, vt.Round) 78 | } 79 | 80 | return fmt.Fprintf(w, `PRECOMMIT: 81 | Height=%d 82 | Round=%d 83 | BlockHash=%x 84 | `, vt.Height, vt.Round, vt.BlockHash) 85 | } 86 | -------------------------------------------------------------------------------- /tm/tmconsensus/votesummary_test.go: -------------------------------------------------------------------------------- 1 | package tmconsensus_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/gordian-engine/gordian/tm/tmconsensus" 8 | "github.com/gordian-engine/gordian/tm/tmconsensus/tmconsensustest" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestVoteSummary_powers(t *testing.T) { 13 | t.Parallel() 14 | 15 | fx := tmconsensustest.NewEd25519Fixture(4) 16 | 17 | ctx, cancel := context.WithCancel(context.Background()) 18 | defer cancel() 19 | 20 | vals := fx.Vals() 21 | 22 | vs := tmconsensus.NewVoteSummary() 23 | vs.SetAvailablePower(vals) 24 | 25 | prevoteMap := fx.PrevoteProofMap(ctx, 1, 0, map[string][]int{ 26 | "": {0}, 27 | "some_block": {1, 2, 3}, 28 | }) 29 | 30 | precommitMap := fx.PrevoteProofMap(ctx, 1, 0, map[string][]int{ 31 | "": {0}, 32 | "some_block": {1, 2, 3}, 33 | }) 34 | 35 | vs.SetVotePowers(vals, prevoteMap, precommitMap) 36 | nilPow := vals[0].Power 37 | blockPow := vals[1].Power + vals[2].Power + vals[3].Power 38 | 39 | require.Equal(t, nilPow+blockPow, vs.AvailablePower) 40 | 41 | t.Run("prevotes", func(t *testing.T) { 42 | require.Equal(t, vs.AvailablePower, vs.TotalPrevotePower) 43 | require.Equal(t, "some_block", vs.MostVotedPrevoteHash) 44 | require.Equal(t, map[string]uint64{ 45 | "": nilPow, 46 | "some_block": blockPow, 47 | }, vs.PrevoteBlockPower) 48 | }) 49 | 50 | t.Run("precommits", func(t *testing.T) { 51 | require.Equal(t, vs.AvailablePower, vs.TotalPrecommitPower) 52 | require.Equal(t, "some_block", vs.MostVotedPrecommitHash) 53 | require.Equal(t, map[string]uint64{ 54 | "": nilPow, 55 | "some_block": blockPow, 56 | }, vs.PrecommitBlockPower) 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /tm/tmconsensus/votetarget.go: -------------------------------------------------------------------------------- 1 | package tmconsensus 2 | 3 | // VoteTarget is the reference of the block targeted for a prevote or precommit. 4 | type VoteTarget struct { 5 | Height uint64 6 | Round uint32 7 | 8 | // While the block hash is conventionally []byte, 9 | // we use a string here for simpler map keys 10 | // and because the hash is intended to be immutable after creation. 11 | // Note that an empty string indicates a nil vote. 12 | BlockHash string 13 | } 14 | -------------------------------------------------------------------------------- /tm/tmdebug/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmdebug contains utilities useful for debugging components of Gordian's tm package tree. 2 | // 3 | // Most types have exported fields that must be set directly. 4 | // 5 | // None of the APIs in this package should be considered stable between versions of Gordian. 6 | // 7 | // Depending on what is being debugged, you may want to make changes to the 8 | // exported types and methods in this package. 9 | package tmdebug 10 | -------------------------------------------------------------------------------- /tm/tmdriver/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmdriver contains types for the driver to interact with the consensus engine. 2 | // The driver could be considered as the "application" to the consensus engine, 3 | // but we use the term driver here because this is a low-level interface to the engine 4 | // and it clearly disambiguates from the userspace application that is likely interacting 5 | // with another layer such as the Cosmos SDK. 6 | // 7 | // While other packages are focused on other primitives that the driver must be aware of, 8 | // this package focuses specifically and directly on the engine-driver interactions. 9 | package tmdriver 10 | -------------------------------------------------------------------------------- /tm/tmdriver/requests.go: -------------------------------------------------------------------------------- 1 | package tmdriver 2 | 3 | import ( 4 | "github.com/gordian-engine/gordian/tm/tmconsensus" 5 | ) 6 | 7 | // InitChainRequest is sent from the engine to the driver, 8 | // ensuring that the consensus store is in an appropriate initial state. 9 | // 10 | // InitChainRequest does not have an associated context like the other request types, 11 | // because it is not associated with the lifecycle of a single step or round. 12 | type InitChainRequest struct { 13 | Genesis tmconsensus.ExternalGenesis 14 | 15 | Resp chan InitChainResponse 16 | } 17 | 18 | // InitChainResponse is sent by the driver in response to an [InitChainRequest]. 19 | type InitChainResponse struct { 20 | // The app state hash to use in the first proposed block's PrevAppStateHash field. 21 | AppStateHash []byte 22 | 23 | // The validators for the consensus engine to use in the first proposed block. 24 | // If nil, the engine will use the GenesisValidators from the request. 25 | Validators []tmconsensus.Validator 26 | } 27 | 28 | // FinalizeBlockRequest is sent from the state machine to the driver, 29 | // notifying the driver that the given header represents the block that is to be committed. 30 | // 31 | // The driver must evaluate the block corresponding to the header 32 | // and return the validators to set as NextValidators on the subsequent block; 33 | // and it must return the resulting app state hash, 34 | // to be used as PrevAppStateHash in the subsequent block. 35 | // 36 | // Consumers of this value may assume that Resp is buffered and sends will not block. 37 | type FinalizeBlockRequest struct { 38 | Header tmconsensus.Header 39 | Round uint32 40 | 41 | Resp chan FinalizeBlockResponse 42 | } 43 | 44 | // FinalizeBlockResponse is sent by the driver in response to a [FinalizeBlockRequest]. 45 | type FinalizeBlockResponse struct { 46 | // For an unambiguous indicator of the block the driver finalized. 47 | Height uint64 48 | Round uint32 49 | BlockHash []byte 50 | 51 | // The resulting validators after evaluating the block. 52 | // If we are finalizing the block at height H, 53 | // this value will be used as the NextValidators field in block at height H+1, 54 | // thereby becoming the current validators at height H+2. 55 | Validators []tmconsensus.Validator 56 | 57 | // The app state after evaluating the block. 58 | AppStateHash []byte 59 | } 60 | -------------------------------------------------------------------------------- /tm/tmengine/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmengine contains the [Engine] and its supporting types 2 | // which are used to actually create a consensus engine. 3 | package tmengine 4 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmeil/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmeil is shorthand for tmengine internal link. 2 | // It has a similar intended purpose to the [tmelink] package, 3 | // except whereas tmelink exposes types for use outside the tmengine tree, 4 | // this package is scoped to engine internals. 5 | package tmeil 6 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmemetrics/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmemetrics contains the internals for tmengine metrics. 2 | package tmemetrics 3 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmemetrics/metrics.go: -------------------------------------------------------------------------------- 1 | package tmemetrics 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | ) 8 | 9 | // Metrics is the set of metrics for an engine. 10 | // This type is declared here, but aliased in [tmengine]. 11 | type Metrics struct { 12 | MirrorCommittingHeight uint64 13 | MirrorCommittingRound uint32 14 | 15 | MirrorVotingHeight uint64 16 | MirrorVotingRound uint32 17 | 18 | StateMachineHeight uint64 19 | StateMachineRound uint32 20 | } 21 | 22 | func (m Metrics) LogValue() slog.Value { 23 | return slog.GroupValue( 24 | slog.String("mirror_committing_hr", fmt.Sprintf("%d/%d", m.MirrorCommittingHeight, m.MirrorCommittingRound)), 25 | 26 | slog.String("mirror_voting_hr", fmt.Sprintf("%d/%d", m.MirrorVotingHeight, m.MirrorVotingRound)), 27 | 28 | slog.String("state_machine_hr", fmt.Sprintf("%d/%d", m.StateMachineHeight, m.StateMachineRound)), 29 | ) 30 | } 31 | 32 | type MirrorMetrics struct { 33 | // Voting. 34 | VH uint64 35 | VR uint32 36 | 37 | // Committing. 38 | CH uint64 39 | CR uint32 40 | } 41 | 42 | type StateMachineMetrics struct { 43 | H uint64 44 | R uint32 45 | } 46 | 47 | type Collector struct { 48 | mCh chan MirrorMetrics 49 | sCh chan StateMachineMetrics 50 | 51 | outCh chan<- Metrics 52 | 53 | done chan struct{} 54 | } 55 | 56 | func NewCollector(ctx context.Context, bufSize int, outCh chan<- Metrics) *Collector { 57 | c := &Collector{ 58 | mCh: make(chan MirrorMetrics, bufSize), 59 | sCh: make(chan StateMachineMetrics, bufSize), 60 | 61 | outCh: outCh, 62 | 63 | done: make(chan struct{}), 64 | } 65 | go c.background(ctx) 66 | return c 67 | } 68 | 69 | func (c *Collector) UpdateMirror(m MirrorMetrics) { 70 | select { 71 | case c.mCh <- m: 72 | default: 73 | } 74 | } 75 | 76 | func (c *Collector) UpdateStateMachine(m StateMachineMetrics) { 77 | select { 78 | case c.sCh <- m: 79 | default: 80 | } 81 | } 82 | 83 | func (c *Collector) Wait() { 84 | <-c.done 85 | } 86 | 87 | func (c *Collector) background(ctx context.Context) { 88 | defer close(c.done) 89 | 90 | var cur Metrics 91 | 92 | var gotM, gotS, outdated bool 93 | for { 94 | // Don't attempt to send the output until 95 | // we've written both mirror and state machine metrics. 96 | var outCh chan<- Metrics 97 | if gotM && gotS && outdated { 98 | outCh = c.outCh 99 | } 100 | 101 | select { 102 | case <-ctx.Done(): 103 | return 104 | 105 | case m := <-c.mCh: 106 | cur.MirrorCommittingHeight = m.CH 107 | cur.MirrorCommittingRound = m.CR 108 | cur.MirrorVotingHeight = m.VH 109 | cur.MirrorVotingRound = m.VR 110 | 111 | gotM = true 112 | outdated = true 113 | 114 | case s := <-c.sCh: 115 | cur.StateMachineHeight = s.H 116 | cur.StateMachineRound = s.R 117 | 118 | gotS = true 119 | outdated = true 120 | 121 | case outCh <- cur: 122 | // Okay. 123 | outdated = false 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmmirror/backfillcommitstatus.go: -------------------------------------------------------------------------------- 1 | package tmmirror 2 | 3 | type backfillCommitStatus uint8 4 | 5 | //go:generate go run golang.org/x/tools/cmd/stringer -type backfillCommitStatus -trimprefix=backfillCommit 6 | const ( 7 | // Invalid zero value. 8 | backfillCommitInvalid backfillCommitStatus = iota 9 | 10 | // Commit info was successfully backfilled. 11 | backfillCommitAccepted 12 | 13 | // The commit proof's PubKeyHash doesn't match what is in the round view. 14 | backfillCommitPubKeyHashMismatch 15 | 16 | // The backfill was rejected. 17 | // Maybe this should be more granular to indicate the reason it was rejected. 18 | backfillCommitRejected 19 | ) 20 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmmirror/backfillcommitstatus_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type backfillCommitStatus -trimprefix=backfillCommit"; DO NOT EDIT. 2 | 3 | package tmmirror 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[backfillCommitInvalid-0] 12 | _ = x[backfillCommitAccepted-1] 13 | _ = x[backfillCommitPubKeyHashMismatch-2] 14 | _ = x[backfillCommitRejected-3] 15 | } 16 | 17 | const _backfillCommitStatus_name = "InvalidAcceptedPubKeyHashMismatchRejected" 18 | 19 | var _backfillCommitStatus_index = [...]uint8{0, 7, 15, 33, 41} 20 | 21 | func (i backfillCommitStatus) String() string { 22 | if i >= backfillCommitStatus(len(_backfillCommitStatus_index)-1) { 23 | return "backfillCommitStatus(" + strconv.FormatInt(int64(i), 10) + ")" 24 | } 25 | return _backfillCommitStatus_name[_backfillCommitStatus_index[i]:_backfillCommitStatus_index[i+1]] 26 | } 27 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmmirror/internal/tmi/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmi is the internal package for tmmirror. 2 | // 3 | // The tmmirror package wants to only expose the core Mirror type, 4 | // but there are many other types that have wide scope, 5 | // so organizing them into an internal package for finer control 6 | // over what is exported and what isn't, is appropriate here. 7 | package tmi 8 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmmirror/internal/tmi/gossipviewmanager.go: -------------------------------------------------------------------------------- 1 | package tmi 2 | 3 | import ( 4 | "github.com/gordian-engine/gordian/tm/tmconsensus" 5 | "github.com/gordian-engine/gordian/tm/tmengine/tmelink" 6 | ) 7 | 8 | type gossipViewManager struct { 9 | out chan<- tmelink.NetworkViewUpdate 10 | 11 | // When the kernel transitions from one voting round to another, 12 | // we need to emit the nil-committed round to the gossip strategy. 13 | // This field holds that value until it is sent to the gossip strategy. 14 | NilVotedRound *tmconsensus.VersionedRoundView 15 | 16 | Committing, Voting, NextRound OutgoingView 17 | } 18 | 19 | func newGossipViewManager(out chan<- tmelink.NetworkViewUpdate) gossipViewManager { 20 | return gossipViewManager{out: out} 21 | } 22 | 23 | func (m *gossipViewManager) Output() gossipStrategyOutput { 24 | o := gossipStrategyOutput{m: m} 25 | 26 | // TODO: The eager cloning here likely creates extra garbage that we accidentally can't use, 27 | // but we should be able to reduce it by overwriting existing values, 28 | // or by using pooled VRVs. 29 | 30 | // In each check whether the view has been sent, 31 | // we unconditionally (re)assign the output channel. 32 | // If we don't hit any of those checks, the output channel will be nil, 33 | // so that case will not be considered in the select. 34 | 35 | if !m.Committing.HasBeenSent() { 36 | o.Ch = m.out 37 | 38 | val := m.Committing.VRV.Clone() 39 | o.Val.Committing = &val 40 | } 41 | 42 | if !m.Voting.HasBeenSent() { 43 | o.Ch = m.out 44 | 45 | val := m.Voting.VRV.Clone() 46 | o.Val.Voting = &val 47 | } 48 | 49 | if !m.NextRound.HasBeenSent() { 50 | o.Ch = m.out 51 | 52 | val := m.NextRound.VRV.Clone() 53 | o.Val.NextRound = &val 54 | } 55 | 56 | // The nil voted round handling is a little different. 57 | // There is not particular version handling for a nil voted round; 58 | // whatever we had when we advanced the round, we send. 59 | if m.NilVotedRound != nil { 60 | o.Ch = m.out 61 | 62 | o.Val.NilVotedRound = m.NilVotedRound 63 | } 64 | 65 | return o 66 | } 67 | 68 | type gossipStrategyOutput struct { 69 | m *gossipViewManager 70 | 71 | Ch chan<- tmelink.NetworkViewUpdate 72 | Val tmelink.NetworkViewUpdate 73 | } 74 | 75 | // MarkSent updates o's GossipViewManager to indicate the values in o 76 | // have successfully been sent. 77 | func (o gossipStrategyOutput) MarkSent() { 78 | if o.Val.Committing != nil { 79 | o.m.Committing.MarkSent() 80 | } 81 | 82 | if o.Val.Voting != nil { 83 | o.m.Voting.MarkSent() 84 | } 85 | 86 | if o.Val.NextRound != nil { 87 | o.m.NextRound.MarkSent() 88 | } 89 | 90 | // Always clear the NilVotedRound; no version tracking involved there. 91 | o.m.NilVotedRound = nil 92 | } 93 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmmirror/internal/tmi/kernel_debug.go: -------------------------------------------------------------------------------- 1 | //go:build debug 2 | 3 | package tmi 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/gordian-engine/gordian/gassert" 9 | "github.com/gordian-engine/gordian/tm/tmengine/tmelink" 10 | ) 11 | 12 | // invariantReplayedHeaderResponse asserts that err is 13 | // one of the few acceptable error types to send back on the [ReplayedHeaderResponse]. 14 | func invariantReplayedHeaderResponse(env gassert.Env, err error) { 15 | if !env.Enabled("tm.engine.mirror.kernel.replayed_header_response") { 16 | return 17 | } 18 | 19 | if err == nil { 20 | return 21 | } 22 | 23 | switch err.(type) { 24 | case tmelink.ReplayedHeaderValidationError, 25 | tmelink.ReplayedHeaderOutOfSyncError, 26 | tmelink.ReplayedHeaderInternalError: 27 | return 28 | } 29 | 30 | env.HandleAssertionFailure(fmt.Errorf( 31 | "illegal error type %T returned in response to replayed header: %w", err, err, 32 | )) 33 | } 34 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmmirror/internal/tmi/kernel_nodebug.go: -------------------------------------------------------------------------------- 1 | //go:build !debug 2 | 3 | // Code generated by github.com/gordian-engine/gordian/gassert/cmd/generate-nodebug kernel_debug.go; DO NOT EDIT. 4 | 5 | package tmi 6 | 7 | import ( 8 | "github.com/gordian-engine/gordian/gassert" 9 | ) 10 | 11 | func invariantReplayedHeaderResponse(env gassert.Env, err error) {} 12 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmmirror/internal/tmi/lag.go: -------------------------------------------------------------------------------- 1 | package tmi 2 | 3 | import ( 4 | "github.com/gordian-engine/gordian/tm/tmengine/tmelink" 5 | ) 6 | 7 | // lagManager holds the current lag state and whether it has been sent. 8 | type lagManager struct { 9 | outCh chan<- tmelink.LagState 10 | 11 | state tmelink.LagState 12 | 13 | sent bool 14 | } 15 | 16 | func newLagManager(out chan<- tmelink.LagState) lagManager { 17 | return lagManager{outCh: out} 18 | } 19 | 20 | func (m *lagManager) SetState( 21 | s tmelink.LagStatus, 22 | committingHeight, needHeight uint64, 23 | ) { 24 | if m.outCh == nil { 25 | // The lag manager should rarely be unset, 26 | // but no need to copy a few values around if we never output anything. 27 | return 28 | } 29 | 30 | if m.state.Status != s { 31 | m.state.Status = s 32 | m.sent = false 33 | } 34 | 35 | m.state.CommittingHeight = committingHeight 36 | m.state.NeedHeight = needHeight 37 | } 38 | 39 | // Output returns a LagOutput, 40 | // containing a destination channel and a LagState value to send. 41 | // 42 | // If the most recent lag state has already been sent, 43 | // the output channel is nil, so the send will block forever. 44 | func (m *lagManager) Output() LagOutput { 45 | if m.outCh == nil || m.sent { 46 | // Lag output not configured, or already up to date; 47 | // nil output channel will block sends forever. 48 | return LagOutput{} 49 | } 50 | 51 | return LagOutput{ 52 | m: m, 53 | Ch: m.outCh, 54 | Val: m.state, 55 | } 56 | } 57 | 58 | // MarkSent must be called after a successful send of o.State to o.Ch. 59 | func (o LagOutput) MarkSent() { 60 | o.m.sent = true 61 | } 62 | 63 | // LagOutput is the value returned by [*LagManager.Output]. 64 | type LagOutput struct { 65 | m *lagManager 66 | Ch chan<- tmelink.LagState 67 | Val tmelink.LagState 68 | } 69 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmmirror/internal/tmi/networkheightround.go: -------------------------------------------------------------------------------- 1 | package tmi 2 | 3 | import "context" 4 | 5 | // NetworkHeightRound is the Mirror's view of the different rounds in the network. 6 | // 7 | // This is a convenience type for now, and it may change as the Mirror evolves. 8 | type NetworkHeightRound struct { 9 | VotingHeight uint64 10 | VotingRound uint32 11 | 12 | CommittingHeight uint64 // Is this field necessary? Would CommittingHeight ever differ from VotingHeight-1? 13 | CommittingRound uint32 14 | } 15 | 16 | // NetworkHeightRoundFromStore parses the outputs of [tmstore.MirrorStore.NetworkHeightRoundFromStore] 17 | // such that you can call: 18 | // 19 | // nhr, err := NetworkHeightRoundFromStore(s.NetworkHeightRound(ctx)) 20 | func NetworkHeightRoundFromStore( 21 | votingHeight uint64, votingRound uint32, 22 | committingHeight uint64, committingRound uint32, 23 | e error, 24 | ) (NetworkHeightRound, error) { 25 | return NetworkHeightRound{ 26 | VotingHeight: votingHeight, 27 | VotingRound: votingRound, 28 | CommittingHeight: committingHeight, 29 | CommittingRound: committingRound, 30 | }, e 31 | } 32 | 33 | // ForStore explodes nhr into positional arguments for [tmstore.MirrorStore.SetNetworkHeightRound] 34 | // such that you can call: 35 | // 36 | // err := s.SetNetworkHeightRound(nhr.ForStore(ctx)) 37 | func (nhr NetworkHeightRound) ForStore(ctx context.Context) ( 38 | context.Context, uint64, uint32, uint64, uint32, 39 | ) { 40 | return ctx, 41 | nhr.VotingHeight, nhr.VotingRound, 42 | nhr.CommittingHeight, nhr.CommittingRound 43 | } 44 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmmirror/internal/tmi/phcheck.go: -------------------------------------------------------------------------------- 1 | package tmi 2 | 3 | import ( 4 | "github.com/gordian-engine/gordian/gcrypto" 5 | "github.com/gordian-engine/gordian/tm/tmconsensus" 6 | ) 7 | 8 | type PHCheckRequest struct { 9 | PH tmconsensus.ProposedHeader 10 | Resp chan PHCheckResponse 11 | } 12 | 13 | type PHCheckResponse struct { 14 | Status PHCheckStatus 15 | 16 | // If the status is PHCheckAcceptable, this is the matching public key 17 | // so that the calling goroutine can validate the proposed header signature. 18 | ProposerPubKey gcrypto.PubKey 19 | 20 | // If the status is PHCheckAcceptable -- which can only happen when 21 | // the proposed header matches the voting or committing heights -- 22 | // this is the hash of the previous block. 23 | // If the proposed height is committing, this is the hash of the committed block. 24 | // If the proposed height is voting, then this is the hash of the committing view's block. 25 | // 26 | // This byte slice must not be modified. 27 | PrevBlockHash []byte 28 | 29 | // The validator set corresponding to the PrevBlockHash. 30 | // This value must also not be modified. 31 | PrevValidatorSet tmconsensus.ValidatorSet 32 | 33 | // If the status is PHCheckNextHeight, this is a clone of the voting view. 34 | VotingRoundView *tmconsensus.RoundView 35 | } 36 | 37 | type PHCheckStatus uint8 38 | 39 | //go:generate go run golang.org/x/tools/cmd/stringer -type PHCheckStatus -trimprefix=PHCheck 40 | const ( 41 | // Invalid value for 0. 42 | PHCheckInvalid PHCheckStatus = iota 43 | 44 | // We don't have the proposed header and it looks like it could be applied. 45 | // The calling goroutine from the Mirror (not the Kernel) 46 | // must still perform signature, hash, and any other validation. 47 | PHCheckAcceptable 48 | 49 | // Special case: we need to apply the previous commit info into the voting height. 50 | PHCheckNextHeight 51 | 52 | // We already have a proposed header with this signature. 53 | // It is possible that the proposed header is maliciously crafted, 54 | // with an invalid signature that matches an existing valid signature. 55 | // If we do propagate this through the network, 56 | // a node missing the proposed header will reject the original sender. 57 | PHCheckAlreadyHaveSignature 58 | 59 | // The header would have possibly been acceptable, 60 | // but the reported proposer public key did not match the known validators for that height. 61 | PHCheckSignerUnrecognized 62 | 63 | // The proposed header references an out-of-bounds round that is too old. 64 | PHCheckRoundTooOld 65 | 66 | // The proposed header references an out-of-bounds round that is too far in the future. 67 | PHCheckRoundTooFarInFuture 68 | ) 69 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmmirror/internal/tmi/phcheckstatus_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type PHCheckStatus -trimprefix=PHCheck"; DO NOT EDIT. 2 | 3 | package tmi 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[PHCheckInvalid-0] 12 | _ = x[PHCheckAcceptable-1] 13 | _ = x[PHCheckNextHeight-2] 14 | _ = x[PHCheckAlreadyHaveSignature-3] 15 | _ = x[PHCheckSignerUnrecognized-4] 16 | _ = x[PHCheckRoundTooOld-5] 17 | _ = x[PHCheckRoundTooFarInFuture-6] 18 | } 19 | 20 | const _PHCheckStatus_name = "InvalidAcceptableNextHeightAlreadyHaveSignatureSignerUnrecognizedRoundTooOldRoundTooFarInFuture" 21 | 22 | var _PHCheckStatus_index = [...]uint8{0, 7, 17, 27, 47, 65, 76, 95} 23 | 24 | func (i PHCheckStatus) String() string { 25 | if i >= PHCheckStatus(len(_PHCheckStatus_index)-1) { 26 | return "PHCheckStatus(" + strconv.FormatInt(int64(i), 10) + ")" 27 | } 28 | return _PHCheckStatus_name[_PHCheckStatus_index[i]:_PHCheckStatus_index[i+1]] 29 | } 30 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmmirror/internal/tmi/snapshot.go: -------------------------------------------------------------------------------- 1 | package tmi 2 | 3 | import "github.com/gordian-engine/gordian/tm/tmconsensus" 4 | 5 | // Snapshot is a copy of the kernel's state, 6 | // used in methods running on other goroutines 7 | // that need to know the kernel's current view of the world. 8 | type Snapshot struct { 9 | Voting, Committing *tmconsensus.VersionedRoundView 10 | } 11 | 12 | // snapshotRequest is used when an external goroutine 13 | // needs an up-to-date copy of the kernel state, 14 | // and it knows exactly which view it is looking for. 15 | type SnapshotRequest struct { 16 | Snapshot *Snapshot 17 | 18 | // TODO: if allocating a Ready channel every request shows up in profiles at all, 19 | // the mirror could manage a pool of response channels. 20 | Ready chan struct{} 21 | 22 | // Which fields to include in the request. 23 | Fields RVFieldFlags 24 | } 25 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmmirror/internal/tmi/view.go: -------------------------------------------------------------------------------- 1 | package tmi 2 | 3 | import ( 4 | "github.com/gordian-engine/gordian/tm/tmconsensus" 5 | ) 6 | 7 | // ViewID is the identifier to distinguish View values from one another. 8 | type ViewID uint8 9 | 10 | //go:generate go run golang.org/x/tools/cmd/stringer -type ViewID -trimprefix=ViewID . 11 | const ( 12 | ViewIDNotFound ViewID = iota 13 | 14 | ViewIDVoting 15 | ViewIDCommitting 16 | ViewIDNextRound 17 | ViewIDNextHeight 18 | ) 19 | 20 | // View holds a maintained round view and associated metadata. 21 | type View struct { 22 | VRV tmconsensus.VersionedRoundView 23 | Outgoing OutgoingView 24 | } 25 | 26 | func (v *View) UpdateOutgoing() { 27 | v.VRV.Version++ 28 | 29 | if v.Outgoing.HasBeenSent() { 30 | // Current slices and maps could be in use by the outgoing consumer, 31 | // so use a whole new clone. 32 | v.Outgoing.VRV = v.VRV.Clone() 33 | return 34 | } 35 | 36 | // TODO: as a memory optimization, we should be able to modify the outgoing VRV in-place. 37 | // Since it hasn't been sent yet, there should be no references outside of v. 38 | // But for now we will continue to clone it. 39 | v.Outgoing.VRV = v.VRV.Clone() 40 | } 41 | 42 | type OutgoingView struct { 43 | SentH uint64 // Height. 44 | SentR uint32 // Round. 45 | SentV uint32 // RoundView version. 46 | 47 | VRV tmconsensus.VersionedRoundView 48 | } 49 | 50 | func (v OutgoingView) HasBeenSent() bool { 51 | return v.SentH == v.VRV.Height && 52 | v.SentR == v.VRV.Round && 53 | v.SentV == v.VRV.Version 54 | } 55 | 56 | func (v *OutgoingView) MarkSent() { 57 | v.SentH = v.VRV.Height 58 | v.SentR = v.VRV.Round 59 | v.SentV = v.VRV.Version 60 | } 61 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmmirror/internal/tmi/viewid_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type ViewID -trimprefix=ViewID ."; DO NOT EDIT. 2 | 3 | package tmi 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ViewIDNotFound-0] 12 | _ = x[ViewIDVoting-1] 13 | _ = x[ViewIDCommitting-2] 14 | _ = x[ViewIDNextRound-3] 15 | _ = x[ViewIDNextHeight-4] 16 | } 17 | 18 | const _ViewID_name = "NotFoundVotingCommittingNextRoundNextHeight" 19 | 20 | var _ViewID_index = [...]uint8{0, 8, 14, 24, 33, 43} 21 | 22 | func (i ViewID) String() string { 23 | if i >= ViewID(len(_ViewID_index)-1) { 24 | return "ViewID(" + strconv.FormatInt(int64(i), 10) + ")" 25 | } 26 | return _ViewID_name[_ViewID_index[i]:_ViewID_index[i+1]] 27 | } 28 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmmirror/internal/tmi/viewlookup.go: -------------------------------------------------------------------------------- 1 | package tmi 2 | 3 | import ( 4 | "github.com/gordian-engine/gordian/tm/tmconsensus" 5 | ) 6 | 7 | // RVFieldFlags is used when looking up 8 | // a [tmconsensus.RoundView] or [tmconsensus.VersionedRoundView] 9 | // to indicate which fields should be set in the response. 10 | // 11 | // We assume that it is relatively expensive to copy or clone 12 | // the potentially large sets of proposed blocks or votes, 13 | // so by requesting specific fields, we avoid unnecessary work 14 | // and make a possibly non-negligible garbage reduction. 15 | type RVFieldFlags uint8 16 | 17 | const ( 18 | RVValidators RVFieldFlags = 1 << iota 19 | RVProposedBlocks 20 | RVPrevotes 21 | RVPrecommits 22 | RVVoteSummary 23 | RVPrevCommitProof 24 | 25 | RVAll = (RVValidators | RVProposedBlocks | RVPrevotes | RVPrecommits | RVVoteSummary | RVPrevCommitProof) 26 | ) 27 | 28 | // ViewLookupRequest is a request to copy a view from the kernel, 29 | // by looking up an existing view by its height and round. 30 | type ViewLookupRequest struct { 31 | H uint64 32 | R uint32 33 | 34 | // Which fields on VRV to populate. 35 | Fields RVFieldFlags 36 | 37 | // Reference to a VersionedRoundView, 38 | // to be populated by the kernel if there is a matching view for H and R. 39 | VRV *tmconsensus.VersionedRoundView 40 | 41 | // Reason for looking up the view; for debugging. 42 | // Must not be empty. 43 | Reason string 44 | 45 | // The requester must set this to a 1-buffered channel. 46 | // Once the kernel sends a response, 47 | // the VRV field may be inspected. 48 | Resp chan ViewLookupResponse 49 | } 50 | 51 | type ViewLookupResponse struct { 52 | // The ID of the matching view, or notFoundViewID (0) if no view matched. 53 | ID ViewID 54 | 55 | // The status is viewFound if request.VRV is populated, 56 | // otherwise it explains the reason why no view was found. 57 | Status ViewLookupStatus 58 | } 59 | 60 | // ViewLookupStatus indicates the reason a view lookup failed to match an existing view. 61 | type ViewLookupStatus uint8 62 | 63 | //go:generate go run golang.org/x/tools/cmd/stringer -type ViewLookupStatus -trimprefix=View 64 | const ( 65 | ViewFound ViewLookupStatus = iota 66 | 67 | // Earlier than the committing height and round. 68 | ViewBeforeCommitting 69 | 70 | // Correct committing height but round is too far. 71 | // It should be impossible for two well-behaving clients 72 | // to disagree on which round is being committed in a height. 73 | ViewWrongCommit 74 | 75 | // Same height as the voting view, but an earlier round. 76 | ViewOrphaned 77 | 78 | // Same height as voting view but a later round than even NextRound. 79 | // If the incoming data is valid, then we may have missed some votes. 80 | ViewLaterVotingRound 81 | 82 | // The requested height and round is beyond NextHeight and NextRound. 83 | // The data may still be valid. 84 | ViewFuture 85 | ) 86 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmmirror/internal/tmi/viewlookupstatus_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type ViewLookupStatus -trimprefix=View"; DO NOT EDIT. 2 | 3 | package tmi 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ViewFound-0] 12 | _ = x[ViewBeforeCommitting-1] 13 | _ = x[ViewWrongCommit-2] 14 | _ = x[ViewOrphaned-3] 15 | _ = x[ViewLaterVotingRound-4] 16 | _ = x[ViewFuture-5] 17 | } 18 | 19 | const _ViewLookupStatus_name = "FoundBeforeCommittingWrongCommitOrphanedLaterVotingRoundFuture" 20 | 21 | var _ViewLookupStatus_index = [...]uint8{0, 5, 21, 32, 40, 56, 62} 22 | 23 | func (i ViewLookupStatus) String() string { 24 | if i >= ViewLookupStatus(len(_ViewLookupStatus_index)-1) { 25 | return "ViewLookupStatus(" + strconv.FormatInt(int64(i), 10) + ")" 26 | } 27 | return _ViewLookupStatus_name[_ViewLookupStatus_index[i]:_ViewLookupStatus_index[i+1]] 28 | } 29 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmmirror/internal/tmi/votedistribution.go: -------------------------------------------------------------------------------- 1 | package tmi 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/bits-and-blooms/bitset" 10 | "github.com/gordian-engine/gordian/gcrypto" 11 | "github.com/gordian-engine/gordian/tm/tmconsensus" 12 | ) 13 | 14 | // voteDistribution was an artifact of the no-longer-present tmconsensus.RoundState2 type. 15 | // This type should be removed in favor of the VoteSummary field of tmconsensus.VersionedRoundView. 16 | type voteDistribution struct { 17 | AvailableVotePower uint64 18 | 19 | VotePowerPresent uint64 20 | 21 | // Vote power by block hash. 22 | // Empty string key indicates a nil vote. 23 | BlockVotePower map[string]uint64 24 | } 25 | 26 | func newVoteDistribution(proofs map[string]gcrypto.CommonMessageSignatureProof, vals []tmconsensus.Validator) voteDistribution { 27 | d := voteDistribution{ 28 | BlockVotePower: make(map[string]uint64, len(proofs)), 29 | } 30 | 31 | valsByKey := make(map[string]uint64, len(vals)) 32 | for _, v := range vals { 33 | valsByKey[string(v.PubKey.PubKeyBytes())] = v.Power 34 | 35 | d.AvailableVotePower += v.Power 36 | } 37 | 38 | // TODO: derive hash of trustedVals, ensure each proof matches that hash. 39 | // Otherwise we risk reading an invalid proof and incorrectly calculating vote power. 40 | 41 | // TODO: ensure we don't double count a validator, 42 | // if one public key is present in multiple votes somehow. 43 | 44 | var bs bitset.BitSet 45 | for blockHash, proof := range proofs { 46 | proof.SignatureBitSet(&bs) 47 | for i, ok := bs.NextSet(0); ok && int(i) < len(vals); i, ok = bs.NextSet(i + 1) { 48 | pow := vals[int(i)].Power 49 | d.BlockVotePower[string(blockHash)] += pow 50 | d.VotePowerPresent += pow 51 | } 52 | } 53 | 54 | return d 55 | } 56 | 57 | // LogValue produces an slog.Value for cases where the VoteDistribution needs to be logged. 58 | func (d voteDistribution) LogValue() slog.Value { 59 | voteBlocks := make([]string, 0, len(d.BlockVotePower)) 60 | for hash, pow := range d.BlockVotePower { 61 | if hash == "" { 62 | voteBlocks = append(voteBlocks, fmt.Sprintf("nil => %d", pow)) 63 | } else { 64 | voteBlocks = append(voteBlocks, fmt.Sprintf("%x => %d", hash, pow)) 65 | } 66 | } 67 | sort.Strings(voteBlocks) 68 | return slog.GroupValue( 69 | slog.Uint64("vote_power_present", d.VotePowerPresent), 70 | slog.Uint64("available_vote_power", d.AvailableVotePower), 71 | slog.String("block_vote_power", strings.Join(voteBlocks, ", ")), 72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmmirror/internal/tmi/votes.go: -------------------------------------------------------------------------------- 1 | package tmi 2 | 3 | import "github.com/gordian-engine/gordian/gcrypto" 4 | 5 | type AddPrevoteRequest struct { 6 | H uint64 7 | R uint32 8 | 9 | PrevoteUpdates map[string]VoteUpdate 10 | 11 | Response chan AddVoteResult 12 | } 13 | 14 | type AddPrecommitRequest struct { 15 | H uint64 16 | R uint32 17 | 18 | PrecommitUpdates map[string]VoteUpdate 19 | 20 | Response chan AddVoteResult 21 | } 22 | 23 | // VoteUpdate is part of AddPrevoteRequest and AddPrecommitRequest, 24 | // indicating the new vote content and the previous version. 25 | // The kernel uses the previous version to decide if the update 26 | // can be applied or if the update is stale. 27 | type VoteUpdate struct { 28 | Proof gcrypto.CommonMessageSignatureProof 29 | PrevVersion uint32 30 | } 31 | 32 | // AddVoteResult is the result when applying an AddPrevoteRequest or AddPrecommitRequest. 33 | type AddVoteResult uint8 34 | 35 | const ( 36 | _ AddVoteResult = iota // Invalid. 37 | 38 | AddVoteAccepted // Votes successfully applied. 39 | AddVoteConflict // Version conflict when applying votes; do a retry. 40 | AddVoteOutOfDate // Height and round too old; message should be ignored. 41 | ) 42 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmmirror/tmmirrortest/fixture_test.go: -------------------------------------------------------------------------------- 1 | package tmmirrortest_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/bits-and-blooms/bitset" 9 | "github.com/gordian-engine/gordian/tm/tmengine/internal/tmmirror" 10 | "github.com/gordian-engine/gordian/tm/tmengine/internal/tmmirror/internal/tmi" 11 | "github.com/gordian-engine/gordian/tm/tmengine/internal/tmmirror/tmmirrortest" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestFixture_CommitInitialHeight(t *testing.T) { 16 | for _, nVals := range []int{2, 4} { 17 | nVals := nVals 18 | t.Run(fmt.Sprintf("with %d validators", nVals), func(t *testing.T) { 19 | t.Parallel() 20 | 21 | ctx, cancel := context.WithCancel(context.Background()) 22 | defer cancel() 23 | 24 | mfx := tmmirrortest.NewFixture(ctx, t, nVals) 25 | 26 | committers := make([]int, nVals) 27 | for i := range committers { 28 | committers[i] = i 29 | } 30 | mfx.CommitInitialHeight( 31 | ctx, 32 | []byte("initial_height"), 0, 33 | committers, 34 | ) 35 | 36 | // Now assert that the stores have the expected content. 37 | phs, _, precommits, err := mfx.Cfg.RoundStore.LoadRoundState(ctx, 1, 0) 38 | require.NoError(t, err) 39 | 40 | require.Len(t, phs, 1) 41 | 42 | ph := phs[0] 43 | require.Equal(t, []byte("initial_height"), ph.Header.DataID) 44 | 45 | require.Len(t, precommits.BlockSignatures, 1) 46 | fullPrecommits, err := precommits.ToFullPrecommitProofMap( 47 | 1, 0, 48 | mfx.Fx.ValSet(), 49 | mfx.Fx.SignatureScheme, mfx.Fx.CommonMessageSignatureProofScheme, 50 | ) 51 | require.NoError(t, err) 52 | 53 | precommitProof := fullPrecommits[string(ph.Header.Hash)] 54 | require.NotNil(t, precommitProof) 55 | 56 | var bs bitset.BitSet 57 | precommitProof.SignatureBitSet(&bs) 58 | require.Equal(t, uint(nVals), bs.Count()) 59 | 60 | // The mirror store has the right height and round. 61 | nhr, err := tmi.NetworkHeightRoundFromStore(mfx.Cfg.Store.NetworkHeightRound(ctx)) 62 | require.NoError(t, err) 63 | 64 | require.Equal(t, tmmirror.NetworkHeightRound{ 65 | CommittingHeight: 1, 66 | CommittingRound: 0, 67 | VotingHeight: 2, 68 | VotingRound: 0, 69 | }, nhr) 70 | 71 | // And if we generate another proposed block, it is at the right height. 72 | nextPH := mfx.Fx.NextProposedHeader([]byte("x"), 0) 73 | 74 | require.Equal(t, uint64(2), nextPH.Header.Height) 75 | require.Zero(t, nextPH.Round) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmstate/internal/tsi/roundlifecycle_debug.go: -------------------------------------------------------------------------------- 1 | //go:build debug 2 | 3 | package tsi 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | ) 9 | 10 | func (rlc *RoundLifecycle) invariantCycleFinalization() { 11 | if !rlc.AssertEnv.Enabled("tm.engine.state_machine.rlc") { 12 | return 13 | } 14 | 15 | var err error 16 | if len(rlc.FinalizedValSet.Validators) == 0 { 17 | err = errors.New("rlc.FinalizedValidators is empty") 18 | } 19 | 20 | if rlc.FinalizedAppStateHash == "" { 21 | err = errors.Join(err, errors.New("rlc.FinalizedAppStateHash is empty")) 22 | } 23 | 24 | if rlc.FinalizedBlockHash == "" { 25 | err = errors.Join(err, errors.New("rlc.FinalizedBlockHash is empty")) 26 | } 27 | 28 | if err != nil { 29 | rlc.AssertEnv.HandleAssertionFailure( 30 | fmt.Errorf("invariant for CycleFinalization failed at %d/%d: %w", rlc.H, rlc.R, err), 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmstate/internal/tsi/roundlifecycle_nodebug.go: -------------------------------------------------------------------------------- 1 | //go:build !debug 2 | 3 | // Code generated by github.com/gordian-engine/gordian/gassert/cmd/generate-nodebug roundlifecycle_debug.go; DO NOT EDIT. 4 | 5 | package tsi 6 | 7 | func (rlc *RoundLifecycle) invariantCycleFinalization() {} 8 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmstate/internal/tsi/step_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type Step -trimprefix=Step ."; DO NOT EDIT. 2 | 3 | package tsi 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[StepInvalid-0] 12 | _ = x[StepAwaitingProposal-1] 13 | _ = x[StepAwaitingPrevotes-2] 14 | _ = x[StepPrevoteDelay-3] 15 | _ = x[StepAwaitingPrecommits-4] 16 | _ = x[StepPrecommitDelay-5] 17 | _ = x[StepCommitWait-6] 18 | _ = x[StepAwaitingFinalization-7] 19 | } 20 | 21 | const _Step_name = "InvalidAwaitingProposalAwaitingPrevotesPrevoteDelayAwaitingPrecommitsPrecommitDelayCommitWaitAwaitingFinalization" 22 | 23 | var _Step_index = [...]uint8{0, 7, 23, 39, 51, 69, 83, 93, 113} 24 | 25 | func (i Step) String() string { 26 | if i >= Step(len(_Step_index)-1) { 27 | return "Step(" + strconv.FormatInt(int64(i), 10) + ")" 28 | } 29 | return _Step_name[_Step_index[i]:_Step_index[i+1]] 30 | } 31 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmstate/tmstatetest/proposedheaders.go: -------------------------------------------------------------------------------- 1 | package tmstatetest 2 | 3 | import ( 4 | "github.com/gordian-engine/gordian/tm/tmconsensus" 5 | "github.com/gordian-engine/gordian/tm/tmconsensus/tmconsensustest" 6 | ) 7 | 8 | // ProposedHeaderMutator is returned by [UnacceptableProposedHeaderMutations] 9 | // to be used in table-driven tests when modifying an otherwise good proposed header 10 | // to be ignored by the state machine. 11 | type ProposedHeaderMutator struct { 12 | Name string 13 | Mutate func(*tmconsensus.ProposedHeader) 14 | } 15 | 16 | // UnacceptableProposedHeaderMutations returns a slice of mutators 17 | // that can be used in table-driven tests to ensure that the state machine 18 | // ignores certain invalid proposed headers. 19 | // 20 | // The nCurVals and nNextVals arguments are used to determine 21 | // an incorrect number of validators to set in the proposed header, 22 | // in order to be ignored. 23 | func UnacceptableProposedHeaderMutations( 24 | hs tmconsensus.HashScheme, nCurVals, nNextVals int, 25 | ) []ProposedHeaderMutator { 26 | return []ProposedHeaderMutator{ 27 | { 28 | Name: "wrong PrevAppStateHash", 29 | Mutate: func(ph *tmconsensus.ProposedHeader) { 30 | ph.Header.PrevAppStateHash = []byte("wrong") 31 | }, 32 | }, 33 | { 34 | Name: "extra current validator", 35 | Mutate: func(ph *tmconsensus.ProposedHeader) { 36 | var err error 37 | ph.Header.ValidatorSet, err = tmconsensus.NewValidatorSet( 38 | tmconsensustest.DeterministicValidatorsEd25519(nCurVals+1).Vals(), 39 | hs, 40 | ) 41 | if err != nil { 42 | panic(err) 43 | } 44 | }, 45 | }, 46 | { 47 | Name: "extra next validator", 48 | Mutate: func(ph *tmconsensus.ProposedHeader) { 49 | var err error 50 | ph.Header.NextValidatorSet, err = tmconsensus.NewValidatorSet( 51 | tmconsensustest.DeterministicValidatorsEd25519(nNextVals+1).Vals(), 52 | hs, 53 | ) 54 | if err != nil { 55 | panic(err) 56 | } 57 | }, 58 | }, 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tm/tmengine/internal/tmstate/tmstatetest/roundtimer_test.go: -------------------------------------------------------------------------------- 1 | package tmstatetest_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/gordian-engine/gordian/internal/gtest" 8 | "github.com/gordian-engine/gordian/tm/tmengine/internal/tmstate/tmstatetest" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestMockRoundTimer_sync(t *testing.T) { 13 | ctx := context.Background() 14 | for _, tc := range []struct { 15 | name string 16 | notifyFn func(rt *tmstatetest.MockRoundTimer, h uint64, r uint32) <-chan struct{} 17 | timerFn func(rt *tmstatetest.MockRoundTimer, h uint64, r uint32) (<-chan struct{}, func()) 18 | }{ 19 | { 20 | name: "proposal", 21 | notifyFn: func(rt *tmstatetest.MockRoundTimer, h uint64, r uint32) <-chan struct{} { 22 | return rt.ProposalStartNotification(h, r) 23 | }, 24 | timerFn: func(rt *tmstatetest.MockRoundTimer, h uint64, r uint32) (<-chan struct{}, func()) { 25 | return rt.ProposalTimer(ctx, h, r) 26 | }, 27 | }, 28 | { 29 | name: "prevote delay", 30 | notifyFn: func(rt *tmstatetest.MockRoundTimer, h uint64, r uint32) <-chan struct{} { 31 | return rt.PrevoteDelayStartNotification(h, r) 32 | }, 33 | timerFn: func(rt *tmstatetest.MockRoundTimer, h uint64, r uint32) (<-chan struct{}, func()) { 34 | return rt.PrevoteDelayTimer(ctx, h, r) 35 | }, 36 | }, 37 | { 38 | name: "precommit delay", 39 | notifyFn: func(rt *tmstatetest.MockRoundTimer, h uint64, r uint32) <-chan struct{} { 40 | return rt.PrecommitDelayStartNotification(h, r) 41 | }, 42 | timerFn: func(rt *tmstatetest.MockRoundTimer, h uint64, r uint32) (<-chan struct{}, func()) { 43 | return rt.PrecommitDelayTimer(ctx, h, r) 44 | }, 45 | }, 46 | { 47 | name: "commit wait", 48 | notifyFn: func(rt *tmstatetest.MockRoundTimer, h uint64, r uint32) <-chan struct{} { 49 | return rt.CommitWaitStartNotification(h, r) 50 | }, 51 | timerFn: func(rt *tmstatetest.MockRoundTimer, h uint64, r uint32) (<-chan struct{}, func()) { 52 | return rt.CommitWaitTimer(ctx, h, r) 53 | }, 54 | }, 55 | } { 56 | tc := tc 57 | t.Run(tc.name, func(t *testing.T) { 58 | t.Parallel() 59 | 60 | var rt tmstatetest.MockRoundTimer 61 | 62 | ch := tc.notifyFn(&rt, 1, 0) 63 | require.NotNil(t, ch) 64 | gtest.NotSending(t, ch) 65 | 66 | elapsed, cancel := tc.timerFn(&rt, 1, 0) 67 | defer cancel() 68 | require.NotNil(t, elapsed) 69 | 70 | select { 71 | case <-ch: 72 | // Okay. 73 | default: 74 | t.Fatalf("%s start notification should have been closed immediately upon starting the timer", tc.name) 75 | } 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tm/tmengine/metrics.go: -------------------------------------------------------------------------------- 1 | package tmengine 2 | 3 | import "github.com/gordian-engine/gordian/tm/tmengine/internal/tmemetrics" 4 | 5 | // Metrics are the metrics for subsystems within the [Engine]. 6 | // The fields in this type should not be considered stable 7 | // and may change without notice between releases. 8 | // 9 | // The type alias is somewhat unfortunate, 10 | // but the alternative would be creating yet another package... 11 | type Metrics = tmemetrics.Metrics 12 | -------------------------------------------------------------------------------- /tm/tmengine/mirror.go: -------------------------------------------------------------------------------- 1 | package tmengine 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log/slog" 7 | 8 | "github.com/gordian-engine/gordian/tm/tmconsensus" 9 | "github.com/gordian-engine/gordian/tm/tmengine/internal/tmmirror" 10 | ) 11 | 12 | // The Mirror follows the state of the active validators on the network, 13 | // replicating the blocks and votes they produce. 14 | // 15 | // The Mirror is normally an internal component of a full [Engine] 16 | // including a state machine connected to a user-defined application. 17 | // However, in some cases, it may be desirable to run a Mirror by itself 18 | // for the sake of tracking the state of the rest of the network. 19 | type Mirror interface { 20 | tmconsensus.FineGrainedConsensusHandler 21 | 22 | // Wait blocks until the Mirror is finished. 23 | // Stop the mirror by canceling the context passed to [NewMirror]. 24 | Wait() 25 | } 26 | 27 | func NewMirror(ctx context.Context, log *slog.Logger, opts ...Opt) (Mirror, error) { 28 | // We borrow the engine options to configure the mirror, 29 | // so we need an Engine value to collect the options. 30 | // Note that we never start the Engine we instantiate. 31 | var e Engine 32 | 33 | var err error 34 | for _, opt := range opts { 35 | err = errors.Join(opt(&e, nil)) 36 | } 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | cfg := e.mCfg 42 | cfg.InitialHeight = e.genesis.InitialHeight 43 | cfg.InitialValidatorSet = e.genesis.GenesisValidatorSet 44 | 45 | if err := validateMirrorSettings(cfg); err != nil { 46 | return nil, err 47 | } 48 | 49 | m, err := tmmirror.NewMirror(ctx, log, cfg) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return m, nil 55 | } 56 | 57 | func validateMirrorSettings(cfg tmmirror.MirrorConfig) error { 58 | var err error 59 | 60 | if cfg.Store == nil { 61 | err = errors.Join(err, errors.New("no mirror store set (use tmengine.WithMirrorStore)")) 62 | } 63 | 64 | if cfg.CommittedHeaderStore == nil { 65 | err = errors.Join(err, errors.New("no committed header store set (use tmengine.WithCommittedHeaderStore)")) 66 | } 67 | 68 | if cfg.RoundStore == nil { 69 | err = errors.Join(err, errors.New("no round store set (use tmengine.WithRoundStore)")) 70 | } 71 | if cfg.ValidatorStore == nil { 72 | err = errors.Join(err, errors.New("no validator store set (use tmengine.WithValidatorStore)")) 73 | } 74 | 75 | // TODO: validate InitialHeight and InitialValidators? 76 | 77 | if cfg.HashScheme == nil { 78 | err = errors.Join(err, errors.New("no hash scheme set (use tmengine.WithHashScheme)")) 79 | } 80 | if cfg.SignatureScheme == nil { 81 | err = errors.Join(err, errors.New("no signature scheme set (use tmengine.WithSignatureScheme)")) 82 | } 83 | if cfg.CommonMessageSignatureProofScheme == nil { 84 | err = errors.Join(err, errors.New("no common message signature proof scheme set (use tmengine.WithCommonMessageSignatureProofScheme)")) 85 | } 86 | 87 | return err 88 | } 89 | -------------------------------------------------------------------------------- /tm/tmengine/timeoutstrategy.go: -------------------------------------------------------------------------------- 1 | package tmengine 2 | 3 | import "time" 4 | 5 | // TimeoutStrategy informs the state machine how to calculate timeouts. 6 | // While the individual methods all include a height parameter, 7 | // the height will rarely if ever be used in calculating the timeout duration. 8 | // The height is more intended as a mechanism to coordinate changing the timeouts 9 | // after a certain height. 10 | type TimeoutStrategy interface { 11 | ProposalTimeout(height uint64, round uint32) time.Duration 12 | PrevoteDelayTimeout(height uint64, round uint32) time.Duration 13 | PrecommitDelayTimeout(height uint64, round uint32) time.Duration 14 | CommitWaitTimeout(height uint64, round uint32) time.Duration 15 | } 16 | 17 | // LinearTimeoutStrategy provides timeout durations that increase linearly with round increases. 18 | // If any of the provided values are zero, reasonable defaults are used. 19 | type LinearTimeoutStrategy struct { 20 | ProposalBase time.Duration 21 | ProposalIncrement time.Duration 22 | 23 | PrevoteDelayBase time.Duration 24 | PrevoteDelayIncrement time.Duration 25 | 26 | PrecommitDelayBase time.Duration 27 | PrecommitDelayIncrement time.Duration 28 | 29 | CommitWaitBase time.Duration 30 | CommitWaitIncrement time.Duration 31 | } 32 | 33 | func (s LinearTimeoutStrategy) ProposalTimeout(_ uint64, round uint32) time.Duration { 34 | b := s.ProposalBase 35 | if b == 0 { 36 | b = 5 * time.Second 37 | } 38 | i := s.ProposalIncrement 39 | if i == 0 { 40 | i = 500 * time.Millisecond 41 | } 42 | return b + (time.Duration(round) * i) 43 | } 44 | 45 | func (s LinearTimeoutStrategy) PrevoteDelayTimeout(_ uint64, round uint32) time.Duration { 46 | b := s.PrevoteDelayBase 47 | if b == 0 { 48 | b = 5 * time.Second 49 | } 50 | i := s.PrevoteDelayIncrement 51 | if i == 0 { 52 | i = 500 * time.Millisecond 53 | } 54 | return b + (time.Duration(round) * i) 55 | } 56 | 57 | func (s LinearTimeoutStrategy) PrecommitDelayTimeout(_ uint64, round uint32) time.Duration { 58 | b := s.PrecommitDelayBase 59 | if b == 0 { 60 | b = 5 * time.Second 61 | } 62 | i := s.PrecommitDelayIncrement 63 | if i == 0 { 64 | i = 500 * time.Millisecond 65 | } 66 | return b + (time.Duration(round) * i) 67 | } 68 | 69 | func (s LinearTimeoutStrategy) CommitWaitTimeout(_ uint64, round uint32) time.Duration { 70 | b := s.CommitWaitBase 71 | if b == 0 { 72 | b = 2 * time.Second 73 | } 74 | i := s.CommitWaitIncrement 75 | if i == 0 { 76 | i = 500 * time.Millisecond 77 | } 78 | return b + (time.Duration(round) * i) 79 | } 80 | -------------------------------------------------------------------------------- /tm/tmengine/tmelink/blockdataarrival.go: -------------------------------------------------------------------------------- 1 | package tmelink 2 | 3 | // BlockDataArrival is shared with the engine's state machine, 4 | // to indicate that a block's data has arrived. 5 | // 6 | // The mechanism by which block data arrives is driver-dependent; 7 | // for example, it may be actively fetched upon observing a proposed block, 8 | // or there may be a subscription interface where the the block data is passively received. 9 | // 10 | // Upon arrival of this data, the driver sends this value on a designated channel 11 | // so that the consensus strategy may re-evaluate its proposed blocks, 12 | // now being able to fully evaluate the proposed block with the data. 13 | type BlockDataArrival struct { 14 | // The height and round of the arrived block data. 15 | // The height and round are compared against the state machine's current height and round, 16 | // and data arriving late or early are safely ignored. 17 | Height uint64 18 | Round uint32 19 | 20 | // The DataID of the proposed block, whose data has arrived. 21 | ID string 22 | } 23 | -------------------------------------------------------------------------------- /tm/tmengine/tmelink/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmelink contains types used by the internals of the 2 | // [github.com/gordian-engine/gordian/tm/tmengine.Engine] 3 | // that need to be exposed outside of the tmengine package. 4 | // In other words, the types in this package are used to "link" 5 | // individual components of the engine. 6 | // 7 | // It exists as a separate package to avoid a circular dependency 8 | // between tmengine and its internal packages. 9 | package tmelink 10 | -------------------------------------------------------------------------------- /tm/tmengine/tmelink/lagstate.go: -------------------------------------------------------------------------------- 1 | package tmelink 2 | 3 | // LagState is a value sent from engine internals to the driver, 4 | // when the engine has an updated belief about 5 | // whether it is lagging the rest of the network. 6 | // 7 | // LagState is the combination of a [LagStatus], 8 | // an always-preseent committing height, 9 | // and a sometimes-present "needed" height. 10 | // 11 | // If the Status is [LagStatusKnownMissing], then the NeedHeight field will be non-zero, 12 | // indicating the final needed height to be fully synchronized. 13 | // 14 | // New LagState values are only sent wen the Status field changes. 15 | // An updated CommittingHeight without a Status change, 16 | // will not result in a new value being sent. 17 | type LagState struct { 18 | Status LagStatus 19 | 20 | CommittingHeight uint64 21 | 22 | NeedHeight uint64 23 | } 24 | 25 | type LagStatus uint8 26 | 27 | //go:generate go run golang.org/x/tools/cmd/stringer -type LagStatus -trimprefix=LagStatus . 28 | 29 | const ( 30 | // At startup, nothing known yet. 31 | LagStatusInitializing LagStatus = iota 32 | 33 | // We believe we are up to date wtih the network. 34 | LagStatusUpToDate 35 | 36 | // We think we are behind, but we don't know how many blocks we need yet. 37 | LagStatusAssumedBehind 38 | 39 | // We know we are missing some range of blocks. 40 | // This is the only status for which [LagState.NeedHeight] is set. 41 | LagStatusKnownMissing 42 | ) 43 | -------------------------------------------------------------------------------- /tm/tmengine/tmelink/lagstatus_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type LagStatus -trimprefix=LagStatus ."; DO NOT EDIT. 2 | 3 | package tmelink 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[LagStatusInitializing-0] 12 | _ = x[LagStatusUpToDate-1] 13 | _ = x[LagStatusAssumedBehind-2] 14 | _ = x[LagStatusKnownMissing-3] 15 | } 16 | 17 | const _LagStatus_name = "InitializingUpToDateAssumedBehindKnownMissing" 18 | 19 | var _LagStatus_index = [...]uint8{0, 12, 20, 33, 45} 20 | 21 | func (i LagStatus) String() string { 22 | if i >= LagStatus(len(_LagStatus_index)-1) { 23 | return "LagStatus(" + strconv.FormatInt(int64(i), 10) + ")" 24 | } 25 | return _LagStatus_name[_LagStatus_index[i]:_LagStatus_index[i+1]] 26 | } 27 | -------------------------------------------------------------------------------- /tm/tmengine/tmelink/networkviewupdate.go: -------------------------------------------------------------------------------- 1 | package tmelink 2 | 3 | import "github.com/gordian-engine/gordian/tm/tmconsensus" 4 | 5 | // NetworkViewUpdate is a set of versioned round views, representing the engine's view of the network, 6 | // that the engine is intended to send to the gossip strategy. 7 | // 8 | // The individual values may be nil during a particular send 9 | // if the engine has already sent an up-to-date value to the gossip strategy. 10 | type NetworkViewUpdate struct { 11 | Committing, Voting, NextRound *tmconsensus.VersionedRoundView 12 | 13 | // The other views are all standard, 14 | // but this view may be a little less obvious. 15 | // In the context of the Mirror component of the Engine, 16 | // as soon as the mirror detects a nil commit for a round, 17 | // it effectively discards that view and advances the round. 18 | // In doing so, it risks not distributing the precommits required 19 | // for all other validators to cross the threshold. 20 | // 21 | // In normal operations, one would expect a regular re-broadcast of current state 22 | // which would eventually bring validators to the same round; 23 | // but we can instead eagerly distribute the details that caused a round to vote nil. 24 | NilVotedRound *tmconsensus.VersionedRoundView 25 | } 26 | -------------------------------------------------------------------------------- /tm/tmengine/tmelink/phfetcher.go: -------------------------------------------------------------------------------- 1 | package tmelink 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gordian-engine/gordian/tm/tmconsensus" 7 | ) 8 | 9 | // ProposedHeaderFetcher contains the input and output channels to fetch proposed headers. 10 | // The engine uses this when there are sufficient votes for a proposed header, 11 | // and the engine does not have the proposed header. 12 | type ProposedHeaderFetcher struct { 13 | // FetchRequests is the channel for the engine 14 | // to send requests to fetch a proposed header 15 | // at a given height and with a given hash. 16 | // 17 | // Once a proposed header matching the height and hash has been found, 18 | // that header is sent to the FetchedProposedHeaders channel. 19 | // 20 | // The ProposedHeaderFetchRequest struct has a context field. 21 | // The context is associated with the fetch for this particular header. 22 | // Canceling the context will abort any in-progress requests to find the header. 23 | // If the context is cancelled after the proposed header has been enqueued into FetchedProposedHeaders, 24 | // there is no effect. 25 | // 26 | // A ProposedHeaderFetcher should have an upper limit on the number of outstanding fetch requests. 27 | // If the number of in-flight requests is at its limit, 28 | // the send to this channel will block. 29 | FetchRequests chan<- ProposedHeaderFetchRequest 30 | 31 | // FetchedProposedHeaders is the single channel that sends any header 32 | // discovered as a result of a call to FetchProposedHeader. 33 | FetchedProposedHeaders <-chan tmconsensus.ProposedHeader 34 | } 35 | 36 | // ProposedHeaderFetchRequest is used to make requests to fetch missed proposed headers. 37 | type ProposedHeaderFetchRequest struct { 38 | // Context associated with the request. 39 | // Canceling this context will abort the request, if it is still in-flight. 40 | Ctx context.Context 41 | 42 | // The height to search for the proposed header. 43 | Height uint64 44 | 45 | // The block hash of the header to search for. 46 | BlockHash string 47 | } 48 | -------------------------------------------------------------------------------- /tm/tmengine/tmelink/replayedheader.go: -------------------------------------------------------------------------------- 1 | package tmelink 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gordian-engine/gordian/tm/tmconsensus" 7 | ) 8 | 9 | // ReplayedHeaderRequest is sent from the Driver to the Engine 10 | // during mirror catchup. 11 | type ReplayedHeaderRequest struct { 12 | Header tmconsensus.Header 13 | Proof tmconsensus.CommitProof 14 | 15 | Resp chan<- ReplayedHeaderResponse 16 | } 17 | 18 | // ReplayedHeaderResponse is the response to [ReplayedHeaderRequest] 19 | // sent from the Engine internals back to the Driver. 20 | // 21 | // If the header was replayed successfully, 22 | // the Err field is nil. 23 | // 24 | // Otherwise, the error will be of type 25 | // [ReplayedHeaderValidationError], [ReplayedHeaderOutOfSyncError], 26 | // or [ReplayedHeaderInternalError]. 27 | type ReplayedHeaderResponse struct { 28 | Err error 29 | } 30 | 31 | // ReplayedHeaderValidationError indicates that the engine 32 | // failed to validate the replayed header,j 33 | // for example a hash mismatch or insufficient signatures. 34 | // On this error type, the driver should note that the source of the header 35 | // may be maliciously constructing headers. 36 | type ReplayedHeaderValidationError struct { 37 | Err error 38 | } 39 | 40 | func (e ReplayedHeaderValidationError) Error() string { 41 | return "validation of replayed header failed: " + e.Err.Error() 42 | } 43 | 44 | func (e ReplayedHeaderValidationError) Unwrap() error { 45 | return e.Err 46 | } 47 | 48 | // ReplayedHeaderOutOfSyncError indicates that the replayed header 49 | // had an invalid height, either before or after the current voting height. 50 | // 51 | // If many headers are attempting to be replayed concurrently, 52 | // this may be an expected error type. 53 | // If the driver is only replaying one block at a time, 54 | // this likely indicates a critical issue in the driver's logic. 55 | type ReplayedHeaderOutOfSyncError struct { 56 | WantHeight, GotHeight uint64 57 | } 58 | 59 | func (e ReplayedHeaderOutOfSyncError) Error() string { 60 | return fmt.Sprintf("received header at height %d, expected %d", e.GotHeight, e.WantHeight) 61 | } 62 | 63 | // ReplayedHeaderInternalError indicates an internal error to the engine. 64 | // Upon receiving this error, the driver may assume the mirror 65 | // has experienced an unrecoverable error. 66 | type ReplayedHeaderInternalError struct { 67 | Err error 68 | } 69 | 70 | func (e ReplayedHeaderInternalError) Error() string { 71 | return "internal error while handling replayed header: " + e.Err.Error() 72 | } 73 | 74 | func (e ReplayedHeaderInternalError) Unwrap() error { 75 | return e.Err 76 | } 77 | -------------------------------------------------------------------------------- /tm/tmengine/tmelink/tmelinktest/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmelinktest contains helpers for tests involving the [tmelink] package. 2 | package tmelinktest 3 | -------------------------------------------------------------------------------- /tm/tmengine/tmelink/tmelinktest/phfetcher.go: -------------------------------------------------------------------------------- 1 | package tmelinktest 2 | 3 | import ( 4 | "github.com/gordian-engine/gordian/tm/tmconsensus" 5 | "github.com/gordian-engine/gordian/tm/tmengine/tmelink" 6 | ) 7 | 8 | // PHFetcher can be used in tests where a [tmelink.ProposedHeaderFetcher] is needed. 9 | type PHFetcher struct { 10 | // Bidirectional request channel, 11 | // so tests can inspect requests. 12 | ReqCh chan tmelink.ProposedHeaderFetchRequest 13 | 14 | // Bidirectional output channel, 15 | // so tests can send responses. 16 | FetchedCh chan tmconsensus.ProposedHeader 17 | } 18 | 19 | func NewPHFetcher(reqSz, fetchedSz int) PHFetcher { 20 | return PHFetcher{ 21 | ReqCh: make(chan tmelink.ProposedHeaderFetchRequest, reqSz), 22 | FetchedCh: make(chan tmconsensus.ProposedHeader, fetchedSz), 23 | } 24 | } 25 | 26 | // ProposedHeaderFetcher returns a ProposedHeaderFetcher struct, 27 | // as the engine and its internal components expect. 28 | func (f PHFetcher) ProposedHeaderFetcher() tmelink.ProposedHeaderFetcher { 29 | return tmelink.ProposedHeaderFetcher{ 30 | FetchRequests: f.ReqCh, 31 | FetchedProposedHeaders: f.FetchedCh, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tm/tmengine/tmenginetest/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmenginetest contains types useful for tests involving the [tmengine] package. 2 | package tmenginetest 3 | -------------------------------------------------------------------------------- /tm/tmgossip/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmgossip defines the gossip [Strategy] that the 2 | // [github.com/gordian-engine/gordian/tm/tmengine.Engine] uses 3 | // to push state updates to the peer-to-peer network. 4 | package tmgossip 5 | -------------------------------------------------------------------------------- /tm/tmgossip/strategy.go: -------------------------------------------------------------------------------- 1 | package tmgossip 2 | 3 | import ( 4 | "github.com/gordian-engine/gordian/tm/tmengine/tmelink" 5 | ) 6 | 7 | // Strategy is a gossip strategy, whose purpose is to observe changes to round state 8 | // and send messages to the p2p network. 9 | // Therefore, when a Strategy is initialized, it should be aware of a [tmp2p.NetworkBroadcaster], 10 | // which should already be available somewhere close to main.go where the strategy is created. 11 | // 12 | // The outer interface is simple. 13 | // The engine provides the strategy with a read-only channel of tmelink.NetworkViewUpdate 14 | // to provide round state updates as they are discovered, 15 | // and when the engine is shutting down it will call the strategy's Wait method. 16 | type Strategy interface { 17 | // Start provides the channel of NetworkViewUpdate for the strategy to begin running. 18 | // It is incorrect to call Start more than once. 19 | Start(updates <-chan tmelink.NetworkViewUpdate) 20 | 21 | // Wait blocks until the strategy is finished. 22 | // The engine calls this method when the engine itself is shutting down. 23 | Wait() 24 | } 25 | -------------------------------------------------------------------------------- /tm/tmgossip/tmgossiptest/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmgossiptest contains implementations of 2 | // [github.com/gordian-engine/gordian/tm/tmgossip.Strategy] 3 | // that are useful to tests that require gossip strategies. 4 | package tmgossiptest 5 | -------------------------------------------------------------------------------- /tm/tmgossip/tmgossiptest/nopstrategy.go: -------------------------------------------------------------------------------- 1 | package tmgossiptest 2 | 3 | import "github.com/gordian-engine/gordian/tm/tmengine/tmelink" 4 | 5 | // NopStrategy is a no-op [github.com/gordian-engine/gordian/tm/tmgossip.Strategy] 6 | // for use in tests where a placeholder strategy is needed. 7 | type NopStrategy struct{} 8 | 9 | func (NopStrategy) Start(<-chan tmelink.NetworkViewUpdate) {} 10 | func (NopStrategy) Wait() {} 11 | -------------------------------------------------------------------------------- /tm/tmgossip/tmgossiptest/passthroughstrategy.go: -------------------------------------------------------------------------------- 1 | package tmgossiptest 2 | 3 | import "github.com/gordian-engine/gordian/tm/tmengine/tmelink" 4 | 5 | type PassThroughStrategy struct { 6 | Ready chan struct{} 7 | 8 | Updates <-chan tmelink.NetworkViewUpdate 9 | } 10 | 11 | func NewPassThroughStrategy() *PassThroughStrategy { 12 | return &PassThroughStrategy{ 13 | Ready: make(chan struct{}), 14 | } 15 | } 16 | 17 | func (s *PassThroughStrategy) Start(ch <-chan tmelink.NetworkViewUpdate) { 18 | s.Updates = ch 19 | close(s.Ready) 20 | } 21 | 22 | func (s *PassThroughStrategy) Wait() {} 23 | -------------------------------------------------------------------------------- /tm/tmintegration/consensusfixture.go: -------------------------------------------------------------------------------- 1 | package tmintegration 2 | 3 | import "github.com/gordian-engine/gordian/tm/tmconsensus/tmconsensustest" 4 | 5 | type ConsensusFixtureFactory interface { 6 | NewConsensusFixture(nVals int) *tmconsensustest.Fixture 7 | } 8 | 9 | type Ed25519ConsensusFixtureFactory struct{} 10 | 11 | func (f Ed25519ConsensusFixtureFactory) NewConsensusFixture(nVals int) *tmconsensustest.Fixture { 12 | return tmconsensustest.NewEd25519Fixture(nVals) 13 | } 14 | -------------------------------------------------------------------------------- /tm/tmintegration/daisychain.go: -------------------------------------------------------------------------------- 1 | package tmintegration 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | 7 | "github.com/gordian-engine/gordian/gcrypto" 8 | "github.com/gordian-engine/gordian/tm/tmgossip" 9 | "github.com/gordian-engine/gordian/tm/tmp2p" 10 | "github.com/gordian-engine/gordian/tm/tmp2p/tmp2ptest" 11 | ) 12 | 13 | type DaisyChainFactory struct { 14 | e *Env 15 | } 16 | 17 | func NewDaisyChainFactory(e *Env) DaisyChainFactory { 18 | return DaisyChainFactory{e: e} 19 | } 20 | 21 | func (f DaisyChainFactory) NewNetwork(ctx context.Context, log *slog.Logger, reg *gcrypto.Registry) (tmp2ptest.Network, error) { 22 | // We don't need the gcrypto registry for the daisy chain network, 23 | // because we only transmit in-memory values, 24 | // without serializing and deserializing across the network. 25 | n := tmp2ptest.NewDaisyChainNetwork(ctx, log) 26 | 27 | return &tmp2ptest.GenericNetwork[*tmp2ptest.DaisyChainConnection]{ 28 | Network: n, 29 | }, nil 30 | } 31 | 32 | func (f DaisyChainFactory) NewGossipStrategy(ctx context.Context, idx int, conn tmp2p.Connection) (tmgossip.Strategy, error) { 33 | log := f.e.RootLogger.With("sys", "chattygossip", "idx", idx) 34 | return tmgossip.NewChattyStrategy(ctx, log, conn.ConsensusBroadcaster()), nil 35 | } 36 | -------------------------------------------------------------------------------- /tm/tmintegration/daisychain_inmem_test.go: -------------------------------------------------------------------------------- 1 | package tmintegration_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gordian-engine/gordian/tm/tmintegration" 7 | ) 8 | 9 | type DaisyChainInmemFactory struct { 10 | tmintegration.DaisyChainFactory 11 | 12 | tmintegration.ConsensusFixtureFactory 13 | 14 | tmintegration.InmemStoreFactory 15 | tmintegration.InmemSchemeFactory 16 | } 17 | 18 | func TestDaisyChainInmem(t *testing.T) { 19 | t.Parallel() 20 | 21 | tmintegration.RunIntegrationTest(t, func(e *tmintegration.Env) tmintegration.Factory { 22 | return DaisyChainInmemFactory{ 23 | DaisyChainFactory: tmintegration.NewDaisyChainFactory(e), 24 | 25 | ConsensusFixtureFactory: tmintegration.Ed25519ConsensusFixtureFactory{}, 26 | } 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /tm/tmintegration/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmintegration defines integration tests around consensus engines, 2 | // using user-specified network and store definitions. 3 | package tmintegration 4 | -------------------------------------------------------------------------------- /tm/tmintegration/factory.go: -------------------------------------------------------------------------------- 1 | package tmintegration 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | 7 | "github.com/gordian-engine/gordian/gcrypto" 8 | "github.com/gordian-engine/gordian/tm/tmconsensus" 9 | "github.com/gordian-engine/gordian/tm/tmconsensus/tmconsensustest" 10 | "github.com/gordian-engine/gordian/tm/tmgossip" 11 | "github.com/gordian-engine/gordian/tm/tmp2p" 12 | "github.com/gordian-engine/gordian/tm/tmp2p/tmp2ptest" 13 | "github.com/gordian-engine/gordian/tm/tmstore" 14 | ) 15 | 16 | // Env contains some of the primitives of the current test environment, 17 | // to inform the creation of a [Factory]. 18 | type Env struct { 19 | // The RootLogger can be used when the Factory 20 | // needs a logger in a created value. 21 | RootLogger *slog.Logger 22 | 23 | // Inline interface to avoid directly depending on testing package. 24 | tb interface { 25 | Cleanup(func()) 26 | 27 | TempDir() string 28 | } 29 | } 30 | 31 | // TempDir returns the path to a new temporary directory, 32 | // in case the factory needs a place to write data to disk. 33 | func (e *Env) TempDir() string { 34 | return e.tb.TempDir() 35 | } 36 | 37 | // Cleanup calls fn when the test is complete, 38 | // regardless of whether the test passed or failed. 39 | func (e *Env) Cleanup(fn func()) { 40 | e.tb.Cleanup(fn) 41 | } 42 | 43 | type NewFactoryFunc func(e *Env) Factory 44 | 45 | type Factory interface { 46 | // NewNetwork will be called only once per test. 47 | // The implementer may assume that the context will be canceled 48 | // at or before the test's completion. 49 | NewNetwork(context.Context, *slog.Logger, *gcrypto.Registry) (tmp2ptest.Network, error) 50 | 51 | NewActionStore(context.Context, int) (tmstore.ActionStore, error) 52 | NewCommittedHeaderStore(context.Context, int) (tmstore.CommittedHeaderStore, error) 53 | NewFinalizationStore(context.Context, int) (tmstore.FinalizationStore, error) 54 | NewMirrorStore(context.Context, int) (tmstore.MirrorStore, error) 55 | NewRoundStore(context.Context, int) (tmstore.RoundStore, error) 56 | NewStateMachineStore(context.Context, int) (tmstore.StateMachineStore, error) 57 | NewValidatorStore(context.Context, int, tmconsensus.HashScheme) (tmstore.ValidatorStore, error) 58 | 59 | // TODO: fixture probably takes precedence over this? 60 | HashScheme(context.Context, int) (tmconsensus.HashScheme, error) 61 | SignatureScheme(context.Context, int) (tmconsensus.SignatureScheme, error) 62 | CommonMessageSignatureProofScheme(context.Context, int) (gcrypto.CommonMessageSignatureProofScheme, error) 63 | 64 | NewGossipStrategy(context.Context, int, tmp2p.Connection) (tmgossip.Strategy, error) 65 | 66 | NewConsensusFixture(nVals int) *tmconsensustest.Fixture 67 | } 68 | -------------------------------------------------------------------------------- /tm/tmintegration/inmem.go: -------------------------------------------------------------------------------- 1 | package tmintegration 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gordian-engine/gordian/gcrypto" 7 | "github.com/gordian-engine/gordian/tm/tmconsensus" 8 | "github.com/gordian-engine/gordian/tm/tmconsensus/tmconsensustest" 9 | "github.com/gordian-engine/gordian/tm/tmstore" 10 | "github.com/gordian-engine/gordian/tm/tmstore/tmmemstore" 11 | ) 12 | 13 | // InmemStoreFactory is meant to be embedded in another [tmintegration.Factory] 14 | // to provide in-memory implementations of stores. 15 | type InmemStoreFactory struct{} 16 | 17 | func (f InmemStoreFactory) NewActionStore(ctx context.Context, idx int) (tmstore.ActionStore, error) { 18 | return tmmemstore.NewActionStore(), nil 19 | } 20 | 21 | func (f InmemStoreFactory) NewFinalizationStore(ctx context.Context, idx int) (tmstore.FinalizationStore, error) { 22 | return tmmemstore.NewFinalizationStore(), nil 23 | } 24 | 25 | func (f InmemStoreFactory) NewCommittedHeaderStore(ctx context.Context, idx int) (tmstore.CommittedHeaderStore, error) { 26 | return tmmemstore.NewCommittedHeaderStore(), nil 27 | } 28 | 29 | func (f InmemStoreFactory) NewMirrorStore(ctx context.Context, idx int) (tmstore.MirrorStore, error) { 30 | return tmmemstore.NewMirrorStore(), nil 31 | } 32 | 33 | func (f InmemStoreFactory) NewRoundStore(ctx context.Context, idx int) (tmstore.RoundStore, error) { 34 | return tmmemstore.NewRoundStore(), nil 35 | } 36 | 37 | func (f InmemStoreFactory) NewStateMachineStore(ctx context.Context, idx int) (tmstore.StateMachineStore, error) { 38 | return tmmemstore.NewStateMachineStore(), nil 39 | } 40 | 41 | func (f InmemStoreFactory) NewValidatorStore(ctx context.Context, idx int, hs tmconsensus.HashScheme) (tmstore.ValidatorStore, error) { 42 | return tmmemstore.NewValidatorStore(hs), nil 43 | } 44 | 45 | type InmemSchemeFactory struct{} 46 | 47 | func (f InmemSchemeFactory) HashScheme(ctx context.Context, idx int) (tmconsensus.HashScheme, error) { 48 | return tmconsensustest.SimpleHashScheme{}, nil 49 | } 50 | 51 | func (f InmemSchemeFactory) SignatureScheme(ctx context.Context, idx int) (tmconsensus.SignatureScheme, error) { 52 | return tmconsensustest.SimpleSignatureScheme{}, nil 53 | } 54 | 55 | func (f InmemSchemeFactory) CommonMessageSignatureProofScheme(ctx context.Context, idx int) (gcrypto.CommonMessageSignatureProofScheme, error) { 56 | return gcrypto.SimpleCommonMessageSignatureProofScheme{}, nil 57 | } 58 | -------------------------------------------------------------------------------- /tm/tmintegration/libp2p.go: -------------------------------------------------------------------------------- 1 | package tmintegration 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | 8 | "github.com/gordian-engine/gordian/gcrypto" 9 | "github.com/gordian-engine/gordian/tm/tmcodec/tmjson" 10 | "github.com/gordian-engine/gordian/tm/tmgossip" 11 | "github.com/gordian-engine/gordian/tm/tmp2p" 12 | "github.com/gordian-engine/gordian/tm/tmp2p/tmlibp2p" 13 | "github.com/gordian-engine/gordian/tm/tmp2p/tmlibp2p/tmlibp2ptest" 14 | "github.com/gordian-engine/gordian/tm/tmp2p/tmp2ptest" 15 | ) 16 | 17 | // Libp2pFactory provides a Network and GossipStrategy for integration tests. 18 | // This makes it straightforward to compose separate stores and schemes for integration tests. 19 | type Libp2pFactory struct { 20 | e *Env 21 | } 22 | 23 | func NewLibp2pFactory(e *Env) Libp2pFactory { 24 | return Libp2pFactory{e: e} 25 | } 26 | 27 | func (f Libp2pFactory) NewNetwork(ctx context.Context, log *slog.Logger, reg *gcrypto.Registry) (tmp2ptest.Network, error) { 28 | codec := tmjson.MarshalCodec{ 29 | CryptoRegistry: reg, 30 | } 31 | n, err := tmlibp2ptest.NewNetwork(ctx, log, codec) 32 | if err != nil { 33 | return nil, fmt.Errorf("failed to build network: %w", err) 34 | } 35 | 36 | return &tmp2ptest.GenericNetwork[*tmlibp2p.Connection]{ 37 | Network: n, 38 | }, nil 39 | } 40 | 41 | func (f Libp2pFactory) NewGossipStrategy(ctx context.Context, idx int, conn tmp2p.Connection) (tmgossip.Strategy, error) { 42 | log := f.e.RootLogger.With("sys", "chattygossip", "idx", idx) 43 | return tmgossip.NewChattyStrategy(ctx, log, conn.ConsensusBroadcaster()), nil 44 | } 45 | -------------------------------------------------------------------------------- /tm/tmintegration/libp2p_inmem_test.go: -------------------------------------------------------------------------------- 1 | package tmintegration_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gordian-engine/gordian/tm/tmintegration" 7 | ) 8 | 9 | // Libp2pInmemFactory uses a basic libp2p factory 10 | // along with in-mem stores and the default scheme factories. 11 | type Libp2pInmemFactory struct { 12 | tmintegration.Libp2pFactory 13 | 14 | tmintegration.ConsensusFixtureFactory 15 | 16 | tmintegration.InmemStoreFactory 17 | tmintegration.InmemSchemeFactory 18 | } 19 | 20 | func TestLibp2pInmem(t *testing.T) { 21 | tmintegration.RunIntegrationTest(t, func(e *tmintegration.Env) tmintegration.Factory { 22 | lf := tmintegration.NewLibp2pFactory(e) 23 | return Libp2pInmemFactory{ 24 | Libp2pFactory: lf, 25 | 26 | ConsensusFixtureFactory: tmintegration.Ed25519ConsensusFixtureFactory{}, 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /tm/tmp2p/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmp2p contains high-level interfaces for the peer-to-peer layer for [tmengine]. 2 | package tmp2p 3 | -------------------------------------------------------------------------------- /tm/tmp2p/network.go: -------------------------------------------------------------------------------- 1 | package tmp2p 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gordian-engine/gordian/tm/tmconsensus" 7 | ) 8 | 9 | // Connection is the generalized connection to the p2p network. 10 | // 11 | // It contains different methods to work with specific layers of the p2p network, 12 | // such as ConsensusBroadcaster to work with only broadcasting consensus messages. 13 | // 14 | // It also enables disconnecting from the p2p network altogether 15 | // (which must invalidate further use of the connection). 16 | // and dynamically changing the underlying [tmconsensus.ConsensusHandler]. 17 | type Connection interface { 18 | // ConsensusBroadcaster returns a ConsensusBroadcaster derived from this connection, 19 | // or nil if the connection does not support consensus broadcasting. 20 | ConsensusBroadcaster() ConsensusBroadcaster 21 | 22 | // Set the underlying consensus handler, 23 | // controlling how to respond to incoming consensus messages from the network. 24 | // The Connection implementation may have special handling for nil values. 25 | // 26 | // This is a method at runtime rather than a parameter to the constructor, 27 | // because you typically already need a connection before you can create the engine; 28 | // then once you have a running engine you call conn.SetConsensusHandler(e) 29 | // so that new messages are validated based on the engine's state. 30 | SetConsensusHandler(context.Context, tmconsensus.ConsensusHandler) 31 | 32 | // Disconnect the connection, rendering it unusable. 33 | Disconnect() 34 | 35 | // Disconnected returns a channel that is closed after Disconnect() completes. 36 | Disconnected() <-chan struct{} 37 | } 38 | 39 | // ConsensusBroadcaster is the set of methods to publish consensus messages to the network. 40 | type ConsensusBroadcaster interface { 41 | OutgoingProposedHeaders() chan<- tmconsensus.ProposedHeader 42 | 43 | OutgoingPrevoteProofs() chan<- tmconsensus.PrevoteSparseProof 44 | OutgoingPrecommitProofs() chan<- tmconsensus.PrecommitSparseProof 45 | } 46 | -------------------------------------------------------------------------------- /tm/tmp2p/tmlibp2p/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmlibp2p contains implementations of the [tmp2p] interfaces 2 | // using [github.com/libp2p/go-libp2p]. 3 | // 4 | // Note that this package will likely be moved to its own module 5 | // in a separate git repository, in the future. 6 | package tmlibp2p 7 | -------------------------------------------------------------------------------- /tm/tmp2p/tmlibp2p/host.go: -------------------------------------------------------------------------------- 1 | package tmlibp2p 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/libp2p/go-libp2p" 7 | pubsub "github.com/libp2p/go-libp2p-pubsub" 8 | p2phost "github.com/libp2p/go-libp2p/core/host" 9 | ) 10 | 11 | // Host is a libp2p host and a pubsub connection. 12 | type Host struct { 13 | h p2phost.Host 14 | 15 | ps *pubsub.PubSub 16 | } 17 | 18 | // HostOptions holds libp2p configuration for the host and pubsub value. 19 | type HostOptions struct { 20 | // Options are passed when creating a new libp2p host 21 | // (which is lower level than the Host type in this tmlibp2p package). 22 | Options []libp2p.Option 23 | 24 | // Currently PubSubOptions are always applied to NewGossipSub. 25 | PubSubOptions []pubsub.Option 26 | } 27 | 28 | func NewHost(ctx context.Context, opts HostOptions) (*Host, error) { 29 | h, err := libp2p.New(opts.Options...) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | ps, err := pubsub.NewGossipSub(ctx, h, opts.PubSubOptions...) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return &Host{ 40 | h: h, 41 | ps: ps, 42 | }, nil 43 | } 44 | 45 | // Libp2pHost returns the underlying libp2p host value. 46 | func (h *Host) Libp2pHost() p2phost.Host { 47 | return h.h 48 | } 49 | 50 | // PubSub returns the underlying libp2p pubsub value. 51 | func (h *Host) PubSub() *pubsub.PubSub { 52 | return h.ps 53 | } 54 | 55 | // Close closes the underlying libp2p host and returns its error. 56 | func (h *Host) Close() error { 57 | return h.h.Close() 58 | } 59 | -------------------------------------------------------------------------------- /tm/tmp2p/tmlibp2p/tmlibp2ptest/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmlibp2ptest provides the [Network] type 2 | // that is useful for tests that need to instantiate 3 | // a Gordian peer-to-peer network backed by [tmlibp2p]. 4 | package tmlibp2ptest 5 | -------------------------------------------------------------------------------- /tm/tmp2p/tmlibp2p/tmlibp2ptest/network_test.go: -------------------------------------------------------------------------------- 1 | package tmlibp2ptest_test 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "testing" 7 | 8 | "github.com/gordian-engine/gordian/gcrypto" 9 | "github.com/gordian-engine/gordian/tm/tmcodec/tmjson" 10 | "github.com/gordian-engine/gordian/tm/tmp2p/tmlibp2p" 11 | "github.com/gordian-engine/gordian/tm/tmp2p/tmlibp2p/tmlibp2ptest" 12 | "github.com/gordian-engine/gordian/tm/tmp2p/tmp2ptest" 13 | ) 14 | 15 | func TestLibp2pNetwork_Compliance(t *testing.T) { 16 | tmp2ptest.TestNetworkCompliance( 17 | t, 18 | func(ctx context.Context, log *slog.Logger) (tmp2ptest.Network, error) { 19 | reg := new(gcrypto.Registry) 20 | gcrypto.RegisterEd25519(reg) 21 | codec := tmjson.MarshalCodec{ 22 | CryptoRegistry: reg, 23 | } 24 | n, err := tmlibp2ptest.NewNetwork(ctx, log, codec) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return &tmp2ptest.GenericNetwork[*tmlibp2p.Connection]{ 29 | Network: n, 30 | }, nil 31 | }, 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /tm/tmp2p/tmp2ptest/channelbroadcaster.go: -------------------------------------------------------------------------------- 1 | package tmp2ptest 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/gordian-engine/gordian/tm/tmconsensus" 9 | ) 10 | 11 | // ChannelBroadcaster satisfies the tmp2p.Broadcaster interface, 12 | // emitting to exported channels. 13 | // However, because this is meant to be used in tests, 14 | // there are gracious 5-second timeouts associated with the channels. 15 | // If a channel is blocked sending for that duration, ChannelBroadcaster panics. 16 | type ChannelBroadcaster struct { 17 | phInCh, phOutCh chan tmconsensus.ProposedHeader 18 | 19 | prevoteInCh, prevoteOutCh chan tmconsensus.PrevoteSparseProof 20 | 21 | precommitInCh, precommitOutCh chan tmconsensus.PrecommitSparseProof 22 | } 23 | 24 | func NewChannelBroadcaster(ctx context.Context) *ChannelBroadcaster { 25 | cb := &ChannelBroadcaster{ 26 | phInCh: make(chan tmconsensus.ProposedHeader, 1), 27 | phOutCh: make(chan tmconsensus.ProposedHeader), 28 | 29 | prevoteInCh: make(chan tmconsensus.PrevoteSparseProof, 1), 30 | prevoteOutCh: make(chan tmconsensus.PrevoteSparseProof), 31 | 32 | precommitInCh: make(chan tmconsensus.PrecommitSparseProof, 1), 33 | precommitOutCh: make(chan tmconsensus.PrecommitSparseProof), 34 | } 35 | 36 | go cb.background(ctx) 37 | return cb 38 | } 39 | 40 | func (cb *ChannelBroadcaster) background(ctx context.Context) { 41 | for { 42 | select { 43 | case <-ctx.Done(): 44 | return 45 | case ph := <-cb.phInCh: 46 | sendOrPanic(ctx, cb.phOutCh, ph) 47 | case proof := <-cb.prevoteInCh: 48 | sendOrPanic(ctx, cb.prevoteOutCh, proof) 49 | case proof := <-cb.precommitInCh: 50 | sendOrPanic(ctx, cb.precommitOutCh, proof) 51 | } 52 | } 53 | } 54 | 55 | func (cb *ChannelBroadcaster) OutgoingProposedHeaders() chan<- tmconsensus.ProposedHeader { 56 | return cb.phInCh 57 | } 58 | 59 | // ProposedBlocks is the channel for the test to read, 60 | // to inspect proposed blocks that have been broadcast. 61 | func (cb *ChannelBroadcaster) ProposedBlocks() <-chan tmconsensus.ProposedHeader { 62 | return cb.phOutCh 63 | } 64 | 65 | func (cb *ChannelBroadcaster) OutgoingPrevoteProofs() chan<- tmconsensus.PrevoteSparseProof { 66 | return cb.prevoteInCh 67 | } 68 | 69 | // PrevoteProofs is the channel for the test to read, 70 | // to inspect prevote proofs that have been broadcast. 71 | func (cb *ChannelBroadcaster) PrevoteProofs() <-chan tmconsensus.PrevoteSparseProof { 72 | return cb.prevoteOutCh 73 | } 74 | 75 | func (cb *ChannelBroadcaster) OutgoingPrecommitProofs() chan<- tmconsensus.PrecommitSparseProof { 76 | return cb.precommitInCh 77 | } 78 | 79 | // PrecommitProofs is the channel for the test to read, 80 | // to inspect precommit proofs that have been broadcast. 81 | func (cb *ChannelBroadcaster) PrecommitProofs() <-chan tmconsensus.PrecommitSparseProof { 82 | return cb.precommitOutCh 83 | } 84 | 85 | func sendOrPanic[T any](ctx context.Context, ch chan<- T, val T) { 86 | tick := time.NewTimer(5 * time.Second) 87 | defer tick.Stop() 88 | 89 | select { 90 | case <-ctx.Done(): 91 | case ch <- val: 92 | case <-tick.C: 93 | panic(fmt.Errorf("channel of type %T not read within 5 seconds", val)) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tm/tmp2p/tmp2ptest/daisychainnetwork_test.go: -------------------------------------------------------------------------------- 1 | package tmp2ptest_test 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "testing" 7 | 8 | "github.com/gordian-engine/gordian/tm/tmp2p/tmp2ptest" 9 | ) 10 | 11 | func TestDaisyChainNetwork_Compliance(t *testing.T) { 12 | tmp2ptest.TestNetworkCompliance( 13 | t, 14 | func(ctx context.Context, log *slog.Logger) (tmp2ptest.Network, error) { 15 | n := tmp2ptest.NewDaisyChainNetwork(ctx, log) 16 | return &tmp2ptest.GenericNetwork[*tmp2ptest.DaisyChainConnection]{ 17 | Network: n, 18 | }, nil 19 | }, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /tm/tmp2p/tmp2ptest/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmp2ptest contains compliance tests for tmp2p implementations, 2 | // and it also provides some other types that are useful in tests. 3 | package tmp2ptest 4 | -------------------------------------------------------------------------------- /tm/tmstore/actionstore.go: -------------------------------------------------------------------------------- 1 | package tmstore 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gordian-engine/gordian/gcrypto" 7 | "github.com/gordian-engine/gordian/tm/tmconsensus" 8 | ) 9 | 10 | // ActionStore stores the active actions the current state machine and application take; 11 | // specifically, proposed blocks, prevotes, and precommits. 12 | type ActionStore interface { 13 | SaveProposedHeaderAction(context.Context, tmconsensus.ProposedHeader) error 14 | 15 | SavePrevoteAction(ctx context.Context, pubKey gcrypto.PubKey, vt tmconsensus.VoteTarget, sig []byte) error 16 | SavePrecommitAction(ctx context.Context, pubKey gcrypto.PubKey, vt tmconsensus.VoteTarget, sig []byte) error 17 | 18 | // LoadActions returns all actions recorded for this round. 19 | LoadActions(ctx context.Context, height uint64, round uint32) (RoundActions, error) 20 | } 21 | 22 | // RoundActions contains all three possible actions the current validator 23 | // may have taken for a single round. 24 | type RoundActions struct { 25 | Height uint64 26 | Round uint32 27 | 28 | ProposedHeader tmconsensus.ProposedHeader 29 | 30 | PubKey gcrypto.PubKey 31 | 32 | PrevoteTarget string // Block hash or empty string for nil. 33 | PrevoteSignature string // Immutable signature. 34 | 35 | PrecommitTarget string // Block hash or empty string for nil. 36 | PrecommitSignature string // Immutable signature. 37 | } 38 | -------------------------------------------------------------------------------- /tm/tmstore/committedheaderstore.go: -------------------------------------------------------------------------------- 1 | package tmstore 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gordian-engine/gordian/tm/tmconsensus" 7 | ) 8 | 9 | // CommittedHeaderStore is the store that the Engine's Mirror uses for committed block headers. 10 | // The committed headers always lag the voting round by one height. 11 | // The subsequent header's PrevCommitProof field is the canonical proof. 12 | // But while the engine is still voting on that height, 13 | // the [RoundStore] contains the most up to date previous commit proof for the committing header. 14 | // 15 | // The proofs associated with the committed headers must be considered subjective. 16 | // That is, while the proof is expected to represent >2/3 voting power for the header, 17 | // it is not necessarily the same set of signatures 18 | // as in the subsequent block's PrevCommitProof field. 19 | type CommittedHeaderStore interface { 20 | SaveCommittedHeader(ctx context.Context, ch tmconsensus.CommittedHeader) error 21 | 22 | LoadCommittedHeader(ctx context.Context, height uint64) (tmconsensus.CommittedHeader, error) 23 | } 24 | -------------------------------------------------------------------------------- /tm/tmstore/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmstore contains the store interfaces for the gordian/tm tree. 2 | package tmstore 3 | 4 | // Compile-time assertion that all the stores can be embedded in a single interface. 5 | // This would only fail if there were conflicting method names across different stores. 6 | // 7 | // This could possibly get promoted to a named interface later, 8 | // but right now there is no place where we need all stores explicitly in one value. 9 | var _ interface { 10 | ActionStore 11 | CommittedHeaderStore 12 | FinalizationStore 13 | MirrorStore 14 | RoundStore 15 | ValidatorStore 16 | } 17 | -------------------------------------------------------------------------------- /tm/tmstore/finalizationstore.go: -------------------------------------------------------------------------------- 1 | package tmstore 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gordian-engine/gordian/tm/tmconsensus" 7 | ) 8 | 9 | // FinalizationStore stores and retrieves the block finalizations 10 | // that the local validator has computed. 11 | type FinalizationStore interface { 12 | SaveFinalization( 13 | ctx context.Context, 14 | height uint64, round uint32, 15 | blockHash string, 16 | valSet tmconsensus.ValidatorSet, 17 | appStateHash string, 18 | ) error 19 | 20 | LoadFinalizationByHeight(ctx context.Context, height uint64) ( 21 | round uint32, 22 | blockHash string, 23 | valSet tmconsensus.ValidatorSet, 24 | appStateHash string, 25 | err error, 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /tm/tmstore/mirrorstore.go: -------------------------------------------------------------------------------- 1 | package tmstore 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // MirrorStore contains values that an engine's Mirror needs to read and write. 8 | type MirrorStore interface { 9 | // Set and get the "network height and round", which is what the Mirror 10 | // believes to be the current round for voting and the current round 11 | // for a block being committed. 12 | // 13 | // While an ephemeral copy of this value lives in the Mirror, 14 | // it needs to be persisted to disk in order to resume upon process restart. 15 | SetNetworkHeightRound( 16 | ctx context.Context, 17 | votingHeight uint64, votingRound uint32, 18 | committingHeight uint64, committingRound uint32, 19 | ) error 20 | 21 | NetworkHeightRound(context.Context) ( 22 | votingHeight uint64, votingRound uint32, 23 | committingHeight uint64, committingRound uint32, 24 | err error, 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /tm/tmstore/roundstore.go: -------------------------------------------------------------------------------- 1 | package tmstore 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gordian-engine/gordian/tm/tmconsensus" 7 | ) 8 | 9 | // RoundStore stores and retrieves the proposed headers, prevotes, and precommits 10 | // observed during each round. 11 | type RoundStore interface { 12 | // SaveRoundProposedHeader saves the given proposed block header 13 | // as a candidate proposed header in the given height and round. 14 | SaveRoundProposedHeader(ctx context.Context, ph tmconsensus.ProposedHeader) error 15 | 16 | // SaveRoundReplayedHeader saves the header as one 17 | // that is about to be committed in the given height, 18 | // due to mirror catchup. 19 | // 20 | // In the normal mirror flow, the replayed header is saved, 21 | // and then OverwriteRoundPrecommitProofs is called. 22 | SaveRoundReplayedHeader(ctx context.Context, h tmconsensus.Header) error 23 | 24 | // The overwrite proofs methods overwrite existing entries 25 | // for the corresponding proof at the given height and round. 26 | // TODO: these methods should both accept sparse proofs, 27 | // as sparse proofs are more suited to storage. 28 | OverwriteRoundPrevoteProofs( 29 | ctx context.Context, 30 | height uint64, 31 | round uint32, 32 | proofs tmconsensus.SparseSignatureCollection, 33 | ) error 34 | OverwriteRoundPrecommitProofs( 35 | ctx context.Context, 36 | height uint64, 37 | round uint32, 38 | proofs tmconsensus.SparseSignatureCollection, 39 | ) error 40 | 41 | // LoadRoundState returns the saved proposed blocks and votes 42 | // for the given height and round. 43 | // The order of the proposed blocks in the pbs slice is undefined 44 | // and may differ from one call to another. 45 | // 46 | // Note that in the event of replayed blocks during a mirror catchup, 47 | // there may be ProposedHeader values without its PubKey, Signature, or Annotations fields set. 48 | // 49 | // If there are no proposed blocks or votes at the given height and round, 50 | // [tmconsensus.RoundUnknownError] is returned. 51 | // If at least one proposed block, prevote, or precommit exists at the height and round, 52 | // a nil error is returned. 53 | LoadRoundState(ctx context.Context, height uint64, round uint32) ( 54 | phs []tmconsensus.ProposedHeader, 55 | prevotes, precommits tmconsensus.SparseSignatureCollection, 56 | err error, 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /tm/tmstore/statemachinestore.go: -------------------------------------------------------------------------------- 1 | package tmstore 2 | 3 | import "context" 4 | 5 | // StateMachineStore contains values that an engine's state machine needs to read and write. 6 | type StateMachineStore interface { 7 | // Track the state machine's current height and round, 8 | // so that it can pick up where it left off, after a process restart. 9 | SetStateMachineHeightRound( 10 | ctx context.Context, 11 | height uint64, round uint32, 12 | ) error 13 | 14 | StateMachineHeightRound(context.Context) ( 15 | height uint64, round uint32, 16 | err error, 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /tm/tmstore/tmmemstore/actionstore_test.go: -------------------------------------------------------------------------------- 1 | package tmmemstore_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gordian-engine/gordian/tm/tmconsensus/tmconsensustest" 7 | "github.com/gordian-engine/gordian/tm/tmstore" 8 | "github.com/gordian-engine/gordian/tm/tmstore/tmmemstore" 9 | "github.com/gordian-engine/gordian/tm/tmstore/tmstoretest" 10 | ) 11 | 12 | func TestActionStore(t *testing.T) { 13 | t.Parallel() 14 | 15 | tmstoretest.TestActionStoreCompliance(t, func(func(func())) (tmstore.ActionStore, error) { 16 | return tmmemstore.NewActionStore(), nil 17 | }, tmconsensustest.NewEd25519Fixture) 18 | } 19 | -------------------------------------------------------------------------------- /tm/tmstore/tmmemstore/committedheaderstore.go: -------------------------------------------------------------------------------- 1 | package tmmemstore 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/gordian-engine/gordian/tm/tmconsensus" 8 | ) 9 | 10 | type CommittedHeaderStore struct { 11 | mu sync.RWMutex 12 | 13 | chs map[uint64]tmconsensus.CommittedHeader 14 | } 15 | 16 | func NewCommittedHeaderStore() *CommittedHeaderStore { 17 | return &CommittedHeaderStore{ 18 | chs: make(map[uint64]tmconsensus.CommittedHeader), 19 | } 20 | } 21 | 22 | func (s *CommittedHeaderStore) SaveCommittedHeader(_ context.Context, ch tmconsensus.CommittedHeader) error { 23 | s.mu.Lock() 24 | defer s.mu.Unlock() 25 | 26 | s.chs[ch.Header.Height] = ch 27 | 28 | return nil 29 | } 30 | 31 | func (s *CommittedHeaderStore) LoadCommittedHeader(_ context.Context, height uint64) (tmconsensus.CommittedHeader, error) { 32 | s.mu.RLock() 33 | defer s.mu.RUnlock() 34 | 35 | ch, ok := s.chs[height] 36 | if !ok { 37 | return tmconsensus.CommittedHeader{}, tmconsensus.HeightUnknownError{Want: height} 38 | } 39 | 40 | return ch, nil 41 | } 42 | -------------------------------------------------------------------------------- /tm/tmstore/tmmemstore/committedheaderstore_test.go: -------------------------------------------------------------------------------- 1 | package tmmemstore_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gordian-engine/gordian/tm/tmconsensus/tmconsensustest" 7 | "github.com/gordian-engine/gordian/tm/tmstore" 8 | "github.com/gordian-engine/gordian/tm/tmstore/tmmemstore" 9 | "github.com/gordian-engine/gordian/tm/tmstore/tmstoretest" 10 | ) 11 | 12 | func TestMemCommittedHeaderStore(t *testing.T) { 13 | t.Parallel() 14 | 15 | tmstoretest.TestCommittedHeaderStoreCompliance(t, func(func(func())) (tmstore.CommittedHeaderStore, error) { 16 | return tmmemstore.NewCommittedHeaderStore(), nil 17 | }, tmconsensustest.NewEd25519Fixture) 18 | } 19 | -------------------------------------------------------------------------------- /tm/tmstore/tmmemstore/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmmemstore contains in-memory implementations of stores defined in [tmstore]. 2 | package tmmemstore 3 | -------------------------------------------------------------------------------- /tm/tmstore/tmmemstore/finalizationstore.go: -------------------------------------------------------------------------------- 1 | package tmmemstore 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/gordian-engine/gordian/tm/tmconsensus" 8 | "github.com/gordian-engine/gordian/tm/tmstore" 9 | ) 10 | 11 | type FinalizationStore struct { 12 | mu sync.RWMutex 13 | 14 | byHeight map[uint64]fin 15 | } 16 | 17 | type fin struct { 18 | H uint64 19 | R uint32 20 | BlockHash string 21 | ValSet tmconsensus.ValidatorSet 22 | AppStateHash string 23 | } 24 | 25 | func NewFinalizationStore() *FinalizationStore { 26 | return &FinalizationStore{ 27 | byHeight: make(map[uint64]fin), 28 | } 29 | } 30 | 31 | func (s *FinalizationStore) SaveFinalization( 32 | ctx context.Context, 33 | height uint64, round uint32, 34 | blockHash string, 35 | valSet tmconsensus.ValidatorSet, 36 | appStateHash string, 37 | ) error { 38 | s.mu.Lock() 39 | defer s.mu.Unlock() 40 | 41 | if _, ok := s.byHeight[height]; ok { 42 | return tmstore.FinalizationOverwriteError{Height: height} 43 | } 44 | 45 | s.byHeight[height] = fin{ 46 | H: height, R: round, 47 | BlockHash: blockHash, 48 | ValSet: valSet, 49 | AppStateHash: appStateHash, 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func (s *FinalizationStore) LoadFinalizationByHeight(ctx context.Context, height uint64) ( 56 | round uint32, 57 | blockHash string, 58 | valSet tmconsensus.ValidatorSet, 59 | appStateHash string, 60 | err error, 61 | ) { 62 | s.mu.RLock() 63 | defer s.mu.RUnlock() 64 | 65 | fin, ok := s.byHeight[height] 66 | if !ok { 67 | return 0, "", tmconsensus.ValidatorSet{}, "", tmconsensus.HeightUnknownError{Want: height} 68 | } 69 | 70 | return fin.R, fin.BlockHash, fin.ValSet, fin.AppStateHash, nil 71 | } 72 | -------------------------------------------------------------------------------- /tm/tmstore/tmmemstore/finalizationstore_test.go: -------------------------------------------------------------------------------- 1 | package tmmemstore_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gordian-engine/gordian/tm/tmconsensus/tmconsensustest" 7 | "github.com/gordian-engine/gordian/tm/tmstore" 8 | "github.com/gordian-engine/gordian/tm/tmstore/tmmemstore" 9 | "github.com/gordian-engine/gordian/tm/tmstore/tmstoretest" 10 | ) 11 | 12 | func TestMemFinalizationStore(t *testing.T) { 13 | t.Parallel() 14 | 15 | tmstoretest.TestFinalizationStoreCompliance(t, func(func(func())) (tmstore.FinalizationStore, error) { 16 | return tmmemstore.NewFinalizationStore(), nil 17 | }, tmconsensustest.NewEd25519Fixture) 18 | } 19 | -------------------------------------------------------------------------------- /tm/tmstore/tmmemstore/memmultistore_test.go: -------------------------------------------------------------------------------- 1 | package tmmemstore_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gordian-engine/gordian/tm/tmconsensus/tmconsensustest" 7 | "github.com/gordian-engine/gordian/tm/tmstore/tmmemstore" 8 | "github.com/gordian-engine/gordian/tm/tmstore/tmstoretest" 9 | ) 10 | 11 | // MemMultiStore embeds all the tmmemstore types, 12 | // in order to run [tmstoretest.TestMultiStoreCompliance]. 13 | // Since all of the store types are intended to act independently, 14 | // we do not anticipate the multi store tests failing for the MemMultiStore. 15 | type MemMultiStore struct { 16 | *tmmemstore.ActionStore 17 | *tmmemstore.CommittedHeaderStore 18 | *tmmemstore.FinalizationStore 19 | *tmmemstore.MirrorStore 20 | *tmmemstore.RoundStore 21 | *tmmemstore.ValidatorStore 22 | } 23 | 24 | func NewMemMultiStore() *MemMultiStore { 25 | return &MemMultiStore{ 26 | ActionStore: tmmemstore.NewActionStore(), 27 | CommittedHeaderStore: tmmemstore.NewCommittedHeaderStore(), 28 | FinalizationStore: tmmemstore.NewFinalizationStore(), 29 | MirrorStore: tmmemstore.NewMirrorStore(), 30 | RoundStore: tmmemstore.NewRoundStore(), 31 | ValidatorStore: tmmemstore.NewValidatorStore(tmconsensustest.SimpleHashScheme{}), 32 | } 33 | } 34 | 35 | func TestMemMultiStoreCompliance(t *testing.T) { 36 | t.Parallel() 37 | 38 | tmstoretest.TestMultiStoreCompliance( 39 | t, 40 | func(cleanup func(func())) (*MemMultiStore, error) { 41 | return NewMemMultiStore(), nil 42 | }, 43 | tmconsensustest.NewEd25519Fixture, 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /tm/tmstore/tmmemstore/mirrorstore.go: -------------------------------------------------------------------------------- 1 | package tmmemstore 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/gordian-engine/gordian/tm/tmstore" 8 | ) 9 | 10 | // MirrorStore is an in-memory implementation of [tmstore.MirrorStore]. 11 | type MirrorStore struct { 12 | mu sync.RWMutex 13 | 14 | votingHeight uint64 15 | votingRound uint32 16 | 17 | committingHeight uint64 18 | committingRound uint32 19 | } 20 | 21 | func NewMirrorStore() *MirrorStore { 22 | return new(MirrorStore) 23 | } 24 | 25 | func (s *MirrorStore) SetNetworkHeightRound( 26 | _ context.Context, 27 | votingHeight uint64, votingRound uint32, 28 | committingHeight uint64, committingRound uint32, 29 | ) error { 30 | s.mu.Lock() 31 | defer s.mu.Unlock() 32 | 33 | // TODO: should this validate that we only move forward? 34 | // And should it validate that committingHeight == votingHeight-1? 35 | 36 | s.votingHeight = votingHeight 37 | s.votingRound = votingRound 38 | s.committingHeight = committingHeight 39 | s.committingRound = committingRound 40 | 41 | return nil 42 | } 43 | 44 | func (s *MirrorStore) NetworkHeightRound(_ context.Context) ( 45 | votingHeight uint64, votingRound uint32, 46 | committingHeight uint64, committingRound uint32, 47 | err error, 48 | ) { 49 | s.mu.RLock() 50 | defer s.mu.RUnlock() 51 | 52 | if s.votingHeight == 0 { 53 | return 0, 0, 0, 0, tmstore.ErrStoreUninitialized 54 | } 55 | 56 | return s.votingHeight, s.votingRound, 57 | s.committingHeight, s.committingRound, nil 58 | } 59 | -------------------------------------------------------------------------------- /tm/tmstore/tmmemstore/mirrorstore_test.go: -------------------------------------------------------------------------------- 1 | package tmmemstore_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gordian-engine/gordian/tm/tmstore" 7 | "github.com/gordian-engine/gordian/tm/tmstore/tmmemstore" 8 | "github.com/gordian-engine/gordian/tm/tmstore/tmstoretest" 9 | ) 10 | 11 | func TestMemMirrorStore(t *testing.T) { 12 | t.Parallel() 13 | 14 | tmstoretest.TestMirrorStoreCompliance(t, func(func(func())) (tmstore.MirrorStore, error) { 15 | return tmmemstore.NewMirrorStore(), nil 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /tm/tmstore/tmmemstore/roundstore_test.go: -------------------------------------------------------------------------------- 1 | package tmmemstore_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gordian-engine/gordian/tm/tmconsensus/tmconsensustest" 7 | "github.com/gordian-engine/gordian/tm/tmstore" 8 | "github.com/gordian-engine/gordian/tm/tmstore/tmmemstore" 9 | "github.com/gordian-engine/gordian/tm/tmstore/tmstoretest" 10 | ) 11 | 12 | func TestMemRoundStore(t *testing.T) { 13 | t.Parallel() 14 | 15 | tmstoretest.TestRoundStoreCompliance(t, func(func(func())) (tmstore.RoundStore, error) { 16 | return tmmemstore.NewRoundStore(), nil 17 | }, tmconsensustest.NewEd25519Fixture) 18 | } 19 | -------------------------------------------------------------------------------- /tm/tmstore/tmmemstore/statemachinestore.go: -------------------------------------------------------------------------------- 1 | package tmmemstore 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/gordian-engine/gordian/tm/tmstore" 8 | ) 9 | 10 | type StateMachineStore struct { 11 | mu sync.Mutex 12 | h uint64 13 | r uint32 14 | } 15 | 16 | func NewStateMachineStore() *StateMachineStore { 17 | return new(StateMachineStore) 18 | } 19 | 20 | func (s *StateMachineStore) SetStateMachineHeightRound( 21 | _ context.Context, 22 | height uint64, round uint32, 23 | ) error { 24 | s.mu.Lock() 25 | defer s.mu.Unlock() 26 | 27 | s.h = height 28 | s.r = round 29 | return nil 30 | } 31 | 32 | func (s *StateMachineStore) StateMachineHeightRound(_ context.Context) ( 33 | height uint64, round uint32, 34 | err error, 35 | ) { 36 | s.mu.Lock() 37 | defer s.mu.Unlock() 38 | 39 | if s.h == 0 { 40 | return 0, 0, tmstore.ErrStoreUninitialized 41 | } 42 | 43 | return s.h, s.r, nil 44 | } 45 | -------------------------------------------------------------------------------- /tm/tmstore/tmmemstore/statemachinestore_test.go: -------------------------------------------------------------------------------- 1 | package tmmemstore_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/gordian-engine/gordian/tm/tmstore" 8 | "github.com/gordian-engine/gordian/tm/tmstore/tmmemstore" 9 | "github.com/gordian-engine/gordian/tm/tmstore/tmstoretest" 10 | ) 11 | 12 | func TestStateMachineStore(t *testing.T) { 13 | t.Parallel() 14 | 15 | tmstoretest.TestStateMachineStoreCompliance(t, func(ctx context.Context, _ func(func())) (tmstore.StateMachineStore, error) { 16 | return tmmemstore.NewStateMachineStore(), nil 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /tm/tmstore/tmmemstore/validatorstore.go: -------------------------------------------------------------------------------- 1 | package tmmemstore 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "slices" 7 | "sync" 8 | 9 | "github.com/gordian-engine/gordian/gcrypto" 10 | "github.com/gordian-engine/gordian/tm/tmconsensus" 11 | "github.com/gordian-engine/gordian/tm/tmstore" 12 | ) 13 | 14 | type ValidatorStore struct { 15 | hs tmconsensus.HashScheme 16 | 17 | mu sync.RWMutex 18 | keys map[string][]gcrypto.PubKey 19 | pows map[string][]uint64 20 | } 21 | 22 | func NewValidatorStore(hs tmconsensus.HashScheme) *ValidatorStore { 23 | return &ValidatorStore{ 24 | hs: hs, 25 | 26 | keys: make(map[string][]gcrypto.PubKey), 27 | pows: make(map[string][]uint64), 28 | } 29 | } 30 | 31 | func (s *ValidatorStore) SavePubKeys(_ context.Context, keys []gcrypto.PubKey) (string, error) { 32 | hash, err := s.hs.PubKeys(keys) 33 | if err != nil { 34 | return "", err 35 | } 36 | sHash := string(hash) 37 | 38 | s.mu.Lock() 39 | defer s.mu.Unlock() 40 | 41 | if _, ok := s.keys[sHash]; ok { 42 | return sHash, tmstore.PubKeysAlreadyExistError{ExistingHash: sHash} 43 | } 44 | 45 | // TODO: should this clone the public keys? 46 | s.keys[sHash] = keys 47 | return sHash, nil 48 | } 49 | 50 | func (s *ValidatorStore) SaveVotePowers(_ context.Context, pows []uint64) (string, error) { 51 | hash, err := s.hs.VotePowers(pows) 52 | if err != nil { 53 | return "", err 54 | } 55 | sHash := string(hash) 56 | 57 | s.mu.Lock() 58 | defer s.mu.Unlock() 59 | 60 | if _, ok := s.pows[sHash]; ok { 61 | return sHash, tmstore.VotePowersAlreadyExistError{ExistingHash: sHash} 62 | } 63 | 64 | s.pows[sHash] = slices.Clone(pows) 65 | return sHash, nil 66 | } 67 | 68 | func (s *ValidatorStore) LoadPubKeys(_ context.Context, hash string) ([]gcrypto.PubKey, error) { 69 | s.mu.RLock() 70 | defer s.mu.RUnlock() 71 | 72 | keys, ok := s.keys[hash] 73 | if !ok { 74 | return nil, tmstore.NoPubKeyHashError{Want: hash} 75 | } 76 | 77 | return keys, nil 78 | } 79 | 80 | func (s *ValidatorStore) LoadVotePowers(_ context.Context, hash string) ([]uint64, error) { 81 | s.mu.RLock() 82 | defer s.mu.RUnlock() 83 | 84 | pows, ok := s.pows[hash] 85 | if !ok { 86 | return nil, tmstore.NoVotePowerHashError{Want: hash} 87 | } 88 | 89 | return pows, nil 90 | } 91 | 92 | func (s *ValidatorStore) LoadValidators(_ context.Context, keyHash, powHash string) ([]tmconsensus.Validator, error) { 93 | s.mu.RLock() 94 | defer s.mu.RUnlock() 95 | 96 | var err error 97 | keys, ok := s.keys[keyHash] 98 | if !ok { 99 | err = tmstore.NoPubKeyHashError{Want: keyHash} 100 | } 101 | pows, ok := s.pows[powHash] 102 | if !ok { 103 | err = errors.Join(err, tmstore.NoVotePowerHashError{Want: powHash}) 104 | } 105 | 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | if len(keys) != len(pows) { 111 | return nil, tmstore.PubKeyPowerCountMismatchError{ 112 | NPubKeys: len(keys), 113 | NVotePower: len(pows), 114 | } 115 | } 116 | 117 | vals := make([]tmconsensus.Validator, len(keys)) 118 | for i, k := range keys { 119 | vals[i] = tmconsensus.Validator{ 120 | PubKey: k, 121 | Power: pows[i], 122 | } 123 | } 124 | 125 | return vals, nil 126 | } 127 | -------------------------------------------------------------------------------- /tm/tmstore/tmmemstore/validatorstore_test.go: -------------------------------------------------------------------------------- 1 | package tmmemstore_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gordian-engine/gordian/tm/tmconsensus/tmconsensustest" 7 | "github.com/gordian-engine/gordian/tm/tmstore" 8 | "github.com/gordian-engine/gordian/tm/tmstore/tmmemstore" 9 | "github.com/gordian-engine/gordian/tm/tmstore/tmstoretest" 10 | ) 11 | 12 | func TestMemValidatorStore(t *testing.T) { 13 | t.Parallel() 14 | 15 | tmstoretest.TestValidatorStoreCompliance(t, func(func(func())) (tmstore.ValidatorStore, error) { 16 | return tmmemstore.NewValidatorStore(tmconsensustest.SimpleHashScheme{}), nil 17 | }, tmconsensustest.NewEd25519Fixture) 18 | } 19 | -------------------------------------------------------------------------------- /tm/tmstore/tmstoretest/committedheaderstorecompliance.go: -------------------------------------------------------------------------------- 1 | package tmstoretest 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/gordian-engine/gordian/tm/tmconsensus" 8 | "github.com/gordian-engine/gordian/tm/tmstore" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | type CommittedHeaderStoreFactory func(cleanup func(func())) (tmstore.CommittedHeaderStore, error) 13 | 14 | func TestCommittedHeaderStoreCompliance(t *testing.T, f CommittedHeaderStoreFactory, fxf FixtureFactory) { 15 | t.Run("happy path", func(t *testing.T) { 16 | t.Parallel() 17 | 18 | ctx, cancel := context.WithCancel(context.Background()) 19 | defer cancel() 20 | 21 | s, err := f(t.Cleanup) 22 | require.NoError(t, err) 23 | 24 | fx := fxf(4) 25 | 26 | ph1 := fx.NextProposedHeader([]byte("app_data_1"), 0) 27 | ph1.Header.PrevAppStateHash = []byte("initial_app_state") // TODO: this should be automatically set in the fixture. 28 | 29 | // This tends to be nil from the store. 30 | // Ensure it is nil here too. 31 | require.Empty(t, ph1.Header.PrevCommitProof.Proofs) 32 | ph1.Header.PrevCommitProof.Proofs = nil 33 | 34 | fx.RecalculateHash(&ph1.Header) 35 | fx.SignProposal(ctx, &ph1, 0) 36 | 37 | voteMap := map[string][]int{ 38 | string(ph1.Header.Hash): {0, 1, 2}, 39 | "": {3}, 40 | } 41 | precommitProofs1 := fx.PrecommitProofMap(ctx, 1, 0, voteMap) 42 | fx.CommitBlock(ph1.Header, []byte("app_state_height_1"), 0, precommitProofs1) 43 | 44 | ph2 := fx.NextProposedHeader([]byte("app_data_2"), 0) 45 | ph2.Round = 1 46 | 47 | ch := tmconsensus.CommittedHeader{ 48 | Header: ph1.Header, 49 | Proof: ph2.Header.PrevCommitProof, 50 | } 51 | 52 | require.NoError(t, s.SaveCommittedHeader(ctx, ch)) 53 | 54 | got, err := s.LoadCommittedHeader(ctx, 1) 55 | require.NoError(t, err) 56 | 57 | require.Equal(t, ch, got) 58 | 59 | voteMap = map[string][]int{ 60 | string(ph2.Header.Hash): {0, 1, 3}, 61 | "": {2}, 62 | } 63 | precommitProofs2 := fx.PrecommitProofMap(ctx, 2, 1, voteMap) 64 | fx.CommitBlock(ph2.Header, []byte("app_state_height_2"), 1, precommitProofs2) 65 | 66 | ph3 := fx.NextProposedHeader([]byte("app_data_3"), 0) 67 | 68 | ch = tmconsensus.CommittedHeader{ 69 | Header: ph2.Header, 70 | Proof: ph3.Header.PrevCommitProof, 71 | } 72 | 73 | require.NoError(t, s.SaveCommittedHeader(ctx, ch)) 74 | 75 | got, err = s.LoadCommittedHeader(ctx, 2) 76 | require.NoError(t, err) 77 | 78 | require.Equal(t, ch, got) 79 | }) 80 | 81 | t.Run("HeightUnknownError when height not found", func(t *testing.T) { 82 | t.Parallel() 83 | 84 | ctx, cancel := context.WithCancel(context.Background()) 85 | defer cancel() 86 | 87 | s, err := f(t.Cleanup) 88 | require.NoError(t, err) 89 | 90 | _, err = s.LoadCommittedHeader(ctx, 1) 91 | require.Error(t, err) 92 | require.ErrorIs(t, err, tmconsensus.HeightUnknownError{Want: 1}) 93 | 94 | _, err = s.LoadCommittedHeader(ctx, 5) 95 | require.Error(t, err) 96 | require.ErrorIs(t, err, tmconsensus.HeightUnknownError{Want: 5}) 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /tm/tmstore/tmstoretest/doc.go: -------------------------------------------------------------------------------- 1 | // Package tmstoretest contains the compliance tests for implementations of the [tmstore] interfaces. 2 | // 3 | // Refer to [github.com/gordian-engine/gordian/tm/tmmemstore] for examples of calling the compliance tests. 4 | package tmstoretest 5 | -------------------------------------------------------------------------------- /tm/tmstore/tmstoretest/finalizationstorecompliance.go: -------------------------------------------------------------------------------- 1 | package tmstoretest 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/gordian-engine/gordian/tm/tmconsensus" 8 | "github.com/gordian-engine/gordian/tm/tmstore" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | type FinalizationStoreFactory func(cleanup func(func())) (tmstore.FinalizationStore, error) 13 | 14 | func TestFinalizationStoreCompliance(t *testing.T, f FinalizationStoreFactory, fxf FixtureFactory) { 15 | t.Run("round trip", func(t *testing.T) { 16 | t.Parallel() 17 | 18 | ctx, cancel := context.WithCancel(context.Background()) 19 | defer cancel() 20 | 21 | s, err := f(t.Cleanup) 22 | require.NoError(t, err) 23 | 24 | valSet := fxf(3).ValSet() 25 | 26 | require.NoError(t, s.SaveFinalization(ctx, 1, 3, "my_block_hash", valSet, "my_app_state_hash")) 27 | 28 | round, blockHash, newValSet, appStateHash, err := s.LoadFinalizationByHeight(ctx, 1) 29 | require.NoError(t, err) 30 | require.Equal(t, uint32(3), round) 31 | require.Equal(t, "my_block_hash", blockHash) 32 | require.True(t, valSet.Equal(newValSet)) 33 | require.Equal(t, "my_app_state_hash", appStateHash) 34 | }) 35 | 36 | t.Run("returns HeightUnknownError when loading unknown height", func(t *testing.T) { 37 | t.Parallel() 38 | 39 | ctx, cancel := context.WithCancel(context.Background()) 40 | defer cancel() 41 | 42 | s, err := f(t.Cleanup) 43 | require.NoError(t, err) 44 | 45 | _, _, _, _, err = s.LoadFinalizationByHeight(ctx, 10) 46 | require.Error(t, err) 47 | require.ErrorIs(t, err, tmconsensus.HeightUnknownError{Want: 10}) 48 | }) 49 | 50 | t.Run("returns FinalizationOverwriteError on a double save", func(t *testing.T) { 51 | t.Parallel() 52 | 53 | ctx, cancel := context.WithCancel(context.Background()) 54 | defer cancel() 55 | 56 | s, err := f(t.Cleanup) 57 | require.NoError(t, err) 58 | 59 | valSet := fxf(3).ValSet() 60 | 61 | require.NoError(t, s.SaveFinalization(ctx, 1, 3, "my_block_hash", valSet, "my_app_state_hash")) 62 | 63 | // Overwrite error even with exact same values. 64 | expErr := tmstore.FinalizationOverwriteError{Height: 1} 65 | require.ErrorIs(t, s.SaveFinalization(ctx, 1, 3, "my_block_hash", valSet, "my_app_state_hash"), expErr) 66 | 67 | // Overwrite error with same round and different hashes. 68 | require.ErrorIs(t, s.SaveFinalization(ctx, 1, 3, "my_block_hash_2", valSet, "my_app_state_hash_2"), expErr) 69 | 70 | // Overwrite error with different round. 71 | require.ErrorIs(t, s.SaveFinalization(ctx, 1, 100, "my_block_hash_2", valSet, "my_app_state_hash_2"), expErr) 72 | 73 | // Original values unmodified. 74 | round, blockHash, newValSet, appStateHash, err := s.LoadFinalizationByHeight(ctx, 1) 75 | require.NoError(t, err) 76 | require.Equal(t, uint32(3), round) 77 | require.Equal(t, "my_block_hash", blockHash) 78 | require.True(t, valSet.Equal(newValSet)) 79 | require.Equal(t, "my_app_state_hash", appStateHash) 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /tm/tmstore/tmstoretest/fixturefactory.go: -------------------------------------------------------------------------------- 1 | package tmstoretest 2 | 3 | import "github.com/gordian-engine/gordian/tm/tmconsensus/tmconsensustest" 4 | 5 | // FixtureFactory is used in every store compliance test, 6 | // to produce validators and signatures. 7 | // 8 | // [tmconsensustest.NewEd25519Fixture] should be used by default 9 | // in the core Gordian code, 10 | // but having this as part of compliance test signatures 11 | // makes it possible to assert that various store types 12 | // are compatible with other key schemes. 13 | type FixtureFactory func(nVals int) *tmconsensustest.Fixture 14 | -------------------------------------------------------------------------------- /tm/tmstore/tmstoretest/mirrorstorecompliance.go: -------------------------------------------------------------------------------- 1 | package tmstoretest 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/gordian-engine/gordian/tm/tmstore" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | type MirrorStoreFactory func(cleanup func(func())) (tmstore.MirrorStore, error) 12 | 13 | // TestMirrorStoreCompliance does not require a FixtureFactory 14 | // because there is no representation of validators or signatures in the mirror store. 15 | func TestMirrorStoreCompliance(t *testing.T, f MirrorStoreFactory) { 16 | t.Run("returns ErrStoreUninitialized before first update", func(t *testing.T) { 17 | t.Parallel() 18 | 19 | ctx, cancel := context.WithCancel(context.Background()) 20 | defer cancel() 21 | 22 | s, err := f(t.Cleanup) 23 | require.NoError(t, err) 24 | 25 | _, _, _, _, err = s.NetworkHeightRound(ctx) 26 | require.Error(t, err) 27 | require.ErrorIs(t, err, tmstore.ErrStoreUninitialized) 28 | }) 29 | 30 | t.Run("returns stored value", func(t *testing.T) { 31 | t.Parallel() 32 | 33 | ctx, cancel := context.WithCancel(context.Background()) 34 | defer cancel() 35 | 36 | s, err := f(t.Cleanup) 37 | require.NoError(t, err) 38 | 39 | // Voting on 1/0 first. 40 | require.NoError(t, s.SetNetworkHeightRound(ctx, 1, 0, 0, 0)) 41 | vh, vr, ch, cr, err := s.NetworkHeightRound(ctx) 42 | require.NoError(t, err) 43 | require.Equal(t, uint64(1), vh) 44 | require.Zero(t, vr) 45 | require.Zero(t, ch) 46 | require.Zero(t, cr) 47 | 48 | // Then voting on 1/1. 49 | require.NoError(t, s.SetNetworkHeightRound(ctx, 1, 1, 0, 0)) 50 | vh, vr, ch, cr, err = s.NetworkHeightRound(ctx) 51 | require.NoError(t, err) 52 | require.Equal(t, uint64(1), vh) 53 | require.Equal(t, uint32(1), vr) 54 | require.Zero(t, ch) 55 | require.Zero(t, cr) 56 | 57 | // Now voting on 2/0, committing 1/1. 58 | require.NoError(t, s.SetNetworkHeightRound(ctx, 2, 0, 1, 1)) 59 | vh, vr, ch, cr, err = s.NetworkHeightRound(ctx) 60 | require.NoError(t, err) 61 | require.Equal(t, uint64(2), vh) 62 | require.Zero(t, vr) 63 | require.Equal(t, uint64(1), ch) 64 | require.Equal(t, uint32(1), cr) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /tm/tmstore/tmstoretest/statemachinestorecompliance.go: -------------------------------------------------------------------------------- 1 | package tmstoretest 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/gordian-engine/gordian/tm/tmstore" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | type StateMachineStoreFactory func(ctx context.Context, cleanup func(func())) (tmstore.StateMachineStore, error) 12 | 13 | func TestStateMachineStoreCompliance(t *testing.T, f StateMachineStoreFactory) { 14 | t.Run("returns ErrStoreUninitialized before first update", func(t *testing.T) { 15 | t.Parallel() 16 | 17 | ctx, cancel := context.WithCancel(context.Background()) 18 | defer cancel() 19 | 20 | s, err := f(ctx, t.Cleanup) 21 | require.NoError(t, err) 22 | 23 | _, _, err = s.StateMachineHeightRound(ctx) 24 | require.Error(t, err) 25 | require.ErrorIs(t, err, tmstore.ErrStoreUninitialized) 26 | }) 27 | 28 | t.Run("returns stored value", func(t *testing.T) { 29 | t.Parallel() 30 | 31 | ctx, cancel := context.WithCancel(context.Background()) 32 | defer cancel() 33 | 34 | s, err := f(ctx, t.Cleanup) 35 | require.NoError(t, err) 36 | 37 | require.NoError(t, s.SetStateMachineHeightRound(ctx, 1, 0)) 38 | 39 | h, r, err := s.StateMachineHeightRound(ctx) 40 | require.NoError(t, err) 41 | require.Equal(t, uint64(1), h) 42 | require.Zero(t, r) 43 | 44 | require.NoError(t, s.SetStateMachineHeightRound(ctx, 1, 1)) 45 | 46 | h, r, err = s.StateMachineHeightRound(ctx) 47 | require.NoError(t, err) 48 | require.Equal(t, uint64(1), h) 49 | require.Equal(t, uint32(1), r) 50 | 51 | require.NoError(t, s.SetStateMachineHeightRound(ctx, 2, 0)) 52 | 53 | h, r, err = s.StateMachineHeightRound(ctx) 54 | require.NoError(t, err) 55 | require.Equal(t, uint64(2), h) 56 | require.Zero(t, r) 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /tm/tmstore/validatorstore.go: -------------------------------------------------------------------------------- 1 | package tmstore 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gordian-engine/gordian/gcrypto" 7 | "github.com/gordian-engine/gordian/tm/tmconsensus" 8 | ) 9 | 10 | // ValidatorStore manages storage and retrieval of sets of validators, 11 | // split into sets of public keys and sets of voting powers. 12 | type ValidatorStore interface { 13 | // SavePubKeys saves the ordered collection of public keys, 14 | // and returns the calculated hash to retrieve the same set of keys later. 15 | SavePubKeys(context.Context, []gcrypto.PubKey) (string, error) 16 | 17 | // SavePubKeys saves the ordered collection of vote powers, 18 | // and returns the calculated hash to retrieve the same set of powers later. 19 | SaveVotePowers(context.Context, []uint64) (string, error) 20 | 21 | // LoadPubKeys loads the set of public keys belonging to the given hash. 22 | LoadPubKeys(context.Context, string) ([]gcrypto.PubKey, error) 23 | 24 | // LoadVotePowers loads the set of vote powers belonging to the given hash. 25 | LoadVotePowers(context.Context, string) ([]uint64, error) 26 | 27 | // LoadValidators loads a set of validators constituted of the public keys and vote powers 28 | // corresponding to the given hashes. 29 | LoadValidators(ctx context.Context, keyHash, powHash string) ([]tmconsensus.Validator, error) 30 | } 31 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | // For the tools.go pattern, see: 4 | // https://go.dev/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 5 | 6 | package gordian 7 | 8 | import ( 9 | // For stringer, used in a few go:generate calls. 10 | _ "golang.org/x/tools/cmd/stringer" 11 | ) 12 | --------------------------------------------------------------------------------