diff --git a/docs/docs/01-ibc/02-integration.md b/docs/docs/01-ibc/02-integration.md index dcf2e52efad..104909aad60 100644 --- a/docs/docs/01-ibc/02-integration.md +++ b/docs/docs/01-ibc/02-integration.md @@ -184,7 +184,7 @@ func NewApp(...args) *App { #### IBC v2 Router -With IBC v2, there is a new [router](https://github.com/cosmos/ibc-go/blob/main/modules/core/api/router.go) that needs to register the routes for a portID to a given IBCModule. +With IBC v2, there is a new [router](https://github.com/cosmos/ibc-go/blob/main/modules/core/api/router.go) that needs to register the routes for a portID to a given IBCModule. It routes IBCv2 messages based on the prefixes of port IDs. For example, if a route named `someModule` exists, messages addressed to port IDs like `someModuleRandomPort1`, `someModuleRandomPort2`, etc., will be passed to the corresponding module. ```go // IBC v2 router creation diff --git a/go.mod b/go.mod index 846110abeb3..aa9f54a1dbe 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/DataDog/datadog-go v4.8.3+incompatible // indirect github.com/DataDog/zstd v1.5.5 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/armon/go-radix v1.0.0 // indirect github.com/aws/aws-sdk-go v1.44.224 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect diff --git a/go.sum b/go.sum index 7dcfe13a9f2..c7a23b7a66a 100644 --- a/go.sum +++ b/go.sum @@ -246,6 +246,8 @@ github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= diff --git a/modules/core/api/router.go b/modules/core/api/router.go index 753bfc7f9c0..6e90d1dbe66 100644 --- a/modules/core/api/router.go +++ b/modules/core/api/router.go @@ -4,48 +4,54 @@ import ( "errors" "fmt" + radix "github.com/armon/go-radix" sdk "github.com/cosmos/cosmos-sdk/types" ) // Router contains all the module-defined callbacks required by IBC Protocol V2. type Router struct { - // routes is a map from portID to IBCModule - routes map[string]IBCModule + // routes is a radix trie that provides a prefix-based + // look-up structure. It maps portIDs and their prefixes to + // IBCModules. + routes radix.Tree } // NewRouter creates a new Router instance. func NewRouter() *Router { return &Router{ - routes: make(map[string]IBCModule), + routes: *radix.New(), } } -// AddRoute registers a route for a given portID to a given IBCModule. -func (rtr *Router) AddRoute(portID string, cbs IBCModule) *Router { - if !sdk.IsAlphaNumeric(portID) { +// AddRoute registers a route for a given port ID prefix to a given IBCModule. +// Panics if a prefix of portIDprefix is already a registered route. +func (rtr *Router) AddRoute(portIDprefix string, cbs IBCModule) *Router { + if !sdk.IsAlphaNumeric(portIDprefix) { panic(errors.New("route expressions can only contain alphanumeric characters")) } - if rtr.HasRoute(portID) { - panic(fmt.Errorf("route %s has already been registered", portID)) + prefixExists, prefix := rtr.HasRoute(portIDprefix) + if prefixExists { + panic(fmt.Errorf("route %s has already been covered by registered prefix: %s", portIDprefix, prefix)) } - rtr.routes[portID] = cbs + rtr.routes.Insert(portIDprefix, cbs) return rtr } // Route returns the IBCModule for a given portID. func (rtr *Router) Route(portID string) IBCModule { - route, ok := rtr.routes[portID] + _, route, ok := rtr.routes.LongestPrefix(portID) if !ok { panic(fmt.Sprintf("no route for %s", portID)) } - return route + return route.(IBCModule) } -// HasRoute returns true if the Router has a module registered for the portID or false otherwise. -func (rtr *Router) HasRoute(portID string) bool { - _, ok := rtr.routes[portID] - return ok +// HasPrefixRoute returns true if the Router has a module registered for the given portID or its prefix. +// Returns false otherwise. +func (rtr *Router) HasRoute(portID string) (bool, string) { + prefix, _, ok := rtr.routes.LongestPrefix(portID) + return ok, prefix } diff --git a/modules/core/api/router_test.go b/modules/core/api/router_test.go index 9e0e8be5921..2a76ec01771 100644 --- a/modules/core/api/router_test.go +++ b/modules/core/api/router_test.go @@ -35,13 +35,29 @@ func (suite *APITestSuite) TestRouter() { suite.Require().True(router.HasRoute("port03")) }, }, + { + name: "success: prefix based routing works", + malleate: func() { + router.AddRoute("somemodule", &mockv2.IBCModule{}) + router.AddRoute("port01", &mockv2.IBCModule{}) + }, + assertionFn: func() { + suite.Require().True(router.HasRoute("somemodule")) + suite.Require().True(router.HasRoute("somemoduleport01")) + ok, prefix := router.HasRoute("somemoduleport01") + suite.Require().Equal(true, ok) + suite.Require().Equal("somemodule", prefix) + suite.Require().NotNil(router.Route("somemoduleport01")) + suite.Require().True(router.HasRoute("port01")) + }, + }, { name: "failure: panics on duplicate module", malleate: func() { router.AddRoute("port01", &mockv2.IBCModule{}) }, assertionFn: func() { - suite.Require().PanicsWithError("route port01 has already been registered", func() { + suite.Require().PanicsWithError("route port01 has already been covered by registered prefix: port01", func() { router.AddRoute("port01", &mockv2.IBCModule{}) }) }, @@ -55,6 +71,16 @@ func (suite *APITestSuite) TestRouter() { }) }, }, + { + name: "failure: panics conflicting routes registered", + malleate: func() {}, + assertionFn: func() { + suite.Require().PanicsWithError("route someModuleWithSpecificPath has already been covered by registered prefix: someModule", func() { + router.AddRoute("someModule", &mockv2.IBCModule{}) + router.AddRoute("someModuleWithSpecificPath", &mockv2.IBCModule{}) + }) + }, + }, } for _, tc := range testCases { suite.Run(tc.name, func() {