Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - detect double atx-merging malfeasance #6135

Closed
wants to merge 9 commits into from
6 changes: 6 additions & 0 deletions activation/e2e/checkpoint_merged_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,12 @@ func Test_CheckpointAfterMerge(t *testing.T) {
require.Equal(t, i, marriage.Index)
}

checkpointedMerged, err := atxs.Get(newDB, mergedATX.ID())
require.NoError(t, err)
require.True(t, checkpointedMerged.Golden())
require.NotNil(t, checkpointedMerged.MarriageATX)
require.Equal(t, marriageATX.ID(), *checkpointedMerged.MarriageATX)

// 4. Spawn new ATX handler and builder using the new DB
poetDb = activation.NewPoetDb(newDB, logger.Named("poetDb"))
cdb = datastore.NewCachedDB(newDB, logger)
Expand Down
38 changes: 38 additions & 0 deletions activation/handler_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@

atx := &types.ActivationTx{
PublishEpoch: watx.PublishEpoch,
MarriageATX: watx.MarriageATX,
Coinbase: watx.Coinbase,
BaseTickHeight: baseTickHeight,
NumUnits: parts.effectiveUnits,
Expand Down Expand Up @@ -674,6 +675,14 @@
return true, proof, nil
}

proof, err = h.checkDoubleMerge(tx, watx)
if err != nil {
return false, nil, fmt.Errorf("checking double merge: %w", err)

Check warning on line 680 in activation/handler_v2.go

View check run for this annotation

Codecov / codecov/patch

activation/handler_v2.go#L680

Added line #L680 was not covered by tests
}
if proof != nil {
return true, proof, nil
}

// TODO: contextual validation:
// 1. check double-publish
// 2. check previous ATX
Expand Down Expand Up @@ -703,6 +712,35 @@
return nil, nil
}

func (h *HandlerV2) checkDoubleMerge(tx *sql.Tx, watx *wire.ActivationTxV2) (*mwire.MalfeasanceProof, error) {
if watx.MarriageATX == nil {
return nil, nil
}
id, err := atxs.AtxWithMarriage(tx, *watx.MarriageATX, watx.PublishEpoch)
switch {
case errors.Is(err, sql.ErrNotFound):
return nil, nil
case err != nil:
return nil, fmt.Errorf("checking for ATXs with the same marriage ATX: %w", err)

Check warning on line 724 in activation/handler_v2.go

View check run for this annotation

Codecov / codecov/patch

activation/handler_v2.go#L723-L724

Added lines #L723 - L724 were not covered by tests
}

h.logger.Debug("second merged ATX for single marriage - creating malfeasance proof",
zap.Stringer("marriage_atx", *watx.MarriageATX),
zap.Stringer("atx", watx.ID()),
zap.Stringer("other atx", id),
poszu marked this conversation as resolved.
Show resolved Hide resolved
zap.Stringer("smesher_id", watx.SmesherID),
)

// FIXME: implement the proof
proof := &mwire.MalfeasanceProof{
Proof: mwire.Proof{
Type: mwire.DoubleMarry,
Data: &mwire.DoubleMarryProof{},
},
}
return proof, nil
}

// Store an ATX in the DB.
// TODO: detect malfeasance and create proofs.
func (h *HandlerV2) storeAtx(
Expand Down
147 changes: 119 additions & 28 deletions activation/handler_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -616,18 +616,16 @@ func TestHandlerV2_ProcessSoloATX(t *testing.T) {
func marryIDs(
t testing.TB,
atxHandler *v2TestHandler,
sig *signing.EdSigner,
signers []*signing.EdSigner,
golden types.ATXID,
num int,
) (marriage *wire.ActivationTxV2, other []*wire.ActivationTxV2) {
sig := signers[0]
mATX := newInitialATXv2(t, golden)
mATX.Marriages = []wire.MarriageCertificate{{
Signature: sig.Sign(signing.MARRIAGE, sig.NodeID().Bytes()),
}}

for range num {
signer, err := signing.NewEdSigner()
require.NoError(t, err)
for _, signer := range signers[1:] {
atx := atxHandler.createAndProcessInitial(t, signer)
other = append(other, atx)
mATX.Marriages = append(mATX.Marriages, wire.MarriageCertificate{
Expand All @@ -647,20 +645,27 @@ func marryIDs(

func TestHandlerV2_ProcessMergedATX(t *testing.T) {
poszu marked this conversation as resolved.
Show resolved Hide resolved
t.Parallel()
golden := types.RandomATXID()
sig, err := signing.NewEdSigner()
require.NoError(t, err)
var (
golden = types.RandomATXID()
signers []*signing.EdSigner
equivocationSet []types.NodeID
)
for range 4 {
sig, err := signing.NewEdSigner()
require.NoError(t, err)
signers = append(signers, sig)
equivocationSet = append(equivocationSet, sig.NodeID())
}
sig := signers[0]

t.Run("happy case", func(t *testing.T) {
atxHandler := newV2TestHandler(t, golden)

// Marry IDs
mATX, otherATXs := marryIDs(t, atxHandler, sig, golden, 2)
mATX, otherATXs := marryIDs(t, atxHandler, signers, golden)
previousATXs := []types.ATXID{mATX.ID()}
equivocationSet := []types.NodeID{sig.NodeID()}
for _, atx := range otherATXs {
previousATXs = append(previousATXs, atx.ID())
equivocationSet = append(equivocationSet, atx.SmesherID)
}

// Process a merged ATX
Expand Down Expand Up @@ -698,12 +703,10 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) {
atxHandler.tickSize = tickSize

// Marry IDs
mATX, otherATXs := marryIDs(t, atxHandler, sig, golden, 4)
mATX, otherATXs := marryIDs(t, atxHandler, signers, golden)
previousATXs := []types.ATXID{mATX.ID()}
equivocationSet := []types.NodeID{sig.NodeID()}
for _, atx := range otherATXs {
previousATXs = append(previousATXs, atx.ID())
equivocationSet = append(equivocationSet, atx.SmesherID)
}

// Process a merged ATX
Expand Down Expand Up @@ -770,12 +773,10 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) {
atxHandler := newV2TestHandler(t, golden)

// Marry IDs
mATX, otherATXs := marryIDs(t, atxHandler, sig, golden, 2)
mATX, otherATXs := marryIDs(t, atxHandler, signers, golden)
previousATXs := []types.ATXID{}
equivocationSet := []types.NodeID{sig.NodeID()}
for _, atx := range otherATXs {
previousATXs = append(previousATXs, atx.ID())
equivocationSet = append(equivocationSet, atx.SmesherID)
}

// Process a merged ATX
Expand Down Expand Up @@ -807,12 +808,10 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) {
atxHandler := newV2TestHandler(t, golden)

// Marry IDs
mATX, otherATXs := marryIDs(t, atxHandler, sig, golden, 1)
mATX, otherATXs := marryIDs(t, atxHandler, signers[:2], golden)
previousATXs := []types.ATXID{mATX.ID()}
equivocationSet := []types.NodeID{sig.NodeID()}
for _, atx := range otherATXs {
previousATXs = append(previousATXs, atx.ID())
equivocationSet = append(equivocationSet, atx.SmesherID)
}

// Process a merged ATX
Expand Down Expand Up @@ -841,12 +840,10 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) {
atxHandler := newV2TestHandler(t, golden)

// Marry IDs
mATX, otherATXs := marryIDs(t, atxHandler, sig, golden, 1)
mATX, otherATXs := marryIDs(t, atxHandler, signers[:2], golden)
previousATXs := []types.ATXID{mATX.ID()}
equivocationSet := []types.NodeID{sig.NodeID()}
for _, atx := range otherATXs {
previousATXs = append(previousATXs, atx.ID())
equivocationSet = append(equivocationSet, atx.SmesherID)
}

// Process a merged ATX
Expand Down Expand Up @@ -874,11 +871,7 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) {
atxHandler := newV2TestHandler(t, golden)

// Marry IDs
mATX, otherATXs := marryIDs(t, atxHandler, sig, golden, 1)
equivocationSet := []types.NodeID{sig.NodeID()}
for _, atx := range otherATXs {
equivocationSet = append(equivocationSet, atx.SmesherID)
}
mATX, _ := marryIDs(t, atxHandler, signers, golden)

prev := atxs.CheckpointAtx{
Epoch: mATX.PublishEpoch + 1,
Expand Down Expand Up @@ -940,6 +933,104 @@ func TestHandlerV2_ProcessMergedATX(t *testing.T) {
require.Error(t, err)
require.Nil(t, p)
})
t.Run("publishing two merged ATXs by one marriage set is malfeasance", func(t *testing.T) {
poszu marked this conversation as resolved.
Show resolved Hide resolved
atxHandler := newV2TestHandler(t, golden)

// Marry 4 IDs
mATX, otherATXs := marryIDs(t, atxHandler, signers, golden)
previousATXs := []types.ATXID{mATX.ID()}
for _, atx := range otherATXs {
previousATXs = append(previousATXs, atx.ID())
}

// Process a merged ATX for 2 IDs
merged := newSoloATXv2(t, mATX.PublishEpoch+2, mATX.ID(), mATX.ID())
merged.NiPosts[0].Posts = []wire.SubPostV2{}
for i := range equivocationSet[:2] {
post := wire.SubPostV2{
MarriageIndex: uint32(i),
PrevATXIndex: uint32(i),
NumUnits: 4,
}
merged.NiPosts[0].Posts = append(merged.NiPosts[0].Posts, post)
}

mATXID := mATX.ID()

merged.MarriageATX = &mATXID
merged.PreviousATXs = []types.ATXID{mATX.ID(), otherATXs[0].ID()}
merged.Sign(sig)

atxHandler.expectMergedAtxV2(merged, equivocationSet, []uint64{100})
for _, id := range equivocationSet {
atxHandler.mtortoise.EXPECT().OnMalfeasance(id)
}
p, err := atxHandler.processATX(context.Background(), "", merged, time.Now())
require.NoError(t, err)
require.Nil(t, p)

// Process a second merged ATX for the same equivocation set, but different IDs
merged = newSoloATXv2(t, mATX.PublishEpoch+2, mATX.ID(), mATX.ID())
merged.NiPosts[0].Posts = []wire.SubPostV2{}
for i := range equivocationSet[:2] {
post := wire.SubPostV2{
MarriageIndex: uint32(i + 2),
PrevATXIndex: uint32(i),
NumUnits: 4,
}
merged.NiPosts[0].Posts = append(merged.NiPosts[0].Posts, post)
}

mATXID = mATX.ID()
merged.MarriageATX = &mATXID
merged.PreviousATXs = []types.ATXID{otherATXs[1].ID(), otherATXs[2].ID()}
merged.Sign(signers[2])

atxHandler.expectMergedAtxV2(merged, equivocationSet, []uint64{100})
p, err = atxHandler.processATX(context.Background(), "", merged, time.Now())
require.NoError(t, err)
require.NotNil(t, p)
})
t.Run("publishing two merged ATXs (one checkpointed)", func(t *testing.T) {
atxHandler := newV2TestHandler(t, golden)

mATX, otherATXs := marryIDs(t, atxHandler, signers, golden)
mATXID := mATX.ID()

// Insert checkpointed merged ATX
checkpointedATX := &atxs.CheckpointAtx{
Epoch: mATX.PublishEpoch + 2,
ID: types.RandomATXID(),
SmesherID: signers[0].NodeID(),
MarriageATX: &mATXID,
}
require.NoError(t, atxs.AddCheckpointed(atxHandler.cdb, checkpointedATX))

// create and process another merged ATX
merged := newSoloATXv2(t, checkpointedATX.Epoch, mATX.ID(), golden)
merged.NiPosts[0].Posts = []wire.SubPostV2{}
for i := range equivocationSet[2:] {
post := wire.SubPostV2{
MarriageIndex: uint32(i + 2),
PrevATXIndex: uint32(i),
NumUnits: 4,
}
merged.NiPosts[0].Posts = append(merged.NiPosts[0].Posts, post)
}

merged.MarriageATX = &mATXID
merged.PreviousATXs = []types.ATXID{otherATXs[1].ID(), otherATXs[2].ID()}
merged.Sign(signers[2])
atxHandler.expectMergedAtxV2(merged, equivocationSet, []uint64{100})
for _, id := range equivocationSet {
atxHandler.mtortoise.EXPECT().OnMalfeasance(id)
}
// TODO: this could be syntactically validated as all nodes in the network
// should already have the checkpointed merged ATX.
p, err := atxHandler.processATX(context.Background(), "", merged, time.Now())
require.NoError(t, err)
require.NotNil(t, p)
})
}

func TestCollectDeps_AtxV2(t *testing.T) {
Expand Down
4 changes: 4 additions & 0 deletions checkpoint/recovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,10 @@ func checkpointData(fs afero.Fs, file string, newGenesis types.LayerID) (*recove
cAtx.ID = types.ATXID(types.BytesToHash(atx.ID))
cAtx.Epoch = types.EpochID(atx.Epoch)
cAtx.CommitmentATX = types.ATXID(types.BytesToHash(atx.CommitmentAtx))
if len(atx.MarriageAtx) == 32 {
marriageATXID := types.ATXID(atx.MarriageAtx)
cAtx.MarriageATX = &marriageATXID
}
cAtx.SmesherID = types.BytesToNodeID(atx.PublicKey)
cAtx.NumUnits = atx.NumUnits
cAtx.VRFNonce = types.VRFPostIndex(atx.VrfNonce)
Expand Down
3 changes: 2 additions & 1 deletion checkpoint/recovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,8 @@ func TestRecover_OwnAtxInCheckpoint(t *testing.T) {
require.NoError(t, err)
atxid, err := hex.DecodeString("98e47278c1f58acfd2b670a730f28898f74eb140482a07b91ff81f9ff0b7d9f4")
require.NoError(t, err)
atx := newAtx(types.ATXID(atxid), types.EmptyATXID, nil, 3, 1, 0, nid)
atx := &types.ActivationTx{SmesherID: types.NodeID(nid)}
atx.SetID(types.ATXID(atxid))

cfg := &checkpoint.RecoverConfig{
GoldenAtx: goldenAtx,
Expand Down
5 changes: 5 additions & 0 deletions checkpoint/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,15 @@ func checkpointDB(
if mal, ok := malicious[catx.SmesherID]; ok && mal {
continue
}
var marriageAtx []byte
if catx.MarriageATX != nil {
marriageAtx = catx.MarriageATX.Bytes()
}
checkpoint.Data.Atxs = append(checkpoint.Data.Atxs, types.AtxSnapshot{
ID: catx.ID.Bytes(),
Epoch: catx.Epoch.Uint32(),
CommitmentAtx: catx.CommitmentATX.Bytes(),
MarriageAtx: marriageAtx,
VrfNonce: uint64(catx.VRFNonce),
NumUnits: catx.NumUnits,
BaseTickHeight: catx.BaseTickHeight,
Expand Down
Loading
Loading