From 49fc95785ed1d8d8e5f30d2688700ad54bbccbef Mon Sep 17 00:00:00 2001 From: ylembachar Date: Fri, 13 Feb 2026 16:11:18 +0100 Subject: [PATCH 1/4] fix: order fired triggers by eon, identity --- .../shutterservice/database/sql/queries/shutterservice.sql | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rolling-shutter/keyperimpl/shutterservice/database/sql/queries/shutterservice.sql b/rolling-shutter/keyperimpl/shutterservice/database/sql/queries/shutterservice.sql index 29dc9e8a3..7575de362 100644 --- a/rolling-shutter/keyperimpl/shutterservice/database/sql/queries/shutterservice.sql +++ b/rolling-shutter/keyperimpl/shutterservice/database/sql/queries/shutterservice.sql @@ -143,4 +143,7 @@ WHERE NOT EXISTS ( -- not decrypted yet WHERE e.identity_prefix = f.identity_prefix AND e.sender = f.sender AND e.decrypted = true -); +) +ORDER BY + e.eon ASC, + e.identity ASC; \ No newline at end of file From ff3d47c9f84b557e03f95b3a3ee1549afa9cee06 Mon Sep 17 00:00:00 2001 From: ylembachar Date: Fri, 13 Feb 2026 16:41:41 +0100 Subject: [PATCH 2/4] chore: regenerate query bindings after trigger ordering change --- .../shutterservice/database/shutterservice.sqlc.gen.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rolling-shutter/keyperimpl/shutterservice/database/shutterservice.sqlc.gen.go b/rolling-shutter/keyperimpl/shutterservice/database/shutterservice.sqlc.gen.go index 2900b9b22..2b904070d 100644 --- a/rolling-shutter/keyperimpl/shutterservice/database/shutterservice.sqlc.gen.go +++ b/rolling-shutter/keyperimpl/shutterservice/database/shutterservice.sqlc.gen.go @@ -215,6 +215,9 @@ WHERE NOT EXISTS ( -- not decrypted yet AND e.sender = f.sender AND e.decrypted = true ) +ORDER BY + e.eon ASC, + e.identity ASC ` type GetUndecryptedFiredTriggersRow struct { From a1d4c4be4ac17ebf50120de8ffc2e0159b07e5ad Mon Sep 17 00:00:00 2001 From: ylembachar Date: Fri, 20 Feb 2026 12:56:03 +0100 Subject: [PATCH 3/4] test: add unit test to verify key share ordering --- .../shutterservice/newblock_test.go | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/rolling-shutter/keyperimpl/shutterservice/newblock_test.go b/rolling-shutter/keyperimpl/shutterservice/newblock_test.go index 1379f83df..15ddea01a 100644 --- a/rolling-shutter/keyperimpl/shutterservice/newblock_test.go +++ b/rolling-shutter/keyperimpl/shutterservice/newblock_test.go @@ -1,6 +1,7 @@ package shutterservice import ( + "bytes" "context" "math" "math/big" @@ -9,8 +10,11 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/jackc/pgx/v4/pgxpool" + pubsub "github.com/libp2p/go-libp2p-pubsub" "gotest.tools/assert" + obskeyper "github.com/shutter-network/rolling-shutter/rolling-shutter/chainobserver/db/keyper" corekeyperdatabase "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/database" "github.com/shutter-network/rolling-shutter/rolling-shutter/keyper/epochkghandler" servicedatabase "github.com/shutter-network/rolling-shutter/rolling-shutter/keyperimpl/shutterservice/database" @@ -405,3 +409,137 @@ func TestShouldNotTriggerDecryptionBeforeActivation(t *testing.T) { assert.NilError(t, err) assert.Equal(t, trigger, false) } + +func setupEventBasedOrderingTest( + ctx context.Context, + t *testing.T, + dbpool *pgxpool.Pool, +) (*Keyper, *servicedatabase.Queries, int64) { + t.Helper() + + const keyperIndex = uint64(1) + testsetup.InitializeEon(ctx, t, dbpool, config, keyperIndex) + + eon := config.GetEon() + if eon > math.MaxInt64 { + t.Fatalf("Eon is too large: %d", eon) + } + eonInt64 := int64(eon) + + privateKey, sender, err := generateRandomAccount() + assert.NilError(t, err) + + kpr := &Keyper{ + dbpool: dbpool, + config: &Config{ + Chain: &ChainConfig{ + Node: &configuration.EthnodeConfig{ + PrivateKey: &keys.ECDSAPrivate{Key: privateKey}, + }, + }, + }, + } + + err = obskeyper.New(dbpool).InsertKeyperSet(ctx, obskeyper.InsertKeyperSetParams{ + KeyperConfigIndex: 1, + ActivationBlockNumber: 0, + Keypers: []string{sender.Hex()}, + Threshold: 1, + }) + assert.NilError(t, err) + + return kpr, servicedatabase.New(dbpool), eonInt64 +} + +func TestFiredTriggersProducesOrderedShares(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := context.Background() + dbpool, dbclose := testsetup.NewTestDBPool(ctx, t, servicedatabase.Definition) + t.Cleanup(dbclose) + + kpr, serviceDB, eon := setupEventBasedOrderingTest(ctx, t, dbpool) + + type row struct { + identity byte + prefix byte + sender string + } + + inserted := []row{ + {identity: 0x04, prefix: 0x14, sender: "0x0000000000000000000000000000000000000011"}, + {identity: 0x03, prefix: 0x13, sender: "0x0000000000000000000000000000000000000011"}, + {identity: 0x02, prefix: 0x12, sender: "0x0000000000000000000000000000000000000011"}, + {identity: 0x01, prefix: 0x11, sender: "0x0000000000000000000000000000000000000011"}, + {identity: 0x05, prefix: 0x15, sender: "0x0000000000000000000000000000000000000011"}, + } + + for i, r := range inserted { + _, err := serviceDB.InsertEventTriggerRegisteredEvent(ctx, servicedatabase.InsertEventTriggerRegisteredEventParams{ + BlockNumber: int64(100 + i), + BlockHash: []byte{byte(100 + i)}, + TxIndex: 0, + LogIndex: 0, + Eon: eon, + IdentityPrefix: b32(r.prefix), + Sender: r.sender, + Definition: []byte{0x01}, + ExpirationBlockNumber: 10_000, + Identity: b32(r.identity), + }) + assert.NilError(t, err) + + err = serviceDB.InsertFiredTrigger(ctx, servicedatabase.InsertFiredTriggerParams{ + Eon: eon, + IdentityPrefix: b32(r.prefix), + Sender: r.sender, + BlockNumber: int64(200 + i), + BlockHash: []byte{byte(200 + i)}, + TxIndex: 0, + LogIndex: 0, + }) + assert.NilError(t, err) + } + + triggers, err := kpr.prepareEventBasedTriggers(ctx) + assert.NilError(t, err) + assert.Equal(t, len(triggers), 1) + assert.Equal(t, len(triggers[0].IdentityPreimages), len(inserted)) + for i := 1; i < len(triggers[0].IdentityPreimages); i++ { + assert.Assert(t, bytes.Compare( + triggers[0].IdentityPreimages[i-1], + triggers[0].IdentityPreimages[i], + ) < 0) + } + + coreDB := corekeyperdatabase.New(dbpool) + triggerBlockNumber := triggers[0].BlockNumber + if triggerBlockNumber > math.MaxInt64 { + t.Fatalf("BlockNumber is too large: %d", triggerBlockNumber) + } + + triggerEon, err := coreDB.GetEonForBlockNumber(ctx, int64(triggerBlockNumber)) + assert.NilError(t, err) + + keyShareHandler := &epochkghandler.KeyShareHandler{ + InstanceID: config.GetInstanceID(), + KeyperAddress: config.GetAddress(), + MaxNumKeysPerMessage: config.GetMaxNumKeysPerMessage(), + DBPool: dbpool, + } + msg, err := keyShareHandler.ConstructDecryptionKeyShares(ctx, triggerEon, triggers[0].IdentityPreimages) + assert.NilError(t, err) + + validator := epochkghandler.NewDecryptionKeyShareHandler(config, dbpool) + res, err := validator.ValidateMessage(ctx, msg) + assert.Equal(t, res, pubsub.ValidationAccept) + assert.NilError(t, err) +} + +func b32(last byte) []byte { + b := make([]byte, 32) + b[31] = last + return b +} From 829f885f7990003014f08832498e5dedf41043db Mon Sep 17 00:00:00 2001 From: ylembachar Date: Fri, 20 Feb 2026 16:16:42 +0100 Subject: [PATCH 4/4] fix: sort event trigger identities in code --- .../shutterservice/database/shutterservice.sqlc.gen.go | 3 --- .../shutterservice/database/sql/queries/shutterservice.sql | 5 +---- rolling-shutter/keyperimpl/shutterservice/newblock.go | 5 ++++- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/rolling-shutter/keyperimpl/shutterservice/database/shutterservice.sqlc.gen.go b/rolling-shutter/keyperimpl/shutterservice/database/shutterservice.sqlc.gen.go index 2b904070d..2900b9b22 100644 --- a/rolling-shutter/keyperimpl/shutterservice/database/shutterservice.sqlc.gen.go +++ b/rolling-shutter/keyperimpl/shutterservice/database/shutterservice.sqlc.gen.go @@ -215,9 +215,6 @@ WHERE NOT EXISTS ( -- not decrypted yet AND e.sender = f.sender AND e.decrypted = true ) -ORDER BY - e.eon ASC, - e.identity ASC ` type GetUndecryptedFiredTriggersRow struct { diff --git a/rolling-shutter/keyperimpl/shutterservice/database/sql/queries/shutterservice.sql b/rolling-shutter/keyperimpl/shutterservice/database/sql/queries/shutterservice.sql index 7575de362..29dc9e8a3 100644 --- a/rolling-shutter/keyperimpl/shutterservice/database/sql/queries/shutterservice.sql +++ b/rolling-shutter/keyperimpl/shutterservice/database/sql/queries/shutterservice.sql @@ -143,7 +143,4 @@ WHERE NOT EXISTS ( -- not decrypted yet WHERE e.identity_prefix = f.identity_prefix AND e.sender = f.sender AND e.decrypted = true -) -ORDER BY - e.eon ASC, - e.identity ASC; \ No newline at end of file +); diff --git a/rolling-shutter/keyperimpl/shutterservice/newblock.go b/rolling-shutter/keyperimpl/shutterservice/newblock.go index 65b86eb71..174a5063a 100644 --- a/rolling-shutter/keyperimpl/shutterservice/newblock.go +++ b/rolling-shutter/keyperimpl/shutterservice/newblock.go @@ -234,10 +234,13 @@ func (kpr *Keyper) prepareEventBasedTriggers(ctx context.Context) ([]epochkghand identities = append(identities, firedTrigger.Identity) } + sortedIdentityPreimages := sortIdentityPreimages(identities) + decryptionTrigger := epochkghandler.DecryptionTrigger{ BlockNumber: uint64(eonStruct.ActivationBlockNumber), - IdentityPreimages: identities, + IdentityPreimages: sortedIdentityPreimages, } + decryptionTriggers = append(decryptionTriggers, decryptionTrigger) } return decryptionTriggers, nil