Skip to content

Commit 3088b28

Browse files
committed
feat(fip-0118): add reward actor changes
* state representation * msg param & return type representation * migration * golden test fixtures ported from rust for cbor representations Ref: https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0118.md Ref: filecoin-project/builtin-actors#1774 Closes: #460
1 parent ce3be5c commit 3088b28

14 files changed

Lines changed: 5143 additions & 176 deletions

builtin/v19/gen/gen.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,31 @@ func main() {
4747
}
4848

4949
if err := gen.WriteTupleEncodersToFile("./builtin/v19/reward/cbor_gen.go", "reward",
50-
// actor state
5150
reward.State{},
52-
// method params and returns
51+
reward.StreamsState{},
52+
reward.Stream{},
53+
reward.WeightRecord{},
54+
reward.WeightRecordUpdate{},
55+
reward.ExplicitDistribution{},
56+
reward.RecipientShare{},
57+
reward.RecipientAmount{},
58+
reward.Tombstone{},
59+
reward.PendingWrite{},
60+
reward.DistributionInit{},
61+
reward.RegisterStreamPayload{},
62+
reward.SetDistributionPayload{},
63+
reward.StreamAccrual{},
5364
reward.ThisEpochRewardReturn{},
5465
reward.AwardBlockRewardParams{},
66+
reward.SetWeightRecordsParams{},
67+
reward.StepWeightRecordsParams{},
68+
reward.RegisterStreamParams{},
69+
reward.RemoveStreamParams{},
70+
reward.SetDistributionParams{},
71+
reward.SetSharesParams{},
72+
reward.CancelPendingParams{},
73+
reward.ClaimParams{},
74+
reward.ClaimReturn{},
5575
); err != nil {
5676
panic(err)
5777
}

builtin/v19/migration/reward.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package migration
2+
3+
import (
4+
"context"
5+
6+
"github.com/filecoin-project/go-address"
7+
"github.com/filecoin-project/go-state-types/abi"
8+
"github.com/filecoin-project/go-state-types/big"
9+
reward18 "github.com/filecoin-project/go-state-types/builtin/v18/reward"
10+
reward19 "github.com/filecoin-project/go-state-types/builtin/v19/reward"
11+
smoothing19 "github.com/filecoin-project/go-state-types/builtin/v19/util/smoothing"
12+
"github.com/filecoin-project/go-state-types/migration"
13+
"github.com/ipfs/go-cid"
14+
cbor "github.com/ipfs/go-ipld-cbor"
15+
"golang.org/x/xerrors"
16+
)
17+
18+
type rewardMigrator struct {
19+
outCodeCID cid.Cid
20+
streams *reward19.StreamsState
21+
accruals []reward19.StreamAccrual
22+
swaTimelockEpochs abi.ChainEpoch
23+
swaActor address.Address
24+
}
25+
26+
func newRewardMigrator(config RewardMigrationConfig, activationEpoch abi.ChainEpoch, outCodeCID cid.Cid) (*rewardMigrator, error) {
27+
streams, accruals, err := reward19.ValidateMigrationStreams(config.Streams, activationEpoch)
28+
if err != nil {
29+
return nil, xerrors.Errorf("invalid reward migration streams: %w", err)
30+
}
31+
if config.SWATimelockEpochs < 0 {
32+
return nil, xerrors.Errorf("SWA timelock is negative")
33+
}
34+
if config.SWAActor.Protocol() != address.ID {
35+
return nil, xerrors.Errorf("SWA actor is not an ID address")
36+
}
37+
return &rewardMigrator{
38+
outCodeCID: outCodeCID,
39+
streams: streams,
40+
accruals: accruals,
41+
swaTimelockEpochs: config.SWATimelockEpochs,
42+
swaActor: config.SWAActor,
43+
}, nil
44+
}
45+
46+
func (m rewardMigrator) MigratedCodeCID() cid.Cid {
47+
return m.outCodeCID
48+
}
49+
50+
func (m rewardMigrator) Deferred() bool {
51+
return false
52+
}
53+
54+
func (m rewardMigrator) MigrateState(ctx context.Context, store cbor.IpldStore, in migration.ActorMigrationInput) (*migration.ActorMigrationResult, error) {
55+
var inState reward18.State
56+
if err := store.Get(ctx, in.Head, &inState); err != nil {
57+
return nil, xerrors.Errorf("failed to load reward state for %s: %w", in.Address, err)
58+
}
59+
60+
streamsRoot, err := store.Put(ctx, m.streams)
61+
if err != nil {
62+
return nil, xerrors.Errorf("failed to put reward streams state: %w", err)
63+
}
64+
outState := reward19.State{
65+
CumsumBaseline: inState.CumsumBaseline,
66+
CumsumRealized: inState.CumsumRealized,
67+
EffectiveNetworkTime: inState.EffectiveNetworkTime,
68+
EffectiveBaselinePower: inState.EffectiveBaselinePower,
69+
ThisEpochReward: inState.ThisEpochReward,
70+
ThisEpochRewardSmoothed: smoothing19.FilterEstimate{PositionEstimate: inState.ThisEpochRewardSmoothed.PositionEstimate, VelocityEstimate: inState.ThisEpochRewardSmoothed.VelocityEstimate},
71+
ThisEpochBaselinePower: inState.ThisEpochBaselinePower,
72+
Epoch: inState.Epoch,
73+
TotalMintedReward: inState.TotalStoragePowerReward,
74+
TotalBurnMinted: big.Zero(),
75+
TotalExplicitMinted: big.Zero(),
76+
Accrued: append([]reward19.StreamAccrual(nil), m.accruals...),
77+
SWATimelockEpochs: m.swaTimelockEpochs,
78+
SWAActor: m.swaActor,
79+
StreamsRoot: streamsRoot,
80+
}
81+
newHead, err := store.Put(ctx, &outState)
82+
if err != nil {
83+
return nil, xerrors.Errorf("failed to put new reward state: %w", err)
84+
}
85+
return &migration.ActorMigrationResult{NewCodeCID: m.outCodeCID, NewHead: newHead}, nil
86+
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package migration
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/filecoin-project/go-address"
9+
"github.com/filecoin-project/go-state-types/abi"
10+
"github.com/filecoin-project/go-state-types/big"
11+
reward18 "github.com/filecoin-project/go-state-types/builtin/v18/reward"
12+
smoothing18 "github.com/filecoin-project/go-state-types/builtin/v18/util/smoothing"
13+
reward19 "github.com/filecoin-project/go-state-types/builtin/v19/reward"
14+
"github.com/filecoin-project/go-state-types/migration"
15+
"github.com/ipfs/go-cid"
16+
cbor "github.com/ipfs/go-ipld-cbor"
17+
"github.com/stretchr/testify/require"
18+
)
19+
20+
func migrationIDAddress(t *testing.T, id uint64) address.Address {
21+
t.Helper()
22+
addr, err := address.NewIDAddress(id)
23+
require.NoError(t, err)
24+
return addr
25+
}
26+
27+
func validRewardMigrationConfig(t *testing.T, activationEpoch abi.ChainEpoch) RewardMigrationConfig {
28+
t.Helper()
29+
pct := reward19.Denom / 100
30+
return RewardMigrationConfig{
31+
SWATimelockEpochs: 20_160,
32+
SWAActor: migrationIDAddress(t, 100),
33+
Streams: []reward19.RegisterStreamParams{
34+
{
35+
ID: 1,
36+
Weight: reward19.WeightRecord{VStart: 95 * pct, Slope: -1, TStart: activationEpoch, Floor: 50 * pct, Cap: 95 * pct},
37+
ActivationEpoch: activationEpoch,
38+
},
39+
{
40+
ID: 2,
41+
Weight: reward19.WeightRecord{VStart: 5 * pct, Slope: 1, TStart: activationEpoch, Floor: 5 * pct, Cap: 10 * pct},
42+
Distribution: &reward19.DistributionInit{
43+
Writer: migrationIDAddress(t, 101),
44+
Shares: []reward19.RecipientShare{{Recipient: migrationIDAddress(t, 102), Share: reward19.Denom}},
45+
},
46+
ActivationEpoch: activationEpoch,
47+
},
48+
},
49+
}
50+
}
51+
52+
func TestRewardMigration(t *testing.T) {
53+
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
54+
defer cancel()
55+
req := require.New(t)
56+
store := cbor.NewMemCborStore()
57+
58+
inState := reward18.State{
59+
CumsumBaseline: big.NewInt(1),
60+
CumsumRealized: big.NewInt(2),
61+
EffectiveNetworkTime: 3,
62+
EffectiveBaselinePower: big.NewInt(4),
63+
ThisEpochReward: abi.NewTokenAmount(5),
64+
ThisEpochRewardSmoothed: smoothing18.NewEstimate(big.NewInt(6), big.NewInt(7)),
65+
ThisEpochBaselinePower: big.NewInt(8),
66+
Epoch: 9,
67+
TotalStoragePowerReward: abi.NewTokenAmount(10),
68+
SimpleTotal: abi.NewTokenAmount(11),
69+
BaselineTotal: abi.NewTokenAmount(12),
70+
}
71+
inHead, err := store.Put(ctx, &inState)
72+
req.NoError(err)
73+
74+
activationEpoch := abi.ChainEpoch(100)
75+
config := validRewardMigrationConfig(t, activationEpoch)
76+
outCodeCID := cid.MustParse("bafy2bzaca4aaaaaaaaaqk")
77+
migrator, err := newRewardMigrator(config, activationEpoch, outCodeCID)
78+
req.NoError(err)
79+
result, err := migrator.MigrateState(ctx, store, migration.ActorMigrationInput{Address: address.TestAddress, Head: inHead})
80+
req.NoError(err)
81+
req.Equal(outCodeCID, result.NewCodeCID)
82+
83+
var outState reward19.State
84+
req.NoError(store.Get(ctx, result.NewHead, &outState))
85+
req.Equal(inState.CumsumBaseline, outState.CumsumBaseline)
86+
req.Equal(inState.CumsumRealized, outState.CumsumRealized)
87+
req.Equal(inState.EffectiveNetworkTime, outState.EffectiveNetworkTime)
88+
req.Equal(inState.EffectiveBaselinePower, outState.EffectiveBaselinePower)
89+
req.Equal(inState.ThisEpochReward, outState.ThisEpochReward)
90+
req.Equal(inState.ThisEpochRewardSmoothed.PositionEstimate, outState.ThisEpochRewardSmoothed.PositionEstimate)
91+
req.Equal(inState.ThisEpochRewardSmoothed.VelocityEstimate, outState.ThisEpochRewardSmoothed.VelocityEstimate)
92+
req.Equal(inState.ThisEpochBaselinePower, outState.ThisEpochBaselinePower)
93+
req.Equal(inState.Epoch, outState.Epoch)
94+
req.Equal(inState.TotalStoragePowerReward, outState.TotalMintedReward)
95+
req.Equal(big.Zero(), outState.TotalBurnMinted)
96+
req.Equal(big.Zero(), outState.TotalExplicitMinted)
97+
req.Equal([]reward19.StreamAccrual{{ID: 2, Amount: big.Zero()}}, outState.Accrued)
98+
req.Equal(config.SWATimelockEpochs, outState.SWATimelockEpochs)
99+
req.Equal(config.SWAActor, outState.SWAActor)
100+
101+
var streams reward19.StreamsState
102+
req.NoError(store.Get(ctx, outState.StreamsRoot, &streams))
103+
req.Len(streams.Streams, 2)
104+
req.Equal(reward19.StreamID(1), streams.Streams[0].ID)
105+
req.Nil(streams.Streams[0].Distribution)
106+
req.Equal(reward19.StreamID(2), streams.Streams[1].ID)
107+
req.Equal(config.Streams[1].Distribution.Writer, streams.Streams[1].Distribution.Writer)
108+
req.Equal(config.Streams[1].Distribution.Shares, streams.Streams[1].Distribution.Shares)
109+
req.Empty(streams.Tombstones)
110+
req.Empty(streams.PendingWrites)
111+
}
112+
113+
func TestNewRewardMigratorRejectsInvalidConfig(t *testing.T) {
114+
activationEpoch := abi.ChainEpoch(100)
115+
outCodeCID := cid.MustParse("bafy2bzaca4aaaaaaaaaqk")
116+
testCases := []struct {
117+
name string
118+
mutate func(*RewardMigrationConfig)
119+
expected string
120+
}{
121+
{
122+
name: "activation epoch mismatch",
123+
mutate: func(config *RewardMigrationConfig) {
124+
config.Streams[0].ActivationEpoch++
125+
},
126+
expected: "activation epoch 101 does not match upgrade epoch 100",
127+
},
128+
{
129+
name: "weight start mismatch",
130+
mutate: func(config *RewardMigrationConfig) {
131+
config.Streams[1].Weight.TStart++
132+
},
133+
expected: "weight start 101 does not match upgrade epoch 100",
134+
},
135+
{
136+
name: "wrong stream count",
137+
mutate: func(config *RewardMigrationConfig) {
138+
config.Streams = config.Streams[:1]
139+
},
140+
expected: "requires exactly two streams",
141+
},
142+
{
143+
name: "wrong bootstrap weight",
144+
mutate: func(config *RewardMigrationConfig) {
145+
config.Streams[0].Weight.VStart--
146+
},
147+
expected: "consensus bootstrap weight is invalid",
148+
},
149+
{
150+
name: "unequal slopes",
151+
mutate: func(config *RewardMigrationConfig) {
152+
config.Streams[1].Weight.Slope++
153+
},
154+
expected: "bootstrap weight slopes are invalid",
155+
},
156+
{
157+
name: "invalid initial distribution",
158+
mutate: func(config *RewardMigrationConfig) {
159+
config.Streams[1].Distribution.Shares[0].Share--
160+
},
161+
expected: "one full-share recipient",
162+
},
163+
{
164+
name: "negative timelock",
165+
mutate: func(config *RewardMigrationConfig) {
166+
config.SWATimelockEpochs = -1
167+
},
168+
expected: "SWA timelock is negative",
169+
},
170+
{
171+
name: "non-ID SWA actor",
172+
mutate: func(config *RewardMigrationConfig) {
173+
addr, err := address.NewDelegatedAddress(10, []byte{1})
174+
require.NoError(t, err)
175+
config.SWAActor = addr
176+
},
177+
expected: "SWA actor is not an ID address",
178+
},
179+
}
180+
181+
for _, tc := range testCases {
182+
t.Run(tc.name, func(t *testing.T) {
183+
config := validRewardMigrationConfig(t, activationEpoch)
184+
tc.mutate(&config)
185+
_, err := newRewardMigrator(config, activationEpoch, outCodeCID)
186+
require.ErrorContains(t, err, tc.expected)
187+
})
188+
}
189+
}

builtin/v19/migration/top.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ package migration
33
import (
44
"context"
55

6+
"github.com/filecoin-project/go-address"
7+
68
adt14 "github.com/filecoin-project/go-state-types/builtin/v14/util/adt"
79

810
system18 "github.com/filecoin-project/go-state-types/builtin/v18/system"
11+
reward19 "github.com/filecoin-project/go-state-types/builtin/v19/reward"
912

1013
"github.com/filecoin-project/go-state-types/abi"
1114
"github.com/filecoin-project/go-state-types/builtin"
@@ -17,9 +20,15 @@ import (
1720
"golang.org/x/xerrors"
1821
)
1922

23+
type RewardMigrationConfig struct {
24+
SWATimelockEpochs abi.ChainEpoch
25+
SWAActor address.Address
26+
Streams []reward19.RegisterStreamParams
27+
}
28+
2029
// MigrateStateTree Migrates the filecoin state tree starting from the global state tree and upgrading all actor state.
2130
// The store must support concurrent writes (even if the configured worker count is 1).
22-
func MigrateStateTree(ctx context.Context, store cbor.IpldStore, newManifestCID cid.Cid, actorsRootIn cid.Cid, priorEpoch abi.ChainEpoch, cfg migration.Config, log migration.Logger, cache migration.MigrationCache) (cid.Cid, error) {
31+
func MigrateStateTree(ctx context.Context, store cbor.IpldStore, newManifestCID cid.Cid, actorsRootIn cid.Cid, priorEpoch abi.ChainEpoch, rewardConfig RewardMigrationConfig, cfg migration.Config, log migration.Logger, cache migration.MigrationCache) (cid.Cid, error) {
2332
if cfg.MaxWorkers <= 0 {
2433
return cid.Undef, xerrors.Errorf("invalid migration config with %d workers", cfg.MaxWorkers)
2534
}
@@ -67,7 +76,13 @@ func MigrateStateTree(ctx context.Context, store cbor.IpldStore, newManifestCID
6776
// Set of prior version code CIDs for actors to defer during iteration, for explicit migration afterwards.
6877
deferredCodeIDs := make(map[cid.Cid]struct{})
6978

79+
reward18CID := cid.Undef
80+
7081
for _, oldEntry := range oldManifestData.Entries {
82+
if oldEntry.Name == manifest.RewardKey {
83+
reward18CID = oldEntry.Code
84+
}
85+
7186
newCodeCID, ok := newManifest.Get(oldEntry.Name)
7287
if !ok {
7388
return cid.Undef, xerrors.Errorf("code cid for %s actor not found in new manifest", oldEntry.Name)
@@ -86,6 +101,19 @@ func MigrateStateTree(ctx context.Context, store cbor.IpldStore, newManifestCID
86101

87102
migrations[systemActor.Code] = systemActorMigrator{OutCodeCID: newSystemCodeCID, ManifestData: newManifest.Data}
88103

104+
if reward18CID == cid.Undef {
105+
return cid.Undef, xerrors.Errorf("code cid for reward actor not found in old manifest")
106+
}
107+
reward19CID, ok := newManifest.Get(manifest.RewardKey)
108+
if !ok {
109+
return cid.Undef, xerrors.Errorf("code cid for reward actor not found in new manifest")
110+
}
111+
rewardMigrator, err := newRewardMigrator(rewardConfig, priorEpoch+1, reward19CID)
112+
if err != nil {
113+
return cid.Undef, xerrors.Errorf("failed to create reward migrator: %w", err)
114+
}
115+
migrations[reward18CID] = migration.CachedMigration(cache, *rewardMigrator)
116+
89117
if len(migrations)+len(deferredCodeIDs) != len(oldManifestData.Entries) {
90118
return cid.Undef, xerrors.Errorf("incomplete migration specification with %d code CIDs, need %d", len(migrations)+len(deferredCodeIDs), len(oldManifestData.Entries))
91119
}

0 commit comments

Comments
 (0)