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

Add modules for deterministic total ordering derivation from partial ord #1335

Merged
merged 11 commits into from
Apr 25, 2022

Conversation

ValarDragon
Copy link
Member

@ValarDragon ValarDragon commented Apr 23, 2022

Component of #1329

What is the purpose of the change

The intention of this logic is to allow us to specify partial orderings when orderings are needed (e.g. beginblock, endblock, upgrade orders),and then have an automatic derivation of the relevant total ordering.

The problem faced right now is the important relations are obscured in app.go at the moment, and its a lot of tedious logic to verify, for an area that can have subtle errors slip through. The aim of this change is to remedy this.

Brief change log

Testing and Verifying

This change added tests and can be verified as follows:

  • dag_test.go provides unit tests for AddFirst and TopologicalSort
  • partialord_test.go demonstrates every exposed API, what it will look for the intended use case in app module ordering, and that the sorted result obeys intended properties.
  • If you want to verify the logic, the dag uses adjacency lists to store edges, and sorts using Kahn's algorithm (The algo is straightforward). The Dag is not intended to be externally used, its solely for the partial ord package, hence the internal package syntax employed.

I mainly wrote this for fun, its not really a priority if people don't have bandwidth to review. If you have a couple minutes, would appreciate feedback on if others agree partialord_test.go is a better API than what exists right now (

osmosis/app/modules.go

Lines 225 to 254 in ed10116

var orderEndBlockers = []string{
lockuptypes.ModuleName,
crisistypes.ModuleName,
govtypes.ModuleName,
stakingtypes.ModuleName,
capabilitytypes.ModuleName,
authtypes.ModuleName,
banktypes.ModuleName,
distrtypes.ModuleName,
slashingtypes.ModuleName,
minttypes.ModuleName,
genutiltypes.ModuleName,
evidencetypes.ModuleName,
authz.ModuleName,
paramstypes.ModuleName,
upgradetypes.ModuleName,
vestingtypes.ModuleName,
ibchost.ModuleName,
ibctransfertypes.ModuleName,
gammtypes.ModuleName,
incentivestypes.ModuleName,
lockuptypes.ModuleName,
poolincentivestypes.ModuleName,
superfluidtypes.ModuleName,
bech32ibctypes.ModuleName,
txfeestypes.ModuleName,
// Note: epochs' endblock should be "real" end of epochs, we keep epochs endblock at the end
epochstypes.ModuleName,
wasm.ModuleName,
}
) and if we should switch to it, pending later logic agreements.

This PR intentionally does not change app.go, and just shows the API via a test, so that we can do a moreful check of all orderings independently of logic / agreeing on the approach.

Documentation

  • Does this introduce a new feature or user-facing behavior changes? no
  • Is a relevant changelog entry added to the Unreleased section in CHANGELOG.md? yes
  • How is the feature or change documented? Both modules are intended to be documented by their module.go's and function godocs.

The intention of this logic is to allow us to specify partial orderings
when orderings are needed (e.g. beginblock, endblock, upgrade orders),
and then have an automatic derivation of the relevant total ordering.

partialord_test.go gives a sense of how the API would look if we went with
this.
Comment on lines +17 to +23
beginBlockOrd := partialord.NewPartialOrdering(moduleNames)
beginBlockOrd.FirstElements("upgrades", "epochs", "capabilities")
beginBlockOrd.After("ibctransfers", "ibc")
beginBlockOrd.After("bech32ibc", "ibctransfers")
beginBlockOrd.Before("mint", "distribution")
// This is purely just to test last functionality, doesn't make sense in context
beginBlockOrd.LastElements("auth", "authz", "wasm")
Copy link
Member Author

@ValarDragon ValarDragon Apr 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be what the app.go ordering logic ends up looking like.

I think in practice, we will just use Before syntax, and not mix After and Before

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we then avoid exposing the unused API?

Copy link
Member Author

@ValarDragon ValarDragon Apr 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, what makes sense depends on the context of a given application

mixing before and after would be quite weird for the reader though

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If my understanding of the inverse relationship between Before and After is right (i.e. beginBlockOrd.After("ibctransfers", "ibc") is the same as beginBlockOrd.Before("ibc", "ibctransfers"))

I'm still not quite sure, why do we need to expose both? In what context do we need both if we can substitute one for the other by switching the order of arguments?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar reason to why int libraries expose > and <=

@codecov-commenter
Copy link

codecov-commenter commented Apr 23, 2022

Codecov Report

Merging #1335 (dea3eb5) into main (a33c3c2) will increase coverage by 0.29%.
The diff coverage is 73.20%.

@@            Coverage Diff             @@
##             main    #1335      +/-   ##
==========================================
+ Coverage   19.53%   19.82%   +0.29%     
==========================================
  Files         200      202       +2     
  Lines       27532    27685     +153     
==========================================
+ Hits         5377     5489     +112     
- Misses      21153    21175      +22     
- Partials     1002     1021      +19     
Impacted Files Coverage Δ
osmoutils/partialord/internal/dag/dag.go 72.86% <72.86%> (ø)
osmoutils/partialord/partialord.go 75.00% <75.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 44099c5...dea3eb5. Read the comment docs.

@ValarDragon
Copy link
Member Author

ValarDragon commented Apr 23, 2022

I did a brief survey of other dag implementations before implementing this one, and I couldn't find any that followed either of:

  • Being maintained & publicly exposed
  • Being deterministic

Folks like hashicorp maintain an unexposed dag internally for dependency tracking things, but they don't have the same determinism requirements we have in the SDK. They put it in an internal package, so we couldn't use it even if we wanted. Seemed excessive to copy that rather than just writing a short file here imo, especially given how simple it is for this use case, and the ease of custom helpers like AddFirstElements and AddLastElements.

Copy link
Member

@p0mvn p0mvn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is an awesome design, was really interesting to review this. Great work! I really like how much more readable and less prone to user input errors the new API will make it in app.go

Most of my comments are readability-related nits. However, I think that we should add more tests:

  • Test for AddLast in dag_test.to
  • Tests in partialord_test.go related to the sequence of calls of the API
    • Aside from helping to spot bugs, if any, these tests should be helpful to understand the usage of the API

osmoutils/partialord/internal/dag/dag.go Outdated Show resolved Hide resolved
osmoutils/partialord/internal/dag/dag.go Show resolved Hide resolved
osmoutils/partialord/internal/dag/dag.go Show resolved Hide resolved
osmoutils/partialord/internal/dag/dag.go Show resolved Hide resolved
osmoutils/partialord/internal/dag/dag.go Show resolved Hide resolved
Comment on lines +17 to +23
beginBlockOrd := partialord.NewPartialOrdering(moduleNames)
beginBlockOrd.FirstElements("upgrades", "epochs", "capabilities")
beginBlockOrd.After("ibctransfers", "ibc")
beginBlockOrd.After("bech32ibc", "ibctransfers")
beginBlockOrd.Before("mint", "distribution")
// This is purely just to test last functionality, doesn't make sense in context
beginBlockOrd.LastElements("auth", "authz", "wasm")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we then avoid exposing the unused API?

osmoutils/partialord/partialord_test.go Show resolved Hide resolved
osmoutils/partialord/internal/dag/dag_test.go Show resolved Hide resolved
osmoutils/partialord/internal/dag/dag.go Outdated Show resolved Hide resolved
osmoutils/partialord/internal/dag/dag.go Show resolved Hide resolved
@ValarDragon
Copy link
Member Author

Thanks for the detailed review!

Copy link
Member

@mattverse mattverse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! This is an awesome implementation, and I love how we could existing problems of manually listing orders of Enblockers using dag and total ordering!

General question I have would be what were the possible places in this implementation that could have caused non determinism and how do we ensure "deterministic ordering"?

Comment on lines +39 to +47
directedEdgeList := make([]map[int]int8, len(dag.nodeNameToId))
for i := 0; i < len(dag.nodeNameToId); i++ {
originalEdgeList := dag.directedEdgeList[i]
directedEdgeList[i] = make(map[int]int8, len(originalEdgeList))
for k, v := range originalEdgeList {
directedEdgeList[i][k] = v
}
}
// we re-use nodeNameToId and idToNodeNames as these are fixed at dag creation.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason we iterate to copy Dag instead of directly using dag.directedEdgeList? Is that to ensure we deep copy edge list?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the topological sort algorithm used basically removes edges from every node marked as 'done', So we have to remove these edges from a copy.

More space efficient things can technically be done, but should never be important at the relevant sizes here.

osmoutils/partialord/internal/dag/dag.go Show resolved Hide resolved
osmoutils/partialord/internal/dag/dag.go Show resolved Hide resolved
ValarDragon and others added 3 commits April 24, 2022 10:56
Co-authored-by: Matt, Park <45252226+mattverse@users.noreply.github.com>
Copy link
Member

@p0mvn p0mvn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@ValarDragon
Copy link
Member Author

Thanks for the really thorough reviews here 🙏

@ValarDragon ValarDragon merged commit a572cb3 into main Apr 25, 2022
@ValarDragon ValarDragon deleted the dev/partial_ordering branch April 25, 2022 16:54
@github-actions github-actions bot mentioned this pull request Mar 15, 2024
@github-actions github-actions bot mentioned this pull request May 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

4 participants