diff --git a/.github/workflows/protect-main.yaml b/.github/workflows/protect-main.yaml new file mode 100644 index 0000000000..83adadb9eb --- /dev/null +++ b/.github/workflows/protect-main.yaml @@ -0,0 +1,26 @@ +name: Protect main + +on: + pull_request: + branches: + - main + types: + - opened + - synchronize + - reopened + - edited + +jobs: + require-development: + name: Require development source branch + runs-on: ubuntu-latest + steps: + - name: Check source branch + env: + HEAD_REF: ${{ github.head_ref }} + run: | + if [ "$HEAD_REF" != "development" ]; then + echo "::error::PRs targeting main must come from the development branch (got '$HEAD_REF'). Open this PR against development instead, then promote development to main." + exit 1 + fi + echo "PR is from development → main. OK." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4bce83121b..1385a6ed83 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -94,6 +94,9 @@ jobs: with: ref: ${{ github.ref }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 @@ -103,18 +106,25 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build and push + - name: Prepare tags + id: prep run: | TAG=${{ needs.validate.outputs.tag }} DOCKER_TAG=${TAG//+/-} - docker build \ - -t canopynetwork/canopy:${DOCKER_TAG} \ - -t canopynetwork/canopy:latest \ - --build-arg BRANCH=${TAG} \ - --build-arg BUILD_PATH=cmd/cli \ - ./.docker/auto-update/ - docker push canopynetwork/canopy:${DOCKER_TAG} - docker push canopynetwork/canopy:latest + echo "docker_tag=${DOCKER_TAG}" >> $GITHUB_OUTPUT + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ./.docker/auto-update/ + platforms: linux/amd64,linux/arm64 + push: true + tags: | + canopynetwork/canopy:${{ steps.prep.outputs.docker_tag }} + canopynetwork/canopy:latest + build-args: | + BRANCH=${{ github.event.inputs.tag }} + BUILD_PATH=cmd/cli docker-go: name: Docker - Go Plugin @@ -127,6 +137,9 @@ jobs: with: ref: ${{ github.ref }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -136,19 +149,26 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build and push + - name: Prepare tags + id: prep run: | TAG=${{ needs.validate.outputs.tag }} DOCKER_TAG=${TAG//+/-} - docker build \ - -t canopynetwork/canopy:go-${DOCKER_TAG} \ - -t canopynetwork/canopy:go-latest \ - --build-arg BRANCH=${TAG} \ - --build-arg BUILD_PATH=cmd/cli \ - -f ./.docker/auto-update/Dockerfile.go \ - ./.docker/auto-update/ - docker push canopynetwork/canopy:go-${DOCKER_TAG} - docker push canopynetwork/canopy:go-latest + echo "docker_tag=${DOCKER_TAG}" >> $GITHUB_OUTPUT + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ./.docker/auto-update/ + file: ./.docker/auto-update/Dockerfile.go + platforms: linux/amd64,linux/arm64 + push: true + tags: | + canopynetwork/canopy-plugin-go:${{ steps.prep.outputs.docker_tag }} + canopynetwork/canopy-plugin-go:latest + build-args: | + BRANCH=${{ github.event.inputs.tag }} + BUILD_PATH=cmd/cli docker-python: name: Docker - Python Plugin @@ -161,6 +181,9 @@ jobs: with: ref: ${{ github.ref }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -170,19 +193,26 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build and push + - name: Prepare tags + id: prep run: | TAG=${{ needs.validate.outputs.tag }} DOCKER_TAG=${TAG//+/-} - docker build \ - -t canopynetwork/canopy:python-${DOCKER_TAG} \ - -t canopynetwork/canopy:python-latest \ - --build-arg BRANCH=${TAG} \ - --build-arg BUILD_PATH=cmd/cli \ - -f ./.docker/auto-update/Dockerfile.python \ - ./.docker/auto-update/ - docker push canopynetwork/canopy:python-${DOCKER_TAG} - docker push canopynetwork/canopy:python-latest + echo "docker_tag=${DOCKER_TAG}" >> $GITHUB_OUTPUT + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ./.docker/auto-update/ + file: ./.docker/auto-update/Dockerfile.python + platforms: linux/amd64,linux/arm64 + push: true + tags: | + canopynetwork/canopy-plugin-python:${{ steps.prep.outputs.docker_tag }} + canopynetwork/canopy-plugin-python:latest + build-args: | + BRANCH=${{ github.event.inputs.tag }} + BUILD_PATH=cmd/cli docker-typescript: name: Docker - TypeScript Plugin @@ -195,6 +225,9 @@ jobs: with: ref: ${{ github.ref }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -204,19 +237,26 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build and push + - name: Prepare tags + id: prep run: | TAG=${{ needs.validate.outputs.tag }} DOCKER_TAG=${TAG//+/-} - docker build \ - -t canopynetwork/canopy:typescript-${DOCKER_TAG} \ - -t canopynetwork/canopy:typescript-latest \ - --build-arg BRANCH=${TAG} \ - --build-arg BUILD_PATH=cmd/cli \ - -f ./.docker/auto-update/Dockerfile.typescript \ - ./.docker/auto-update/ - docker push canopynetwork/canopy:typescript-${DOCKER_TAG} - docker push canopynetwork/canopy:typescript-latest + echo "docker_tag=${DOCKER_TAG}" >> $GITHUB_OUTPUT + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ./.docker/auto-update/ + file: ./.docker/auto-update/Dockerfile.typescript + platforms: linux/amd64,linux/arm64 + push: true + tags: | + canopynetwork/canopy-plugin-typescript:${{ steps.prep.outputs.docker_tag }} + canopynetwork/canopy-plugin-typescript:latest + build-args: | + BRANCH=${{ github.event.inputs.tag }} + BUILD_PATH=cmd/cli docker-kotlin: name: Docker - Kotlin Plugin @@ -229,6 +269,9 @@ jobs: with: ref: ${{ github.ref }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -238,19 +281,26 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build and push + - name: Prepare tags + id: prep run: | TAG=${{ needs.validate.outputs.tag }} DOCKER_TAG=${TAG//+/-} - docker build \ - -t canopynetwork/canopy:kotlin-${DOCKER_TAG} \ - -t canopynetwork/canopy:kotlin-latest \ - --build-arg BRANCH=${TAG} \ - --build-arg BUILD_PATH=cmd/cli \ - -f ./.docker/auto-update/Dockerfile.kotlin \ - ./.docker/auto-update/ - docker push canopynetwork/canopy:kotlin-${DOCKER_TAG} - docker push canopynetwork/canopy:kotlin-latest + echo "docker_tag=${DOCKER_TAG}" >> $GITHUB_OUTPUT + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ./.docker/auto-update/ + file: ./.docker/auto-update/Dockerfile.kotlin + platforms: linux/amd64,linux/arm64 + push: true + tags: | + canopynetwork/canopy-plugin-kotlin:${{ steps.prep.outputs.docker_tag }} + canopynetwork/canopy-plugin-kotlin:latest + build-args: | + BRANCH=${{ github.event.inputs.tag }} + BUILD_PATH=cmd/cli docker-csharp: name: Docker - C# Plugin @@ -263,6 +313,9 @@ jobs: with: ref: ${{ github.ref }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -272,16 +325,23 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build and push + - name: Prepare tags + id: prep run: | TAG=${{ needs.validate.outputs.tag }} DOCKER_TAG=${TAG//+/-} - docker build \ - -t canopynetwork/canopy:csharp-${DOCKER_TAG} \ - -t canopynetwork/canopy:csharp-latest \ - --build-arg BRANCH=${TAG} \ - --build-arg BUILD_PATH=cmd/cli \ - -f ./.docker/auto-update/Dockerfile.csharp \ - ./.docker/auto-update/ - docker push canopynetwork/canopy:csharp-${DOCKER_TAG} - docker push canopynetwork/canopy:csharp-latest + echo "docker_tag=${DOCKER_TAG}" >> $GITHUB_OUTPUT + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ./.docker/auto-update/ + file: ./.docker/auto-update/Dockerfile.csharp + platforms: linux/amd64,linux/arm64 + push: true + tags: | + canopynetwork/canopy-plugin-csharp:${{ steps.prep.outputs.docker_tag }} + canopynetwork/canopy-plugin-csharp:latest + build-args: | + BRANCH=${{ github.event.inputs.tag }} + BUILD_PATH=cmd/cli diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 85de74592d..0493fafd11 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -84,9 +84,10 @@ discuss your intended approach for solving the problem in the comments for an ex - **Update the CHANGELOG** for all enhancements and bug fixes. Include the corresponding date, issue number if one exists and current version if any. Check the format of the [CHANGELOG](.docs/CHANGELOG.md). -- **Use the repo's default main branch.** Branch from and +- **Target the `development` branch.** The repo's default branch is still `main`, but do **not** + open pull requests against `main`. Branch from and [submit your pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) - to the repo's default branch `main`. + to `development`. - **[Resolve any merge conflicts](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/resolving-a-merge-conflict-on-github)** diff --git a/bft/bft.go b/bft/bft.go index ca9c3e60ba..011966cff4 100644 --- a/bft/bft.go +++ b/bft/bft.go @@ -155,12 +155,9 @@ func (b *BFT) Start() { func() { b.Controller.Lock() defer b.Controller.Unlock() - // calculate time since - since := time.Since(resetBFT.StartTime) - // allow if 'since' is less than 1 block old - if int(since.Milliseconds()) < b.Config.BlockTimeMS() { + processTime = validProcessTime(resetBFT.StartTime, time.Duration(b.Config.BlockTimeMS())*time.Millisecond) + if processTime != 0 { b.log.Infof("Using included timestamp to calculate process time: %s", resetBFT.StartTime.Format(time.StampMilli)) - processTime = since } // if is a root-chain update reset back to round 0 but maintain locks to prevent 'fork attacks' // else increment the height and don't maintain locks @@ -185,6 +182,13 @@ func (b *BFT) Start() { } } +func validProcessTime(start time.Time, maxAge time.Duration) time.Duration { + if since := time.Since(start); since > 0 && since < maxAge { + return since + } + return 0 +} + // HandlePhase() is the main BFT Phase stepping loop func (b *BFT) HandlePhase() { stopTimers := func() { b.PhaseTimer.Stop() } @@ -609,8 +613,8 @@ func (b *BFT) Pacemaker() bool { continue } totalVotedPower += validator.VotingPower - // if totalVotePower >= +33%, it's safe to advance to that round - if totalVotedPower >= lib.Uint64ReducePercentage(b.ValidatorSet.MinimumMaj23, 50) { + // if totalVotePower > 1/3, it's safe to advance to that round + if totalVotedPower > b.ValidatorSet.TotalPower/3 { pacemakerRound = vote.Qc.Header.Round // set the highest round where +1/3rds have been break } diff --git a/bft/bft_test.go b/bft/bft_test.go index 3592c2fa5d..6139d517b2 100644 --- a/bft/bft_test.go +++ b/bft/bft_test.go @@ -89,6 +89,12 @@ func TestStartElectionPhase(t *testing.T) { } } +func TestValidProcessTimeRejectsFutureTimestamp(t *testing.T) { + require.Zero(t, validProcessTime(time.Now().Add(time.Hour), time.Second)) + require.Zero(t, validProcessTime(time.Now().Add(-time.Hour), time.Second)) + require.Positive(t, validProcessTime(time.Now().Add(-time.Millisecond), time.Second)) +} + func TestStartElectionVotePhase(t *testing.T) { tests := []struct { name string @@ -745,6 +751,19 @@ func TestPacemaker(t *testing.T) { } } +func TestPacemakerRequiresMoreThanOneThird(t *testing.T) { + c := newTestConsensus(t, Propose, 2) + c.bft.ValidatorSet.ValidatorSet.ValidatorSet[0].VotingPower = 67 + c.bft.ValidatorSet.ValidatorSet.ValidatorSet[1].VotingPower = 33 + c.bft.ValidatorSet.TotalPower = 100 + msg := &Message{Qc: &QC{Header: c.view(RoundInterrupt, 3)}} + require.NoError(t, msg.Sign(c.valKeys[1])) + require.NoError(t, c.bft.HandleMessage(msg)) + + c.bft.Pacemaker() + require.Equal(t, uint64(1), c.bft.Round) +} + func TestScheduleForceRound(t *testing.T) { c := newTestConsensus(t, Election, 1) c.bft.Round = 2 diff --git a/bft/mock_test.go b/bft/mock_test.go index 11524fa33c..165be33a70 100644 --- a/bft/mock_test.go +++ b/bft/mock_test.go @@ -239,6 +239,7 @@ func (tc *testConsensus) simLead(t *testing.T, mk crypto.MultiPublicKeyI, round ResultsHash: resHash, Block: block, BlockHash: blkHash, + ProposerKey: tc.bft.ProposerKey, Signature: &lib.AggregateSignature{ Signature: as, Bitmap: mk.Bitmap(), @@ -276,6 +277,7 @@ func (tc *testConsensus) simVote(t *testing.T, round uint64, phase Phase, callba Header: tc.view(phase, round), BlockHash: blkHash, ResultsHash: resHash, + ProposerKey: tc.bft.ProposerKey, }, } // execute callback on the message to allow custom phase functionality @@ -354,6 +356,7 @@ func (tc *testConsensus) newPartialQCDoubleSign(t *testing.T, phase Phase) { Header: tc.view(phase-1, 1), BlockHash: crypto.Hash([]byte("some proposal")), ResultsHash: crypto.Hash([]byte("some results")), + ProposerKey: tc.valKeys[0].PublicKey().Bytes(), } // create the bytes to be signed by the 'double signers' sb := qc.SignBytes() diff --git a/bft/msg.go b/bft/msg.go index 577528237a..e9ca9c99e5 100644 --- a/bft/msg.go +++ b/bft/msg.go @@ -81,6 +81,10 @@ func (b *BFT) CheckProposerMessage(x *Message, p *validateMessageParams) (isPart if err = x.Qc.CheckBasic(); err != nil { return } + // ensure the sender is justified as the proposer + if !bytes.Equal(x.Qc.ProposerKey, x.Signature.PublicKey) { + return false, lib.ErrInvalidSigner() + } // if an unexpected root height if x.Qc.Header.RootHeight != p.rootHeight { // load the proper committee @@ -136,10 +140,6 @@ func (b *BFT) CheckProposerMessage(x *Message, p *validateMessageParams) (isPart return false, lib.ErrInvalidQCCommitteeHeight() } if x.Header.Phase == Propose { - // ensure the sender is justified as the proposer - if !bytes.Equal(x.Qc.ProposerKey, x.Signature.PublicKey) { - return false, lib.ErrInvalidSigner() - } // ensure the block isn't nil if x.Qc.Block == nil { return false, lib.ErrNilBlock() diff --git a/bft/msg_test.go b/bft/msg_test.go index 735ed6570b..97d376dda5 100644 --- a/bft/msg_test.go +++ b/bft/msg_test.go @@ -289,3 +289,14 @@ func TestProposerMessageRejectsInvalidHighQC(t *testing.T) { require.Error(t, errI) require.Equal(t, lib.CodeInvalidAggregateSignature, errI.Code()) } + +func TestProposerMessageRejectsPostElectionRelay(t *testing.T) { + c := newTestConsensus(t, Precommit, 3) + c.simPrecommitPhase(t, 0) + msg := c.bft.Proposals[0][phaseToString(Precommit)][0] + require.NoError(t, msg.Sign(c.valKeys[1])) + + err := c.bft.HandleMessage(msg) + require.Error(t, err) + require.Equal(t, lib.CodeInvalidSigner, err.Code()) +} diff --git a/bft/vote.go b/bft/vote.go index 411bee00dd..228e732ede 100644 --- a/bft/vote.go +++ b/bft/vote.go @@ -5,6 +5,11 @@ import ( "github.com/canopy-network/canopy/lib/crypto" ) +const ( + maxVDFElementSize = 1024 + maxVDFIterations = uint64(1<<31 - 1) +) + // LEADER TRACKING AND AGGREGATING MESSAGES FROM REPLICAS // NOTE: A 'Vote' is a digital signature of SignBytes from a Replica Validator. By signing a message and sending it to the Leader, @@ -52,14 +57,14 @@ func (b *BFT) AddVote(vote *Message) lib.ErrorI { b.Controller.Lock() defer b.Controller.Unlock() voteSet := b.getVoteSet(vote) - // handle high qc and byzantine evidence (only applicable if ELECTION-VOTE) - if err := b.handleHighQCVDFAndEvidence(vote); err != nil { - return err - } // add the vote to the set if err := b.addSigToVoteSet(vote, voteSet); err != nil { return err } + // optional attachments don't invalidate the signed vote + if err := b.handleHighQCVDFAndEvidence(vote); err != nil { + b.log.Warnf("Ignoring invalid vote attachment: %s", err.Error()) + } return nil } @@ -143,6 +148,9 @@ func (b *BFT) handleHighQCVDFAndEvidence(vote *Message) lib.ErrorI { } // pre handle VDF if enabled if b.Config.RunVDF && vote.Vdf != nil && vote.Vdf.Iterations != 0 { + if len(vote.Vdf.Proof) > maxVDFElementSize || len(vote.Vdf.Output) > maxVDFElementSize || vote.Vdf.Iterations > maxVDFIterations { + return lib.ErrInvalidVDF() + } // save the obtained VDF vote to be processed at the PROPOSE phase b.VDFCache = append(b.VDFCache, vote) } diff --git a/bft/vote_test.go b/bft/vote_test.go index 999f3bd764..67e071f399 100644 --- a/bft/vote_test.go +++ b/bft/vote_test.go @@ -2,10 +2,11 @@ package bft import ( "bytes" + "testing" + "github.com/canopy-network/canopy/lib" "github.com/canopy-network/canopy/lib/crypto" "github.com/stretchr/testify/require" - "testing" ) func TestAddVote(t *testing.T) { @@ -92,3 +93,31 @@ func TestAddVote(t *testing.T) { }) } } + +func TestAddVoteAttachments(t *testing.T) { + consensus := newTestConsensus(t, ElectionVote, 4) + consensus.bft.Config.RunVDF = true + newVote := func(i int, vdf *crypto.VDF) *Message { + return &Message{ + Qc: &QC{Header: &lib.View{Phase: ElectionVote}}, + Signature: &lib.Signature{ + PublicKey: consensus.valKeys[i].PublicKey().Bytes(), + Signature: bytes.Repeat([]byte("F"), 96), + }, + Vdf: vdf, + } + } + require.NoError(t, consensus.bft.AddVote(newVote(0, nil))) + err := consensus.bft.AddVote(newVote(0, &crypto.VDF{Proof: make([]byte, maxVDFElementSize+1), Iterations: 1})) + require.ErrorContains(t, err, "duplicate vote") + for i, vdf := range []*crypto.VDF{ + {Proof: make([]byte, maxVDFElementSize+1), Iterations: 1}, + {Output: make([]byte, maxVDFElementSize+1), Iterations: 1}, + {Iterations: maxVDFIterations + 1}, + } { + vote := newVote(i+1, vdf) + require.NoError(t, consensus.bft.AddVote(vote)) + require.NotZero(t, consensus.bft.getVoteSet(vote).TotalVotedPower) + } + require.Empty(t, consensus.bft.VDFCache) +} diff --git a/cmd/rpc/server.go b/cmd/rpc/server.go index d5febcd844..1e78cd824f 100644 --- a/cmd/rpc/server.go +++ b/cmd/rpc/server.go @@ -33,7 +33,7 @@ const ( // uses golang's semver naming convention // https://pkg.go.dev/golang.org/x/mod/semver - SoftwareVersion = "v0.1.22+beta" + SoftwareVersion = "v0.1.23+beta" ContentType = "Content-MessageType" ApplicationJSON = "application/json; charset=utf-8" diff --git a/controller/block.go b/controller/block.go index 04d702b6ad..7aafb31252 100644 --- a/controller/block.go +++ b/controller/block.go @@ -564,6 +564,10 @@ func (c *Controller) ApplyAndValidateBlock(block *lib.Block, commit bool) (b *li // HandlePeerBlock() validates and handles an inbound certificate (with a block) from a remote peer func (c *Controller) HandlePeerBlock(msg *lib.BlockMessage, syncing bool) (*lib.QuorumCertificate, lib.ErrorI) { + return c.handlePeerBlock(msg, syncing, false) +} + +func (c *Controller) handlePeerBlock(msg *lib.BlockMessage, syncing, verifyQC bool) (*lib.QuorumCertificate, lib.ErrorI) { // log the start of 'peer block handling' c.log.Info("Handling peer block") // define a convenience variable for the certificate @@ -599,7 +603,7 @@ func (c *Controller) HandlePeerBlock(msg *lib.BlockMessage, syncing bool) (*lib. } } } - if !syncing || qc.Header.Height%CheckpointFrequency == 0 { + if verifyQC || !syncing || qc.Header.Height%CheckpointFrequency == 0 { // load the committee from the root chain using the root height embedded in the certificate message v, err := c.Consensus.LoadCommittee(c.LoadRootChainId(qc.Header.Height), qc.Header.RootHeight) if err != nil { diff --git a/controller/consensus.go b/controller/consensus.go index 617acf43ac..9271e36247 100644 --- a/controller/consensus.go +++ b/controller/consensus.go @@ -183,7 +183,7 @@ func (c *Controller) processQueue(startHeight, stopHeight uint64, queue map[uint // lock the controller c.Lock() // process the block message received from the peer - _, err := c.HandlePeerBlock(blockMsg, true) + _, err := c.handlePeerBlock(blockMsg, true, height+1 == stopHeight) // unlock controller c.Unlock() // check error from HandlePeerBlock @@ -316,11 +316,6 @@ func (c *Controller) ListenForConsensus() { } // execute in a sub-function to unify error handling and enable 'defer' functionality if err := func() (err lib.ErrorI) { - // check and add the message to the cache to prevent duplicates - if ok := cache.Add(msg); !ok { - // duplicate, exit - return - } // create a new 'consensus message' to unmarshal the bytes to bftMsg := new(bft.Message) // try to unmarshal into a consensus message @@ -328,6 +323,14 @@ func (c *Controller) ListenForConsensus() { // exit with error return } + // direct consensus messages must come from their signer; gossip messages may come from a relay + if !validConsensusSender(msg.Sender.Address.PublicKey, bftMsg, c.P2P.GossipMode()) { + return lib.ErrInvalidSigner() + } + // check and add the message to the cache to prevent duplicates + if ok := cache.Add(msg); !ok { + return + } // check whether the message should be gossiped gossip, exit := c.ShouldGossip(bftMsg) if gossip { @@ -353,6 +356,10 @@ func (c *Controller) ListenForConsensus() { } } +func validConsensusSender(sender []byte, msg *bft.Message, gossipMode bool) bool { + return msg.GetSignature() != nil && (gossipMode || bytes.Equal(sender, msg.Signature.PublicKey)) +} + // ShouldGossip() controls whether a consensus message should be gossiped func (c *Controller) ShouldGossip(msg *bft.Message) (gossip bool, exit bool) { // only gossip when enabled diff --git a/controller/controller_test.go b/controller/controller_test.go index f9c056b766..f5a273378d 100644 --- a/controller/controller_test.go +++ b/controller/controller_test.go @@ -5,9 +5,19 @@ import ( "path/filepath" "testing" + "github.com/canopy-network/canopy/bft" + "github.com/canopy-network/canopy/lib" "github.com/stretchr/testify/require" ) +func TestValidConsensusSender(t *testing.T) { + msg := &bft.Message{Signature: &lib.Signature{PublicKey: []byte{1}}} + require.True(t, validConsensusSender([]byte{1}, msg, false)) + require.False(t, validConsensusSender([]byte{2}, msg, false)) + require.True(t, validConsensusSender([]byte{2}, msg, true)) + require.False(t, validConsensusSender([]byte{1}, &bft.Message{}, true)) +} + func TestResolvePluginCtlPath(t *testing.T) { wd, err := os.Getwd() require.NoError(t, err) diff --git a/fsm/automatic.go b/fsm/automatic.go index 3c151cb571..dcf789e8ad 100644 --- a/fsm/automatic.go +++ b/fsm/automatic.go @@ -9,6 +9,9 @@ import ( // BeginBlock() is code that is executed at the start of `applying` the block func (s *StateMachine) BeginBlock() (lib.Events, lib.ErrorI) { + if s.Metrics != nil { + s.Metrics.RestrictedTxCount.Set(0) + } s.events.Refer(lib.EventStageBeginBlock) // execute plugin begin block if enabled if s.Plugin != nil { diff --git a/fsm/error.go b/fsm/error.go index 2aada4e619..1fd4ed2f6a 100644 --- a/fsm/error.go +++ b/fsm/error.go @@ -20,6 +20,10 @@ func ErrUnauthorizedTx() lib.ErrorI { return lib.NewError(lib.CodeUnauthorizedTx, lib.StateMachineModule, "unauthorized tx") } +func ErrRestrictedAddress() lib.ErrorI { + return lib.NewError(lib.CodeUnauthorizedTx, lib.StateMachineModule, "restricted address") +} + func ErrInvalidTxMessage() lib.ErrorI { return lib.NewError(lib.CodeInvalidTxMessage, lib.StateMachineModule, "invalid transaction message") } diff --git a/fsm/state.go b/fsm/state.go index 059008775b..d27ef90aba 100644 --- a/fsm/state.go +++ b/fsm/state.go @@ -2,6 +2,7 @@ package fsm import ( "context" + "encoding/hex" "fmt" "math" "runtime/debug" @@ -813,6 +814,27 @@ func (s *StateMachine) Discard() { s.store. func (s *StateMachine) ProposalVoteConfig() GovProposalVoteConfig { return s.proposeVoteConfig } func (s *StateMachine) SetProposalVoteConfig(c GovProposalVoteConfig) { s.proposeVoteConfig = c } +// isRestricted() checks if an address is restricted from transacting +func (s *StateMachine) isRestricted(address []byte) bool { + if len(address) != crypto.AddressSize { + return false + } + value := hex.EncodeToString(address) + if _, found := lib.RestrictedAddresses[value]; found { + return true + } + // AcceptAllProposals bypasses only node-local restrictions that may vary across validators and affect BFT consensus; the deterministic hardcoded list always applies. + if s.proposeVoteConfig == AcceptAllProposals { + return false + } + for _, configured := range s.Config.RestrictedAddresses { + if strings.TrimPrefix(strings.ToLower(configured), "0x") == value { + return true + } + } + return false +} + var _ lib.PluginCompatibleFSM = new(StateMachine) // StateRead() implements the 'state read' interface for plugins diff --git a/fsm/state_test.go b/fsm/state_test.go index 2efd9bdb14..23de4ca0f1 100644 --- a/fsm/state_test.go +++ b/fsm/state_test.go @@ -2,6 +2,7 @@ package fsm import ( "context" + "encoding/hex" "encoding/json" "os" "slices" @@ -897,3 +898,21 @@ func TestConformStateToParamUpdate_MinimumStake_ViaMessageHandler(t *testing.T) require.NoError(t, err) require.Equal(t, uint64(0), val3.UnstakingHeight, "delegate 3 should NOT be unstaking") } + +func TestRestrictedAddresses(t *testing.T) { + hardcoded, err := hex.DecodeString("0330070fd38ec3bb94f58fa55d40368271e9e54a") + require.NoError(t, err) + dynamic, err := hex.DecodeString("1111111111111111111111111111111111111111") + require.NoError(t, err) + sm := &StateMachine{Config: lib.Config{StateMachineConfig: lib.StateMachineConfig{ + RestrictedAddresses: []string{"0x1111111111111111111111111111111111111111"}, + }}} + + require.True(t, sm.isRestricted(hardcoded)) + require.False(t, sm.isRestricted(dynamic), "a committed +2/3 block must override local additions") + sm.SetProposalVoteConfig(ProposalApproveList) + require.True(t, sm.isRestricted(dynamic), "validators must apply local additions while voting") + require.False(t, sm.isRestricted(make([]byte, 20))) + require.False(t, sm.isRestricted(nil)) + require.Len(t, lib.RestrictedAddresses, 124) +} diff --git a/fsm/transaction.go b/fsm/transaction.go index 4facd9c19b..c3a6f4ecac 100644 --- a/fsm/transaction.go +++ b/fsm/transaction.go @@ -179,6 +179,12 @@ func (s *StateMachine) CheckTx(transaction []byte, txHash string, batchVerifier if err != nil { return } + if s.isRestricted(sender.Bytes()) || s.isRestricted(recipient) { + if s.Metrics != nil { + s.Metrics.RestrictedTxCount.Inc() + } + return nil, ErrRestrictedAddress() + } if s.Metrics != nil { s.Metrics.CheckTxSignatureTime.Observe(time.Since(signatureStartTime).Seconds()) } diff --git a/fsm/transaction_test.go b/fsm/transaction_test.go index dca3cab270..59cb75dc2b 100644 --- a/fsm/transaction_test.go +++ b/fsm/transaction_test.go @@ -10,6 +10,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" ethCrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" @@ -344,6 +346,45 @@ func TestCheckTx(t *testing.T) { } } +func TestCheckTxRejectsRestrictedSignerForAnyMessage(t *testing.T) { + sm := newTestStateMachine(t) + restrictedTxCount := prometheus.NewGauge(prometheus.GaugeOpts{}) + sm.Metrics = &lib.Metrics{FSMMetrics: lib.FSMMetrics{ + CheckTxDecodeTime: prometheus.NewHistogram(prometheus.HistogramOpts{}), + CheckTxReplayTime: prometheus.NewHistogram(prometheus.HistogramOpts{}), + CheckTxMessageTime: prometheus.NewHistogram(prometheus.HistogramOpts{}), + CheckTxSignatureTime: prometheus.NewHistogram(prometheus.HistogramOpts{}), + RestrictedTxCount: restrictedTxCount, + }} + kg := newTestKeyGroup(t) + sm.Config.RestrictedAddresses = []string{kg.Address.String()} + sm.SetProposalVoteConfig(ProposalApproveList) + require.NoError(t, sm.UpdateParam("fee", ParamStakeFee, &lib.UInt64Wrapper{Value: 1})) + tx, err := NewStakeTx(kg.PrivateKey, kg.PublicKey.Bytes(), newTestAddress(t, 1), "", []uint64{1}, 1, 1, 1, 1, sm.Height(), true, false, "") + require.NoError(t, err) + bz, err := lib.Marshal(tx) + require.NoError(t, err) + _, errI := sm.CheckTx(bz, "", nil) + require.ErrorContains(t, errI, "restricted address") + metric := new(dto.Metric) + require.NoError(t, restrictedTxCount.Write(metric)) + require.Equal(t, float64(1), metric.GetGauge().GetValue()) +} + +func TestBeginBlockResetsRestrictedTxCount(t *testing.T) { + sm := newTestStateMachine(t) + sm.height = 1 + restrictedTxCount := prometheus.NewGauge(prometheus.GaugeOpts{}) + restrictedTxCount.Set(2) + sm.Metrics = &lib.Metrics{FSMMetrics: lib.FSMMetrics{RestrictedTxCount: restrictedTxCount}} + + _, err := sm.BeginBlock() + require.NoError(t, err) + metric := new(dto.Metric) + require.NoError(t, restrictedTxCount.Write(metric)) + require.Zero(t, metric.GetGauge().GetValue()) +} + func TestCheckTxAcceptsSerializedMultiBLSSigner(t *testing.T) { sm := newTestStateMachine(t) require.NoError(t, sm.UpdateParam("fee", ParamSendFee, &lib.UInt64Wrapper{Value: 1})) diff --git a/lib/config.go b/lib/config.go index cb08c06703..48b44586fd 100644 --- a/lib/config.go +++ b/lib/config.go @@ -181,9 +181,10 @@ const ( // StateMachineConfig houses FSM level options type StateMachineConfig struct { - InitialTokensPerBlock uint64 `json:"initialTokensPerBlock"` // initial micro tokens minted per block (before halvenings) - BlocksPerHalvening uint64 `json:"blocksPerHalvening"` // number of blocks between block reward halvings - FaucetAddress string `json:"faucetAddress"` // if set: "send" txs from this address will auto-mint on insufficient funds (dev/test only) + InitialTokensPerBlock uint64 `json:"initialTokensPerBlock"` // initial micro tokens minted per block (before halvenings) + BlocksPerHalvening uint64 `json:"blocksPerHalvening"` // number of blocks between block reward halvings + FaucetAddress string `json:"faucetAddress"` // if set: "send" txs from this address will auto-mint on insufficient funds (dev/test only) + RestrictedAddresses []string `json:"restrictedAddresses,omitempty"` // additional locally restricted addresses used when voting on proposals } // DefaultStateMachineConfig returns FSM defaults @@ -391,3 +392,131 @@ func NewConfigFromFile(filepath string) (Config, error) { // exit return c, nil } + +// RestrictedAddresses were populated from OFAC's 2026-08-28 SDN.XML: https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports/SDN.XML +var RestrictedAddresses = map[string]struct{}{ + "0330070fd38ec3bb94f58fa55d40368271e9e54a": {}, + "038989cbb1710c72b9920dc4fa529158f463e72c": {}, + "04dba1194ee10112fe6c3207c0687def0e78bacf": {}, + "08723392ed15743cc38513c4925f5e6be5c17243": {}, + "08b2efdcdb8822efe5ad0eae55517cf5dc544251": {}, + "0931ca4d13bb4ba75d9b7132ab690265d749a5e7": {}, + "098b716b8aaf21512996dc57eb0615e2383e2f96": {}, + "0ee5067b06776a89ccc7dc8ee369984ad7db5e06": {}, + "12de548f79a50d2bd05481c8515c1ef5183666a9": {}, + "14779cec0b117d5194c750c55ea1f42086631964": {}, + "175d44451403edf28469df03a9280c1197adb92c": {}, + "1967d8af5bd86a497fb3dd7899a020e47560daaf": {}, + "1999ef52700c34de7ec2b68a28aafb37db0c5ade": {}, + "19aa5fe80d33a56d56c78e82ea5e50e5d80b4dff": {}, + "19f8f2b0915daa12a3f5c9cf01df9e24d53794f7": {}, + "1b8579cf6ab12ea6b74ac5fa41f3829a3cb61e6e": {}, + "1cab8177ace78b1b6b1c393371f4f2dcae40cbeb": {}, + "1d19b52b54e7ef5ea1a4b40b616165e798eac9f8": {}, + "1da5821544e25c636c1417ba96ade4cf6d2f9b5a": {}, + "21b8d56bda776bbe68655a16895afd96f5534fed": {}, + "252a8bd2319d8a555b872990601221b3a2053bce": {}, + "2711d73d559f62f4f855ee21f38378f528e07985": {}, + "2c7dcd774b33e10367f7d6385479e04f97d179dc": {}, + "2f389ce8bd8ff92de3402ffce4691d17fc4f6535": {}, + "308ed4b7b49797e1a98d3818bff6fe5385410370": {}, + "32da24ca413f3e7b53145d4737e172c3bdf81e3e": {}, + "35fb6f6db4fb05e6a4ce86f2c93691425626d4b1": {}, + "38735f03b30fbc022ddd06abed01f0ca823c6a94": {}, + "39d908dac893cbcb53cc86e0ecc369aa4def1a29": {}, + "3ad9db589d201a710ed237c829c7860ba86510fc": {}, + "3cbded43efdaf0fc77b9c55f6fc9988fcc9b757d": {}, + "3cffd56b47b7b41c56258d9c7731abadc360e073": {}, + "3e37627deaa754090fbfbb8bd226c1ce66d255e9": {}, + "4060cbf80734193f521a3cc6fd4e985df2825279": {}, + "43fa21d92141ba9db43052492e0deee5aa5f0a93": {}, + "48549a34ae37b12f6a30566245176994e17c6b4a": {}, + "4f428c11dc82388fa5136d636e613ad923eb700b": {}, + "4f47bc496083c727c5fbe3ce9cdf2b0f6496270c": {}, + "502371699497d08d5339c870851898d6d72521dd": {}, + "530a64c0ce595026a4a556b703644228179e2d57": {}, + "532b77b33a040587e9fd1800088225f99b8b0e8a": {}, + "53b6936513e738f44fb50d2b9476730c0ab3bfc1": {}, + "5512d943ed1f7c8a43f3435c85f7ab68b30121b0": {}, + "56de1527136f76a809e5b14ded6103eecd072ba7": {}, + "57ec89a0c056163a0314e413320f9b3abe761259": {}, + "5a14e72060c11313e38738009254a90968f58f51": {}, + "5a7a51bfb49f190e5a6060a5bc6052ac14a3b59f": {}, + "5d5b5dafecbf31bdb08bfd3edad4f2694372d0ef": {}, + "5f48c2a71b2cc96e3f0ccae4e39318ff0dc375b2": {}, + "67d40ee1a85bf4a4bb7ffae16de985e8427b6b45": {}, + "6b0736fed0634e15e19cc57fba19cd179c13abca": {}, + "6b69e2a7545c166417a80c61a77562052bffa9c5": {}, + "6be0ae71e6c41f2f9d0d1a3b8d0f75e6f6a0b46e": {}, + "6f1ca141a28907f78ebaa64fb83a9088b02a8352": {}, + "6fac4d18c912343bf86fa7049364dd4e424ab9c0": {}, + "72a5843cc08275c8171e582972aa4fda8c397b2a": {}, + "747afb5c7a7fc34b547cd0fdebf9b91759c5a52b": {}, + "76ea76ca4eb727f18956ab93445a94c5280412b9": {}, + "797d7ae72ebddcdea2a346c1834e04d1f8df102b": {}, + "7ced75026204ac29c34bea98905d4c949f27361e": {}, + "7db418b5d567a4e0e8c59ad71be1fce48f3e6107": {}, + "7f03679b56d8772530efa516b58bb83d4829e881": {}, + "7f19720a857f834887fc9a7bc0a0fbe7fc7f8102": {}, + "7f367cc41522ce07553e823bf3be79a889debe1b": {}, + "7ff9cfad3877f21d41da833e2f775db0569ee3d9": {}, + "83e5bc4ffa856bb84bb88581f5dd62a433a25e0d": {}, + "8576acc5c05d6ce88f4e49bf65bdf0c62f91353c": {}, + "8694ed130432be2cd3efff2e4d9dc52351dc7423": {}, + "8ac5381fcd9e7395d14e02986c344aada84b4bc6": {}, + "8d79c73daae8630c88de372ba8f57592fa987607": {}, + "8dce2aac0de82bdcaf6b4373b79f94331b8e4995": {}, + "901bb9583b24d97e995513c6778dc6888ab6870e": {}, + "931546d9e66836abf687d2bc64b30407bac8c568": {}, + "95584c303fcd48af5c6b9873015f2ad0ca84eae3": {}, + "961c5be54a2ffc17cf4cb021d863c42dacd47fc1": {}, + "9697749a9e8d6c119d8eeb0d6268a1b99c40684c": {}, + "97b1043abd9e6fc31681635166d430a458d14f9c": {}, + "983a81ca6fb1e441266d2fbcb7d8e530ac2e05a2": {}, + "9be599d7867f5e1a2d7ec6db9710df2b98a15573": {}, + "9c2bc757b66f24d60f016b6237f8cdd414a879fa": {}, + "9dd7fa4b4950154f7e75bdd8a77266b99b94ec08": {}, + "9f4cda013e354b8fc285bf4b9a60460cee7f7ea9": {}, + "a0e1c89ef1a489c9c7de96311ed5ce5d32c20e4b": {}, + "a40cfbfc8534ffc84e20a7d8bbc3729b26a35f6f": {}, + "a7e5d5a720f06526557c513402f2e6b5fa20b008": {}, + "ac4cc4b68ea24bbfaac8fd127b67ed445accce22": {}, + "b338962b92cd818d6aef0a32a9ecd01212a71f33": {}, + "b5a69da691670f62510793f79a9b36c7db1a7b7c": {}, + "b637f84b66876ebf609c2a4208905f9ddac9d075": {}, + "b6f5ec1a0a9cd1526536d3f0426c429529471f40": {}, + "bb69e01921b17cd22080968bcc96ba6115da6062": {}, + "bd3276f265b83b5e828c05f46cde9d10a1521a24": {}, + "c103b7dc095c904b92081eef0c1640081ec01c10": {}, + "c2a3829f459b3edd87791c74cd45402ba0a20be3": {}, + "c455f7fd3e0e12afd51fba5c106909934d8a0e4a": {}, + "cb74874f1e06fcf80a306e06e5379a44b488ba2d": {}, + "d04e33461fea8302c5e1e13895b60cee8aefda7f": {}, + "d0975b32cea532eadddfc9c60481976e39db3472": {}, + "d5ed34b52ac4ab84d8fa8a231a3218bbf01ed510": {}, + "d81414abc631c6cadae1c6198b0c2b15a9b4fde5": {}, + "d8500c631dc32fa18645b7436344a99e4825e10e": {}, + "d882cfc20f52f2599d84b8e8d58c7fb62cfe344b": {}, + "db2720ebad55399117ddb4c4a4afd9a4ccada8fe": {}, + "dcbeffbecce100cce9e4b153c4e15cb885643193": {}, + "e05f529f5284d75624eba386cb716928c3b54a2a": {}, + "e1d865c3d669dcc8c57c8d023140cb204e672ee4": {}, + "e1e4c5e5ed8f03ae61b581e2def126025f2b9401": {}, + "e3d35f68383732649669aa990832e017340dbca5": {}, + "e7aa314c77f4233c18c6cc84384a9247c0cf367b": {}, + "e950dc316b836e4eefb8308bf32bf7c72a1358ff": {}, + "eb507efa9ee692a4c774ad1de9f3cb26fc459da3": {}, + "ed6e0a7e4ac94d976eebfb82ccf777a3c6bad921": {}, + "ef85a6fafa5942a964dc618e94e230881d29ce2a": {}, + "efe301d259f525ca1ba74a7977b80d5b060b3cca": {}, + "f1c4c44d2dcbcfa704349e3b57628dbd8404e597": {}, + "f2235d55b2950a0b1317469d72d07ae65b2e27cb": {}, + "f3701f445b6bdafedbca97d1e477357839e4120d": {}, + "f4377eda661e04b6dda78969796ed31658d602d4": {}, + "f45ecc3a59c7911181c659ce9115854c6175be91": {}, + "f7b31119c2682c88d88d455dbb9d5932c65cf1be": {}, + "fac583c0cf07ea434052c49115a4682172ab6b4f": {}, + "fb3eff152ea55d1bfa04dbdd509a80fd7b72cdeb": {}, + "fda1ec4a6178d4916b001a065422d31ebe5f62ff": {}, + "fec8a60023265364d066a1212fde3930f6ae8da7": {}, +} diff --git a/lib/config_test.go b/lib/config_test.go index 7687f2b839..e92224c722 100644 --- a/lib/config_test.go +++ b/lib/config_test.go @@ -1,6 +1,8 @@ package lib import ( + "bytes" + "encoding/json" "os" "testing" @@ -41,3 +43,14 @@ func TestFileConfig(t *testing.T) { // compare got vs expected require.Equal(t, config, got) } + +func TestRestrictedAddressesOmitEmpty(t *testing.T) { + bz, err := json.Marshal(DefaultConfig()) + require.NoError(t, err) + require.NotContains(t, string(bz), "restrictedAddresses") + c := DefaultConfig() + c.RestrictedAddresses = []string{"01"} + bz, err = json.Marshal(c) + require.NoError(t, err) + require.True(t, bytes.Contains(bz, []byte(`"restrictedAddresses":["01"]`))) +} diff --git a/lib/crypto/classgroup.go b/lib/crypto/classgroup.go index 4e72b6fb6a..94b314c469 100644 --- a/lib/crypto/classgroup.go +++ b/lib/crypto/classgroup.go @@ -332,6 +332,9 @@ func (group *ClassGroup) Decode(bz []byte, discriminant *big.Int) (err error) { if err = cdc.Unmarshal(bz, classGroup); err != nil { return err } + if len(classGroup.A) == 0 || len(classGroup.B) == 0 { + return fmt.Errorf("invalid empty class-group coefficient") + } // convert the byte slices to big.Int values a := decodeBigInt(classGroup.A) b := decodeBigInt(classGroup.B) diff --git a/lib/crypto/vdf.go b/lib/crypto/vdf.go index 9c5d0ba122..a88ce86cb8 100644 --- a/lib/crypto/vdf.go +++ b/lib/crypto/vdf.go @@ -30,7 +30,12 @@ func GenerateVDF(seed []byte, iterations int, stop <-chan struct{}) (out []byte, } // VerifyVDF() verifies VDF bytes given a seed and iterations -func VerifyVDF(seed, out, proof []byte, iterations int) bool { +func VerifyVDF(seed, out, proof []byte, iterations int) (valid bool) { + defer func() { + if recover() != nil { + valid = false + } + }() discriminant, classGroup := initVDF(seed) y, p := new(ClassGroup), new(ClassGroup) if err := y.Decode(out, discriminant); err != nil { diff --git a/lib/crypto/vdf_test.go b/lib/crypto/vdf_test.go index b1eb285e2a..b74480fd18 100644 --- a/lib/crypto/vdf_test.go +++ b/lib/crypto/vdf_test.go @@ -117,6 +117,17 @@ func TestRandomInput(t *testing.T) { } } +func TestVerifyVDFRejectsMalformedProofs(t *testing.T) { + seed := []byte{0xde, 0xad, 0xbe, 0xef} + const iterations = 50 + validOut, validProof := GenerateVDF(seed, iterations, nil) + zeroCoefficient := []byte{0x0a, 0x01, 0x00, 0x12, 0x01, 0x00} + require.False(t, VerifyVDF(seed, nil, validProof, iterations)) + require.False(t, VerifyVDF(seed, validOut, nil, iterations)) + require.False(t, VerifyVDF(seed, zeroCoefficient, validProof, iterations)) + require.False(t, VerifyVDF(seed, validOut, zeroCoefficient, iterations)) +} + func TestInterruptGenerator(t *testing.T) { seed := []byte{0xde, 0xad, 0xbe, 0xef} stop := make(chan struct{}) diff --git a/lib/metrics.go b/lib/metrics.go index d9a835fb04..5c78d8685d 100644 --- a/lib/metrics.go +++ b/lib/metrics.go @@ -174,6 +174,7 @@ type FSMMetrics struct { CheckTxReplayTime prometheus.Histogram // how long does replay validation take? CheckTxMessageTime prometheus.Histogram // how long does message validation/fee/signer resolution take? CheckTxSignatureTime prometheus.Histogram // how long does signature validation take? + RestrictedTxCount prometheus.Gauge // how many restricted transactions were rejected in the current block? StateOperationTime *prometheus.HistogramVec ValidatorStatus *prometheus.GaugeVec // what's the status of this validator? ValidatorType *prometheus.GaugeVec // what's the type of this validator? @@ -547,6 +548,10 @@ func NewMetricsServer(nodeAddress crypto.AddressI, chainID float64, softwareVers Name: "canopy_fsm_check_tx_signature_time", Help: "Execution time of signature validation in CheckTx", }), + RestrictedTxCount: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "canopy_fsm_restricted_tx_count", + Help: "Number of restricted signer or recipient transactions rejected in the current block", + }), StateOperationTime: promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "canopy_fsm_state_operation_time", Help: "Execution time of hot FSM state operations",