diff --git a/CHANGELOG.md b/CHANGELOG.md index d7e352d816..9958920db6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,13 @@ See [RELEASE](./RELEASE.md) for workflow instructions. ### Improvements +* [#5199](https://github.com/spacemeshos/go-spacemesh/pull/5199) Adds smesherID to rewards table. Historically rewards + were keyed by (coinbase, layer). Now the primary key has changed to (smesherID, layer), which allows querying rewards + by any subset of layer, smesherID, and coinbase. While this change does add smesherID to existing API endpoints + (`GlobalStateService.{AccountDataQuery,AccountDataStream,GlobalStateStream}`), it does not yet expose an endpoint to + query rewards by smesherID. Additionally, it does not re-index old data. Rewards will contain smesherID going forward, + but to refresh data for all rewards, a node will have to delete its database and resync from genesis. + ## 1.3.0 ### Upgrade information diff --git a/api/grpcserver/globalstate_service.go b/api/grpcserver/globalstate_service.go index e0b5983826..7acf61404b 100644 --- a/api/grpcserver/globalstate_service.go +++ b/api/grpcserver/globalstate_service.go @@ -143,7 +143,7 @@ func (s GlobalStateService) AccountDataQuery( // if filterTxReceipt {} if filterReward { - dbRewards, err := s.mesh.GetRewards(addr) + dbRewards, err := s.mesh.GetRewardsByCoinbase(addr) if err != nil { return nil, status.Errorf(codes.Internal, "error getting rewards data") } @@ -153,10 +153,8 @@ func (s GlobalStateService) AccountDataQuery( Layer: &pb.LayerNumber{Number: r.Layer.Uint32()}, Total: &pb.Amount{Value: r.TotalReward}, LayerReward: &pb.Amount{Value: r.LayerReward}, - // Leave this out for now as this is changing - // See https://github.com/spacemeshos/go-spacemesh/issues/2275 - // LayerComputed: 0, - Coinbase: &pb.AccountId{Address: addr.String()}, + Coinbase: &pb.AccountId{Address: addr.String()}, + Smesher: &pb.SmesherId{Id: r.SmesherID[:]}, }, }}) } @@ -291,10 +289,8 @@ func (s GlobalStateService) AccountDataStream( Layer: &pb.LayerNumber{Number: reward.Layer.Uint32()}, Total: &pb.Amount{Value: reward.Total}, LayerReward: &pb.Amount{Value: reward.LayerReward}, - // Leave this out for now as this is changing - // See https://github.com/spacemeshos/go-spacemesh/issues/2275 - // LayerComputed: 0, - Coinbase: &pb.AccountId{Address: addr.String()}, + Coinbase: &pb.AccountId{Address: addr.String()}, + Smesher: &pb.SmesherId{Id: reward.SmesherID[:]}, }, }}} if err := stream.Send(resp); err != nil { @@ -424,10 +420,8 @@ func (s GlobalStateService) GlobalStateStream( Layer: &pb.LayerNumber{Number: reward.Layer.Uint32()}, Total: &pb.Amount{Value: reward.Total}, LayerReward: &pb.Amount{Value: reward.LayerReward}, - // Leave this out for now as this is changing - // See https://github.com/spacemeshos/go-spacemesh/issues/2275 - // LayerComputed: 0, - Coinbase: &pb.AccountId{Address: reward.Coinbase.String()}, + Coinbase: &pb.AccountId{Address: reward.Coinbase.String()}, + Smesher: &pb.SmesherId{Id: reward.SmesherID[:]}, }, }}} if err := stream.Send(resp); err != nil { diff --git a/api/grpcserver/globalstate_service_test.go b/api/grpcserver/globalstate_service_test.go index 4612c3e46e..0814d45e6a 100644 --- a/api/grpcserver/globalstate_service_test.go +++ b/api/grpcserver/globalstate_service_test.go @@ -100,7 +100,7 @@ func TestGlobalStateService(t *testing.T) { t.Parallel() c, ctx := setupGlobalStateService(t) - c.meshAPI.EXPECT().GetRewards(addr1).Return([]*types.Reward{ + c.meshAPI.EXPECT().GetRewardsByCoinbase(addr1).Return([]*types.Reward{ { Layer: layerFirst, TotalReward: rewardAmount, @@ -130,7 +130,7 @@ func TestGlobalStateService(t *testing.T) { t.Parallel() c, ctx := setupGlobalStateService(t) - c.meshAPI.EXPECT().GetRewards(addr1).Return([]*types.Reward{ + c.meshAPI.EXPECT().GetRewardsByCoinbase(addr1).Return([]*types.Reward{ { Layer: layerFirst, TotalReward: rewardAmount, @@ -159,12 +159,13 @@ func TestGlobalStateService(t *testing.T) { t.Parallel() c, ctx := setupGlobalStateService(t) - c.meshAPI.EXPECT().GetRewards(addr1).Return([]*types.Reward{ + c.meshAPI.EXPECT().GetRewardsByCoinbase(addr1).Return([]*types.Reward{ { Layer: layerFirst, TotalReward: rewardAmount, LayerReward: rewardAmount, Coinbase: addr1, + SmesherID: rewardSmesherID, }, }, nil) c.conStateAPI.EXPECT().GetBalance(addr1).Return(accountBalance, nil) @@ -188,12 +189,13 @@ func TestGlobalStateService(t *testing.T) { t.Parallel() c, ctx := setupGlobalStateService(t) - c.meshAPI.EXPECT().GetRewards(addr1).Return([]*types.Reward{ + c.meshAPI.EXPECT().GetRewardsByCoinbase(addr1).Return([]*types.Reward{ { Layer: layerFirst, TotalReward: rewardAmount, LayerReward: rewardAmount, Coinbase: addr1, + SmesherID: rewardSmesherID, }, }, nil) c.conStateAPI.EXPECT().GetBalance(addr1).Return(accountBalance, nil) diff --git a/api/grpcserver/grpcserver_test.go b/api/grpcserver/grpcserver_test.go index cfcf3ae356..f5e909d0f0 100644 --- a/api/grpcserver/grpcserver_test.go +++ b/api/grpcserver/grpcserver_test.go @@ -78,23 +78,24 @@ var ( postGenesisEpoch = types.EpochID(2) genesisID = types.Hash20{} - addr1 types.Address - addr2 types.Address - prevAtxID = types.ATXID(types.HexToHash32("44444")) - chlng = types.HexToHash32("55555") - poetRef = []byte("66666") - nipost = newNIPostWithChallenge(&chlng, poetRef) - challenge = newChallenge(1, prevAtxID, prevAtxID, postGenesisEpoch) - globalAtx *types.VerifiedActivationTx - globalAtx2 *types.VerifiedActivationTx - globalTx *types.Transaction - globalTx2 *types.Transaction - ballot1 = genLayerBallot(types.LayerID(11)) - block1 = genLayerBlock(types.LayerID(11), nil) - block2 = genLayerBlock(types.LayerID(11), nil) - block3 = genLayerBlock(types.LayerID(11), nil) - meshAPIMock = &MeshAPIMock{} - conStateAPI = &ConStateAPIMock{ + addr1 types.Address + addr2 types.Address + rewardSmesherID = types.RandomNodeID() + prevAtxID = types.ATXID(types.HexToHash32("44444")) + chlng = types.HexToHash32("55555") + poetRef = []byte("66666") + nipost = newNIPostWithChallenge(&chlng, poetRef) + challenge = newChallenge(1, prevAtxID, prevAtxID, postGenesisEpoch) + globalAtx *types.VerifiedActivationTx + globalAtx2 *types.VerifiedActivationTx + globalTx *types.Transaction + globalTx2 *types.Transaction + ballot1 = genLayerBallot(types.LayerID(11)) + block1 = genLayerBlock(types.LayerID(11), nil) + block2 = genLayerBlock(types.LayerID(11), nil) + block3 = genLayerBlock(types.LayerID(11), nil) + meshAPIMock = &MeshAPIMock{} + conStateAPI = &ConStateAPIMock{ returnTx: make(map[types.TransactionID]*types.Transaction), layerApplied: make(map[types.TransactionID]*types.LayerID), balances: make(map[types.Address]*big.Int), @@ -260,13 +261,26 @@ func (m *MeshAPIMock) ProcessedLayer() types.LayerID { return layerVerified } -func (m *MeshAPIMock) GetRewards(types.Address) (rewards []*types.Reward, err error) { +func (m *MeshAPIMock) GetRewardsByCoinbase(types.Address) (rewards []*types.Reward, err error) { return []*types.Reward{ { Layer: layerFirst, TotalReward: rewardAmount, LayerReward: rewardAmount, Coinbase: addr1, + SmesherID: rewardSmesherID, + }, + }, nil +} + +func (m *MeshAPIMock) GetRewardsBySmesherId(types.NodeID) (rewards []*types.Reward, err error) { + return []*types.Reward{ + { + Layer: layerFirst, + TotalReward: rewardAmount, + LayerReward: rewardAmount, + Coinbase: addr1, + SmesherID: rewardSmesherID, }, }, nil } @@ -1728,6 +1742,7 @@ func TestAccountDataStream_comprehensive(t *testing.T) { Total: rewardAmount, LayerReward: rewardAmount * 2, Coinbase: addr1, + SmesherID: rewardSmesherID, }) res, err := stream.Recv() @@ -1783,6 +1798,7 @@ func TestGlobalStateStream_comprehensive(t *testing.T) { Total: rewardAmount, LayerReward: rewardAmount * 2, Coinbase: addr1, + SmesherID: rewardSmesherID, }) res, err := stream.Recv() require.NoError(t, err, "got error from stream") @@ -1894,7 +1910,7 @@ func checkAccountDataQueryItemReward(t *testing.T, dataItem any) { require.Equal(t, uint64(rewardAmount), x.Reward.Total.Value) require.Equal(t, uint64(rewardAmount), x.Reward.LayerReward.Value) require.Equal(t, addr1.String(), x.Reward.Coinbase.Address) - require.Nil(t, x.Reward.Smesher) + require.Equal(t, rewardSmesherID.Bytes(), x.Reward.Smesher.Id) } func checkAccountMeshDataItemTx(t *testing.T, dataItem any) { @@ -1925,6 +1941,7 @@ func checkAccountDataItemReward(t *testing.T, dataItem any) { require.Equal(t, layerFirst.Uint32(), x.Reward.Layer.Number) require.Equal(t, uint64(rewardAmount*2), x.Reward.LayerReward.Value) require.Equal(t, addr1.String(), x.Reward.Coinbase.Address) + require.Equal(t, rewardSmesherID.Bytes(), x.Reward.Smesher.Id) } func checkAccountDataItemAccount(t *testing.T, dataItem any) { @@ -1946,6 +1963,7 @@ func checkGlobalStateDataReward(t *testing.T, dataItem any) { require.Equal(t, layerFirst.Uint32(), x.Reward.Layer.Number) require.Equal(t, uint64(rewardAmount*2), x.Reward.LayerReward.Value) require.Equal(t, addr1.String(), x.Reward.Coinbase.Address) + require.Equal(t, rewardSmesherID.Bytes(), x.Reward.Smesher.Id) } func checkGlobalStateDataAccountWrapper(t *testing.T, dataItem any) { diff --git a/api/grpcserver/interface.go b/api/grpcserver/interface.go index 033da8f5e9..e938c24435 100644 --- a/api/grpcserver/interface.go +++ b/api/grpcserver/interface.go @@ -79,7 +79,8 @@ type genesisTimeAPI interface { type meshAPI interface { GetATXs(context.Context, []types.ATXID) (map[types.ATXID]*types.VerifiedActivationTx, []types.ATXID) GetLayer(types.LayerID) (*types.Layer, error) - GetRewards(types.Address) ([]*types.Reward, error) + GetRewardsByCoinbase(types.Address) ([]*types.Reward, error) + GetRewardsBySmesherId(id types.NodeID) ([]*types.Reward, error) LatestLayer() types.LayerID LatestLayerInState() types.LayerID ProcessedLayer() types.LayerID diff --git a/api/grpcserver/mocks.go b/api/grpcserver/mocks.go index 088b026228..29f628f595 100644 --- a/api/grpcserver/mocks.go +++ b/api/grpcserver/mocks.go @@ -1368,41 +1368,80 @@ func (c *meshAPIGetLayerCall) DoAndReturn(f func(types.LayerID) (*types.Layer, e return c } -// GetRewards mocks base method. -func (m *MockmeshAPI) GetRewards(arg0 types.Address) ([]*types.Reward, error) { +// GetRewardsByCoinbase mocks base method. +func (m *MockmeshAPI) GetRewardsByCoinbase(arg0 types.Address) ([]*types.Reward, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRewards", arg0) + ret := m.ctrl.Call(m, "GetRewardsByCoinbase", arg0) ret0, _ := ret[0].([]*types.Reward) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetRewards indicates an expected call of GetRewards. -func (mr *MockmeshAPIMockRecorder) GetRewards(arg0 any) *meshAPIGetRewardsCall { +// GetRewardsByCoinbase indicates an expected call of GetRewardsByCoinbase. +func (mr *MockmeshAPIMockRecorder) GetRewardsByCoinbase(arg0 any) *meshAPIGetRewardsByCoinbaseCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRewards", reflect.TypeOf((*MockmeshAPI)(nil).GetRewards), arg0) - return &meshAPIGetRewardsCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRewardsByCoinbase", reflect.TypeOf((*MockmeshAPI)(nil).GetRewardsByCoinbase), arg0) + return &meshAPIGetRewardsByCoinbaseCall{Call: call} } -// meshAPIGetRewardsCall wrap *gomock.Call -type meshAPIGetRewardsCall struct { +// meshAPIGetRewardsByCoinbaseCall wrap *gomock.Call +type meshAPIGetRewardsByCoinbaseCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *meshAPIGetRewardsCall) Return(arg0 []*types.Reward, arg1 error) *meshAPIGetRewardsCall { +func (c *meshAPIGetRewardsByCoinbaseCall) Return(arg0 []*types.Reward, arg1 error) *meshAPIGetRewardsByCoinbaseCall { c.Call = c.Call.Return(arg0, arg1) return c } // Do rewrite *gomock.Call.Do -func (c *meshAPIGetRewardsCall) Do(f func(types.Address) ([]*types.Reward, error)) *meshAPIGetRewardsCall { +func (c *meshAPIGetRewardsByCoinbaseCall) Do(f func(types.Address) ([]*types.Reward, error)) *meshAPIGetRewardsByCoinbaseCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *meshAPIGetRewardsCall) DoAndReturn(f func(types.Address) ([]*types.Reward, error)) *meshAPIGetRewardsCall { +func (c *meshAPIGetRewardsByCoinbaseCall) DoAndReturn(f func(types.Address) ([]*types.Reward, error)) *meshAPIGetRewardsByCoinbaseCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// GetRewardsBySmesherId mocks base method. +func (m *MockmeshAPI) GetRewardsBySmesherId(id types.NodeID) ([]*types.Reward, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRewardsBySmesherId", id) + ret0, _ := ret[0].([]*types.Reward) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRewardsBySmesherId indicates an expected call of GetRewardsBySmesherId. +func (mr *MockmeshAPIMockRecorder) GetRewardsBySmesherId(id any) *meshAPIGetRewardsBySmesherIdCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRewardsBySmesherId", reflect.TypeOf((*MockmeshAPI)(nil).GetRewardsBySmesherId), id) + return &meshAPIGetRewardsBySmesherIdCall{Call: call} +} + +// meshAPIGetRewardsBySmesherIdCall wrap *gomock.Call +type meshAPIGetRewardsBySmesherIdCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *meshAPIGetRewardsBySmesherIdCall) Return(arg0 []*types.Reward, arg1 error) *meshAPIGetRewardsBySmesherIdCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *meshAPIGetRewardsBySmesherIdCall) Do(f func(types.NodeID) ([]*types.Reward, error)) *meshAPIGetRewardsBySmesherIdCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *meshAPIGetRewardsBySmesherIdCall) DoAndReturn(f func(types.NodeID) ([]*types.Reward, error)) *meshAPIGetRewardsBySmesherIdCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/cmd/bootstrapper/generator_test.go b/cmd/bootstrapper/generator_test.go index 72b03841b2..b15a5526c3 100644 --- a/cmd/bootstrapper/generator_test.go +++ b/cmd/bootstrapper/generator_test.go @@ -15,6 +15,7 @@ import ( pb "github.com/spacemeshos/api/release/go/spacemesh/v1" "github.com/spf13/afero" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "go.uber.org/zap/zaptest" "github.com/spacemeshos/go-spacemesh/api/grpcserver" @@ -44,22 +45,6 @@ var bitcoinResponse1 string //go:embed bitcoinResponse2.json var bitcoinResponse2 string -type MeshAPIMock struct{} - -func (m *MeshAPIMock) LatestLayer() types.LayerID { panic("not implemented") } -func (m *MeshAPIMock) LatestLayerInState() types.LayerID { panic("not implemented") } -func (m *MeshAPIMock) ProcessedLayer() types.LayerID { panic("not implemented") } -func (m *MeshAPIMock) GetRewards(types.Address) ([]*types.Reward, error) { panic("not implemented") } -func (m *MeshAPIMock) GetLayer(types.LayerID) (*types.Layer, error) { panic("not implemented") } - -func (m *MeshAPIMock) GetATXs( - context.Context, - []types.ATXID, -) (map[types.ATXID]*types.VerifiedActivationTx, []types.ATXID) { - panic("not implemented") -} -func (m *MeshAPIMock) MeshHash(types.LayerID) (types.Hash32, error) { panic("not implemented") } - func createAtxs(tb testing.TB, db sql.Executor, epoch types.EpochID, atxids []types.ATXID) { for _, id := range atxids { atx := &types.ActivationTx{InnerActivationTx: types.InnerActivationTx{ @@ -82,7 +67,8 @@ func launchServer(tb testing.TB, cdb *datastore.CachedDB) (grpcserver.Config, fu cfg := grpcserver.DefaultTestConfig() grpcService := grpcserver.New("127.0.0.1:0", zaptest.NewLogger(tb).Named("grpc"), cfg) jsonService := grpcserver.NewJSONHTTPServer("127.0.0.1:0", zaptest.NewLogger(tb).Named("grpc.JSON")) - s := grpcserver.NewMeshService(cdb, &MeshAPIMock{}, nil, nil, 0, types.Hash20{}, 0, 0, 0) + s := grpcserver.NewMeshService(cdb, grpcserver.NewMockmeshAPI(gomock.NewController(tb)), nil, nil, + 0, types.Hash20{}, 0, 0, 0) pb.RegisterMeshServiceServer(grpcService.GrpcServer, s) // start gRPC and json servers diff --git a/common/types/block.go b/common/types/block.go index ca866dec53..050c5a71bb 100644 --- a/common/types/block.go +++ b/common/types/block.go @@ -106,10 +106,11 @@ type AnyReward struct { Weight RatNum } -// CoinbaseReward contains the reward information by coinbase, used as an interface to VM. +// CoinbaseReward contains the reward information by coinbase and smesher, used as an interface to VM. type CoinbaseReward struct { - Coinbase Address - Weight RatNum + SmesherID NodeID + Coinbase Address + Weight RatNum } // Initialize calculates and sets the Block's cached blockID. diff --git a/common/types/transaction.go b/common/types/transaction.go index f559767d3c..6b43f60f38 100644 --- a/common/types/transaction.go +++ b/common/types/transaction.go @@ -148,6 +148,7 @@ type Reward struct { TotalReward uint64 LayerReward uint64 Coinbase Address + SmesherID NodeID } // NewRawTx computes id from raw bytes and returns the object. diff --git a/common/types/transaction_scale.go b/common/types/transaction_scale.go index a48ec0cec7..968fbf09c9 100644 --- a/common/types/transaction_scale.go +++ b/common/types/transaction_scale.go @@ -73,6 +73,13 @@ func (t *Reward) EncodeScale(enc *scale.Encoder) (total int, err error) { } total += n } + { + n, err := scale.EncodeByteArray(enc, t.SmesherID[:]) + if err != nil { + return total, err + } + total += n + } return total, nil } @@ -108,6 +115,13 @@ func (t *Reward) DecodeScale(dec *scale.Decoder) (total int, err error) { } total += n } + { + n, err := scale.DecodeByteArray(dec, t.SmesherID[:]) + if err != nil { + return total, err + } + total += n + } return total, nil } diff --git a/events/reporter.go b/events/reporter.go index e62e83af09..f3fe7c9280 100644 --- a/events/reporter.go +++ b/events/reporter.go @@ -387,6 +387,7 @@ type Reward struct { Total uint64 LayerReward uint64 Coinbase types.Address + SmesherID types.NodeID } // Transaction wraps a tx with its layer ID and validity info. diff --git a/genvm/rewards.go b/genvm/rewards.go index bebb22917b..a4c9294e8a 100644 --- a/genvm/rewards.go +++ b/genvm/rewards.go @@ -61,6 +61,7 @@ func (v *VM) addRewards( reward := types.Reward{ Layer: lctx.Layer, Coinbase: blockReward.Coinbase, + SmesherID: blockReward.SmesherID, TotalReward: totalReward.Uint64(), LayerReward: subsidyReward.Uint64(), } diff --git a/genvm/vm.go b/genvm/vm.go index 7b7b9f4aa5..128fc14781 100644 --- a/genvm/vm.go +++ b/genvm/vm.go @@ -267,6 +267,7 @@ func (v *VM) Apply( Total: reward.TotalReward, LayerReward: reward.LayerReward, Coinbase: reward.Coinbase, + SmesherID: reward.SmesherID, }) } diff --git a/genvm/vm_test.go b/genvm/vm_test.go index fd8bc546c1..2982341d44 100644 --- a/genvm/vm_test.go +++ b/genvm/vm_test.go @@ -429,8 +429,11 @@ func (t *tester) rewards(all ...reward) []types.CoinbaseReward { var rst []types.CoinbaseReward for _, rew := range all { rat := new(big.Rat).SetFloat64(rew.share) + address := t.accounts[rew.address].getAddress() rst = append(rst, types.CoinbaseReward{ - Coinbase: t.accounts[rew.address].getAddress(), + Coinbase: address, + // smesherID doesn't matter but must be set. Derive it arbitrarily from the coinbase. + SmesherID: types.BytesToNodeID(address.Bytes()), Weight: types.RatNum{ Num: rat.Num().Uint64(), Denom: rat.Denom().Uint64(), diff --git a/mesh/executor.go b/mesh/executor.go index cb7e22e183..9ac9e59950 100644 --- a/mesh/executor.go +++ b/mesh/executor.go @@ -181,8 +181,9 @@ func (e *Executor) convertRewards(rewards []types.AnyReward) ([]types.CoinbaseRe return nil, fmt.Errorf("exec convert rewards: %w", err) } res = append(res, types.CoinbaseReward{ - Coinbase: atx.Coinbase, - Weight: r.Weight, + SmesherID: atx.NodeID, + Coinbase: atx.Coinbase, + Weight: r.Weight, }) } sort.Slice(res, func(i, j int) bool { diff --git a/mesh/executor_test.go b/mesh/executor_test.go index 1ac09d2ca2..3564252c42 100644 --- a/mesh/executor_test.go +++ b/mesh/executor_test.go @@ -63,7 +63,7 @@ func makeResults(lid types.LayerID, txs ...types.Transaction) []types.Transactio return results } -func createATX(t testing.TB, db sql.Executor, cb types.Address) types.ATXID { +func createATX(t testing.TB, db sql.Executor, cb types.Address) (types.ATXID, types.NodeID) { sig, err := signing.NewEdSigner() require.NoError(t, err) nonce := types.VRFPostIndex(1) @@ -81,7 +81,7 @@ func createATX(t testing.TB, db sql.Executor, cb types.Address) types.ATXID { vAtx, err := atx.Verify(0, 1) require.NoError(t, err) require.NoError(t, atxs.Add(db, vAtx)) - return vAtx.ID() + return vAtx.ID(), sig.NodeID() } func TestExecutor_Execute(t *testing.T) { @@ -128,24 +128,28 @@ func TestExecutor_Execute(t *testing.T) { lid = lid.Add(1) cbs := []types.Address{{1, 2, 3}, {2, 3, 4}} + atxid1, nodeId1 := createATX(t, te.db, cbs[0]) + atxid2, nodeId2 := createATX(t, te.db, cbs[1]) rewards := []types.AnyReward{ { - AtxID: createATX(t, te.db, cbs[0]), + AtxID: atxid1, Weight: types.RatNum{Num: 1, Denom: 3}, }, { - AtxID: createATX(t, te.db, cbs[1]), + AtxID: atxid2, Weight: types.RatNum{Num: 2, Denom: 3}, }, } expRewards := []types.CoinbaseReward{ { - Coinbase: cbs[0], - Weight: rewards[0].Weight, + SmesherID: nodeId1, + Coinbase: cbs[0], + Weight: rewards[0].Weight, }, { - Coinbase: cbs[1], - Weight: rewards[1].Weight, + SmesherID: nodeId2, + Coinbase: cbs[1], + Weight: rewards[1].Weight, }, } sort.Slice(expRewards, func(i, j int) bool { @@ -254,24 +258,28 @@ func TestExecutor_ExecuteOptimistic(t *testing.T) { lid := types.GetEffectiveGenesis() tickHeight := uint64(111) cbs := []types.Address{{1, 2, 3}, {2, 3, 4}} + atxId1, nodeId1 := createATX(t, te.db, cbs[0]) + atxId2, nodeId2 := createATX(t, te.db, cbs[1]) rewards := []types.AnyReward{ { - AtxID: createATX(t, te.db, cbs[0]), + AtxID: atxId1, Weight: types.RatNum{Num: 1, Denom: 3}, }, { - AtxID: createATX(t, te.db, cbs[1]), + AtxID: atxId2, Weight: types.RatNum{Num: 2, Denom: 3}, }, } expRewards := []types.CoinbaseReward{ { - Coinbase: cbs[0], - Weight: rewards[0].Weight, + SmesherID: nodeId1, + Coinbase: cbs[0], + Weight: rewards[0].Weight, }, { - Coinbase: cbs[1], - Weight: rewards[1].Weight, + SmesherID: nodeId2, + Coinbase: cbs[1], + Weight: rewards[1].Weight, }, } sort.Slice(expRewards, func(i, j int) bool { diff --git a/mesh/mesh.go b/mesh/mesh.go index 920ef78b03..4f132dc232 100644 --- a/mesh/mesh.go +++ b/mesh/mesh.go @@ -620,9 +620,14 @@ func (msh *Mesh) GetATXs( return atxs, mIds } -// GetRewards retrieves account's rewards by the coinbase address. -func (msh *Mesh) GetRewards(coinbase types.Address) ([]*types.Reward, error) { - return rewards.List(msh.cdb, coinbase) +// GetRewardsByCoinbase retrieves account's rewards by the coinbase address. +func (msh *Mesh) GetRewardsByCoinbase(coinbase types.Address) ([]*types.Reward, error) { + return rewards.ListByCoinbase(msh.cdb, coinbase) +} + +// GetRewardsBySmesherId retrieves account's rewards by the smesher ID. +func (msh *Mesh) GetRewardsBySmesherId(smesherID types.NodeID) ([]*types.Reward, error) { + return rewards.ListBySmesherId(msh.cdb, smesherID) } // LastVerified returns the latest layer verified by tortoise. diff --git a/sql/database.go b/sql/database.go index 395a2df5b0..a477bfbdc6 100644 --- a/sql/database.go +++ b/sql/database.go @@ -198,13 +198,6 @@ func Open(uri string, opts ...Opt) (*Database, error) { } } } - for i := 0; i < config.connections; i++ { - conn := pool.Get(context.Background()) - if err := registerFunctions(conn); err != nil { - return nil, err - } - pool.Put(conn) - } return db, nil } diff --git a/sql/functions.go b/sql/functions.go deleted file mode 100644 index 07f2c21c85..0000000000 --- a/sql/functions.go +++ /dev/null @@ -1,20 +0,0 @@ -package sql - -import ( - "fmt" - - sqlite "github.com/go-llsqlite/crawshaw" -) - -func registerFunctions(conn *sqlite.Conn) error { - // sqlite doesn't provide native support for uint64, - // it is a problem if we want to sort items using actual uint64 value - // or do arithmetic operations on uint64 in database - // for that we have to add custom functions, another example https://stackoverflow.com/a/8503318 - if err := conn.CreateFunction("add_uint64", true, 2, func(ctx sqlite.Context, values ...sqlite.Value) { - ctx.ResultInt64(int64(uint64(values[0].Int64()) + uint64(values[1].Int64()))) - }, nil, nil); err != nil { - return fmt.Errorf("registering add_uint64: %w", err) - } - return nil -} diff --git a/sql/migrations/state/0008_next.sql b/sql/migrations/state/0008_next.sql new file mode 100644 index 0000000000..77c781b54e --- /dev/null +++ b/sql/migrations/state/0008_next.sql @@ -0,0 +1,15 @@ +DROP INDEX rewards_by_layer; +ALTER TABLE rewards RENAME TO rewards_old; +CREATE TABLE rewards +( + pubkey CHAR(32), + coinbase CHAR(24) NOT NULL, + layer INT NOT NULL, + total_reward UNSIGNED LONG INT, + layer_reward UNSIGNED LONG INT, + PRIMARY KEY (pubkey, layer) +); +CREATE INDEX rewards_by_coinbase ON rewards (coinbase, layer); +CREATE INDEX rewards_by_layer ON rewards (layer asc); +INSERT INTO rewards (coinbase, layer, total_reward, layer_reward) SELECT coinbase, layer, total_reward, layer_reward FROM rewards_old; +DROP TABLE rewards_old; diff --git a/sql/migrations_test.go b/sql/migrations_test.go index f1ff4435b0..31a16c9534 100644 --- a/sql/migrations_test.go +++ b/sql/migrations_test.go @@ -15,5 +15,5 @@ func TestMigrationsAppliedOnce(t *testing.T) { return true }) require.NoError(t, err) - require.Equal(t, version, 7) + require.Equal(t, version, 8) } diff --git a/sql/rewards/rewards.go b/sql/rewards/rewards.go index 5a6b525570..df5ec37b7c 100644 --- a/sql/rewards/rewards.go +++ b/sql/rewards/rewards.go @@ -10,16 +10,13 @@ import ( // Add reward to the database. func Add(db sql.Executor, reward *types.Reward) error { if _, err := db.Exec(` - insert into rewards (coinbase, layer, total_reward, layer_reward) values (?1, ?2, ?3, ?4) - on conflict(coinbase, layer) - do update set - total_reward=add_uint64(total_reward, ?3), - layer_reward=add_uint64(layer_reward, ?4);`, + insert into rewards (pubkey, coinbase, layer, total_reward, layer_reward) values (?1, ?2, ?3, ?4, ?5)`, func(stmt *sql.Statement) { - stmt.BindBytes(1, reward.Coinbase[:]) - stmt.BindInt64(2, int64(reward.Layer.Uint32())) - stmt.BindInt64(3, int64(reward.TotalReward)) - stmt.BindInt64(4, int64(reward.LayerReward)) + stmt.BindBytes(1, reward.SmesherID[:]) + stmt.BindBytes(2, reward.Coinbase[:]) + stmt.BindInt64(3, int64(reward.Layer.Uint32())) + stmt.BindInt64(4, int64(reward.TotalReward)) + stmt.BindInt64(5, int64(reward.LayerReward)) }, nil); err != nil { return fmt.Errorf("insert %+x: %w", reward, err) } @@ -37,20 +34,56 @@ func Revert(db sql.Executor, revertTo types.LayerID) error { return nil } -// List rewards from all layers for the coinbase address. -func List(db sql.Executor, coinbase types.Address) (rst []*types.Reward, err error) { - _, err = db.Exec("select layer, total_reward, layer_reward from rewards where coinbase = ?1 order by layer;", - func(stmt *sql.Statement) { +// ListByKey lists rewards from all layers for the specified smesherID and/or coinbase. +func ListByKey(db sql.Executor, coinbase *types.Address, smesherID *types.NodeID) (rst []*types.Reward, err error) { + var whereClause string + var binder func(*sql.Statement) + if coinbase != nil && smesherID != nil { + whereClause = "pubkey = ?1 and coinbase = ?2" + binder = func(stmt *sql.Statement) { + stmt.BindBytes(1, smesherID[:]) + stmt.BindBytes(2, coinbase[:]) + } + } else if coinbase != nil { + whereClause = "coinbase = ?1" + binder = func(stmt *sql.Statement) { stmt.BindBytes(1, coinbase[:]) - }, func(stmt *sql.Statement) bool { - reward := &types.Reward{ - Coinbase: coinbase, - Layer: types.LayerID(uint32(stmt.ColumnInt64(0))), - TotalReward: uint64(stmt.ColumnInt64(1)), - LayerReward: uint64(stmt.ColumnInt64(2)), - } - rst = append(rst, reward) - return true - }) - return + } + } else if smesherID != nil { + whereClause = "pubkey = ?1" + binder = func(stmt *sql.Statement) { + stmt.BindBytes(1, smesherID[:]) + } + } else { + return nil, fmt.Errorf("must specify coinbase and/or smesherID") + } + stmt := fmt.Sprintf( + "select pubkey, coinbase, layer, total_reward, layer_reward from rewards where %s order by layer;", + whereClause) + _, err = db.Exec(stmt, binder, func(stmt *sql.Statement) bool { + smID := types.NodeID{} + cbase := types.Address{} + stmt.ColumnBytes(0, smID[:]) + stmt.ColumnBytes(1, cbase[:]) + reward := &types.Reward{ + SmesherID: smID, + Coinbase: cbase, + Layer: types.LayerID(uint32(stmt.ColumnInt64(2))), + TotalReward: uint64(stmt.ColumnInt64(3)), + LayerReward: uint64(stmt.ColumnInt64(4)), + } + rst = append(rst, reward) + return true + }) + return rst, err +} + +// ListByCoinbase lists rewards from all layers for the coinbase address. +func ListByCoinbase(db sql.Executor, coinbase types.Address) (rst []*types.Reward, err error) { + return ListByKey(db, &coinbase, nil) +} + +// ListBySmesherId lists rewards from all layers for the smesher ID. +func ListBySmesherId(db sql.Executor, smesherID types.NodeID) (rst []*types.Reward, err error) { + return ListByKey(db, nil, &smesherID) } diff --git a/sql/rewards/rewards_test.go b/sql/rewards/rewards_test.go index 064c984264..6623c8f479 100644 --- a/sql/rewards/rewards_test.go +++ b/sql/rewards/rewards_test.go @@ -2,6 +2,7 @@ package rewards import ( "math" + "sort" "testing" "github.com/stretchr/testify/require" @@ -17,24 +18,30 @@ func TestRewards(t *testing.T) { lyrReward := part / 2 coinbase1 := types.Address{1} coinbase2 := types.Address{2} + smesherID1 := types.NodeID{1} + smesherID2 := types.NodeID{2} + smesherID3 := types.NodeID{3} lid1 := types.LayerID(1) rewards1 := []types.Reward{ { Layer: lid1, Coinbase: coinbase1, + SmesherID: smesherID1, TotalReward: part, LayerReward: lyrReward, }, { Layer: lid1, Coinbase: coinbase1, + SmesherID: smesherID2, TotalReward: part, LayerReward: lyrReward, }, { Layer: lid1, Coinbase: coinbase2, + SmesherID: smesherID3, TotalReward: part, LayerReward: lyrReward, }, @@ -44,6 +51,7 @@ func TestRewards(t *testing.T) { { Layer: lid2, Coinbase: coinbase2, + SmesherID: smesherID2, TotalReward: part, LayerReward: lyrReward, }, @@ -52,45 +60,278 @@ func TestRewards(t *testing.T) { require.NoError(t, Add(db, &reward)) } - got, err := List(db, coinbase1) + got, err := ListByCoinbase(db, coinbase1) require.NoError(t, err) - require.Len(t, got, 1) + require.Len(t, got, 2) require.Equal(t, coinbase1, got[0].Coinbase) require.Equal(t, lid1, got[0].Layer) - require.Equal(t, part*2, got[0].TotalReward) - require.Equal(t, lyrReward*2, got[0].LayerReward) + require.Equal(t, smesherID1, got[0].SmesherID) + require.Equal(t, part, got[0].TotalReward) + require.Equal(t, lyrReward, got[0].LayerReward) + require.Equal(t, coinbase1, got[1].Coinbase) + require.Equal(t, lid1, got[1].Layer) + require.Equal(t, smesherID2, got[1].SmesherID) + require.Equal(t, part, got[1].TotalReward) + require.Equal(t, lyrReward, got[1].LayerReward) - got, err = List(db, coinbase2) + got, err = ListByCoinbase(db, coinbase2) require.NoError(t, err) require.Len(t, got, 2) require.Equal(t, coinbase2, got[0].Coinbase) require.Equal(t, lid1, got[0].Layer) + require.Equal(t, smesherID3, got[0].SmesherID) require.Equal(t, part, got[0].TotalReward) require.Equal(t, lyrReward, got[0].LayerReward) require.Equal(t, coinbase2, got[1].Coinbase) require.Equal(t, lid2, got[1].Layer) + require.Equal(t, smesherID2, got[1].SmesherID) require.Equal(t, part, got[1].TotalReward) require.Equal(t, lyrReward, got[1].LayerReward) + got, err = ListBySmesherId(db, smesherID1) + require.NoError(t, err) + require.Len(t, got, 1) + require.Equal(t, coinbase1, got[0].Coinbase) + require.Equal(t, lid1, got[0].Layer) + require.Equal(t, smesherID1, got[0].SmesherID) + require.Equal(t, part, got[0].TotalReward) + require.Equal(t, lyrReward, got[0].LayerReward) + + got, err = ListBySmesherId(db, smesherID2) + require.NoError(t, err) + require.Len(t, got, 2) + require.Equal(t, coinbase1, got[0].Coinbase) + require.Equal(t, lid1, got[0].Layer) + require.Equal(t, smesherID2, got[0].SmesherID) + require.Equal(t, part, got[0].TotalReward) + require.Equal(t, lyrReward, got[0].LayerReward) + require.Equal(t, coinbase2, got[1].Coinbase) + require.Equal(t, lid2, got[1].Layer) + require.Equal(t, smesherID2, got[1].SmesherID) + require.Equal(t, part, got[1].TotalReward) + require.Equal(t, lyrReward, got[1].LayerReward) + + got, err = ListBySmesherId(db, smesherID3) + require.NoError(t, err) + require.Len(t, got, 1) + require.Equal(t, coinbase2, got[0].Coinbase) + require.Equal(t, lid1, got[0].Layer) + require.Equal(t, smesherID3, got[0].SmesherID) + require.Equal(t, part, got[0].TotalReward) + require.Equal(t, lyrReward, got[0].LayerReward) + unknownAddr := types.Address{1, 2, 3} - got, err = List(db, unknownAddr) + got, err = ListByCoinbase(db, unknownAddr) + require.NoError(t, err) + require.Len(t, got, 0) + + unknownSmesher := types.NodeID{1, 2, 3} + got, err = ListBySmesherId(db, unknownSmesher) require.NoError(t, err) require.Len(t, got, 0) require.NoError(t, Revert(db, lid1)) - got, err = List(db, coinbase1) + got, err = ListByCoinbase(db, coinbase1) + require.NoError(t, err) + require.Len(t, got, 2) + require.Equal(t, coinbase1, got[0].Coinbase) + require.Equal(t, lid1, got[0].Layer) + require.Equal(t, smesherID1, got[0].SmesherID) + require.Equal(t, part, got[0].TotalReward) + require.Equal(t, lyrReward, got[0].LayerReward) + require.Equal(t, coinbase1, got[1].Coinbase) + require.Equal(t, lid1, got[1].Layer) + require.Equal(t, smesherID2, got[1].SmesherID) + require.Equal(t, part, got[1].TotalReward) + require.Equal(t, lyrReward, got[1].LayerReward) + + got, err = ListByCoinbase(db, coinbase2) + require.NoError(t, err) + require.Len(t, got, 1) + require.Equal(t, coinbase2, got[0].Coinbase) + require.Equal(t, lid1, got[0].Layer) + require.Equal(t, smesherID3, got[0].SmesherID) + require.Equal(t, part, got[0].TotalReward) + require.Equal(t, lyrReward, got[0].LayerReward) + + got, err = ListBySmesherId(db, smesherID1) require.NoError(t, err) require.Len(t, got, 1) require.Equal(t, coinbase1, got[0].Coinbase) require.Equal(t, lid1, got[0].Layer) - require.Equal(t, part*2, got[0].TotalReward) - require.Equal(t, lyrReward*2, got[0].LayerReward) + require.Equal(t, smesherID1, got[0].SmesherID) + require.Equal(t, part, got[0].TotalReward) + require.Equal(t, lyrReward, got[0].LayerReward) - got, err = List(db, coinbase2) + got, err = ListBySmesherId(db, smesherID2) + require.NoError(t, err) + require.Len(t, got, 1) + require.Equal(t, coinbase1, got[0].Coinbase) + require.Equal(t, lid1, got[0].Layer) + require.Equal(t, smesherID2, got[0].SmesherID) + require.Equal(t, part, got[0].TotalReward) + require.Equal(t, lyrReward, got[0].LayerReward) + + got, err = ListBySmesherId(db, smesherID3) require.NoError(t, err) require.Len(t, got, 1) require.Equal(t, coinbase2, got[0].Coinbase) require.Equal(t, lid1, got[0].Layer) + require.Equal(t, smesherID3, got[0].SmesherID) require.Equal(t, part, got[0].TotalReward) require.Equal(t, lyrReward, got[0].LayerReward) + + // This should fail: there cannot be two (smesherID, layer) rows. + require.ErrorIs(t, Add(db, &types.Reward{ + Layer: lid1, + SmesherID: smesherID2, + }), sql.ErrObjectExists) + + // This should succeed. SmesherID can be NULL. + require.NoError(t, Add(db, &types.Reward{ + Layer: lid1, + })) + + // This should fail: there cannot be two (NULL, layer) rows. + require.ErrorIs(t, Add(db, &types.Reward{ + Layer: lid1, + }), sql.ErrObjectExists) +} + +func Test_0008Migration_EmptyDBIsNoOp(t *testing.T) { + migrations, err := sql.StateMigrations() + require.NoError(t, err) + sort.Slice(migrations, func(i, j int) bool { return migrations[i].Order() < migrations[j].Order() }) + + // apply previous migrations + db := sql.InMemory( + sql.WithMigrations(migrations[:7]), + ) + + // verify that the DB is empty + _, err = db.Exec("select count(*) from rewards;", func(stmt *sql.Statement) { + }, func(stmt *sql.Statement) bool { + require.Equal(t, int64(0), stmt.ColumnInt64(0)) + return true + }) + require.NoError(t, err) + + // apply the migration + err = migrations[7].Apply(db) + require.NoError(t, err) + + // verify that db is still empty + _, err = db.Exec("select count(*) from rewards;", func(stmt *sql.Statement) { + }, func(stmt *sql.Statement) bool { + require.Equal(t, int64(0), stmt.ColumnInt64(0)) + return true + }) + require.NoError(t, err) +} + +func Test_0008Migration(t *testing.T) { + migrations, err := sql.StateMigrations() + require.NoError(t, err) + sort.Slice(migrations, func(i, j int) bool { return migrations[i].Order() < migrations[j].Order() }) + + // apply previous migrations + db := sql.InMemory( + sql.WithMigrations(migrations[:7]), + ) + + // verify that the DB is empty + _, err = db.Exec("select count(*) from rewards;", func(stmt *sql.Statement) { + }, func(stmt *sql.Statement) bool { + require.Equal(t, int64(0), stmt.ColumnInt64(0)) + return true + }) + require.NoError(t, err) + + // attempt to insert some rewards data + reward := &types.Reward{ + Layer: 9000, + TotalReward: 10, + LayerReward: 20, + Coinbase: types.Address{1}, + } + err = Add(db, reward) + + // this should fail since the un-migrated table doesn't have this column yet + require.ErrorContains(t, err, "table rewards has no column named pubkey") + + // add the row manually + _, err = db.Exec(` + insert into rewards (coinbase, layer, total_reward, layer_reward) values (?1, ?2, ?3, ?4)`, + func(stmt *sql.Statement) { + stmt.BindBytes(1, reward.Coinbase[:]) + stmt.BindInt64(2, int64(reward.Layer.Uint32())) + stmt.BindInt64(3, int64(reward.TotalReward)) + stmt.BindInt64(4, int64(reward.LayerReward)) + }, nil) + require.NoError(t, err) + + // make sure one row was added successfully + _, err = db.Exec("select count(*) from rewards;", func(stmt *sql.Statement) { + }, func(stmt *sql.Statement) bool { + require.Equal(t, int64(1), stmt.ColumnInt64(0)) + return true + }) + require.NoError(t, err) + + // apply the migration + err = migrations[7].Apply(db) + require.NoError(t, err) + + // verify that one row is still present + _, err = db.Exec("select count(*) from rewards;", func(stmt *sql.Statement) { + }, func(stmt *sql.Statement) bool { + require.Equal(t, int64(1), stmt.ColumnInt64(0)) + return true + }) + require.NoError(t, err) + + // verify the data + rewards, err := ListByCoinbase(db, reward.Coinbase) + require.NoError(t, err) + require.Len(t, rewards, 1) + require.Equal(t, reward.Coinbase, rewards[0].Coinbase) + require.Equal(t, reward.TotalReward, rewards[0].TotalReward) + require.Equal(t, reward.LayerReward, rewards[0].LayerReward) + require.Equal(t, reward.Layer, rewards[0].Layer) + // this should not be set + require.Equal(t, types.NodeID{0}, rewards[0].SmesherID) + + // this should return nothing (since smesherID wasn't set) + rewards, err = ListBySmesherId(db, reward.SmesherID) + require.NoError(t, err) + require.Len(t, rewards, 0) + + // add more data and verify that we can read it both ways + reward = &types.Reward{ + Layer: 9001, + TotalReward: 11, + LayerReward: 21, + Coinbase: types.Address{1}, + SmesherID: types.NodeID{2}, + } + + err = Add(db, reward) + require.NoError(t, err) + rewards, err = ListByCoinbase(db, reward.Coinbase) + require.NoError(t, err) + require.Len(t, rewards, 2) + require.Equal(t, reward.Coinbase, rewards[1].Coinbase) + require.Equal(t, reward.TotalReward, rewards[1].TotalReward) + require.Equal(t, reward.LayerReward, rewards[1].LayerReward) + require.Equal(t, reward.Layer, rewards[1].Layer) + require.Equal(t, reward.SmesherID, rewards[1].SmesherID) + + rewards, err = ListBySmesherId(db, reward.SmesherID) + require.NoError(t, err) + require.Len(t, rewards, 1) + require.Equal(t, reward.Coinbase, rewards[0].Coinbase) + require.Equal(t, reward.TotalReward, rewards[0].TotalReward) + require.Equal(t, reward.LayerReward, rewards[0].LayerReward) + require.Equal(t, reward.Layer, rewards[0].Layer) + require.Equal(t, reward.SmesherID, rewards[0].SmesherID) }