Skip to content

Commit

Permalink
feat: add support for new asset proposals in batches
Browse files Browse the repository at this point in the history
Signed-off-by: Jeremy Letang <me@jeremyletang.com>
  • Loading branch information
jeremyletang committed Apr 18, 2024
1 parent ed0816c commit b2ff1e7
Show file tree
Hide file tree
Showing 11 changed files with 1,054 additions and 583 deletions.
12 changes: 12 additions & 0 deletions commands/batch_proposal_submission.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ func checkBatchProposalChanges(terms *protoTypes.BatchProposalTermsChange) Error
errs.Merge(checkNetworkParameterUpdateBatchChanges(c))
case *protoTypes.BatchProposalTermsChange_UpdateAsset:
errs.Merge(checkUpdateAssetBatchChanges(c))
case *protoTypes.BatchProposalTermsChange_NewAsset:
errs.Merge(checkNewAssetBatchChanges(c))
case *protoTypes.BatchProposalTermsChange_NewFreeform:
errs.Merge(checkNewFreeformBatchChanges(c))
case *protoTypes.BatchProposalTermsChange_NewTransfer:
Expand Down Expand Up @@ -208,6 +210,16 @@ func checkUpdateAssetBatchChanges(change *protoTypes.BatchProposalTermsChange_Up
return checkUpdateAsset(change.UpdateAsset).AddPrefix("batch_proposal_submission.terms.changes.")
}

func checkNewAssetBatchChanges(change *protoTypes.BatchProposalTermsChange_NewAsset) Errors {
errs := NewErrors()

if change.NewAsset == nil {
return errs.FinalAddForProperty("batch_proposal_submission.terms.changes.new_asset", ErrIsRequired)
}

return checkBatchNewAssetChanges(change).AddPrefix("batch_proposal_submission.terms.changes.")
}

func checkNewFreeformBatchChanges(change *protoTypes.BatchProposalTermsChange_NewFreeform) Errors {
errs := NewErrors()

Expand Down
42 changes: 42 additions & 0 deletions commands/proposal_submission.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,48 @@ func checkNewAssetChanges(change *protoTypes.ProposalTerms_NewAsset) Errors {
return errs
}

func checkBatchNewAssetChanges(change *protoTypes.BatchProposalTermsChange_NewAsset) Errors {
errs := NewErrors()

if change.NewAsset == nil {
return errs.FinalAddForProperty("proposal_submission.terms.change.new_asset", ErrIsRequired)
}

if change.NewAsset.Changes == nil {
return errs.FinalAddForProperty("proposal_submission.terms.change.new_asset.changes", ErrIsRequired)
}

if len(change.NewAsset.Changes.Name) == 0 {
errs.AddForProperty("proposal_submission.terms.change.new_asset.changes.name", ErrIsRequired)
}
if len(change.NewAsset.Changes.Symbol) == 0 {
errs.AddForProperty("proposal_submission.terms.change.new_asset.changes.symbol", ErrIsRequired)
}

if len(change.NewAsset.Changes.Quantum) <= 0 {
errs.AddForProperty("proposal_submission.terms.change.new_asset.changes.quantum", ErrIsRequired)
} else if quantum, err := num.DecimalFromString(change.NewAsset.Changes.Quantum); err != nil {
errs.AddForProperty("proposal_submission.terms.change.new_asset.changes.quantum", ErrIsNotValidNumber)
} else if quantum.LessThanOrEqual(num.DecimalZero()) {
errs.AddForProperty("proposal_submission.terms.change.new_asset.changes.quantum", ErrMustBePositive)
}

if change.NewAsset.Changes.Source == nil {
return errs.FinalAddForProperty("proposal_submission.terms.change.new_asset.changes.source", ErrIsRequired)
}

switch s := change.NewAsset.Changes.Source.(type) {
case *protoTypes.AssetDetails_BuiltinAsset:
errs.Merge(checkBuiltinAssetSource(s))
case *protoTypes.AssetDetails_Erc20:
errs.Merge(checkERC20AssetSource(s))
default:
return errs.FinalAddForProperty("proposal_submission.terms.change.new_asset.changes.source", ErrIsNotValid)
}

return errs
}

func CheckNewFreeformChanges(change *protoTypes.ProposalTerms_NewFreeform) Errors {
errs := NewErrors()

Expand Down
46 changes: 46 additions & 0 deletions core/governance/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,44 @@ func (e *Engine) OnTick(ctx context.Context, t time.Time) ([]*ToEnact, []*VoteCl
}
}

// then do the same thing for batches
acceptedBatches, rejectedBatches := e.nodeProposalValidation.OnTickBatch(t)
for _, p := range acceptedBatches {
e.log.Info("proposal has been validated by nodes, starting now",
logging.String("proposal-id", p.ID))
p.Open()

e.broker.Send(events.NewProposalEventFromProto(ctx, p.ToProto()))
proposalsEvents := []events.Event{}
for _, v := range p.Proposals {
proposalsEvents = append(proposalsEvents, events.NewProposalEvent(ctx, *v))
}
e.broker.SendBatch(proposalsEvents)

e.startValidatedBatchProposal(p) // can't fail, and proposal has been validated at an ulterior time
}

for _, p := range rejectedBatches {
e.log.Info("proposal has not been validated by nodes",
logging.String("proposal-id", p.ID))
p.Reject(types.ProposalErrorNodeValidationFailed)
e.broker.Send(events.NewProposalEventFromProto(ctx, p.ToProto()))
proposalsEvents := []events.Event{}
for _, v := range p.Proposals {
proposalsEvents = append(proposalsEvents, events.NewProposalEvent(ctx, *v))
}
e.broker.SendBatch(proposalsEvents)

for _, v := range p.Proposals {
// if it's an asset proposal we need to update it's
// state in the asset engine
switch v.Terms.Change.GetTermType() {
case types.ProposalTermsTypeNewAsset:
e.assets.SetRejected(ctx, p.ID)
}
}
}

toBeEnacted := []*ToEnact{}
for i, ep := range preparedToEnact {
// this is the new market proposal, and should already be in the slice
Expand Down Expand Up @@ -645,10 +683,18 @@ func (e *Engine) startValidatedProposal(p *proposal) {
e.activeProposals = append(e.activeProposals, p)
}

func (e *Engine) startValidatedBatchProposal(p *batchProposal) {
e.activeBatchProposals[p.ID] = p
}

func (e *Engine) startTwoStepsProposal(ctx context.Context, p *types.Proposal) error {
return e.nodeProposalValidation.Start(ctx, p)
}

func (e *Engine) startTwoStepsBatchProposal(ctx context.Context, p *types.BatchProposal) error {
return e.nodeProposalValidation.StartBatch(ctx, p)
}

func (e *Engine) isTwoStepsProposal(p *types.Proposal) bool {
return e.nodeProposalValidation.IsNodeValidationRequired(p)
}
Expand Down
37 changes: 32 additions & 5 deletions core/governance/engine_batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,10 @@ func (e *Engine) SubmitBatchProposal(
Reference: bp.Reference,
Rationale: bp.Rationale,
Terms: &types.ProposalTerms{
ClosingTimestamp: bp.ClosingTimestamp,
EnactmentTimestamp: change.EnactmentTime,
Change: change.Change,
ClosingTimestamp: bp.ClosingTimestamp,
EnactmentTimestamp: change.EnactmentTime,
ValidationTimestamp: change.ValidationTime,
Change: change.Change,
},
}

Expand Down Expand Up @@ -121,11 +122,28 @@ func (e *Engine) SubmitBatchProposal(
return nil, errs
}

e.startBatchProposal(bp)
if e.isTwoStepsBatchProposal(bp) {
// set all proposals as WaitForNodeVote then
bp.WaitForNodeVote()
if err := e.startTwoStepsBatchProposal(ctx, bp); err != nil {
bp.RejectWithErr(types.ProposalErrorNodeValidationFailed, err)
if e.log.IsDebug() {
e.log.Debug("Proposal rejected",
logging.String("batch-proposal-id", bp.ID))
}
return nil, err
}
} else {
e.startBatchProposal(bp)
}

return toSubmits, nil
}

func (e *Engine) isTwoStepsBatchProposal(p *types.BatchProposal) bool {
return e.nodeProposalValidation.IsNodeValidationRequiredBatch(p)
}

func (e *Engine) RejectBatchProposal(
ctx context.Context, proposalID string, r types.ProposalError, errorDetails error,
) error {
Expand Down Expand Up @@ -262,7 +280,16 @@ func (e *Engine) evaluateBatchProposals(

func (e *Engine) getBatchProposal(id string) (*batchProposal, bool) {
bp, ok := e.activeBatchProposals[id]
return bp, ok
if ok {
return bp, ok
}

nbp, ok := e.nodeProposalValidation.getBatchProposal(id)
if !ok {
return nil, false
}

return nbp.batchProposal, ok
}

func (e *Engine) validateProposalFromBatch(
Expand Down
106 changes: 106 additions & 0 deletions core/governance/engine_new_asset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,112 @@ func testVotingDuringValidationOfProposalForNewAssetSucceeds(t *testing.T) {
assert.EqualError(t, err, governance.ErrProposalDoesNotExist.Error())
}

func TestVotingDuringValidationOfProposalForNewAssetInBatchSucceeds(t *testing.T) {
// eng := getTestEngine(t, time.Now())

// // when
// proposer := vgrand.RandomStr(5)
// proposalID := vgrand.RandomStr(5)
// proposal := eng.newBatchProposalForNewAsset(proposer, eng.tsvc.GetTimeNow().Add(2*time.Hour))

// // setup
// var bAsset *assets.Asset
// var fcheck func(interface{}, bool)
// var rescheck validators.Resource
// eng.assets.EXPECT().NewAsset(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).DoAndReturn(func(_ context.Context, ref string, assetDetails *types.AssetDetails) (string, error) {
// bAsset = assets.NewAsset(builtin.New(ref, assetDetails))
// return ref, nil
// })
// eng.assets.EXPECT().Get(gomock.Any()).Times(1).DoAndReturn(func(id string) (*assets.Asset, error) {
// return bAsset, nil
// })
// eng.witness.EXPECT().StartCheck(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Do(func(r validators.Resource, f func(interface{}, bool), _ time.Time) error {
// fcheck = f
// rescheck = r
// return nil
// })
// eng.ensureStakingAssetTotalSupply(t, 9)
// eng.ensureAllAssetEnabled(t)
// eng.ensureTokenBalanceForParty(t, proposer, 1)

// // expect
// eng.expectProposalWaitingForNodeVoteEvent(t, proposer, proposal.ID)

// // when
// _, err := eng.submitBatchProposal(t, proposal, proposal.ID, proposalID)

// // then
// require.NoError(t, err)

// // given
// voter1 := vgrand.RandomStr(5)

// // setup
// eng.ensureTokenBalanceForParty(t, voter1, 7)

// // expect
// eng.expectVoteEvent(t, voter1, proposal.ID)

// // then
// err = eng.addYesVote(t, voter1, proposal.ID)

// // call success on the validation
// fcheck(rescheck, true)

// // then
// require.NoError(t, err)
// afterValidation := time.Unix(proposal.Terms.ValidationTimestamp, 0).Add(time.Second)

// // setup
// eng.ensureTokenBalanceForParty(t, voter1, 7)

// // expect
// eng.expectOpenProposalEvent(t, proposer, proposal.ID)
// eng.expectGetMarketState(t, proposal.ID)

// // when
// eng.OnTick(context.Background(), afterValidation)

// // given
// afterClosing := time.Unix(proposal.Terms.ClosingTimestamp, 0).Add(time.Second)

// // expect
// eng.expectPassedProposalEvent(t, proposal.ID)
// eng.expectTotalGovernanceTokenFromVoteEvents(t, "1", "7")
// eng.assets.EXPECT().SetPendingListing(gomock.Any(), proposal.ID).Times(1)

// // when
// eng.OnTick(context.Background(), afterClosing)

// // given
// voter2 := vgrand.RandomStr(5)

// // when
// err = eng.addNoVote(t, voter2, proposal.ID)

// // then
// require.Error(t, err)
// assert.EqualError(t, err, governance.ErrProposalNotOpenForVotes.Error())

// // given
// afterEnactment := time.Unix(proposal.Terms.EnactmentTimestamp, 0).Add(time.Second)

// // when
// // no calculations, no state change, simply removed from governance engine
// toBeEnacted, _ := eng.OnTick(context.Background(), afterEnactment)

// // then
// require.Len(t, toBeEnacted, 1)
// assert.Equal(t, proposal.ID, toBeEnacted[0].Proposal().ID)

// // when
// err = eng.addNoVote(t, voter2, proposal.ID)

// // then
// require.Error(t, err)
// assert.EqualError(t, err, governance.ErrProposalDoesNotExist.Error())
}

func TestNoVotesAnd0RequiredFails(t *testing.T) {
eng := getTestEngine(t, time.Now())

Expand Down
Loading

0 comments on commit b2ff1e7

Please sign in to comment.