From 6b39ce42c5844e58da81dcdad7cf282c40723453 Mon Sep 17 00:00:00 2001 From: Matthias Fasching <5011972+fasmat@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:11:15 +0000 Subject: [PATCH 1/5] chore: Fix errors.As usage for PoetSvcUnstableError (#6217) ## Motivation `errors.Is` is generally used to compare returned errors to a concrete instance of an error like `fs.ErrNotExists` or `io.EOF`, while `errors.As` is used to assign generic errors to an instance of a specific type if needed. --- activation/activation.go | 8 ++++++-- activation/activation_errors.go | 7 ------- activation/activation_test.go | 2 +- activation/nipost_test.go | 22 ++++++++++++++++------ 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/activation/activation.go b/activation/activation.go index 1903d14a97..567332e36e 100644 --- a/activation/activation.go +++ b/activation/activation.go @@ -443,6 +443,7 @@ func (b *Builder) run(ctx context.Context, sig *signing.EdSigner) { b.logger.Warn("failed to publish atx", zap.Error(err)) + poetErr := &PoetSvcUnstableError{} switch { case errors.Is(err, ErrATXChallengeExpired): b.logger.Debug("retrying with new challenge after waiting for a layer") @@ -459,8 +460,11 @@ func (b *Builder) run(ctx context.Context, sig *signing.EdSigner) { return case <-b.layerClock.AwaitLayer(currentLayer.Add(1)): } - case errors.Is(err, ErrPoetServiceUnstable): - b.logger.Warn("retrying after poet retry interval", zap.Duration("interval", b.poetRetryInterval)) + case errors.As(err, &poetErr): + b.logger.Warn("retrying after poet retry interval", + zap.Duration("interval", b.poetRetryInterval), + zap.Error(poetErr.source), + ) select { case <-ctx.Done(): return diff --git a/activation/activation_errors.go b/activation/activation_errors.go index 601541e83c..63027b0c81 100644 --- a/activation/activation_errors.go +++ b/activation/activation_errors.go @@ -8,8 +8,6 @@ import ( var ( // ErrATXChallengeExpired is returned when atx missed its publication window and needs to be regenerated. ErrATXChallengeExpired = errors.New("builder: atx expired") - // ErrPoetServiceUnstable is returned when poet quality of service is low. - ErrPoetServiceUnstable = &PoetSvcUnstableError{} // ErrPoetProofNotReceived is returned when no poet proof was received. ErrPoetProofNotReceived = errors.New("builder: didn't receive any poet proof") ) @@ -28,8 +26,3 @@ func (e *PoetSvcUnstableError) Error() string { } func (e *PoetSvcUnstableError) Unwrap() error { return e.source } - -func (e *PoetSvcUnstableError) Is(target error) bool { - _, ok := target.(*PoetSvcUnstableError) - return ok -} diff --git a/activation/activation_test.go b/activation/activation_test.go index a3c696cb24..ba9acfa0b7 100644 --- a/activation/activation_test.go +++ b/activation/activation_test.go @@ -1076,7 +1076,7 @@ func TestBuilder_RetryPublishActivationTx(t *testing.T) { tries++ t.Logf("try %d: %s", tries, now) if tries < expectedTries { - return nil, ErrPoetServiceUnstable + return nil, &PoetSvcUnstableError{} } close(builderConfirmation) return newNIPostWithPoet(t, []byte("66666")), nil diff --git a/activation/nipost_test.go b/activation/nipost_test.go index a351345d9f..deb0fe588a 100644 --- a/activation/nipost_test.go +++ b/activation/nipost_test.go @@ -713,9 +713,14 @@ func TestNIPSTBuilder_PoetUnstable(t *testing.T) { ) require.NoError(t, err) - nipst, err := nb.BuildNIPost(context.Background(), sig, challenge, - &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}) - require.ErrorIs(t, err, ErrPoetServiceUnstable) + nipst, err := nb.BuildNIPost( + context.Background(), + sig, + challenge, + &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}, + ) + poetErr := &PoetSvcUnstableError{} + require.ErrorAs(t, err, &poetErr) require.Nil(t, nipst) }) t.Run("Submit hangs", func(t *testing.T) { @@ -750,9 +755,14 @@ func TestNIPSTBuilder_PoetUnstable(t *testing.T) { ) require.NoError(t, err) - nipst, err := nb.BuildNIPost(context.Background(), sig, challenge, - &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}) - require.ErrorIs(t, err, ErrPoetServiceUnstable) + nipst, err := nb.BuildNIPost( + context.Background(), + sig, + challenge, + &types.NIPostChallenge{PublishEpoch: postGenesisEpoch + 2}, + ) + poetErr := &PoetSvcUnstableError{} + require.ErrorAs(t, err, &poetErr) require.Nil(t, nipst) }) t.Run("GetProof fails", func(t *testing.T) { From 787351f6a789c0446b37acf27aecc2a7afd1343d Mon Sep 17 00:00:00 2001 From: acud <12988138+acud@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:07:08 +0000 Subject: [PATCH 2/5] ci: merge build and build-tools (#6220) ## Motivation Simplify the build steps by merging the two steps to reduce setup overhead. --- .github/workflows/ci.yml | 50 ++-------------------------------------- 1 file changed, 2 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82aeb61df4..eb3d731d8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,7 +118,7 @@ jobs: - name: lint run: make lint-github-action - build-tools: + build: runs-on: ${{ matrix.os }} needs: filter-changes if: ${{ needs.filter-changes.outputs.nondocchanges == 'true' }} @@ -160,6 +160,7 @@ jobs: with: check-latest: true go-version: ${{ env.go-version }} + cache: ${{ runner.arch != 'arm64' }} - name: setup env run: make install - name: build merge-nodes @@ -168,52 +169,6 @@ jobs: run: make gen-p2p-identity - name: build bootstrapper run: make bootstrapper - - build: - runs-on: ${{ matrix.os }} - needs: filter-changes - if: ${{ needs.filter-changes.outputs.nondocchanges == 'true' }} - strategy: - fail-fast: true - matrix: - os: - - ubuntu-22.04 - - ubuntu-latest-arm-8-cores - - macos-13 - - [self-hosted, macOS, ARM64, go-spacemesh] - - windows-2022 - steps: - - name: Add OpenCL support - Ubuntu - if: ${{ matrix.os == 'ubuntu-22.04' || matrix.os == 'ubuntu-latest-arm-8-cores' }} - run: sudo apt-get update -q && sudo apt-get install -qy ocl-icd-opencl-dev libpocl2 - - name: disable Windows Defender - Windows - if: ${{ matrix.os == 'windows-2022' }} - run: | - Set-MpPreference -DisableRealtimeMonitoring $true - - name: Set new git config - Windows - if: ${{ matrix.os == 'windows-2022' }} - run: | - git config --global pack.window 1 - git config --global core.compression 0 - git config --global http.postBuffer 524288000 - - name: checkout - uses: actions/checkout@v4 - with: - ssh-key: ${{ secrets.GH_ACTION_PRIVATE_KEY }} - - uses: extractions/netrc@v2 - with: - machine: github.com - username: ${{ secrets.GH_ACTION_TOKEN_USER }} - password: ${{ secrets.GH_ACTION_TOKEN }} - if: vars.GOPRIVATE - - name: set up go - uses: actions/setup-go@v5 - with: - check-latest: true - go-version: ${{ env.go-version }} - cache: ${{ runner.arch != 'arm64' }} - - name: setup env - run: make install - name: build timeout-minutes: 5 run: make build @@ -307,7 +262,6 @@ jobs: - filter-changes - quicktests - lint - - build-tools - build - unittests runs-on: ubuntu-22.04 From dcda40692dc566d5eca91d816f80c286ba3e4bf0 Mon Sep 17 00:00:00 2001 From: acud <12988138+acud@users.noreply.github.com> Date: Tue, 6 Aug 2024 18:26:15 +0000 Subject: [PATCH 3/5] test: remove limit of test binaries (#6214) ## Motivation Remove the restriction of number of cores when running tests. --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index c1853c7560..777c1d6007 100644 --- a/Makefile +++ b/Makefile @@ -97,7 +97,7 @@ clear-test-cache: .PHONY: clear-test-cache test: get-libs - @$(ULIMIT) CGO_LDFLAGS="$(CGO_TEST_LDFLAGS)" gotestsum -- -race -timeout 8m -p 1 $(UNIT_TESTS) + @$(ULIMIT) CGO_LDFLAGS="$(CGO_TEST_LDFLAGS)" gotestsum -- -race -timeout 8m $(UNIT_TESTS) .PHONY: test generate: get-libs @@ -145,7 +145,7 @@ lint-github-action: get-libs .PHONY: lint-github-action cover: get-libs - @$(ULIMIT) CGO_LDFLAGS="$(CGO_TEST_LDFLAGS)" go test -coverprofile=cover.out -timeout 0 -p 1 -coverpkg=./... $(UNIT_TESTS) + @$(ULIMIT) CGO_LDFLAGS="$(CGO_TEST_LDFLAGS)" go test -coverprofile=cover.out -timeout 30m -coverpkg=./... $(UNIT_TESTS) .PHONY: cover list-versions: From 00d476fa5757183f65bdae6e92102b9fe75d7110 Mon Sep 17 00:00:00 2001 From: acud <12988138+acud@users.noreply.github.com> Date: Tue, 6 Aug 2024 21:54:52 +0000 Subject: [PATCH 4/5] test(hare): add db cleanup (#6221) ## Motivation Adding a cleanup for the in-memory db we are using in hare. --- hare3/hare_test.go | 10 +++++----- hare3/malfeasance_test.go | 2 +- hare4/eligibility/oracle_test.go | 2 +- hare4/hare_test.go | 10 +++++----- sql/database.go | 13 +++++++++++++ 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/hare3/hare_test.go b/hare3/hare_test.go index dd8139ab12..0df5b7f726 100644 --- a/hare3/hare_test.go +++ b/hare3/hare_test.go @@ -148,8 +148,8 @@ func (n *node) reuseSigner(signer *signing.EdSigner) *node { return n } -func (n *node) withDb() *node { - n.db = sql.InMemory() +func (n *node) withDb(tb testing.TB) *node { + n.db = sql.InMemoryTest(tb) n.atxsdata = atxsdata.New() n.proposals = store.New() return n @@ -342,7 +342,7 @@ func (cl *lockstepCluster) addActive(n int) *lockstepCluster { for i := last; i < last+n; i++ { cl.addNode((&node{t: cl.t, i: i}). withController().withSyncer().withPublisher(). - withClock().withDb().withSigner().withAtx(cl.units.min, cl.units.max). + withClock().withDb(cl.t).withSigner().withAtx(cl.units.min, cl.units.max). withOracle().withHare()) } return cl @@ -353,7 +353,7 @@ func (cl *lockstepCluster) addInactive(n int) *lockstepCluster { for i := last; i < last+n; i++ { cl.addNode((&node{t: cl.t, i: i}). withController().withSyncer().withPublisher(). - withClock().withDb().withSigner(). + withClock().withDb(cl.t).withSigner(). withOracle().withHare()) } return cl @@ -366,7 +366,7 @@ func (cl *lockstepCluster) addEquivocators(n int) *lockstepCluster { cl.addNode((&node{t: cl.t, i: i}). reuseSigner(cl.nodes[i-last].signer). withController().withSyncer().withPublisher(). - withClock().withDb().withAtx(cl.units.min, cl.units.max). + withClock().withDb(cl.t).withAtx(cl.units.min, cl.units.max). withOracle().withHare()) } return cl diff --git a/hare3/malfeasance_test.go b/hare3/malfeasance_test.go index 0f2a0f1491..7ff1cf52b1 100644 --- a/hare3/malfeasance_test.go +++ b/hare3/malfeasance_test.go @@ -26,7 +26,7 @@ type testMalfeasanceHandler struct { } func newTestMalfeasanceHandler(tb testing.TB) *testMalfeasanceHandler { - db := sql.InMemory() + db := sql.InMemoryTest(tb) observer, observedLogs := observer.New(zapcore.WarnLevel) logger := zaptest.NewLogger(tb, zaptest.WrapOptions(zap.WrapCore( func(core zapcore.Core) zapcore.Core { diff --git a/hare4/eligibility/oracle_test.go b/hare4/eligibility/oracle_test.go index d6ae2e4a58..5dfdf877b6 100644 --- a/hare4/eligibility/oracle_test.go +++ b/hare4/eligibility/oracle_test.go @@ -53,7 +53,7 @@ type testOracle struct { } func defaultOracle(tb testing.TB) *testOracle { - db := sql.InMemory() + db := sql.InMemoryTest(tb) atxsdata := atxsdata.New() ctrl := gomock.NewController(tb) diff --git a/hare4/hare_test.go b/hare4/hare_test.go index 70f363a8f6..498a80d23f 100644 --- a/hare4/hare_test.go +++ b/hare4/hare_test.go @@ -159,8 +159,8 @@ func (n *node) reuseSigner(signer *signing.EdSigner) *node { return n } -func (n *node) withDb() *node { - n.db = sql.InMemory() +func (n *node) withDb(tb testing.TB) *node { + n.db = sql.InMemoryTest(tb) n.atxsdata = atxsdata.New() n.proposals = store.New() return n @@ -391,7 +391,7 @@ func (cl *lockstepCluster) addActive(n int) *lockstepCluster { for i := last; i < last+n; i++ { nn := (&node{t: cl.t, i: i}). withController().withSyncer().withPublisher(). - withClock().withDb().withSigner().withAtx(cl.units.min, cl.units.max). + withClock().withDb(cl.t).withSigner().withAtx(cl.units.min, cl.units.max). withStreamRequester().withOracle().withHare() if cl.mockVerify { nn = nn.withVerifier() @@ -406,7 +406,7 @@ func (cl *lockstepCluster) addInactive(n int) *lockstepCluster { for i := last; i < last+n; i++ { cl.addNode((&node{t: cl.t, i: i}). withController().withSyncer().withPublisher(). - withClock().withDb().withSigner(). + withClock().withDb(cl.t).withSigner(). withStreamRequester().withOracle().withHare()) } return cl @@ -419,7 +419,7 @@ func (cl *lockstepCluster) addEquivocators(n int) *lockstepCluster { cl.addNode((&node{t: cl.t, i: i}). reuseSigner(cl.nodes[i-last].signer). withController().withSyncer().withPublisher(). - withClock().withDb().withAtx(cl.units.min, cl.units.max). + withClock().withDb(cl.t).withAtx(cl.units.min, cl.units.max). withStreamRequester().withOracle().withHare()) } return cl diff --git a/sql/database.go b/sql/database.go index 5f86f0bda5..e20a393b63 100644 --- a/sql/database.go +++ b/sql/database.go @@ -11,6 +11,7 @@ import ( "strings" "sync" "sync/atomic" + "testing" "time" sqlite "github.com/go-llsqlite/crawshaw" @@ -176,6 +177,7 @@ func WithQueryCacheSizes(sizes map[QueryCacheKind]int) Opt { type Opt func(c *conf) // InMemory database for testing. +// Please use InMemoryTest for automatic closing of the returned db during `tb.Cleanup`. func InMemory(opts ...Opt) *Database { opts = append(opts, WithConnections(1)) db, err := Open("file::memory:?mode=memory", opts...) @@ -185,6 +187,17 @@ func InMemory(opts ...Opt) *Database { return db } +// InMemoryTest returns an in-mem database for testing and ensures database is closed during `tb.Cleanup`. +func InMemoryTest(tb testing.TB, opts ...Opt) *Database { + opts = append(opts, WithConnections(1)) + db, err := Open("file::memory:?mode=memory", opts...) + if err != nil { + panic(err) + } + tb.Cleanup(func() { db.Close() }) + return db +} + // Open database with options. // // Database is opened in WAL mode and pragma synchronous=normal. From 83b04992a06d8204c8588f52c97fe187680f4499 Mon Sep 17 00:00:00 2001 From: Matthias Fasching <5011972+fasmat@users.noreply.github.com> Date: Wed, 7 Aug 2024 12:33:37 +0000 Subject: [PATCH 5/5] Add double marry malfeasance proof (#6219) ## Motivation Closes https://github.com/spacemeshos/go-spacemesh/issues/6218 --- CHANGELOG.md | 2 +- activation/builder_v2_test.go | 4 +- activation/handler_v2.go | 39 ++- activation/handler_v2_test.go | 30 +- activation/interface.go | 2 +- activation/malfeasance2.go | 2 +- activation/mocks.go | 6 +- activation/nipost_test.go | 2 +- activation/wire/malfeasance.go | 47 ++- activation/wire/malfeasance_double_marry.go | 221 +++++++++++++ .../wire/malfeasance_double_marry_scale.go | 182 +++++++++++ .../wire/malfeasance_double_marry_test.go | 290 ++++++++++++++++++ activation/wire/malfeasance_scale.go | 7 +- activation/wire/wire_v2.go | 56 ++-- activation/wire/wire_v2_test.go | 122 +++----- malfeasance/handler.go | 1 - malfeasance/wire/malfeasance.go | 9 +- malfeasance/wire/malfeasance_scale.go | 8 - systest/Makefile | 2 - 19 files changed, 857 insertions(+), 175 deletions(-) create mode 100644 activation/wire/malfeasance_double_marry.go create mode 100644 activation/wire/malfeasance_double_marry_scale.go create mode 100644 activation/wire/malfeasance_double_marry_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 04bc503248..462b887df9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -708,7 +708,7 @@ and permanent ineligibility for rewards. * [#5470](https://github.com/spacemeshos/go-spacemesh/pull/5470) Fixed a bug in event reporting where the node reports a disconnection from the PoST service as a "PoST failed" event. - Disconnections cannot be avoided completely and do not interrupt the PoST proofing process. As long as the PoST + Disconnections cannot be avoided completely and do not interrupt the PoST proving process. As long as the PoST service reconnects within a reasonable time, the node will continue to operate normally without reporting any errors via the event API. diff --git a/activation/builder_v2_test.go b/activation/builder_v2_test.go index 209570b7c4..0054147f4e 100644 --- a/activation/builder_v2_test.go +++ b/activation/builder_v2_test.go @@ -68,7 +68,7 @@ func TestBuilder_BuildsInitialAtxV2(t *testing.T) { require.Empty(t, atx.Marriages) require.Equal(t, posEpoch+1, atx.PublishEpoch) require.Equal(t, sig.NodeID(), atx.SmesherID) - require.True(t, signing.NewEdVerifier().Verify(signing.ATX, atx.SmesherID, atx.SignedBytes(), atx.Signature)) + require.True(t, signing.NewEdVerifier().Verify(signing.ATX, atx.SmesherID, atx.ID().Bytes(), atx.Signature)) } func TestBuilder_SwitchesToBuildV2(t *testing.T) { @@ -106,5 +106,5 @@ func TestBuilder_SwitchesToBuildV2(t *testing.T) { require.Empty(t, atx2.Marriages) require.Equal(t, atx1.PublishEpoch+1, atx2.PublishEpoch) require.Equal(t, sig.NodeID(), atx2.SmesherID) - require.True(t, signing.NewEdVerifier().Verify(signing.ATX, atx2.SmesherID, atx2.SignedBytes(), atx2.Signature)) + require.True(t, signing.NewEdVerifier().Verify(signing.ATX, atx2.SmesherID, atx2.ID().Bytes(), atx2.Signature)) } diff --git a/activation/handler_v2.go b/activation/handler_v2.go index 155b5db6d1..0f0b22a853 100644 --- a/activation/handler_v2.go +++ b/activation/handler_v2.go @@ -18,6 +18,7 @@ import ( "github.com/spacemeshos/go-spacemesh/activation/wire" "github.com/spacemeshos/go-spacemesh/atxsdata" + "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" "github.com/spacemeshos/go-spacemesh/events" @@ -626,9 +627,7 @@ func (h *HandlerV2) syntacticallyValidateDeps( zap.Int("index", invalidIdx.Index), ) // TODO(mafa): finish proof - proof := &wire.ATXProof{ - ProofType: wire.InvalidPost, - } + var proof wire.Proof if err := h.malPublisher.Publish(ctx, id, proof); err != nil { return nil, fmt.Errorf("publishing malfeasance proof for invalid post: %w", err) } @@ -669,7 +668,7 @@ func (h *HandlerV2) checkMalicious( return nil } - malicious, err = h.checkDoubleMarry(ctx, tx, watx.ID(), marrying) + malicious, err = h.checkDoubleMarry(ctx, tx, watx, marrying) if err != nil { return fmt.Errorf("checking double marry: %w", err) } @@ -704,7 +703,7 @@ func (h *HandlerV2) checkMalicious( func (h *HandlerV2) checkDoubleMarry( ctx context.Context, tx *sql.Tx, - atxID types.ATXID, + atx *wire.ActivationTxV2, marrying []marriage, ) (bool, error) { for _, m := range marrying { @@ -712,10 +711,23 @@ func (h *HandlerV2) checkDoubleMarry( if err != nil { return false, fmt.Errorf("checking if ID is married: %w", err) } - if mATX != atxID { - // TODO(mafa): finish proof - proof := &wire.ATXProof{ - ProofType: wire.DoubleMarry, + if mATX != atx.ID() { + var blob sql.Blob + v, err := atxs.LoadBlob(ctx, tx, mATX.Bytes(), &blob) + if err != nil { + return true, fmt.Errorf("creating double marry proof: %w", err) + } + if v != types.AtxV2 { + h.logger.Fatal("Failed to create double marry malfeasance proof: ATX is not v2", + zap.Stringer("atx_id", mATX), + ) + } + var otherAtx wire.ActivationTxV2 + codec.MustDecode(blob.Bytes, &otherAtx) + + proof, err := wire.NewDoubleMarryProof(tx, atx, &otherAtx, m.id) + if err != nil { + return true, fmt.Errorf("creating double marry proof: %w", err) } return true, h.malPublisher.Publish(ctx, m.id, proof) } @@ -747,9 +759,7 @@ func (h *HandlerV2) checkDoublePost( zap.Uint32("epoch", atx.PublishEpoch.Uint32()), ) // TODO(mafa): finish proof - proof := &wire.ATXProof{ - ProofType: wire.DoublePublish, - } + var proof wire.Proof return true, h.malPublisher.Publish(ctx, id, proof) } return false, nil @@ -776,10 +786,7 @@ func (h *HandlerV2) checkDoubleMerge(ctx context.Context, tx *sql.Tx, watx *wire zap.Stringer("smesher_id", watx.SmesherID), ) - // TODO(mafa): finish proof - proof := &wire.ATXProof{ - ProofType: wire.DoubleMerge, - } + var proof wire.Proof return true, h.malPublisher.Publish(ctx, watx.SmesherID, proof) } diff --git a/activation/handler_v2_test.go b/activation/handler_v2_test.go index 06f52f9b69..3397b709ac 100644 --- a/activation/handler_v2_test.go +++ b/activation/handler_v2_test.go @@ -17,6 +17,7 @@ import ( "github.com/spacemeshos/go-spacemesh/activation/wire" "github.com/spacemeshos/go-spacemesh/atxsdata" + "github.com/spacemeshos/go-spacemesh/codec" "github.com/spacemeshos/go-spacemesh/common/types" "github.com/spacemeshos/go-spacemesh/datastore" "github.com/spacemeshos/go-spacemesh/fetch" @@ -1561,17 +1562,7 @@ func TestHandlerV2_SyntacticallyValidateDeps(t *testing.T) { gomock.Any(), ). Return(verifying.ErrInvalidIndex{Index: 7}) - atxHandler.mMalPublish.EXPECT().Publish( - gomock.Any(), - sig.NodeID(), - gomock.Cond(func(data any) bool { - proof, ok := data.(*wire.ATXProof) - if !ok { - return false - } - return proof.ProofType == wire.InvalidPost - }), - ) + atxHandler.mMalPublish.EXPECT().Publish(gomock.Any(), sig.NodeID(), gomock.Any()) _, err := atxHandler.syntacticallyValidateDeps(context.Background(), atx) vErr := &verifying.ErrInvalidIndex{} require.ErrorAs(t, err, vErr) @@ -1719,13 +1710,18 @@ func Test_Marriages(t *testing.T) { gomock.Any(), sig.NodeID(), gomock.Cond(func(data any) bool { - proof, ok := data.(*wire.ATXProof) - if !ok { - return false - } - return proof.ProofType == wire.DoubleMarry + _, ok := data.(*wire.ProofDoubleMarry) + return ok }), - ) + ).DoAndReturn(func(ctx context.Context, id types.NodeID, proof wire.Proof) error { + malProof := proof.(*wire.ProofDoubleMarry) + nId, err := malProof.Valid(atxHandler.edVerifier) + require.NoError(t, err) + require.Equal(t, sig.NodeID(), nId) + b := codec.MustEncode(malProof) + _ = b + return nil + }) err = atxHandler.processATX(context.Background(), "", atx2, time.Now()) require.NoError(t, err) diff --git a/activation/interface.go b/activation/interface.go index 32c81572c1..21c414e9c5 100644 --- a/activation/interface.go +++ b/activation/interface.go @@ -101,7 +101,7 @@ type syncer interface { // Additionally the publisher will only gossip proofs when the node is in sync, otherwise it will only store them. // and mark the associated identity as malfeasant. type malfeasancePublisher interface { - Publish(ctx context.Context, id types.NodeID, proof *wire.ATXProof) error + Publish(ctx context.Context, id types.NodeID, proof wire.Proof) error } type atxProvider interface { diff --git a/activation/malfeasance2.go b/activation/malfeasance2.go index d4d3a40bc7..ff44452b35 100644 --- a/activation/malfeasance2.go +++ b/activation/malfeasance2.go @@ -10,7 +10,7 @@ import ( // MalfeasancePublisher is the publisher for ATX proofs. type MalfeasancePublisher struct{} -func (p *MalfeasancePublisher) Publish(ctx context.Context, id types.NodeID, proof *wire.ATXProof) error { +func (p *MalfeasancePublisher) Publish(ctx context.Context, id types.NodeID, proof wire.Proof) error { // TODO(mafa): implement me return nil } diff --git a/activation/mocks.go b/activation/mocks.go index dd1c6f637d..38a1a47206 100644 --- a/activation/mocks.go +++ b/activation/mocks.go @@ -1109,7 +1109,7 @@ func (m *MockmalfeasancePublisher) EXPECT() *MockmalfeasancePublisherMockRecorde } // Publish mocks base method. -func (m *MockmalfeasancePublisher) Publish(ctx context.Context, id types.NodeID, proof *wire.ATXProof) error { +func (m *MockmalfeasancePublisher) Publish(ctx context.Context, id types.NodeID, proof wire.Proof) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Publish", ctx, id, proof) ret0, _ := ret[0].(error) @@ -1135,13 +1135,13 @@ func (c *MockmalfeasancePublisherPublishCall) Return(arg0 error) *Mockmalfeasanc } // Do rewrite *gomock.Call.Do -func (c *MockmalfeasancePublisherPublishCall) Do(f func(context.Context, types.NodeID, *wire.ATXProof) error) *MockmalfeasancePublisherPublishCall { +func (c *MockmalfeasancePublisherPublishCall) Do(f func(context.Context, types.NodeID, wire.Proof) error) *MockmalfeasancePublisherPublishCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockmalfeasancePublisherPublishCall) DoAndReturn(f func(context.Context, types.NodeID, *wire.ATXProof) error) *MockmalfeasancePublisherPublishCall { +func (c *MockmalfeasancePublisherPublishCall) DoAndReturn(f func(context.Context, types.NodeID, wire.Proof) error) *MockmalfeasancePublisherPublishCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/activation/nipost_test.go b/activation/nipost_test.go index deb0fe588a..95c3cf8a60 100644 --- a/activation/nipost_test.go +++ b/activation/nipost_test.go @@ -214,7 +214,7 @@ func Test_NIPost_PostClientHandling(t *testing.T) { }) t.Run("connect, disconnect, then cancel before reconnect", func(t *testing.T) { - // post client connects, starts post, disconnects in between and proofing is canceled before reconnection + // post client connects, starts post, disconnects in between and proving is canceled before reconnection sig, err := signing.NewEdSigner() require.NoError(t, err) diff --git a/activation/wire/malfeasance.go b/activation/wire/malfeasance.go index d8e60a4127..019a52d6cc 100644 --- a/activation/wire/malfeasance.go +++ b/activation/wire/malfeasance.go @@ -1,27 +1,60 @@ package wire import ( + "github.com/spacemeshos/go-scale" + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" ) //go:generate scalegen +// MerkleTreeIndex is the index of the leaf containing the given field in the merkle tree. +type MerkleTreeIndex uint16 + +const ( + PublishEpochIndex MerkleTreeIndex = iota + PositioningATXIndex + CoinbaseIndex + InitialPostIndex + PreviousATXsRootIndex + NIPostsRootIndex + VRFNonceIndex + MarriagesRootIndex + MarriageATXIndex +) + // ProofType is an identifier for the type of proof that is encoded in the ATXProof. type ProofType byte const ( - DoublePublish ProofType = iota + 1 - DoubleMarry - DoubleMerge - InvalidPost + // TODO(mafa): legacy types for future migration to new malfeasance proofs. + LegacyDoublePublish ProofType = 0x00 + LegacyInvalidPost ProofType = 0x01 + LegacyInvalidPrevATX ProofType = 0x02 + + DoublePublish ProofType = 0x10 + DoubleMarry ProofType = 0x11 + DoubleMerge ProofType = 0x12 + InvalidPost ProofType = 0x13 ) +// ProofVersion is an identifier for the version of the proof that is encoded in the ATXProof. +type ProofVersion byte + type ATXProof struct { - // LayerID is the layer in which the proof was created. This can be used to implement different versions of the ATX - // proof in the future. - Layer types.LayerID + // Version is the version identifier of the proof. This can be used to extend the ATX proof in the future. + Version ProofVersion // ProofType is the type of proof that is being provided. ProofType ProofType // Proof is the actual proof. Its type depends on the ProofType. Proof []byte `scale:"max=1048576"` // max size of proof is 1MiB } + +// Proof is an interface for all types of proofs that can be provided in an ATXProof. +// Generally the proof should be able to validate itself and be scale encoded. +type Proof interface { + scale.Encodable + + Valid(edVerifier *signing.EdVerifier) (types.NodeID, error) +} diff --git a/activation/wire/malfeasance_double_marry.go b/activation/wire/malfeasance_double_marry.go new file mode 100644 index 0000000000..ea946b29ea --- /dev/null +++ b/activation/wire/malfeasance_double_marry.go @@ -0,0 +1,221 @@ +package wire + +import ( + "errors" + "fmt" + "slices" + + "github.com/spacemeshos/merkle-tree" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/atxs" +) + +//go:generate scalegen + +// ProofDoubleMarry is a proof that two distinct ATXs contain a marriage certificate signed by the same identity. +// +// We are proving the following: +// 1. The ATXs have different IDs. +// 2. Both ATXs have a valid signature. +// 3. Both ATXs contain a marriage certificate created by the same identity. +// 4. Both marriage certificates have valid signatures. +// +// HINT: this works if the identity that publishes the marriage ATX marries themselves. +type ProofDoubleMarry struct { + // NodeID is the node ID that married twice. + NodeID types.NodeID + + Proofs [2]MarryProof +} + +var _ Proof = &ProofDoubleMarry{} + +func NewDoubleMarryProof(db sql.Executor, atx1, atx2 *ActivationTxV2, nodeID types.NodeID) (*ProofDoubleMarry, error) { + if atx1.ID() == atx2.ID() { + return nil, errors.New("ATXs have the same ID") + } + + proof1, err := createMarryProof(db, atx1, nodeID) + if err != nil { + return nil, fmt.Errorf("proof for atx1: %w", err) + } + + proof2, err := createMarryProof(db, atx2, nodeID) + if err != nil { + return nil, fmt.Errorf("proof for atx2: %w", err) + } + + proof := &ProofDoubleMarry{ + NodeID: nodeID, + Proofs: [2]MarryProof{proof1, proof2}, + } + return proof, nil +} + +func createMarryProof(db sql.Executor, atx *ActivationTxV2, nodeID types.NodeID) (MarryProof, error) { + marriageProof, err := marriageProof(atx) + if err != nil { + return MarryProof{}, fmt.Errorf("failed to create proof for ATX 1: %w", err) + } + + marriageIndex := slices.IndexFunc(atx.Marriages, func(cert MarriageCertificate) bool { + if cert.ReferenceAtx == types.EmptyATXID && atx.SmesherID == nodeID { + // special case of the self signed certificate of the ATX publisher + return true + } + refATX, err := atxs.Get(db, cert.ReferenceAtx) + if err != nil { + return false + } + return refATX.SmesherID == nodeID + }) + if marriageIndex == -1 { + return MarryProof{}, fmt.Errorf("does not contain a marriage certificate signed by %s", nodeID.ShortString()) + } + certProof, err := certificateProof(atx.Marriages, uint64(marriageIndex)) + if err != nil { + return MarryProof{}, fmt.Errorf("failed to create certificate proof for ATX 1: %w", err) + } + + proof := MarryProof{ + ATXID: atx.ID(), + + MarriageRoot: types.Hash32(atx.Marriages.Root()), + MarriageProof: marriageProof, + + CertificateReference: atx.Marriages[marriageIndex].ReferenceAtx, + CertificateSignature: atx.Marriages[marriageIndex].Signature, + CertificateIndex: uint64(marriageIndex), + CertificateProof: certProof, + + SmesherID: atx.SmesherID, + Signature: atx.Signature, + } + return proof, nil +} + +func marriageProof(atx *ActivationTxV2) ([]types.Hash32, error) { + tree, err := merkle.NewTreeBuilder(). + WithLeavesToProve(map[uint64]bool{uint64(MarriagesRootIndex): true}). + WithHashFunc(atxTreeHash). + Build() + if err != nil { + return nil, err + } + atx.merkleTree(tree) + proof := tree.Proof() + + proofHashes := make([]types.Hash32, len(proof)) + for i, p := range proof { + proofHashes[i] = types.Hash32(p) + } + return proofHashes, nil +} + +func certificateProof(certs MarriageCertificates, index uint64) ([]types.Hash32, error) { + tree, err := merkle.NewTreeBuilder(). + WithLeavesToProve(map[uint64]bool{index: true}). + WithHashFunc(atxTreeHash). + Build() + if err != nil { + return nil, err + } + certs.merkleTree(tree) + proof := tree.Proof() + + proofHashes := make([]types.Hash32, len(proof)) + for i, p := range proof { + proofHashes[i] = types.Hash32(p) + } + return proofHashes, nil +} + +func (p ProofDoubleMarry) Valid(edVerifier *signing.EdVerifier) (types.NodeID, error) { + if p.Proofs[0].ATXID == p.Proofs[1].ATXID { + return types.EmptyNodeID, errors.New("proofs have the same ATX ID") + } + + if err := p.Proofs[0].Valid(edVerifier, p.NodeID); err != nil { + return types.EmptyNodeID, fmt.Errorf("proof 1 is invalid: %w", err) + } + if err := p.Proofs[1].Valid(edVerifier, p.NodeID); err != nil { + return types.EmptyNodeID, fmt.Errorf("proof 2 is invalid: %w", err) + } + return p.NodeID, nil +} + +type MarryProof struct { + // ATXID is the ID of the ATX being proven. + ATXID types.ATXID + + // MarriageRoot and its proof that it is contained in the ATX. + MarriageRoot types.Hash32 + MarriageProof []types.Hash32 `scale:"max=32"` + + // The signature of the certificate and the proof that the certificate is contained in the MarriageRoot at + // the given index. + CertificateReference types.ATXID + CertificateSignature types.EdSignature + CertificateIndex uint64 + CertificateProof []types.Hash32 `scale:"max=32"` + + // SmesherID is the ID of the smesher that published the ATX. + SmesherID types.NodeID + // Signature is the signature of the ATXID by the smesher. + Signature types.EdSignature +} + +func (p MarryProof) Valid(edVerifier *signing.EdVerifier, nodeID types.NodeID) error { + if !edVerifier.Verify(signing.ATX, p.SmesherID, p.ATXID.Bytes(), p.Signature) { + return errors.New("invalid ATX signature") + } + + if !edVerifier.Verify(signing.MARRIAGE, nodeID, p.SmesherID.Bytes(), p.CertificateSignature) { + return errors.New("invalid certificate signature") + } + + proof := make([][]byte, len(p.MarriageProof)) + for i, h := range p.MarriageProof { + proof[i] = h.Bytes() + } + ok, err := merkle.ValidatePartialTree( + []uint64{uint64(MarriagesRootIndex)}, + [][]byte{p.MarriageRoot.Bytes()}, + proof, + p.ATXID.Bytes(), + atxTreeHash, + ) + if err != nil { + return fmt.Errorf("validate marriage proof: %w", err) + } + if !ok { + return errors.New("invalid marriage proof") + } + + mc := MarriageCertificate{ + ReferenceAtx: p.CertificateReference, + Signature: p.CertificateSignature, + } + + certProof := make([][]byte, len(p.CertificateProof)) + for i, h := range p.CertificateProof { + certProof[i] = h.Bytes() + } + ok, err = merkle.ValidatePartialTree( + []uint64{p.CertificateIndex}, + [][]byte{mc.Root()}, + certProof, + p.MarriageRoot.Bytes(), + atxTreeHash, + ) + if err != nil { + return fmt.Errorf("validate certificate proof: %w", err) + } + if !ok { + return errors.New("invalid certificate proof") + } + return nil +} diff --git a/activation/wire/malfeasance_double_marry_scale.go b/activation/wire/malfeasance_double_marry_scale.go new file mode 100644 index 0000000000..03f70c95fa --- /dev/null +++ b/activation/wire/malfeasance_double_marry_scale.go @@ -0,0 +1,182 @@ +// Code generated by github.com/spacemeshos/go-scale/scalegen. DO NOT EDIT. + +// nolint +package wire + +import ( + "github.com/spacemeshos/go-scale" + "github.com/spacemeshos/go-spacemesh/common/types" +) + +func (t *ProofDoubleMarry) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeByteArray(enc, t.NodeID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeStructArray(enc, t.Proofs[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *ProofDoubleMarry) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + n, err := scale.DecodeByteArray(dec, t.NodeID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeStructArray(dec, t.Proofs[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *MarryProof) EncodeScale(enc *scale.Encoder) (total int, err error) { + { + n, err := scale.EncodeByteArray(enc, t.ATXID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.MarriageRoot[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeStructSliceWithLimit(enc, t.MarriageProof, 32) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.CertificateReference[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.CertificateSignature[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeCompact64(enc, uint64(t.CertificateIndex)) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeStructSliceWithLimit(enc, t.CertificateProof, 32) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.SmesherID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.EncodeByteArray(enc, t.Signature[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} + +func (t *MarryProof) DecodeScale(dec *scale.Decoder) (total int, err error) { + { + n, err := scale.DecodeByteArray(dec, t.ATXID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.MarriageRoot[:]) + if err != nil { + return total, err + } + total += n + } + { + field, n, err := scale.DecodeStructSliceWithLimit[types.Hash32](dec, 32) + if err != nil { + return total, err + } + total += n + t.MarriageProof = field + } + { + n, err := scale.DecodeByteArray(dec, t.CertificateReference[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.CertificateSignature[:]) + if err != nil { + return total, err + } + total += n + } + { + field, n, err := scale.DecodeCompact64(dec) + if err != nil { + return total, err + } + total += n + t.CertificateIndex = uint64(field) + } + { + field, n, err := scale.DecodeStructSliceWithLimit[types.Hash32](dec, 32) + if err != nil { + return total, err + } + total += n + t.CertificateProof = field + } + { + n, err := scale.DecodeByteArray(dec, t.SmesherID[:]) + if err != nil { + return total, err + } + total += n + } + { + n, err := scale.DecodeByteArray(dec, t.Signature[:]) + if err != nil { + return total, err + } + total += n + } + return total, nil +} diff --git a/activation/wire/malfeasance_double_marry_test.go b/activation/wire/malfeasance_double_marry_test.go new file mode 100644 index 0000000000..351aa265ed --- /dev/null +++ b/activation/wire/malfeasance_double_marry_test.go @@ -0,0 +1,290 @@ +package wire + +import ( + "fmt" + "slices" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" + "github.com/spacemeshos/go-spacemesh/sql" + "github.com/spacemeshos/go-spacemesh/sql/atxs" +) + +func Test_DoubleMarryProof(t *testing.T) { + sig, err := signing.NewEdSigner() + require.NoError(t, err) + + otherSig, err := signing.NewEdSigner() + require.NoError(t, err) + + t.Run("valid", func(t *testing.T) { + db := sql.InMemory() + otherAtx := &types.ActivationTx{} + otherAtx.SetID(types.RandomATXID()) + otherAtx.SmesherID = otherSig.NodeID() + require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) + + atx1 := newActivationTxV2( + withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + withMarriageCertificate(otherSig, types.EmptyATXID, otherSig.NodeID()), + withMarriageCertificate(sig, atx1.ID(), otherSig.NodeID()), + ) + atx2.Sign(otherSig) + + proof, err := NewDoubleMarryProof(db, atx1, atx2, otherSig.NodeID()) + require.NoError(t, err) + require.NotNil(t, proof) + + verifier := signing.NewEdVerifier() + id, err := proof.Valid(verifier) + require.NoError(t, err) + require.Equal(t, otherSig.NodeID(), id) + }) + + t.Run("does not contain same certificate owner", func(t *testing.T) { + db := sql.InMemory() + + atx1 := newActivationTxV2( + withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + withMarriageCertificate(otherSig, types.EmptyATXID, otherSig.NodeID()), + ) + atx2.Sign(otherSig) + + proof, err := NewDoubleMarryProof(db, atx1, atx2, otherSig.NodeID()) + require.ErrorContains(t, err, fmt.Sprintf( + "proof for atx1: does not contain a marriage certificate signed by %s", otherSig.NodeID().ShortString(), + )) + require.Nil(t, proof) + + proof, err = NewDoubleMarryProof(db, atx1, atx2, sig.NodeID()) + require.ErrorContains(t, err, fmt.Sprintf( + "proof for atx2: does not contain a marriage certificate signed by %s", sig.NodeID().ShortString(), + )) + require.Nil(t, proof) + }) + + t.Run("same ATX ID", func(t *testing.T) { + atx1 := newActivationTxV2() + atx1.Sign(sig) + + db := sql.InMemory() + proof, err := NewDoubleMarryProof(db, atx1, atx1, sig.NodeID()) + require.ErrorContains(t, err, "ATXs have the same ID") + require.Nil(t, proof) + + // manually construct an invalid proof + proof = &ProofDoubleMarry{ + Proofs: [2]MarryProof{ + { + ATXID: atx1.ID(), + }, + { + ATXID: atx1.ID(), + }, + }, + } + + verifier := signing.NewEdVerifier() + id, err := proof.Valid(verifier) + require.ErrorContains(t, err, "same ATX ID") + require.Equal(t, types.EmptyNodeID, id) + }) + + t.Run("invalid marriage proof", func(t *testing.T) { + db := sql.InMemory() + otherAtx := &types.ActivationTx{} + otherAtx.SetID(types.RandomATXID()) + otherAtx.SmesherID = otherSig.NodeID() + require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) + + atx1 := newActivationTxV2( + withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + withMarriageCertificate(otherSig, types.EmptyATXID, otherSig.NodeID()), + withMarriageCertificate(sig, atx1.ID(), otherSig.NodeID()), + ) + atx2.Sign(otherSig) + + // manually construct an invalid proof + proof1, err := createMarryProof(db, atx1, otherSig.NodeID()) + require.NoError(t, err) + proof2, err := createMarryProof(db, atx2, otherSig.NodeID()) + require.NoError(t, err) + + proof := &ProofDoubleMarry{ + NodeID: otherSig.NodeID(), + Proofs: [2]MarryProof{ + proof1, proof2, + }, + } + + verifier := signing.NewEdVerifier() + proof.Proofs[0].MarriageProof = slices.Clone(proof1.MarriageProof) + proof.Proofs[0].MarriageProof[0] = types.RandomHash() + id, err := proof.Valid(verifier) + require.ErrorContains(t, err, "proof 1 is invalid: invalid marriage proof") + require.Equal(t, types.EmptyNodeID, id) + + proof.Proofs[0].MarriageProof[0] = proof1.MarriageProof[0] + proof.Proofs[1].MarriageProof = slices.Clone(proof2.MarriageProof) + proof.Proofs[1].MarriageProof[0] = types.RandomHash() + id, err = proof.Valid(verifier) + require.ErrorContains(t, err, "proof 2 is invalid: invalid marriage proof") + require.Equal(t, types.EmptyNodeID, id) + }) + + t.Run("invalid certificate proof", func(t *testing.T) { + db := sql.InMemory() + otherAtx := &types.ActivationTx{} + otherAtx.SetID(types.RandomATXID()) + otherAtx.SmesherID = otherSig.NodeID() + require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) + + atx1 := newActivationTxV2( + withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + withMarriageCertificate(otherSig, types.EmptyATXID, otherSig.NodeID()), + withMarriageCertificate(sig, atx1.ID(), otherSig.NodeID()), + ) + atx2.Sign(otherSig) + + // manually construct an invalid proof + proof1, err := createMarryProof(db, atx1, otherSig.NodeID()) + require.NoError(t, err) + proof2, err := createMarryProof(db, atx2, otherSig.NodeID()) + require.NoError(t, err) + + proof := &ProofDoubleMarry{ + NodeID: otherSig.NodeID(), + Proofs: [2]MarryProof{ + proof1, proof2, + }, + } + + verifier := signing.NewEdVerifier() + proof.Proofs[0].CertificateProof = slices.Clone(proof1.CertificateProof) + proof.Proofs[0].CertificateProof[0] = types.RandomHash() + id, err := proof.Valid(verifier) + require.ErrorContains(t, err, "proof 1 is invalid: invalid certificate proof") + require.Equal(t, types.EmptyNodeID, id) + + proof.Proofs[0].CertificateProof[0] = proof1.CertificateProof[0] + proof.Proofs[1].CertificateProof = slices.Clone(proof2.CertificateProof) + proof.Proofs[1].CertificateProof[0] = types.RandomHash() + id, err = proof.Valid(verifier) + require.ErrorContains(t, err, "proof 2 is invalid: invalid certificate proof") + require.Equal(t, types.EmptyNodeID, id) + }) + + t.Run("invalid atx signature", func(t *testing.T) { + db := sql.InMemory() + otherAtx := &types.ActivationTx{} + otherAtx.SetID(types.RandomATXID()) + otherAtx.SmesherID = otherSig.NodeID() + require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) + + atx1 := newActivationTxV2( + withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + withMarriageCertificate(otherSig, types.EmptyATXID, sig.NodeID()), + withMarriageCertificate(sig, atx1.ID(), sig.NodeID()), + ) + atx2.Sign(otherSig) + + proof, err := NewDoubleMarryProof(db, atx1, atx2, otherSig.NodeID()) + require.NoError(t, err) + + verifier := signing.NewEdVerifier() + + proof.Proofs[0].Signature = types.RandomEdSignature() + id, err := proof.Valid(verifier) + require.ErrorContains(t, err, "proof 1 is invalid: invalid ATX signature") + require.Equal(t, types.EmptyNodeID, id) + + proof.Proofs[0].Signature = atx1.Signature + proof.Proofs[1].Signature = types.RandomEdSignature() + id, err = proof.Valid(verifier) + require.ErrorContains(t, err, "proof 2 is invalid: invalid ATX signature") + require.Equal(t, types.EmptyNodeID, id) + }) + + t.Run("invalid certificate signature", func(t *testing.T) { + db := sql.InMemory() + otherAtx := &types.ActivationTx{} + otherAtx.SetID(types.RandomATXID()) + otherAtx.SmesherID = otherSig.NodeID() + require.NoError(t, atxs.Add(db, otherAtx, types.AtxBlob{})) + + atx1 := newActivationTxV2( + withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + withMarriageCertificate(otherSig, otherAtx.ID(), sig.NodeID()), + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + withMarriageCertificate(otherSig, types.EmptyATXID, sig.NodeID()), + withMarriageCertificate(sig, atx1.ID(), sig.NodeID()), + ) + atx2.Sign(otherSig) + + proof, err := NewDoubleMarryProof(db, atx1, atx2, otherSig.NodeID()) + require.NoError(t, err) + + verifier := signing.NewEdVerifier() + + proof.Proofs[0].CertificateSignature = types.RandomEdSignature() + id, err := proof.Valid(verifier) + require.ErrorContains(t, err, "proof 1 is invalid: invalid certificate signature") + require.Equal(t, types.EmptyNodeID, id) + + proof.Proofs[0].CertificateSignature = atx1.Marriages[1].Signature + proof.Proofs[1].CertificateSignature = types.RandomEdSignature() + id, err = proof.Valid(verifier) + require.ErrorContains(t, err, "proof 2 is invalid: invalid certificate signature") + require.Equal(t, types.EmptyNodeID, id) + }) + + t.Run("unknown reference ATX", func(t *testing.T) { + db := sql.InMemory() + + atx1 := newActivationTxV2( + withMarriageCertificate(sig, types.EmptyATXID, sig.NodeID()), + withMarriageCertificate(otherSig, types.RandomATXID(), sig.NodeID()), // unknown reference ATX + ) + atx1.Sign(sig) + + atx2 := newActivationTxV2( + withMarriageCertificate(otherSig, types.EmptyATXID, sig.NodeID()), + withMarriageCertificate(sig, atx1.ID(), sig.NodeID()), + ) + atx2.Sign(otherSig) + + proof, err := NewDoubleMarryProof(db, atx1, atx2, otherSig.NodeID()) + require.Error(t, err) + require.Nil(t, proof) + }) +} diff --git a/activation/wire/malfeasance_scale.go b/activation/wire/malfeasance_scale.go index b329626623..a2cb9dbed7 100644 --- a/activation/wire/malfeasance_scale.go +++ b/activation/wire/malfeasance_scale.go @@ -5,12 +5,11 @@ package wire import ( "github.com/spacemeshos/go-scale" - "github.com/spacemeshos/go-spacemesh/common/types" ) func (t *ATXProof) EncodeScale(enc *scale.Encoder) (total int, err error) { { - n, err := scale.EncodeCompact32(enc, uint32(t.Layer)) + n, err := scale.EncodeCompact8(enc, uint8(t.Version)) if err != nil { return total, err } @@ -35,12 +34,12 @@ func (t *ATXProof) EncodeScale(enc *scale.Encoder) (total int, err error) { func (t *ATXProof) DecodeScale(dec *scale.Decoder) (total int, err error) { { - field, n, err := scale.DecodeCompact32(dec) + field, n, err := scale.DecodeCompact8(dec) if err != nil { return total, err } total += n - t.Layer = types.LayerID(field) + t.Version = ProofVersion(field) } { field, n, err := scale.DecodeCompact8(dec) diff --git a/activation/wire/wire_v2.go b/activation/wire/wire_v2.go index 7cf29c842e..fd5cccbecf 100644 --- a/activation/wire/wire_v2.go +++ b/activation/wire/wire_v2.go @@ -32,7 +32,7 @@ type ActivationTxV2 struct { // All new IDs that are married to this ID are added to the equivocation set // that this ID belongs to. // It must contain a self-marriage certificate (needed for malfeasance proofs). - Marriages []MarriageCertificate `scale:"max=256"` + Marriages MarriageCertificates `scale:"max=256"` // The ID of the ATX containing marriage for the included IDs. // Only required when the ATX includes married IDs. @@ -46,10 +46,6 @@ type ActivationTxV2 struct { blob []byte } -func (atx *ActivationTxV2) SignedBytes() []byte { - return atx.ID().Bytes() -} - func (atx *ActivationTxV2) Blob() types.AtxBlob { if len(atx.blob) == 0 { atx.blob = codec.MustEncode(atx) @@ -71,9 +67,9 @@ func DecodeAtxV2(blob []byte) (*ActivationTxV2, error) { } func (atx *ActivationTxV2) merkleTree(tree *merkle.Tree) { - publishEpoch := make([]byte, 4) - binary.LittleEndian.PutUint32(publishEpoch, atx.PublishEpoch.Uint32()) - tree.AddLeaf(publishEpoch) + var publishEpoch types.Hash32 + binary.LittleEndian.PutUint32(publishEpoch[:], atx.PublishEpoch.Uint32()) + tree.AddLeaf(publishEpoch.Bytes()) tree.AddLeaf(atx.PositioningATX.Bytes()) tree.AddLeaf(atx.Coinbase.Bytes()) @@ -111,23 +107,11 @@ func (atx *ActivationTxV2) merkleTree(tree *merkle.Tree) { } tree.AddLeaf(niPostTree.Root()) - vrfNonce := make([]byte, 8) - binary.LittleEndian.PutUint64(vrfNonce, atx.VRFNonce) - tree.AddLeaf(vrfNonce) + var vrfNonce types.Hash32 + binary.LittleEndian.PutUint64(vrfNonce[:], atx.VRFNonce) + tree.AddLeaf(vrfNonce.Bytes()) - marriagesTree, err := merkle.NewTreeBuilder(). - WithHashFunc(atxTreeHash). - Build() - if err != nil { - panic(err) - } - for _, marriage := range atx.Marriages { - marriagesTree.AddLeaf(marriage.Root()) - } - for i := len(atx.Marriages); i < 256; i++ { - marriagesTree.AddLeaf(types.EmptyHash32.Bytes()) - } - tree.AddLeaf(marriagesTree.Root()) + tree.AddLeaf(atx.Marriages.Root()) if atx.MarriageATX != nil { tree.AddLeaf(atx.MarriageATX.Bytes()) @@ -154,7 +138,7 @@ func (atx *ActivationTxV2) ID() types.ATXID { func (atx *ActivationTxV2) Sign(signer *signing.EdSigner) { atx.SmesherID = signer.NodeID() - atx.Signature = signer.Sign(signing.ATX, atx.SignedBytes()) + atx.Signature = signer.Sign(signing.ATX, atx.ID().Bytes()) } func (atx *ActivationTxV2) TotalNumUnits() uint32 { @@ -167,6 +151,28 @@ func (atx *ActivationTxV2) TotalNumUnits() uint32 { return total } +type MarriageCertificates []MarriageCertificate + +func (mcs MarriageCertificates) Root() []byte { + marriagesTree, err := merkle.NewTreeBuilder(). + WithHashFunc(atxTreeHash). + Build() + if err != nil { + panic(err) + } + mcs.merkleTree(marriagesTree) + return marriagesTree.Root() +} + +func (mcs MarriageCertificates) merkleTree(tree *merkle.Tree) { + for _, marriage := range mcs { + tree.AddLeaf(marriage.Root()) + } + for i := len(mcs); i < 256; i++ { + tree.AddLeaf(types.EmptyHash32.Bytes()) + } +} + type InitialAtxPartsV2 struct { CommitmentATX types.ATXID Post PostV1 diff --git a/activation/wire/wire_v2_test.go b/activation/wire/wire_v2_test.go index 596be06091..e0303affb0 100644 --- a/activation/wire/wire_v2_test.go +++ b/activation/wire/wire_v2_test.go @@ -1,17 +1,59 @@ package wire import ( - "encoding/binary" "math/rand/v2" "testing" fuzz "github.com/google/gofuzz" - "github.com/spacemeshos/merkle-tree" "github.com/stretchr/testify/require" "github.com/spacemeshos/go-spacemesh/common/types" + "github.com/spacemeshos/go-spacemesh/signing" ) +type testAtxV2Opt func(*ActivationTxV2) + +func withMarriageCertificate(sig *signing.EdSigner, refAtx types.ATXID, atxPublisher types.NodeID) testAtxV2Opt { + return func(atx *ActivationTxV2) { + certificate := MarriageCertificate{ + ReferenceAtx: refAtx, + Signature: sig.Sign(signing.MARRIAGE, atxPublisher.Bytes()), + } + atx.Marriages = append(atx.Marriages, certificate) + } +} + +func newActivationTxV2(opts ...testAtxV2Opt) *ActivationTxV2 { + atx := &ActivationTxV2{ + PublishEpoch: rand.N(types.EpochID(255)), + PositioningATX: types.RandomATXID(), + PreviousATXs: make([]types.ATXID, 1+rand.IntN(255)), + NiPosts: []NiPostsV2{ + { + Membership: MerkleProofV2{ + Nodes: make([]types.Hash32, 32), + }, + Challenge: types.RandomHash(), + Posts: []SubPostV2{ + { + MarriageIndex: rand.Uint32N(256), + PrevATXIndex: 0, + Post: PostV1{ + Nonce: 0, + Indices: make([]byte, 800), + Pow: 0, + }, + }, + }, + }, + }, + } + for _, opt := range opts { + opt(atx) + } + return atx +} + func Benchmark_ATXv2ID(b *testing.B) { f := fuzz.New() b.ResetTimer() @@ -83,79 +125,3 @@ func Test_NoATXv2IDCollisions(t *testing.T) { atxIDs = append(atxIDs, id) } } - -const PublishEpochIndex = 0 - -func Test_GenerateDoublePublishProof(t *testing.T) { - atx := &ActivationTxV2{ - PublishEpoch: 10, - PositioningATX: types.RandomATXID(), - PreviousATXs: make([]types.ATXID, 1), - NiPosts: []NiPostsV2{ - { - Membership: MerkleProofV2{ - Nodes: make([]types.Hash32, 32), - }, - Challenge: types.RandomHash(), - Posts: []SubPostV2{ - { - MarriageIndex: rand.Uint32N(256), - PrevATXIndex: 0, - Post: PostV1{ - Nonce: 0, - Indices: make([]byte, 800), - Pow: 0, - }, - }, - }, - }, - }, - } - - proof, err := generatePublishEpochProof(atx) - require.NoError(t, err) - require.NotNil(t, proof) - - // a malfeasance proof for double publish will contain - // - the value of the PublishEpoch (here 10) - 4 bytes - // - the two ATX IDs - 32 bytes each - // - the two signatures (atx.Signature + atx.NodeID) - 64 bytes each - // - two merkle proofs - one per ATX - that is 128 bytes each (4 * 32) - // total: 452 bytes instead of two full ATXs (> 20 kB each in the worst case) - - publishEpoch := make([]byte, 4) - binary.LittleEndian.PutUint32(publishEpoch, atx.PublishEpoch.Uint32()) - ok, err := merkle.ValidatePartialTree( - []uint64{PublishEpochIndex}, - [][]byte{publishEpoch}, - proof, - atx.ID().Bytes(), - atxTreeHash, - ) - require.NoError(t, err) - require.True(t, ok) - - // different PublishEpoch doesn't validate - publishEpoch = []byte{0xFF, 0x00, 0x00, 0x00} - ok, err = merkle.ValidatePartialTree( - []uint64{PublishEpochIndex}, - [][]byte{publishEpoch}, - proof, - atx.ID().Bytes(), - atxTreeHash, - ) - require.NoError(t, err) - require.False(t, ok) -} - -func generatePublishEpochProof(atx *ActivationTxV2) ([][]byte, error) { - tree, err := merkle.NewTreeBuilder(). - WithLeavesToProve(map[uint64]bool{PublishEpochIndex: true}). - WithHashFunc(atxTreeHash). - Build() - if err != nil { - return nil, err - } - atx.merkleTree(tree) - return tree.Proof(), nil -} diff --git a/malfeasance/handler.go b/malfeasance/handler.go index 50890eee62..4949cb4a56 100644 --- a/malfeasance/handler.go +++ b/malfeasance/handler.go @@ -44,7 +44,6 @@ const ( InvalidActivation MalfeasanceType = iota + 10 InvalidBallot InvalidHareMsg - DoubleMarry = MalfeasanceType(wire.DoubleMarry) ) // Handler processes MalfeasanceProof from gossip and, if deems it valid, propagates it to peers. diff --git a/malfeasance/wire/malfeasance.go b/malfeasance/wire/malfeasance.go index b4132ea568..0ffd8e4228 100644 --- a/malfeasance/wire/malfeasance.go +++ b/malfeasance/wire/malfeasance.go @@ -15,7 +15,7 @@ import ( "github.com/spacemeshos/go-spacemesh/common/types" ) -//go:generate scalegen -types MalfeasanceProof,MalfeasanceGossip,AtxProof,BallotProof,HareProof,AtxProofMsg,BallotProofMsg,HareProofMsg,HareMetadata,InvalidPostIndexProof,InvalidPrevATXProof,DoubleMarryProof +//go:generate scalegen -types MalfeasanceProof,MalfeasanceGossip,AtxProof,BallotProof,HareProof,AtxProofMsg,BallotProofMsg,HareProofMsg,HareMetadata,InvalidPostIndexProof,InvalidPrevATXProof const ( MultipleATXs byte = iota + 1 @@ -23,7 +23,6 @@ const ( HareEquivocation InvalidPostIndex InvalidPrevATX - DoubleMarry ) type MalfeasanceProof struct { @@ -325,12 +324,6 @@ type InvalidPrevATXProof struct { func (p *InvalidPrevATXProof) isProof() {} -type DoubleMarryProof struct { - // TODO: implement -} - -func (p *DoubleMarryProof) isProof() {} - func MalfeasanceInfo(smesher types.NodeID, mp *MalfeasanceProof) string { var b strings.Builder b.WriteString(fmt.Sprintf("generate layer: %v\n", mp.Layer)) diff --git a/malfeasance/wire/malfeasance_scale.go b/malfeasance/wire/malfeasance_scale.go index 6e23fd2175..3ec88a1acc 100644 --- a/malfeasance/wire/malfeasance_scale.go +++ b/malfeasance/wire/malfeasance_scale.go @@ -422,11 +422,3 @@ func (t *InvalidPrevATXProof) DecodeScale(dec *scale.Decoder) (total int, err er } return total, nil } - -func (t *DoubleMarryProof) EncodeScale(enc *scale.Encoder) (total int, err error) { - return total, nil -} - -func (t *DoubleMarryProof) DecodeScale(dec *scale.Decoder) (total int, err error) { - return total, nil -} diff --git a/systest/Makefile b/systest/Makefile index 08829590b3..b1f4d07f11 100644 --- a/systest/Makefile +++ b/systest/Makefile @@ -39,8 +39,6 @@ endif command := tests -test.v -test.count=$(count) -test.timeout=60m -test.run=$(test_name) -test.parallel=$(clusters) \ -test.failfast=$(failfast) -clusters=$(clusters) -level=$(level) -configname=$(configname) - - .PHONY: docker docker: @DOCKER_BUILDKIT=1 docker build \